BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / equipment.rs

equipment.rs

equipment.rs 30.9 KiB Download Raw
1//! Rigid equipment, portable embedded assets and a bounded binary container.
2//! Local rotation uses Rz * Ry * Rx (degrees); attachment uses the animated
3//! joint world transform, never a skin matrix or inverse bind a second time.
4
5use std::collections::BTreeSet;
6use std::io::{Cursor, Read, Write};
7use std::path::Path;
8
9use crate::preview::{
10 animated_joint_world, identity, joint_paths, mul, Mat4, PreviewCharacter, PreviewControls,
11};
12use crate::project::portable_relative;
13use serde::{Deserialize, Serialize};
14
15pub const MAGIC: &[u8; 8] = b"PPEQ\r\n\x1a\n";
16pub const VERSION: u32 = 1;
17pub const MAX_FILE: usize = 32 * 1024 * 1024;
18const MAX_META: usize = 4 * 1024 * 1024;
19const MAX_ASSET: usize = 8 * 1024 * 1024;
20const MAX_ASSETS: usize = 32;
21const MAX_VERTICES: usize = 80_000;
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct EmbeddedAsset {
26 pub path: String,
27 #[serde(skip)]
28 pub bytes: Vec<u8>,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct Mesh {
34 pub positions: Vec<[f32; 3]>,
35 pub normals: Vec<[f32; 3]>,
36 pub uvs: Vec<[f32; 2]>,
37 pub indices: Vec<u32>,
38 pub color: [f32; 4],
39 pub albedo: Option<usize>,
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct RigSignature {
45 pub paths: Vec<String>,
46 pub rest_world: Vec<Mat4>,
47}
48impl RigSignature {
49 pub fn of(body: &PreviewCharacter) -> Self {
50 Self {
51 paths: joint_paths(body),
52 rest_world: (0..body.joint_nodes.len())
53 .map(|j| animated_joint_world(body, "", 0.0, j).unwrap_or_else(identity))
54 .collect(),
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct Attachment {
62 pub label: String,
63 /// Original OBJ/MTL/dependency payloads owned by this attachment.
64 pub source_assets: Vec<usize>,
65 pub bones: [Option<String>; 2],
66 pub translation: [f32; 3],
67 pub rotation_deg: [f32; 3],
68 pub meshes: Vec<Mesh>,
69 pub shiny: Option<usize>,
70 pub specular: f32,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[serde(deny_unknown_fields)]
75pub struct Preset {
76 pub name: String,
77 pub provenance: String,
78 pub applies: [bool; 2],
79 pub rigs: [Option<RigSignature>; 2],
80 pub attachments: Vec<Attachment>,
81 pub assets: Vec<EmbeddedAsset>,
82}
83impl Preset {
84 pub fn new(name: String) -> Self {
85 Self {
86 name,
87 provenance: String::new(),
88 applies: [true, true],
89 rigs: [None, None],
90 attachments: Vec::new(),
91 assets: Vec::new(),
92 }
93 }
94 pub fn bind(
95 &mut self,
96 sex: usize,
97 body: &PreviewCharacter,
98 attachment: usize,
99 joint: usize,
100 ) -> Result<(), String> {
101 if sex > 1 {
102 return Err("ongeldig geslacht".into());
103 }
104 let paths = joint_paths(body);
105 let path = paths.get(joint).ok_or("dit bot bestaat niet")?.clone();
106 let item = self
107 .attachments
108 .get_mut(attachment)
109 .ok_or("dit onderdeel bestaat niet")?;
110 item.bones[sex] = Some(path);
111 self.rigs[sex] = Some(RigSignature::of(body));
112 Ok(())
113 }
114 pub fn validate(&self) -> Result<(), String> {
115 if !valid_name(&self.name) {
116 return Err(
117 "equipmentnaam: kleine letters/cijfers/-/_, geen spaties, en niet 'body'".into(),
118 );
119 }
120 if self.provenance.len() > 4096 || self.provenance.contains('\0') {
121 return Err("herkomst/licentie is te lang of bevat NUL".into());
122 }
123 if !self.applies.iter().any(|x| *x) {
124 return Err("kies minstens één geslacht".into());
125 }
126 if self.attachments.len() > 32 || self.assets.len() > MAX_ASSETS {
127 return Err("te veel onderdelen of bestanden".into());
128 }
129 let mut paths = BTreeSet::new();
130 let mut total = 0usize;
131 for asset in &self.assets {
132 portable_relative(&asset.path)?;
133 if asset.path.len() > 240 || asset.bytes.len() > MAX_ASSET || !paths.insert(&asset.path)
134 {
135 return Err("ongeldige, dubbele of te grote asset".into());
136 }
137 total = total
138 .checked_add(asset.bytes.len())
139 .ok_or("te veel assetdata")?;
140 }
141 if total > MAX_FILE {
142 return Err("te veel assetdata".into());
143 }
144 let mut texture_indices = BTreeSet::new();
145 let mut vertices = 0;
146 for rig in self.rigs.iter().flatten() {
147 if rig.paths.len() > 128
148 || rig.paths.len() != rig.rest_world.len()
149 || rig.paths.iter().any(|p| p.len() > 4096)
150 || rig.rest_world.iter().flatten().any(|x| !x.is_finite())
151 {
152 return Err("ongeldige rigbinding".into());
153 }
154 }
155 for attachment in &self.attachments {
156 if attachment.source_assets.len() > MAX_ASSETS
157 || attachment
158 .source_assets
159 .iter()
160 .any(|i| *i >= self.assets.len())
161 {
162 return Err("ongeldige bronassetverwijzing".into());
163 }
164 for sex in 0..2 {
165 if let Some(path) = &attachment.bones[sex] {
166 if !self.rigs[sex]
167 .as_ref()
168 .map(|rig| rig.paths.contains(path))
169 .unwrap_or(false)
170 {
171 return Err("attachment verwijst naar onbekend bot".into());
172 }
173 }
174 }
175 if attachment.label.len() > 128
176 || attachment.bones.iter().flatten().any(|p| p.len() > 4096)
177 || attachment
178 .translation
179 .iter()
180 .chain(attachment.rotation_deg.iter())
181 .any(|x| !x.is_finite() || x.abs() > 100_000.0)
182 || !attachment.specular.is_finite()
183 || !(0.0..=1.0).contains(&attachment.specular)
184 || attachment.meshes.len() > 64
185 {
186 return Err("ongeldige attachment/transformatie".into());
187 }
188 if let Some(index) = attachment.shiny {
189 texture_indices.insert(index);
190 }
191 for mesh in &attachment.meshes {
192 vertices += mesh.positions.len();
193 if vertices > MAX_VERTICES
194 || mesh.indices.len() > MAX_VERTICES * 6
195 || mesh.indices.len() < 3
196 || mesh.indices.len() % 3 != 0
197 || mesh.positions.len() != mesh.normals.len()
198 || mesh.positions.len() != mesh.uvs.len()
199 || mesh.positions.is_empty()
200 || mesh
201 .indices
202 .iter()
203 .any(|i| *i as usize >= mesh.positions.len())
204 || mesh
205 .positions
206 .iter()
207 .flatten()
208 .chain(mesh.normals.iter().flatten())
209 .chain(mesh.uvs.iter().flatten())
210 .chain(mesh.color.iter())
211 .any(|x| !x.is_finite() || x.abs() > 1_000_000.0)
212 {
213 return Err("ongeldige of te grote equipmentmesh".into());
214 }
215 if let Some(index) = mesh.albedo {
216 texture_indices.insert(index);
217 }
218 }
219 }
220 let mut decoded = 0usize;
221 for index in texture_indices {
222 decoded += self.texture(index)?.rgba.len();
223 if decoded > 64 * 1024 * 1024 {
224 return Err("textures samen groter dan 64 MiB".into());
225 }
226 }
227 Ok(())
228 }
229 pub fn texture(&self, index: usize) -> Result<Texture, String> {
230 let asset = self
231 .assets
232 .get(index)
233 .ok_or("textureverwijzing ontbreekt")?;
234 decode_png(&asset.bytes)
235 }
236 /// Exact container byte cost; does not substitute for semantic validation.
237 pub fn encoded_len(&self) -> Result<usize, String> {
238 let meta = serde_json::to_vec(self).map_err(|e| e.to_string())?.len();
239 if meta > MAX_META {
240 return Err("presetmetadata is te groot".into());
241 }
242 let size = self
243 .assets
244 .iter()
245 .try_fold(20 + meta, |size, a| {
246 size.checked_add(4)?.checked_add(a.bytes.len())
247 })
248 .ok_or("preset te groot")?;
249 if size > MAX_FILE {
250 return Err("preset te groot".into());
251 }
252 Ok(size)
253 }
254 pub fn encode(&self) -> Result<Vec<u8>, String> {
255 self.validate()?;
256 let metadata = serde_json::to_vec(self).map_err(|e| e.to_string())?;
257 if metadata.len() > MAX_META {
258 return Err("presetmetadata is te groot".into());
259 }
260 let mut payload = metadata;
261 let meta_len = payload.len();
262 for asset in &self.assets {
263 payload.extend_from_slice(&(asset.bytes.len() as u32).to_le_bytes());
264 payload.extend_from_slice(&asset.bytes);
265 }
266 if payload.len() + 20 > MAX_FILE {
267 return Err("preset is te groot".into());
268 }
269 let mut out = MAGIC.to_vec();
270 out.extend_from_slice(&VERSION.to_le_bytes());
271 out.extend_from_slice(&(meta_len as u32).to_le_bytes());
272 out.extend_from_slice(&checksum(&payload).to_le_bytes());
273 out.extend(payload);
274 Ok(out)
275 }
276 pub fn decode(bytes: &[u8]) -> Result<Self, String> {
277 if bytes.len() < 20 || bytes.len() > MAX_FILE || &bytes[..8] != MAGIC {
278 return Err("ongeldig equipmentbestand of bestand te groot".into());
279 }
280 let number = |offset| u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
281 if number(8) != VERSION {
282 return Err("onbekende equipmentversie".into());
283 }
284 let meta_len = number(12) as usize;
285 if meta_len > MAX_META
286 || meta_len > bytes.len() - 20
287 || checksum(&bytes[20..]) != number(16)
288 {
289 return Err("beschadigde equipmentdata".into());
290 }
291 let mut preset: Self =
292 serde_json::from_slice(&bytes[20..20 + meta_len]).map_err(|e| e.to_string())?;
293 if preset.assets.len() > MAX_ASSETS {
294 return Err("te veel assets".into());
295 }
296 let mut cursor = 20 + meta_len;
297 for asset in &mut preset.assets {
298 if cursor + 4 > bytes.len() {
299 return Err("afgebroken asset".into());
300 }
301 let len = number(cursor) as usize;
302 cursor += 4;
303 if len > MAX_ASSET || len > bytes.len() - cursor {
304 return Err("ongeldige assetlengte".into());
305 }
306 asset.bytes = bytes[cursor..cursor + len].to_vec();
307 cursor += len;
308 }
309 if cursor != bytes.len() {
310 return Err("onverwachte extra equipmentdata".into());
311 }
312 preset.validate()?;
313 Ok(preset)
314 }
315 pub fn read(path: &Path) -> Result<Self, String> {
316 if !path.is_file() {
317 return Err("kies een regulier presetbestand".into());
318 }
319 let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
320 let mut bytes = Vec::new();
321 file.take((MAX_FILE + 1) as u64)
322 .read_to_end(&mut bytes)
323 .map_err(|e| e.to_string())?;
324 Self::decode(&bytes)
325 }
326}
327
328pub fn valid_name(name: &str) -> bool {
329 !name.is_empty()
330 && name.len() <= 64
331 && name != "body"
332 && name
333 .bytes()
334 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-' || c == b'_')
335}
336
337pub fn checksum(bytes: &[u8]) -> u32 {
338 let mut crc = !0u32;
339 for byte in bytes {
340 crc ^= u32::from(*byte);
341 for _ in 0..8 {
342 crc = (crc >> 1) ^ (0xedb88320u32 & (0u32.wrapping_sub(crc & 1)));
343 }
344 }
345 !crc
346}
347
348/// Immutable snapshot publish: a hard link atomically creates the destination
349/// without overwriting an older saved preset. Failure leaves old data intact.
350pub fn write_snapshot(path: &Path, bytes: &[u8]) -> Result<(), String> {
351 if bytes.len() > MAX_FILE {
352 return Err("snapshot te groot".into());
353 }
354 if path.exists() {
355 if !path.is_file() {
356 return Err("snapshot is geen regulier bestand".into());
357 }
358 let mut current = Vec::new();
359 std::fs::File::open(path)
360 .map_err(|e| e.to_string())?
361 .take((MAX_FILE + 1) as u64)
362 .read_to_end(&mut current)
363 .map_err(|e| e.to_string())?;
364 return if current == bytes {
365 Ok(())
366 } else {
367 Err("snapshotnaam bestaat met andere inhoud".into())
368 };
369 }
370 let stamp = std::time::SystemTime::now()
371 .duration_since(std::time::UNIX_EPOCH)
372 .unwrap_or_default()
373 .as_nanos();
374 let temp = path.with_extension(format!("ppe.part-{}-{stamp}", std::process::id()));
375 let result = (|| {
376 let mut file = std::fs::OpenOptions::new()
377 .write(true)
378 .create_new(true)
379 .open(&temp)
380 .map_err(|e| e.to_string())?;
381 file.write_all(bytes)
382 .and_then(|_| file.sync_all())
383 .map_err(|e| e.to_string())?;
384 std::fs::hard_link(&temp, path).map_err(|e| e.to_string())?;
385 #[cfg(unix)]
386 std::fs::File::open(path.parent().ok_or("snapshotmap ontbreekt")?)
387 .and_then(|dir| dir.sync_all())
388 .map_err(|e| e.to_string())?;
389 Ok(())
390 })();
391 let _ = std::fs::remove_file(temp);
392 result
393}
394
395#[derive(Debug, Clone, PartialEq)]
396pub struct Texture {
397 pub content_hash: u64,
398 pub width: u32,
399 pub height: u32,
400 pub rgba: Vec<u8>,
401}
402fn decode_png(bytes: &[u8]) -> Result<Texture, String> {
403 let mut decoder = png::Decoder::new(Cursor::new(bytes));
404 decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
405 let mut reader = decoder.read_info().map_err(|e| e.to_string())?;
406 let info = reader.info();
407 if info.width == 0 || info.height == 0 || info.width > 2048 || info.height > 2048 {
408 return Err("texture is groter dan 2048×2048".into());
409 }
410 let size = reader
411 .output_buffer_size()
412 .ok_or("ongeldige texturegrootte")?;
413 if size > 2048 * 2048 * 8 {
414 return Err("texture is te groot".into());
415 }
416 let mut data = vec![0; size];
417 let info = reader.next_frame(&mut data).map_err(|e| e.to_string())?;
418 let channels = info.color_type.samples();
419 let mut rgba = Vec::with_capacity(info.width as usize * info.height as usize * 4);
420 for pixel in data[..info.buffer_size()].chunks_exact(channels) {
421 match info.color_type {
422 png::ColorType::Grayscale => {
423 rgba.extend_from_slice(&[pixel[0], pixel[0], pixel[0], 255])
424 }
425 png::ColorType::GrayscaleAlpha => {
426 rgba.extend_from_slice(&[pixel[0], pixel[0], pixel[0], pixel[1]])
427 }
428 png::ColorType::Rgb => rgba.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]),
429 png::ColorType::Rgba => rgba.extend_from_slice(pixel),
430 _ => return Err("onbekende texturecodering".into()),
431 }
432 }
433 use std::hash::{Hash, Hasher};
434 let mut hash = std::collections::hash_map::DefaultHasher::new();
435 rgba.hash(&mut hash);
436 Ok(Texture {
437 content_hash: hash.finish(),
438 width: info.width,
439 height: info.height,
440 rgba,
441 })
442}
443
444pub fn local_matrix(translation: [f32; 3], degrees: [f32; 3]) -> Mat4 {
445 let [x, y, z] = degrees.map(f32::to_radians);
446 let (sx, cx) = x.sin_cos();
447 let (sy, cy) = y.sin_cos();
448 let (sz, cz) = z.sin_cos();
449 let rx = [
450 1., 0., 0., 0., 0., cx, sx, 0., 0., -sx, cx, 0., 0., 0., 0., 1.,
451 ];
452 let ry = [
453 cy, 0., -sy, 0., 0., 1., 0., 0., sy, 0., cy, 0., 0., 0., 0., 1.,
454 ];
455 let rz = [
456 cz, sz, 0., 0., -sz, cz, 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
457 ];
458 let mut matrix = mul(rz, mul(ry, rx));
459 matrix[12] = translation[0];
460 matrix[13] = translation[1];
461 matrix[14] = translation[2];
462 matrix
463}
464
465#[derive(Debug, Clone)]
466pub struct Layer {
467 pub mesh: Mesh,
468 pub model: Mat4,
469 pub albedo: Option<std::sync::Arc<Texture>>,
470 pub shiny: Option<std::sync::Arc<Texture>>,
471 pub specular: f32,
472}
473
474#[derive(Default)]
475pub struct TextureCache {
476 entries: std::collections::VecDeque<(Vec<u8>, std::sync::Arc<Texture>)>,
477 bytes: usize,
478}
479impl TextureCache {
480 fn get(&mut self, preset: &Preset, index: usize) -> Result<std::sync::Arc<Texture>, String> {
481 let bytes = &preset.assets.get(index).ok_or("texture ontbreekt")?.bytes;
482 if let Some((_, texture)) = self.entries.iter().find(|(source, _)| source == bytes) {
483 return Ok(texture.clone());
484 }
485 let texture = std::sync::Arc::new(decode_png(bytes)?);
486 let cost = bytes.len() + texture.rgba.len();
487 while self.bytes + cost > 128 * 1024 * 1024 {
488 if let Some((source, image)) = self.entries.pop_front() {
489 self.bytes -= source.len() + image.rgba.len();
490 } else {
491 break;
492 }
493 }
494 self.bytes += cost;
495 self.entries.push_back((bytes.clone(), texture.clone()));
496 Ok(texture)
497 }
498}
499
500pub fn layers(
501 preset: &Preset,
502 sex: usize,
503 body: &PreviewCharacter,
504 controls: &PreviewControls,
505) -> Result<Vec<Layer>, String> {
506 layers_cached(preset, sex, body, controls, &mut TextureCache::default())
507}
508
509pub fn layers_cached(
510 preset: &Preset,
511 sex: usize,
512 body: &PreviewCharacter,
513 controls: &PreviewControls,
514 cache: &mut TextureCache,
515) -> Result<Vec<Layer>, String> {
516 if sex > 1 {
517 return Err("ongeldig geslacht".into());
518 }
519 if !preset.applies[sex] || preset.attachments.is_empty() {
520 return Ok(Vec::new());
521 }
522 let signature = RigSignature::of(body);
523 if preset.rigs[sex].as_ref() != Some(&signature) {
524 return Err("rig wijkt af: kies de botten opnieuw voor dit lichaam".into());
525 }
526 let paths = joint_paths(body);
527 let mut layers = Vec::new();
528 for item in &preset.attachments {
529 let path = item.bones[sex]
530 .as_deref()
531 .ok_or("kies een bot voor dit onderdeel en geslacht")?;
532 let joint = paths
533 .iter()
534 .position(|p| p == path)
535 .ok_or("het gekoppelde bot ontbreekt")?;
536 let world = animated_joint_world(body, &controls.clip_name, controls.time, joint)
537 .ok_or("het gekoppelde bot ontbreekt")?;
538 let model = mul(world, local_matrix(item.translation, item.rotation_deg));
539 let shiny = item.shiny.map(|i| cache.get(preset, i)).transpose()?;
540 for mesh in &item.meshes {
541 layers.push(Layer {
542 mesh: mesh.clone(),
543 model,
544 albedo: mesh.albedo.map(|i| cache.get(preset, i)).transpose()?,
545 shiny: shiny.clone(),
546 specular: item.specular,
547 });
548 }
549 }
550 Ok(layers)
551}
552
553/// Import an OBJ and its declared MTL/PNG dependencies into the preset itself.
554/// No extraction directories, absolute references or parent traversal allowed.
555pub fn import_obj(preset: &mut Preset, source: &Path) -> Result<usize, String> {
556 if source
557 .extension()
558 .and_then(|x| x.to_str())
559 .map(|x| !x.eq_ignore_ascii_case("obj"))
560 .unwrap_or(true)
561 {
562 return Err("kies een rigide OBJ-model met MTL/PNG-bijbestanden".into());
563 }
564 let root = source
565 .parent()
566 .ok_or("modelmap ontbreekt")?
567 .canonicalize()
568 .map_err(|e| e.to_string())?;
569 let name = source
570 .file_name()
571 .and_then(|x| x.to_str())
572 .ok_or("ongeldige modelnaam")?;
573 let mut candidate = preset.clone();
574 if candidate.attachments.len() >= 32 {
575 return Err("te veel onderdelen".into());
576 }
577 compact_assets(&mut candidate)?;
578 let prefix = unique_prefix(&candidate, "part");
579 let obj_bytes = read_asset(&root, name)?;
580 let obj_text = std::str::from_utf8(&obj_bytes).map_err(|e| e.to_string())?;
581 let mut dependencies = BTreeSet::new();
582 dependencies.insert(name.to_string());
583 for line in obj_text.lines() {
584 if let Some(rest) = line.trim().strip_prefix("mtllib ") {
585 let mtl = rest.trim();
586 dependencies.insert(mtl.to_string());
587 let bytes = read_asset(&root, mtl)?;
588 let text = std::str::from_utf8(&bytes).map_err(|e| e.to_string())?;
589 for line in text.lines() {
590 let line = line.trim();
591 if line.starts_with("map_")
592 || line.starts_with("bump ")
593 || line.starts_with("disp ")
594 {
595 let (_, path) = line
596 .split_once(char::is_whitespace)
597 .ok_or("ongeldige textureverwijzing")?;
598 let relative = Path::new(mtl)
599 .parent()
600 .unwrap_or(Path::new(""))
601 .join(path.trim());
602 let relative = relative
603 .to_str()
604 .ok_or("ongeldig texturepad")?
605 .replace('\\', "/");
606 portable_relative(&relative)?;
607 dependencies.insert(relative);
608 }
609 }
610 }
611 }
612 if dependencies.len() + candidate.assets.len() > MAX_ASSETS {
613 return Err("te veel bijbestanden".into());
614 }
615 let mut asset_map = std::collections::BTreeMap::new();
616 let mut total_bytes: usize = candidate.assets.iter().map(|a| a.bytes.len()).sum();
617 for relative in dependencies {
618 let bytes = read_asset(&root, &relative)?;
619 total_bytes += bytes.len();
620 if total_bytes > MAX_FILE {
621 return Err("assets samen te groot".into());
622 }
623 let index = candidate.assets.len();
624 candidate.assets.push(EmbeddedAsset {
625 path: format!("{prefix}/{relative}"),
626 bytes,
627 });
628 asset_map.insert(relative, index);
629 }
630 // A checked callback prevents the parser from reading undeclared/unsafe files.
631 let mut input = std::io::BufReader::new(Cursor::new(obj_bytes));
632 let options = tobj::LoadOptions {
633 triangulate: true,
634 single_index: true,
635 ..Default::default()
636 };
637 let texture_paths = std::cell::RefCell::new(std::collections::BTreeMap::new());
638 let (models, materials) = tobj::load_obj_buf(&mut input, &options, |path| {
639 let relative = path.to_string_lossy().replace('\\', "/");
640 let index = asset_map
641 .get(&relative)
642 .ok_or(tobj::LoadError::OpenFileFailed)?;
643 let mut reader = std::io::BufReader::new(Cursor::new(&candidate.assets[*index].bytes));
644 let (materials, map) = tobj::load_mtl_buf(&mut reader)?;
645 for material in &materials {
646 if let Some(texture) = &material.diffuse_texture {
647 let relative = path
648 .parent()
649 .unwrap_or(Path::new(""))
650 .join(texture)
651 .to_string_lossy()
652 .replace('\\', "/");
653 texture_paths
654 .borrow_mut()
655 .insert(material.name.clone(), relative);
656 }
657 }
658 Ok((materials, map))
659 })
660 .map_err(|e| e.to_string())?;
661 let materials = materials.map_err(|e| e.to_string())?;
662 let texture_paths = texture_paths.into_inner();
663 let mut meshes = Vec::new();
664 let mut vertices: usize = candidate
665 .attachments
666 .iter()
667 .flat_map(|a| &a.meshes)
668 .map(|m| m.positions.len())
669 .sum();
670 for model in models {
671 let source = model.mesh;
672 vertices += source.positions.len() / 3;
673 if vertices > MAX_VERTICES || source.indices.len() > MAX_VERTICES * 6 {
674 return Err("equipmentmesh is te groot".into());
675 }
676 let positions: Vec<[f32; 3]> = source
677 .positions
678 .chunks_exact(3)
679 .map(|x| [x[0], x[1], x[2]])
680 .collect();
681 let mut normals: Vec<[f32; 3]> = source
682 .normals
683 .chunks_exact(3)
684 .map(|x| [x[0], x[1], x[2]])
685 .collect();
686 if normals.len() != positions.len() {
687 normals = compute_normals(&positions, &source.indices)?;
688 }
689 let mut uvs: Vec<[f32; 2]> = source
690 .texcoords
691 .chunks_exact(2)
692 .map(|x| [x[0], x[1]])
693 .collect();
694 if uvs.len() != positions.len() {
695 uvs = vec![[0.; 2]; positions.len()];
696 }
697 let material = source.material_id.and_then(|id| materials.get(id));
698 let color = material.and_then(|m| m.diffuse).unwrap_or([0.7, 0.7, 0.7]);
699 let albedo = material
700 .and_then(|m| texture_paths.get(&m.name))
701 .map(|path| {
702 asset_map
703 .get(path)
704 .copied()
705 .ok_or("ontbrekende diffuse texture")
706 })
707 .transpose()?;
708 meshes.push(Mesh {
709 positions,
710 normals,
711 uvs,
712 indices: source.indices,
713 color: [
714 color[0],
715 color[1],
716 color[2],
717 material.and_then(|m| m.dissolve).unwrap_or(1.),
718 ],
719 albedo,
720 });
721 }
722 if meshes.is_empty() {
723 return Err("het OBJ-bestand heeft geen meshes".into());
724 }
725 candidate.attachments.push(Attachment {
726 label: name.to_string(),
727 source_assets: asset_map.values().copied().collect(),
728 bones: [None, None],
729 translation: [0.; 3],
730 rotation_deg: [0.; 3],
731 meshes,
732 shiny: None,
733 specular: 0.25,
734 });
735 candidate.validate()?;
736 *preset = candidate;
737 Ok(preset.attachments.len() - 1)
738}
739
740pub fn import_shiny(preset: &mut Preset, attachment: usize, source: &Path) -> Result<(), String> {
741 let root = source.parent().ok_or("texturemap ontbreekt")?;
742 let name = source
743 .file_name()
744 .and_then(|x| x.to_str())
745 .ok_or("ongeldige bestandsnaam")?;
746 let bytes = read_asset(root, name)?;
747 decode_png(&bytes)?;
748 let mut candidate = preset.clone();
749 let index = candidate.assets.len();
750 candidate.assets.push(EmbeddedAsset {
751 path: format!("{}/{name}", unique_prefix(&candidate, "shiny")),
752 bytes,
753 });
754 candidate
755 .attachments
756 .get_mut(attachment)
757 .ok_or("onderdeel ontbreekt")?
758 .shiny = Some(index);
759 compact_assets(&mut candidate)?;
760 candidate.validate()?;
761 *preset = candidate;
762 Ok(())
763}
764
765fn unique_prefix(preset: &Preset, kind: &str) -> String {
766 (0..)
767 .map(|i| format!("{kind}-{i}"))
768 .find(|prefix| {
769 !preset
770 .assets
771 .iter()
772 .any(|a| a.path.split('/').next() == Some(prefix.as_str()))
773 })
774 .expect("finite asset list")
775}
776fn compact_assets(preset: &mut Preset) -> Result<(), String> {
777 let used: BTreeSet<usize> = preset
778 .attachments
779 .iter()
780 .flat_map(|a| {
781 a.source_assets
782 .iter()
783 .copied()
784 .chain(a.shiny)
785 .chain(a.meshes.iter().filter_map(|m| m.albedo))
786 })
787 .collect();
788 if used.iter().any(|i| *i >= preset.assets.len()) {
789 return Err("ongeldige assetverwijzing".into());
790 }
791 let mut remap = vec![0; preset.assets.len()];
792 let mut assets = Vec::new();
793 for (i, asset) in preset.assets.iter().enumerate() {
794 if used.contains(&i) {
795 remap[i] = assets.len();
796 assets.push(asset.clone());
797 }
798 }
799 for a in &mut preset.attachments {
800 for i in &mut a.source_assets {
801 *i = remap[*i];
802 }
803 if let Some(i) = a.shiny.as_mut() {
804 *i = remap[*i];
805 }
806 for mesh in &mut a.meshes {
807 if let Some(i) = mesh.albedo.as_mut() {
808 *i = remap[*i];
809 }
810 }
811 }
812 preset.assets = assets;
813 Ok(())
814}
815/// Clear an override and release its no-longer-used embedded texture.
816pub fn clear_shiny(preset: &mut Preset, index: usize) -> Result<(), String> {
817 let mut candidate = preset.clone();
818 candidate
819 .attachments
820 .get_mut(index)
821 .ok_or("onderdeel ontbreekt")?
822 .shiny = None;
823 compact_assets(&mut candidate)?;
824 *preset = candidate;
825 Ok(())
826}
827/// Remove a part and its no-longer-used embedded assets, remapping all indices.
828pub fn remove_attachment(preset: &mut Preset, index: usize) -> Result<(), String> {
829 if index >= preset.attachments.len() {
830 return Err("onderdeel ontbreekt".into());
831 }
832 let mut candidate = preset.clone();
833 candidate.attachments.remove(index);
834 compact_assets(&mut candidate)?;
835 *preset = candidate;
836 Ok(())
837}
838
839fn read_asset(root: &Path, relative: &str) -> Result<Vec<u8>, String> {
840 portable_relative(relative)?;
841 let root = root.canonicalize().map_err(|e| e.to_string())?;
842 let path = root
843 .join(relative)
844 .canonicalize()
845 .map_err(|e| e.to_string())?;
846 if !path.starts_with(&root) {
847 return Err("asset verlaat de modelmap".into());
848 }
849 if !path.is_file() {
850 return Err("asset is geen regulier bestand".into());
851 }
852 let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
853 let mut bytes = Vec::new();
854 file.take((MAX_ASSET + 1) as u64)
855 .read_to_end(&mut bytes)
856 .map_err(|e| e.to_string())?;
857 if bytes.len() > MAX_ASSET {
858 return Err("asset is te groot".into());
859 }
860 Ok(bytes)
861}
862fn compute_normals(positions: &[[f32; 3]], indices: &[u32]) -> Result<Vec<[f32; 3]>, String> {
863 let mut normals = vec![[0.; 3]; positions.len()];
864 for t in indices.chunks_exact(3) {
865 let [a, b, c] = [t[0] as usize, t[1] as usize, t[2] as usize];
866 if [a, b, c].iter().any(|i| *i >= positions.len()) {
867 return Err("ongeldige OBJ-index".into());
868 }
869 let ab = std::array::from_fn::<_, 3, _>(|i| positions[b][i] - positions[a][i]);
870 let ac = std::array::from_fn::<_, 3, _>(|i| positions[c][i] - positions[a][i]);
871 let n = [
872 ab[1] * ac[2] - ab[2] * ac[1],
873 ab[2] * ac[0] - ab[0] * ac[2],
874 ab[0] * ac[1] - ab[1] * ac[0],
875 ];
876 for vertex in [a, b, c] {
877 for (i, value) in n.iter().enumerate() {
878 normals[vertex][i] += value;
879 }
880 }
881 }
882 for normal in &mut normals {
883 let length = normal.iter().map(|v| v * v).sum::<f32>().sqrt();
884 if length > 1e-10 {
885 for v in normal {
886 *v /= length
887 }
888 }
889 }
890 Ok(normals)
891}