1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
use std::collections::HashMap;
use std::sync::Arc;
/// Integer option bag used to configure Draco encoding.
///
/// Options mirror the C++ Draco encoder style: global options apply to the
/// whole geometry, while attribute options override a value for one attribute
/// id and fall back to the global value when unset.
///
/// Common keys include `quantization_bits` (per-attribute precision),
/// `encoding_speed`/`decoding_speed`, `encoding_method`, and
/// `prediction_scheme`. Keys without an explicit setter are read and written
/// with [`get_global_int`](EncoderOptions::get_global_int) /
/// [`set_global_int`](EncoderOptions::set_global_int).
///
/// # Examples
///
/// ```
/// use draco_core::EncoderOptions;
///
/// let mut options = EncoderOptions::new();
/// options.set_global_int("quantization_bits", 14); // default for all attributes
/// options.set_attribute_int(0, "quantization_bits", 10); // override attribute 0
///
/// assert_eq!(options.get_attribute_int(0, "quantization_bits", 0), 10);
/// // Attribute 1 has no override, so it falls back to the global value.
/// assert_eq!(options.get_attribute_int(1, "quantization_bits", 0), 14);
/// ```
#[derive(Debug, Clone, Default)]
pub struct EncoderOptions {
inner: Arc<Inner>,
}
/// The option maps, shared until a setter needs its own copy.
///
/// The encoders clone the whole bag on every `encode` call and keep the copy
/// for the duration. A deep clone paid one allocation per map and per `String`
/// key each time -- pure bookkeeping, and on a tiny mesh a visible share of
/// the per-call floor. Behind an `Arc` the clone is a reference count, and a
/// setter on a bag that is shared at that moment copies the maps once for
/// itself (`Arc::make_mut`); a bag nobody else holds is mutated in place.
#[derive(Debug, Clone, Default)]
struct Inner {
global_options: HashMap<String, i32>,
attribute_options: HashMap<i32, HashMap<String, i32>>,
}
impl EncoderOptions {
/// Creates options with Draco-compatible defaults.
pub fn new() -> Self {
Self::default()
}
/// Returns the configured encoding speed, defaulting to 5.
pub fn get_encoding_speed(&self) -> i32 {
self.get_global_int("encoding_speed", 5)
}
/// Returns the configured decoding speed target, defaulting to 5.
pub fn get_decoding_speed(&self) -> i32 {
self.get_global_int("decoding_speed", 5)
}
/// Returns the maximum speed for both encoding/decoding.
/// Matches C++ ExpertEncoder::GetSpeed() behavior.
pub fn get_speed(&self) -> i32 {
let encoding_speed = self
.inner
.global_options
.get("encoding_speed")
.copied()
.unwrap_or(-1);
let decoding_speed = self
.inner
.global_options
.get("decoding_speed")
.copied()
.unwrap_or(-1);
let max_speed = encoding_speed.max(decoding_speed);
if max_speed == -1 {
5 // Default value
} else {
max_speed
}
}
/// Sets both speeds from a `draco_encoder`-style compression level.
///
/// The CLI's `-cl` runs 0 (least compression) to 10 (most), the opposite
/// sense of `encoding_speed`/`decoding_speed`, and converts with
/// `speed = 10 - compression_level` before calling the same
/// `SetSpeedOptions` this crate mirrors; this method does the same
/// conversion and nothing else. `level` is not range-checked, matching
/// the CLI, which passes an out-of-range `-cl` straight through the same
/// subtraction rather than rejecting it.
pub fn set_compression_level(&mut self, level: i32) {
let speed = 10 - level;
self.set_global_int("encoding_speed", speed);
self.set_global_int("decoding_speed", speed);
}
/// Returns `10 - `[`get_speed`](Self::get_speed)`()`, the CLI's compression
/// level for the speed this instance currently carries.
///
/// A fresh `EncoderOptions` reports 5 here, not the CLI's own default of
/// 7 — the two tools default to different speeds (5 here, 3 there), and
/// this getter reads what is actually set rather than what the CLI would
/// have chosen.
pub fn get_compression_level(&self) -> i32 {
10 - self.get_speed()
}
/// Sets `quantization_bits` for one attribute id.
///
/// Equivalent to `ExpertEncoder::SetAttributeQuantization` and to what the
/// CLI's `-qp`/`-qt`/`-qn`/`-qg` resolve to once they have picked an
/// attribute id for POSITION/TEX_COORD/NORMAL/GENERIC; this method takes
/// the id directly rather than a geometry attribute type.
pub fn set_attribute_quantization(&mut self, att_id: i32, quantization_bits: i32) {
self.set_attribute_int(att_id, "quantization_bits", quantization_bits);
}
/// Returns the `quantization_bits` set for one attribute id, or -1 if
/// none was set for it or globally.
pub fn get_attribute_quantization(&self, att_id: i32) -> i32 {
self.get_attribute_int(att_id, "quantization_bits", -1)
}
/// Returns the forced prediction scheme, or -1 for the encoder default.
pub fn get_prediction_scheme(&self) -> i32 {
self.get_global_int("prediction_scheme", -1)
}
/// Returns the prediction scheme forced for one attribute, falling back to
/// the global setting and then to -1 for the encoder default.
///
/// Upstream reads this per attribute (`GetPredictionMethodFromOptions`), and
/// `get_attribute_int` itself falls back to the global option, so a value
/// set either way is honoured. Crate-internal: callers outside can already
/// express both halves with `set_attribute_int` and `set_prediction_scheme`,
/// and a getter with no matching per-attribute setter would be a half API.
pub(crate) fn get_attribute_prediction_scheme(&self, att_id: i32) -> i32 {
self.get_attribute_int(att_id, "prediction_scheme", -1)
}
/// Forces a prediction scheme by numeric Draco method id.
pub fn set_prediction_scheme(&mut self, value: i32) {
self.set_global_int("prediction_scheme", value);
}
/// Returns the forced encoding method, if one was set.
pub fn get_encoding_method(&self) -> Option<i32> {
self.inner.global_options.get("encoding_method").cloned()
}
/// Forces an encoding method by numeric Draco method id.
pub fn set_encoding_method(&mut self, value: i32) {
self.set_global_int("encoding_method", value);
}
/// Sets the target Draco bitstream version.
pub fn set_version(&mut self, major: u8, minor: u8) {
self.set_global_int("version_major", major as i32);
self.set_global_int("version_minor", minor as i32);
}
/// Returns the target Draco bitstream version, or `(0, 0)` for default.
pub fn get_version(&self) -> (u8, u8) {
let major = self.get_global_int("version_major", -1);
let minor = self.get_global_int("version_minor", -1);
if major == -1 || minor == -1 {
// Default version depends on the encoder type and method,
// but we'll return (0, 0) to indicate "use default".
(0, 0)
} else {
(major as u8, minor as u8)
}
}
/// Sets a global integer option.
pub fn set_global_int(&mut self, key: &str, value: i32) {
Arc::make_mut(&mut self.inner)
.global_options
.insert(key.to_string(), value);
}
/// Returns a global integer option or the supplied default.
pub fn get_global_int(&self, key: &str, default_val: i32) -> i32 {
*self.inner.global_options.get(key).unwrap_or(&default_val)
}
/// Sets an integer option for one attribute id.
pub fn set_attribute_int(&mut self, att_id: i32, key: &str, value: i32) {
Arc::make_mut(&mut self.inner)
.attribute_options
.entry(att_id)
.or_default()
.insert(key.to_string(), value);
}
/// Returns an attribute integer option, falling back to the global value.
pub fn get_attribute_int(&self, att_id: i32, key: &str, default_val: i32) -> i32 {
if let Some(opts) = self.inner.attribute_options.get(&att_id) {
if let Some(val) = opts.get(key) {
return *val;
}
}
// Falls back to the global value, as upstream does.
self.get_global_int(key, default_val)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// `draco_encoder`'s own default is `-cl 7`; this crate's is speed 5.
/// Verified against the CLI: `EncoderOptions::new()` and
/// `draco_encoder -cl 5 -qp 0` produce byte-identical output.
#[test]
fn compression_level_defaults_to_5_not_the_cli_default_of_7() {
let options = EncoderOptions::new();
assert_eq!(options.get_compression_level(), 5);
}
/// `speed = 10 - compression_level`, matching `draco_encoder.cc`'s own
/// conversion before it calls `SetSpeedOptions`.
#[test]
fn set_compression_level_matches_the_cli_conversion() {
let mut options = EncoderOptions::new();
options.set_compression_level(7);
assert_eq!(options.get_speed(), 3);
assert_eq!(options.get_encoding_speed(), 3);
assert_eq!(options.get_decoding_speed(), 3);
assert_eq!(options.get_compression_level(), 7);
}
#[test]
fn set_compression_level_round_trips_the_full_cli_range() {
for level in 0..=10 {
let mut options = EncoderOptions::new();
options.set_compression_level(level);
assert_eq!(options.get_compression_level(), level);
}
}
/// A clone shares the maps, and a setter on either side afterwards
/// changes only the side it was called on.
#[test]
fn a_clone_shares_the_maps_until_one_side_writes() {
let mut options = EncoderOptions::new();
options.set_global_int("quantization_bits", 14);
options.set_attribute_int(1, "quantization_bits", 10);
let mut cloned = options.clone();
assert!(Arc::ptr_eq(&options.inner, &cloned.inner));
cloned.set_attribute_int(1, "quantization_bits", 8);
options.set_global_int("quantization_bits", 12);
assert!(!Arc::ptr_eq(&options.inner, &cloned.inner));
assert_eq!(options.get_attribute_int(1, "quantization_bits", 0), 10);
assert_eq!(options.get_global_int("quantization_bits", 0), 12);
assert_eq!(cloned.get_attribute_int(1, "quantization_bits", 0), 8);
assert_eq!(cloned.get_global_int("quantization_bits", 0), 14);
}
#[test]
fn attribute_quantization_defaults_to_unset() {
let options = EncoderOptions::new();
assert_eq!(options.get_attribute_quantization(0), -1);
}
#[test]
fn set_attribute_quantization_round_trips() {
let mut options = EncoderOptions::new();
options.set_attribute_quantization(0, 14);
options.set_attribute_quantization(1, 10);
assert_eq!(options.get_attribute_quantization(0), 14);
assert_eq!(options.get_attribute_quantization(1), 10);
// Unset attributes fall back to the global value, same as
// `get_attribute_int` -- there is none here, so -1.
assert_eq!(options.get_attribute_quantization(2), -1);
}
}