HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
bezier_curve.hpp
1// Copyright Take Vos 2019-2022.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "../image/module.hpp"
8#include "../geometry/module.hpp"
9#include "../utility/utility.hpp"
10#include "bezier.hpp"
11#include "bezier_point.hpp"
12#include "../macros.hpp"
13#include <tuple>
14#include <limits>
15#include <algorithm>
16
17hi_export_module(hikogui.graphic_path.bezier_curve);
18
19namespace hi { inline namespace v1 {
20
24hi_export struct bezier_curve {
25 enum class Type : uint8_t { None, Linear, Quadratic, Cubic };
26
27 Type type;
28 point2 P1;
29 point2 C1;
30 point2 C2;
31 point2 P2;
32
38
41 bezier_curve(point2 const P1, point2 const P2) noexcept : type(Type::Linear), P1(P1), C1(), C2(), P2(P2) {}
42
45 bezier_curve(point2 const P1, point2 const C1, point2 const P2) noexcept : type(Type::Quadratic), P1(P1), C1(C1), C2(), P2(P2)
46 {
47 }
48
51 bezier_curve(point2 const P1, point2 const C1, point2 const C2, point2 const P2) noexcept :
52 type(Type::Cubic), P1(P1), C1(C1), C2(C2), P2(P2)
53 {
54 }
55
58 bezier_curve(Type const type, point2 const P1, point2 const C1, point2 const C2, point2 const P2) noexcept :
59 type(type), P1(P1), C1(C1), C2(C2), P2(P2)
60 {
61 }
62
70 [[nodiscard]] point2 pointAt(float const t) const noexcept
71 {
72 switch (type) {
73 case Type::Linear:
74 return bezierPointAt(P1, P2, t);
75 case Type::Quadratic:
76 return bezierPointAt(P1, C1, P2, t);
77 case Type::Cubic:
78 return bezierPointAt(P1, C1, C2, P2, t);
79 default:
80 hi_no_default();
81 }
82 }
83
91 [[nodiscard]] constexpr vector2 tangentAt(float const t) const noexcept
92 {
93 switch (type) {
94 case Type::Linear:
95 return bezierTangentAt(P1, P2, t);
96 case Type::Quadratic:
97 return bezierTangentAt(P1, C1, P2, t);
98 case Type::Cubic:
99 return bezierTangentAt(P1, C1, C2, P2, t);
100 default:
101 hi_no_default();
102 }
103 }
104
109 [[nodiscard]] results<float, 3> solveXByY(float const y) const noexcept
110 {
111 switch (type) {
112 case Type::Linear:
113 return bezierFindX(P1, P2, y);
114 case Type::Quadratic:
115 return bezierFindX(P1, C1, P2, y);
116 case Type::Cubic:
117 return bezierFindX(P1, C1, C2, P2, y);
118 default:
119 hi_no_default();
120 }
121 }
122
123 [[nodiscard]] hi_force_inline results<float, 3> solveTForNormalsIntersectingPoint(point2 P) const noexcept
124 {
125 switch (type) {
126 case Type::Linear:
128 case Type::Quadratic:
130 case Type::Cubic:
131 hi_no_default();
132 default:
133 hi_no_default();
134 }
135 }
136
141
142 bezier_curve const *curve = nullptr;
143
146 float t = 0.0f;
147
151
158
161 [[nodiscard]] hi_force_inline constexpr float orthogonality() const noexcept
162 {
163 hilet tangent = curve->tangentAt(t);
164 return cross(normalize(tangent), normalize(PN));
165 };
166
167 [[nodiscard]] hi_force_inline float distance() const noexcept
168 {
169 return std::sqrt(sq_distance);
170 }
171
172 [[nodiscard]] hi_force_inline float signed_distance() const noexcept
173 {
174 hilet d = distance();
175 return orthogonality() < 0.0 ? d : -d;
176 }
177
178 [[nodiscard]] hi_force_inline constexpr bool operator<(sdf_distance_result const& rhs) const noexcept
179 {
180 if (abs(sq_distance - rhs.sq_distance) < 0.01f) {
181 return abs(orthogonality()) > abs(rhs.orthogonality());
182 } else {
183 return sq_distance < rhs.sq_distance;
184 }
185 }
186 };
187
196 [[nodiscard]] sdf_distance_result sdf_distance(point2 P) const noexcept
197 {
198 auto nearest = sdf_distance_result{this};
199
200 hilet ts = solveTForNormalsIntersectingPoint(P);
201 for (auto t : ts) {
202 t = std::clamp(t, 0.0f, 1.0f);
203
204 hilet PN = P - pointAt(t);
205 hilet sq_distance = squared_hypot(PN);
206 if (sq_distance < nearest.sq_distance) {
207 nearest.t = t;
208 nearest.PN = PN;
209 nearest.sq_distance = sq_distance;
210 }
211 }
212
213 return nearest;
214 }
215
222 {
223 hilet outerA = bezier_curve{P1, C1};
224 hilet outerBridge = bezier_curve{C1, C2};
225 hilet outerB = bezier_curve{C2, P2};
226
227 hilet innerA = bezier_curve{outerA.pointAt(t), outerBridge.pointAt(t)};
228 hilet innerB = bezier_curve{outerBridge.pointAt(t), outerB.pointAt(t)};
229
230 hilet newPoint = bezier_curve{innerA.pointAt(t), innerB.pointAt(t)}.pointAt(t);
231
232 return {{P1, outerA.pointAt(t), innerA.pointAt(t), newPoint}, {newPoint, innerB.pointAt(t), outerB.pointAt(t), P2}};
233 }
234
241 {
242 hilet outerA = bezier_curve{P1, C1};
243 hilet outerB = bezier_curve{C1, P2};
244
245 hilet newPoint = bezier_curve{outerA.pointAt(t), outerB.pointAt(t)}.pointAt(t);
246
247 return {{P1, outerA.pointAt(t), newPoint}, {newPoint, outerB.pointAt(t), P2}};
248 }
249
256 {
257 hilet newPoint = pointAt(t);
258
259 return {{P1, newPoint}, {newPoint, P2}};
260 }
261
267 [[nodiscard]] std::pair<bezier_curve, bezier_curve> split(float const t) const noexcept
268 {
269 switch (type) {
270 case Type::Linear:
271 return linearSplit(t);
272 case Type::Quadratic:
273 return quadraticSplit(t);
274 case Type::Cubic:
275 return cubicSplit(t);
276 default:
277 hi_no_default();
278 }
279 }
280
286 {
287 if (flatness() >= minimumFlatness) {
288 r.push_back(*this);
289 } else {
290 hilet[a, b] = split(0.5f);
291 a.subdivideUntilFlat_impl(r, minimumFlatness);
292 b.subdivideUntilFlat_impl(r, minimumFlatness);
293 }
294 }
295
301 {
304 return r;
305 }
306
311 {
312 switch (type) {
313 case Type::Linear:
314 return bezierFlatness(P1, P2);
315 case Type::Quadratic:
316 return bezierFlatness(P1, C1, P2);
317 case Type::Cubic:
318 return bezierFlatness(P1, C1, C2, P2);
319 default:
320 hi_no_default();
321 }
322 }
323
328 [[nodiscard]] bezier_curve toParallelLine(float const offset) const noexcept
329 {
330 hilet[newP1, newP2] = parallelLine(P1, P2, offset);
331 return {newP1, newP2};
332 }
333
334 [[nodiscard]] friend bool operator==(bezier_curve const& lhs, bezier_curve const& rhs) noexcept
335 {
336 if (lhs.type != rhs.type) {
337 return false;
338 }
339 switch (lhs.type) {
340 case bezier_curve::Type::Linear:
341 return (lhs.P1 == rhs.P1) && (lhs.P2 == rhs.P2);
342 case bezier_curve::Type::Quadratic:
343 return (lhs.P1 == rhs.P1) && (lhs.C1 == rhs.C1) && (lhs.P2 == rhs.P2);
344 case bezier_curve::Type::Cubic:
345 return (lhs.P1 == rhs.P1) && (lhs.C1 == rhs.C1) && (lhs.C2 == rhs.C2) && (lhs.P2 == rhs.P2);
346 default:
347 hi_no_default();
348 }
349 }
350
351 [[nodiscard]] friend bezier_curve operator*(transformer2 auto const& lhs, bezier_curve const& rhs) noexcept
352 {
353 return {rhs.type, lhs * rhs.P1, lhs * rhs.C1, lhs * rhs.C2, lhs * rhs.P2};
354 }
355
358 [[nodiscard]] friend bezier_curve operator~(bezier_curve const& rhs) noexcept
359 {
360 return {rhs.type, rhs.P2, rhs.C2, rhs.C1, rhs.P1};
361 }
362};
363
364namespace detail {
365
366[[nodiscard]] constexpr std::vector<float> solveCurvesXByY(std::vector<bezier_curve> const& v, float y) noexcept
367{
369 r.reserve(v.size());
370
371 for (hilet& curve : v) {
372 hilet xValues = curve.solveXByY(y);
373 for (hilet x : xValues) {
374 r.push_back(x);
375 }
376 }
377 return r;
378}
379
380[[nodiscard]] constexpr std::optional<std::vector<std::pair<float, float>>>
381getFillSpansAtY(std::vector<bezier_curve> const& v, float y) noexcept
382{
383 auto xValues = solveCurvesXByY(v, y);
384
385 // Sort x values, each pair is a span.
386 std::sort(xValues.begin(), xValues.end());
387
388 // End-to-end connected curves will yield duplicate values.
389 hilet uniqueEnd = std::unique(xValues.begin(), xValues.end());
390
391 // After removing duplicates, we should end up with pairs of x values.
392 std::size_t const uniqueValueCount = (uniqueEnd - xValues.begin());
393
394 if (uniqueValueCount % 2 != 0) {
395 // Something is wrong in solving the curves. Probably numeric instability.
396 // In any case, just ignore this sample.
397 return {};
398 }
399
400 // Create pairs of values.
403 for (std::size_t i = 0; i < uniqueValueCount; i += 2) {
404 r.emplace_back(xValues[i], xValues[i + 1]);
405 }
406 return r;
407}
408
409constexpr void fillPartialPixels(std::span<uint8_t> row, ssize_t const i, float const startX, float const endX) noexcept
410{
411 hilet pixelCoverage = std::clamp(endX, i + 0.0f, i + 1.0f) - std::clamp(startX, i + 0.0f, i + 1.0f);
412
413 auto& pixel = row[i];
414 pixel = static_cast<uint8_t>(std::min(pixelCoverage * 51.0f + pixel, 255.0f));
415}
416
417constexpr void fillFullPixels(std::span<uint8_t> row, ssize_t const start, ssize_t const size) noexcept
418{
419 if (size < 16) {
420 hilet end = start + size;
421 for (ssize_t i = start; i < end; i++) {
422 row[i] += 0x33;
423 }
424 } else {
425 auto u8p = &row[start];
426 hilet u8end = u8p + size;
427
428 // First add 51 to all pixels up to the alignment.
429 hilet alignedStart = hi::ceil(u8p, sizeof(uint64_t));
430 while (u8p < alignedStart) {
431 *(u8p++) += 0x33;
432 }
433
434 // add 51 for each pixel, 8 pixels at a time.
435 auto u64p = reinterpret_cast<uint64_t *>(u8p);
436 auto const *const u64end = reinterpret_cast<uint64_t const *>(hi::floor(u8end, sizeof(uint64_t)));
437 while (u64p < u64end) {
438 *(u64p++) += 0x3333333333333333ULL;
439 }
440
441 // Add 51 to the last pixels.
442 u8p = reinterpret_cast<uint8_t *>(u64p);
443 while (u8p < u8end) {
444 *(u8p++) += 0x33;
445 }
446 }
447}
448
452constexpr void fillRowSpan(std::span<uint8_t> row, float const startX, float const endX) noexcept
453{
454 if (startX >= row.size() || endX < 0.0f) {
455 return;
456 }
457
459 hilet endXplusOne = endX + 1.0f;
462 hilet endColumn = std::min(endX_int, row.size());
464
465 if (nrColumns == 1) {
466 fillPartialPixels(row, startColumn, startX, endX);
467 } else {
468 fillPartialPixels(row, startColumn, startX, endX);
469 fillFullPixels(row, startColumn + 1, nrColumns - 2);
470 fillPartialPixels(row, endColumn - 1, startX, endX);
471 }
472}
473
474constexpr void fillRow(std::span<uint8_t> row, std::size_t rowY, std::vector<bezier_curve> const& curves) noexcept
475{
476 // 5 times super sampling.
477 for (float y = rowY + 0.1f; y < (rowY + 1); y += 0.2f) {
478 auto optionalSpans = getFillSpansAtY(curves, y);
479 if (!optionalSpans) {
480 // try again, with a slight offset.
481 optionalSpans = getFillSpansAtY(curves, y + 0.01f);
482 }
483
484 if (optionalSpans) {
485 hilet& spans = optionalSpans.value();
486
487 for (hilet& span : spans) {
488 fillRowSpan(row, span.first, span.second);
489 }
490 }
491 }
492}
493
494[[nodiscard]] constexpr float generate_sdf_r8_pixel(point2 point, std::vector<bezier_curve> const& curves) noexcept
495{
496 if (curves.empty()) {
498 }
499
500 auto it = curves.cbegin();
501 auto nearest = (it++)->sdf_distance(point);
502
503 for (; it != curves.cend(); ++it) {
504 hilet distance = it->sdf_distance(point);
505
506 if (distance < nearest) {
508 }
509 }
510
511 return nearest.signed_distance();
512}
513
514} // namespace detail
515
523makeContourFromPoints(std::vector<bezier_point>::const_iterator begin, std::vector<bezier_point>::const_iterator end) noexcept
524{
526
528
529 auto type = bezier_curve::Type::None;
530 auto P1 = point2{};
531 auto C1 = point2{};
532 auto C2 = point2{};
533
534 for (hilet& point : points) {
535 switch (point.type) {
536 case bezier_point::Type::Anchor:
537 switch (type) {
538 case bezier_curve::Type::None:
539 P1 = point.p;
540 type = bezier_curve::Type::Linear;
541 break;
542 case bezier_curve::Type::Linear:
543 r.emplace_back(P1, point.p);
544 P1 = point.p;
545 type = bezier_curve::Type::Linear;
546 break;
547 case bezier_curve::Type::Quadratic:
548 r.emplace_back(P1, C1, point.p);
549 P1 = point.p;
550 type = bezier_curve::Type::Linear;
551 break;
552 case bezier_curve::Type::Cubic:
553 r.emplace_back(P1, C1, C2, point.p);
554 P1 = point.p;
555 type = bezier_curve::Type::Linear;
556 break;
557 default:
558 hi_no_default();
559 }
560 break;
561 case bezier_point::Type::QuadraticControl:
562 C1 = point.p;
563 type = bezier_curve::Type::Quadratic;
564 break;
565 case bezier_point::Type::CubicControl1:
566 C1 = point.p;
567 type = bezier_curve::Type::Cubic;
568 break;
569 case bezier_point::Type::CubicControl2:
570 C2 = point.p;
571 hi_assert(type == bezier_curve::Type::Cubic);
572 break;
573 default:
574 hi_no_default();
575 }
576 }
577
578 return r;
579}
580
588{
589 auto r = std::vector<bezier_curve>{};
590 r.reserve(contour.size());
591
592 for (auto i = contour.rbegin(); i != contour.rend(); i++) {
593 r.push_back(~(*i));
594 }
595
596 return r;
597}
598
611 float offset,
613 float tolerance) noexcept
614{
616 for (hilet& curve : contour) {
617 for (hilet& flatCurve : curve.subdivideUntilFlat(tolerance)) {
618 contourAtOffset.push_back(flatCurve.toParallelLine(offset));
619 }
620 }
621
622 // The resulting path now consists purely of line-segments that may have gaps and overlaps.
623 // This needs to be repaired.
624 std::optional<point2> intersectPoint;
625 auto r = std::vector<bezier_curve>{};
626 for (hilet& curve : contourAtOffset) {
627 if (r.size() == 0) {
628 r.push_back(curve);
629
630 } else if (r.back().P2 == curve.P1) {
631 r.push_back(curve);
632
633 } else if ((intersectPoint = getIntersectionPoint(r.back().P1, r.back().P2, curve.P1, curve.P2))) {
634 r.back().P2 = intersectPoint.value();
635 r.push_back(curve);
636 r.back().P1 = intersectPoint.value();
637
638 } else if (
640 to_bool(intersectPoint = getExtrapolatedIntersectionPoint(r.back().P1, r.back().P2, curve.P1, curve.P2))) {
641 r.back().P2 = intersectPoint.value();
642 r.push_back(curve);
643 r.back().P1 = intersectPoint.value();
644
645 } else {
646 r.emplace_back(r.back().P2, curve.P1);
647 r.push_back(curve);
648 }
649 }
650
651 // Repair the endpoints of the contour as well.
652 if (r.size() > 0 && r.back().P2 != r.front().P1) {
653 if ((intersectPoint = getIntersectionPoint(r.back().P1, r.back().P2, r.front().P1, r.front().P2))) {
654 r.back().P2 = r.front().P1 = intersectPoint.value();
655 } else {
656 r.emplace_back(r.back().P2, r.front().P1);
657 }
658 }
659
660 return r;
661}
662
667constexpr void fill(pixmap_span<uint8_t> image, std::vector<bezier_curve> const& curves) noexcept
668{
669 for (auto y = 0_uz; y < image.height(); y++) {
670 detail::fillRow(image[y], y, curves);
671 }
672}
673
678constexpr void fill(pixmap_span<sdf_r8> image, std::vector<bezier_curve> const& curves) noexcept
679{
680 for (auto row_nr = 0_uz; row_nr != image.height(); ++row_nr) {
681 hilet row = image[row_nr];
682 hilet y = static_cast<float>(row_nr);
683 for (auto column_nr = 0_uz; column_nr != image.width(); ++column_nr) {
684 hilet x = static_cast<float>(column_nr);
685 row[column_nr] = detail::generate_sdf_r8_pixel(point2(x, y), curves);
686 }
687 }
688}
689
690}} // namespace hi::v1
@ end
Start from the end of the file.
@ begin
Start from the beginning of the file.
line_join_style
The way two lines should be joined.
Definition line_join_style.hpp:19
@ miter
The outer edge of both lines are extended until they meet to form a sharp corner.
@ other
The gui_event does not have associated data.
DOXYGEN BUG.
Definition algorithm.hpp:16
geometry/margins.hpp
Definition lookahead_iterator.hpp:5
constexpr results< float, 1 > bezierFindX(point2 P1, point2 P2, float y) noexcept
Definition bezier.hpp:146
std::ptrdiff_t ssize_t
Signed size/index into an array.
Definition misc.hpp:33
constexpr std::vector< bezier_curve > makeContourFromPoints(std::vector< bezier_point >::const_iterator begin, std::vector< bezier_point >::const_iterator end) noexcept
Make a contour of Bezier curves from a list of points.
Definition bezier_curve.hpp:523
hi_force_inline constexpr results< float, 1 > bezierFindTForNormalsIntersectingPoint(point2 P1, point2 P2, point2 P) noexcept
Find t on the line P1->P2 which is closest to P.
Definition bezier.hpp:106
std::optional< point2 > getIntersectionPoint(point2 A1, point2 A2, point2 B1, point2 B2) noexcept
Definition bezier.hpp:260
unit< si_length_tag, double, std::ratio< 254, 720 '000 >::type > points
Points: 1/72 inch.
Definition units.hpp:180
constexpr std::vector< bezier_curve > makeInverseContour(std::vector< bezier_curve > const &contour) noexcept
Inverse a contour.
Definition bezier_curve.hpp:587
std::optional< point2 > getExtrapolatedIntersectionPoint(point2 A1, point2 A2, point2 B1, point2 B2) noexcept
Definition bezier.hpp:291
constexpr std::vector< bezier_curve > makeParallelContour(std::vector< bezier_curve > const &contour, float offset, hi::line_join_style line_join_style, float tolerance) noexcept
Definition bezier_curve.hpp:609
constexpr Out narrow_cast(In const &rhs) noexcept
Cast numeric values without loss of precision.
Definition cast.hpp:377
float bezierFlatness(point2 P1, point2 P2) noexcept
Definition bezier.hpp:213
A high-level geometric vector Part of the high-level vector, point, mat and color types.
Definition vector2.hpp:19
Definition bezier_curve.hpp:24
point2 pointAt(float const t) const noexcept
Definition bezier_curve.hpp:70
point2 P2
Last point.
Definition bezier_curve.hpp:31
void subdivideUntilFlat_impl(std::vector< bezier_curve > &r, float const minimumFlatness) const noexcept
Definition bezier_curve.hpp:285
std::vector< bezier_curve > subdivideUntilFlat(float const tolerance) const noexcept
Definition bezier_curve.hpp:300
bezier_curve(point2 const P1, point2 const C1, point2 const P2) noexcept
Definition bezier_curve.hpp:45
std::pair< bezier_curve, bezier_curve > linearSplit(float const t) const noexcept
Definition bezier_curve.hpp:255
results< float, 3 > solveXByY(float const y) const noexcept
Definition bezier_curve.hpp:109
constexpr vector2 tangentAt(float const t) const noexcept
Definition bezier_curve.hpp:91
sdf_distance_result sdf_distance(point2 P) const noexcept
Find the distance from the point to the curve.
Definition bezier_curve.hpp:196
bezier_curve(Type const type, point2 const P1, point2 const C1, point2 const C2, point2 const P2) noexcept
Definition bezier_curve.hpp:58
std::pair< bezier_curve, bezier_curve > split(float const t) const noexcept
Definition bezier_curve.hpp:267
point2 C2
Control point.
Definition bezier_curve.hpp:30
std::pair< bezier_curve, bezier_curve > quadraticSplit(float const t) const noexcept
Definition bezier_curve.hpp:240
friend bezier_curve operator~(bezier_curve const &rhs) noexcept
Definition bezier_curve.hpp:358
point2 C1
Control point.
Definition bezier_curve.hpp:29
bezier_curve toParallelLine(float const offset) const noexcept
Definition bezier_curve.hpp:328
bezier_curve(point2 const P1, point2 const C1, point2 const C2, point2 const P2) noexcept
Definition bezier_curve.hpp:51
point2 P1
First point.
Definition bezier_curve.hpp:28
float flatness() const noexcept
Definition bezier_curve.hpp:310
std::pair< bezier_curve, bezier_curve > cubicSplit(float const t) const noexcept
Definition bezier_curve.hpp:221
Definition bezier_curve.hpp:137
vector2 PN
The vector between P and N.
Definition bezier_curve.hpp:140
float t
Linear position on the curve-segment, 0.0 and 1.0 are end-points.
Definition bezier_curve.hpp:146
hi_force_inline constexpr float orthogonality() const noexcept
The orthogonality of the line PN and the tangent of the curve at N.
Definition bezier_curve.hpp:161
float sq_distance
The square distance between P and N.
Definition bezier_curve.hpp:150
static constexpr std::vector< bezier_point > normalizePoints(std::vector< bezier_point >::const_iterator const begin, std::vector< bezier_point >::const_iterator const end) noexcept
Definition bezier_point.hpp:41
A non-owning 2D pixel-based image.
Definition pixmap_span.hpp:34
Definition units.hpp:18
T back(T... args)
T distance(T... args)
T emplace_back(T... args)
T front(T... args)
T max(T... args)
T min(T... args)
T push_back(T... args)
T reserve(T... args)
T size(T... args)
T sort(T... args)
T sqrt(T... args)
T unique(T... args)