OXYGENHUB
◆ main openmsx / src / imgui / VectorPath.hh
8.2 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#ifndef VECTOR_PATH_HH
2
#define VECTOR_PATH_HH
3
4
// Compact vector-path format for Dear ImGui drawing.
5
//
6
// Paths are stored as constexpr byte arrays. See VectorPathDsl.hh for the
7
// compile-time packer DSL.
8
//
9
// Coordinates are stored as native-endian values of type CoordT (typically
10
// int8_t, int16_t, or float). The CoordinateTransform converts CoordT to
11
// vec2 for Dear ImGui.
12
//
13
// ----------------------------------------------------------------------------
14
// Single path
15
// ----------------------------------------------------------------------------
16
//
17
//   [segment_count: u8]              N segments after the anchor (N >= 1)
18
//   [anchor_x, anchor_y: CoordT]
19
//   [flags_0: u8]                    segment-type bits for segments 0..7
20
//   [segment 0 payload]
21
//   ...
22
//   [segment 7 payload]
23
//   [flags_1: u8]                    segment-type bits for segments 8..15
24
//   [segment 8 payload]
25
//   ...
26
//
27
// Before segment i (i = 0 .. N-1), when i % 8 == 0, read one flags byte.
28
// Bit (i % 8) of that byte: 0 = line, 1 = cubic Bézier.
29
// Unused bits in the last flags byte are ignored.
30
//
31
// Segment payloads (native-endian CoordT pairs):
32
//   line:  x, y                               (2 * sizeof(CoordT))
33
//   cubic: cx1, cy1, cx2, cy2, ex, ey         (6 * sizeof(CoordT))
34
//
35
// Cubic segments use the same semantics as SVG "C" and ImGui
36
// PathBezierCubicCurveTo (anchor or previous point is the implicit start).
37
//
38
// ----------------------------------------------------------------------------
39
// Collection (stroked paths only)
40
// ----------------------------------------------------------------------------
41
//
42
//   [path_count: u8]                 M paths (M >= 1)
43
//   [flags_0: u8]                    open/closed bits for paths 0..7
44
//   [path 0 blob]                    self-describing single path
45
//   [path 1 blob]
46
//   ...
47
//   [flags_1: u8]                    when M > 8, before path 8
48
//   [path 8 blob]
49
//   ...
50
//
51
// Before path i (i = 0 .. M-1), when i % 8 == 0, read one flags byte.
52
// Bit (i % 8): 0 = open stroke, 1 = closed stroke (ImDrawFlags_Closed).
53
// Each path blob uses the single-path layout above.
54
//
55
// ----------------------------------------------------------------------------
56
// Draw semantics (Dear ImGui)
57
// ----------------------------------------------------------------------------
58
//
59
//   drawPathStroke:        PathStroke on a single path (ImDrawFlags, default open)
60
//   drawPathStrokes:       PathStroke per path; open/closed from collection flags
61
//   drawPathFilled:        PathFillConcave (implicitly closes the contour)
62
//   drawPathFilledStroked: PathFillConcave, then PathStroke with Closed
63
//
64
// Fill functions take a single path blob (no open/closed bit in the data).
65
// Colors and stroke thickness are runtime parameters, not stored in the blob.
66
67
#include "video/gl_mat.hh"
68
#include "video/gl_vec.hh"
69
70
#include <imgui.h>
71
72
#include <cassert>
73
#include <cmath>
74
#include <concepts>
75
#include <cstddef>
76
#include <cstdint>
77
#include <cstring>
78
#include <span>
79
#include <type_traits>
80
81
namespace VectorPath {
82
83
// Sequential reader over a byte span. Each get<T>() advances the read position
84
// and asserts that sizeof(T) bytes are available.
85
class ByteReader {
86
public:
87
	explicit ByteReader(std::span<const uint8_t> data_)
88
		: data(data_) {}
89
90
	template<typename T>
91
	[[nodiscard]] T get()
92
	{
93
		static_assert(std::is_trivially_copyable_v<T>);
94
		assert(pos + sizeof(T) <= data.size());
95
		T v;
96
		std::memcpy(&v, data.data() + pos, sizeof(T));
97
		pos += sizeof(T);
98
		return v;
99
	}
100
101
	[[nodiscard]] bool empty() const { return pos >= data.size(); }
102
103
private:
104
	std::span<const uint8_t> data;
105
	size_t pos = 0;
106
};
107
108
template<typename CoordT>
109
concept WireCoordinate = std::is_trivially_copyable_v<CoordT>;
110
111
// --- Coordinate transforms (template parameter to draw functions) ---
112
113
struct IdentityTransform {
114
	template<typename CoordT>
115
	[[nodiscard]] static gl::vec2 operator()(CoordT x, CoordT y)
116
	{
117
		return {float(x), float(y)};
118
	}
119
};
120
121
struct ScaleTransform {
122
	float scale;
123
124
	template<typename CoordT>
125
	[[nodiscard]] gl::vec2 operator()(CoordT x, CoordT y) const
126
	{
127
		return operator()(gl::vec2{float(x), float(y)});
128
	}
129
	[[nodiscard]] gl::vec2 operator()(gl::vec2 c) const { return c * scale; }
130
};
131
132
struct TranslateTransform {
133
	gl::vec2 translate;
134
135
	template<typename CoordT>
136
	[[nodiscard]] gl::vec2 operator()(CoordT x, CoordT y) const
137
	{
138
		return gl::vec2{float(x), float(y)} + translate;
139
	}
140
};
141
142
struct ScaleTranslateTransform {
143
	float scale;
144
	gl::vec2 translate;
145
146
	template<typename CoordT>
147
	[[nodiscard]] gl::vec2 operator()(CoordT x, CoordT y) const
148
	{
149
		return gl::vec2{float(x), float(y)} * scale + translate;
150
	}
151
};
152
153
struct AffineTransform {
154
	gl::mat2 linear;
155
	gl::vec2 translate;
156
157
	template<typename CoordT>
158
	[[nodiscard]] gl::vec2 operator()(CoordT x, CoordT y) const
159
	{
160
		return linear * gl::vec2{float(x), float(y)} + translate;
161
	}
162
};
163
164
// Build an affine transform: scale, then rotate (radians), then translate.
165
[[nodiscard]] inline AffineTransform makeAffineTransform(
166
	float angleRadians, float scale, gl::vec2 translate)
167
{
168
	float s = std::sin(angleRadians);
169
	float c = std::cos(angleRadians);
170
	auto linear = gl::mat2{gl::vec2{ c, s},
171
	                       gl::vec2(-s, c)} * scale;
172
	return {linear, translate};
173
}
174
175
template<typename T>
176
concept CoordinateTransform = requires(const T& t, int x, int y) {
177
	{ t(x, y) } -> std::convertible_to<gl::vec2>;
178
};
179
180
static_assert(CoordinateTransform<IdentityTransform>);
181
static_assert(CoordinateTransform<ScaleTransform>);
182
static_assert(CoordinateTransform<TranslateTransform>);
183
static_assert(CoordinateTransform<AffineTransform>);
184
185
namespace detail {
186
187
template<typename CoordT, CoordinateTransform TF>
188
void buildPath(ImDrawList* drawList, ByteReader& reader, TF transform)
189
{
190
	auto segmentCount = reader.get<uint8_t>();
191
	assert(segmentCount >= 1);
192
193
	auto getXY = [&] {
194
		auto x = reader.get<CoordT>();
195
		auto y = reader.get<CoordT>();
196
		return transform(x, y);
197
	};
198
199
	drawList->PathClear();
200
	drawList->PathLineTo(getXY());
201
202
	uint8_t flags = 0;
203
	for (uint8_t i = 0; i < segmentCount; ++i) {
204
		if ((i % 8) == 0) {
205
			flags = reader.get<uint8_t>();
206
		}
207
208
		if ((flags >> (i % 8)) & 1) {
209
			auto c1 = getXY();
210
			auto c2 = getXY();
211
			auto e = getXY();
212
			drawList->PathBezierCubicCurveTo(c1, c2, e);
213
		} else {
214
			drawList->PathLineTo(getXY());
215
		}
216
	}
217
}
218
219
} // namespace detail
220
221
// --- Draw API ---
222
223
template<typename CoordT, CoordinateTransform TF>
224
void drawPathStroke(ImDrawList* drawList, std::span<const uint8_t> path,
225
                    TF transform, ImU32 color, float thickness,
226
                    ImDrawFlags flags = ImDrawFlags_None)
227
{
228
	ByteReader reader(path);
229
	detail::buildPath<CoordT>(drawList, reader, transform);
230
	drawList->PathStroke(color, thickness, flags);
231
	assert(reader.empty());
232
}
233
234
template<typename CoordT, CoordinateTransform TF>
235
void drawPathStrokes(ImDrawList* drawList, std::span<const uint8_t> collection,
236
                     TF transform, ImU32 color, float thickness)
237
{
238
	ByteReader reader(collection);
239
	auto pathCount = reader.get<uint8_t>();
240
241
	uint8_t strokeFlags = 0;
242
	for (uint8_t i = 0; i < pathCount; ++i) {
243
		if ((i % 8) == 0) {
244
			strokeFlags = reader.get<uint8_t>();
245
		}
246
247
		bool closed = (strokeFlags >> (i % 8)) & 1;
248
		detail::buildPath<CoordT>(drawList, reader, transform);
249
		drawList->PathStroke(color, thickness,
250
		                     closed ? ImDrawFlags_Closed : ImDrawFlags_None);
251
	}
252
	assert(reader.empty());
253
}
254
255
template<typename CoordT, CoordinateTransform TF>
256
void drawPathFilled(ImDrawList* drawList, std::span<const uint8_t> path,
257
                    TF transform, ImU32 fillColor)
258
{
259
	ByteReader reader(path);
260
	detail::buildPath<CoordT>(drawList, reader, transform);
261
	drawList->PathFillConcave(fillColor);
262
	assert(reader.empty());
263
}
264
265
template<typename CoordT, CoordinateTransform TF>
266
void drawPathFilledStroked(ImDrawList* drawList, std::span<const uint8_t> path,
267
                           TF transform, ImU32 fillColor,
268
                           ImU32 outlineColor, float outlineThickness)
269
{
270
	{
271
		ByteReader reader(path);
272
		detail::buildPath<CoordT>(drawList, reader, transform);
273
		drawList->PathFillConcave(fillColor);
274
		assert(reader.empty());
275
	}
276
	{
277
		ByteReader reader(path);
278
		detail::buildPath<CoordT>(drawList, reader, transform);
279
		drawList->PathStroke(outlineColor, outlineThickness, ImDrawFlags_Closed);
280
		assert(reader.empty());
281
	}
282
}
283
284
} // namespace VectorPath
285
286
#endif