BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / equipment.rs

equipment.rs

equipment.rs 53.6 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";
16/// Container version written; versions 1–3 (shared rigid offsets) still load.
17pub const VERSION: u32 = 4;
18pub const MAX_FILE: usize = 32 * 1024 * 1024;
19const MAX_META: usize = 4 * 1024 * 1024;
20const MAX_ASSET: usize = 8 * 1024 * 1024;
21const MAX_ASSETS: usize = 32;
22pub(crate) const MAX_VERTICES: usize = 80_000;
23/// Vertex budget when the rigid readers load a static (unskinned) *body*;
24/// same ceiling as the FBX body loader.
25pub(crate) const MAX_STATIC_BODY_VERTICES: usize = 1_000_000;
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct EmbeddedAsset {
30 pub path: String,
31 #[serde(skip)]
32 pub bytes: Vec<u8>,
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct Mesh {
38 pub positions: Vec<[f32; 3]>,
39 pub normals: Vec<[f32; 3]>,
40 pub uvs: Vec<[f32; 2]>,
41 pub indices: Vec<u32>,
42 pub color: [f32; 4],
43 pub albedo: Option<usize>,
44 /// Skinned part: the equipment bone whose rest space holds the vertices.
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub bone: Option<usize>,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct RigSignature {
52 pub paths: Vec<String>,
53 pub rest_world: Vec<Mat4>,
54}
55impl RigSignature {
56 pub fn of(body: &PreviewCharacter) -> Self {
57 Self {
58 paths: joint_paths(body),
59 rest_world: (0..body.joint_nodes.len())
60 .map(|j| animated_joint_world(body, "", 0.0, j).unwrap_or_else(identity))
61 .collect(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct Attachment {
69 pub label: String,
70 /// Original OBJ/MTL/dependency payloads owned by this attachment.
71 pub source_assets: Vec<usize>,
72 pub bones: [Option<String>; 2],
73 /// Per sex: rigid (single-bone) attach offset.
74 pub translation: [[f32; 3]; 2],
75 /// Per sex: rigid (single-bone) Euler degrees, Rz·Ry·Rx.
76 pub rotation_deg: [[f32; 3]; 2],
77 pub meshes: Vec<Mesh>,
78 pub shiny: Option<usize>,
79 pub specular: f32,
80 /// Skeleton of a skinned part (Tim t1027u); `None` for rigid parts.
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub skeleton: Option<crate::equipment_rig::Skeleton>,
83 /// Per sex: equipment bone → character bone links of a skinned part.
84 #[serde(default, skip_serializing_if = "crate::equipment_rig::links_empty")]
85 pub bone_links: [Vec<crate::equipment_rig::BoneLink>; 2],
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90pub struct Preset {
91 pub name: String,
92 pub provenance: String,
93 pub applies: [bool; 2],
94 pub rigs: [Option<RigSignature>; 2],
95 pub attachments: Vec<Attachment>,
96 pub assets: Vec<EmbeddedAsset>,
97 /// Semi-transparency allowed for this layer (t1031u; default off = binary alpha).
98 #[serde(default)]
99 pub semi_transparency: bool,
100}
101impl Preset {
102 pub fn new(name: String) -> Self {
103 Self {
104 name,
105 provenance: String::new(),
106 applies: [true, true],
107 rigs: [None, None],
108 attachments: Vec::new(),
109 assets: Vec::new(),
110 semi_transparency: false,
111 }
112 }
113 pub fn bind(
114 &mut self,
115 sex: usize,
116 body: &PreviewCharacter,
117 attachment: usize,
118 joint: usize,
119 ) -> Result<(), String> {
120 if sex > 1 {
121 return Err("ongeldig geslacht".into());
122 }
123 let paths = joint_paths(body);
124 let path = paths.get(joint).ok_or("dit bot bestaat niet")?.clone();
125 let item = self
126 .attachments
127 .get_mut(attachment)
128 .ok_or("dit onderdeel bestaat niet")?;
129 item.bones[sex] = Some(path);
130 self.rigs[sex] = Some(RigSignature::of(body));
131 let paths = &self.rigs[sex].as_ref().unwrap().paths;
132 for a in &mut self.attachments {
133 a.bone_links[sex].retain(|l| paths.contains(&l.target));
134 }
135 Ok(())
136 }
137
138 /// Whether this part already has a bone mapping for `sex`.
139 pub fn attachment_mapped(attachment: &Attachment, sex: usize) -> bool {
140 if sex > 1 {
141 return false;
142 }
143 if attachment.skeleton.is_some() {
144 !attachment.bone_links[sex].is_empty()
145 } else {
146 attachment.bones[sex].is_some()
147 }
148 }
149
150 /// Copy bone mappings and offsets from `from_sex` onto `to_sex` for parts
151 /// that have no mapping on `to_sex` yet. Destination joints are taken from
152 /// `to_body` when the source joint name also exists there.
153 pub fn copy_gender_mapping(
154 &mut self,
155 from_sex: usize,
156 to_sex: usize,
157 to_body: &PreviewCharacter,
158 ) -> Result<CopyGenderMapping, String> {
159 copy_gender_mapping(self, from_sex, to_sex, to_body)
160 }
161
162 pub fn validate(&self) -> Result<(), String> {
163 self.validate_with(MAX_VERTICES)
164 }
165 fn validate_with(&self, max_vertices: usize) -> Result<(), String> {
166 if !valid_name(&self.name) {
167 return Err(
168 "equipmentnaam: kleine letters/cijfers/-/_, geen spaties, en niet 'body'".into(),
169 );
170 }
171 if self.provenance.len() > 4096 || self.provenance.contains('\0') {
172 return Err("herkomst/licentie is te lang of bevat NUL".into());
173 }
174 if !self.applies.iter().any(|x| *x) {
175 return Err("kies minstens één geslacht".into());
176 }
177 if self.attachments.len() > 32 || self.assets.len() > MAX_ASSETS {
178 return Err("te veel onderdelen of bestanden".into());
179 }
180 let mut paths = BTreeSet::new();
181 let mut total = 0usize;
182 for asset in &self.assets {
183 portable_relative(&asset.path)?;
184 if asset.path.len() > 240 || asset.bytes.len() > MAX_ASSET || !paths.insert(&asset.path)
185 {
186 return Err("ongeldige, dubbele of te grote asset".into());
187 }
188 total = total
189 .checked_add(asset.bytes.len())
190 .ok_or("te veel assetdata")?;
191 }
192 if total > MAX_FILE {
193 return Err("te veel assetdata".into());
194 }
195 let mut texture_indices = BTreeSet::new();
196 let mut vertices = 0;
197 for rig in self.rigs.iter().flatten() {
198 if rig.paths.len() > 128
199 || rig.paths.len() != rig.rest_world.len()
200 || rig.paths.iter().any(|p| p.len() > 4096)
201 || rig.rest_world.iter().flatten().any(|x| !x.is_finite())
202 {
203 return Err("ongeldige rigbinding".into());
204 }
205 }
206 for attachment in &self.attachments {
207 if attachment.source_assets.len() > MAX_ASSETS
208 || attachment
209 .source_assets
210 .iter()
211 .any(|i| *i >= self.assets.len())
212 {
213 return Err("ongeldige bronassetverwijzing".into());
214 }
215 for sex in 0..2 {
216 if let Some(path) = &attachment.bones[sex] {
217 if !self.rigs[sex]
218 .as_ref()
219 .map(|rig| rig.paths.contains(path))
220 .unwrap_or(false)
221 {
222 return Err("attachment verwijst naar onbekend bot".into());
223 }
224 }
225 }
226 if attachment.label.len() > 128
227 || attachment.bones.iter().flatten().any(|p| p.len() > 4096)
228 || attachment
229 .translation
230 .iter()
231 .flatten()
232 .chain(attachment.rotation_deg.iter().flatten())
233 .any(|x| !x.is_finite() || x.abs() > 100_000.0)
234 || !attachment.specular.is_finite()
235 || !(0.0..=1.0).contains(&attachment.specular)
236 || (attachment.skeleton.is_none() && attachment.meshes.len() > 64)
237 {
238 return Err("ongeldige attachment/transformatie".into());
239 }
240 crate::equipment_rig::validate_part(attachment, &self.rigs)?;
241 if let Some(index) = attachment.shiny {
242 texture_indices.insert(index);
243 }
244 for mesh in &attachment.meshes {
245 vertices += mesh.positions.len();
246 if vertices > max_vertices
247 || mesh.indices.len() > max_vertices * 6
248 || mesh.indices.len() < 3
249 || mesh.indices.len() % 3 != 0
250 || mesh.positions.len() != mesh.normals.len()
251 || mesh.positions.len() != mesh.uvs.len()
252 || mesh.positions.is_empty()
253 || mesh
254 .indices
255 .iter()
256 .any(|i| *i as usize >= mesh.positions.len())
257 || mesh
258 .positions
259 .iter()
260 .flatten()
261 .chain(mesh.normals.iter().flatten())
262 .chain(mesh.uvs.iter().flatten())
263 .chain(mesh.color.iter())
264 .any(|x| !x.is_finite() || x.abs() > 1_000_000.0)
265 {
266 return Err("ongeldige of te grote equipmentmesh".into());
267 }
268 if let Some(index) = mesh.albedo {
269 texture_indices.insert(index);
270 }
271 }
272 }
273 let mut decoded = 0usize;
274 for index in texture_indices {
275 decoded += self.texture(index)?.rgba.len();
276 if decoded > 64 * 1024 * 1024 {
277 return Err("textures samen groter dan 64 MiB".into());
278 }
279 }
280 Ok(())
281 }
282 pub fn texture(&self, index: usize) -> Result<Texture, String> {
283 let asset = self
284 .assets
285 .get(index)
286 .ok_or("textureverwijzing ontbreekt")?;
287 decode_png(&asset.bytes)
288 }
289 /// Exact container byte cost; does not substitute for semantic validation.
290 pub fn encoded_len(&self) -> Result<usize, String> {
291 let meta = serde_json::to_vec(self).map_err(|e| e.to_string())?.len();
292 if meta > MAX_META {
293 return Err("presetmetadata is te groot".into());
294 }
295 let size = self
296 .assets
297 .iter()
298 .try_fold(20 + meta, |size, a| {
299 size.checked_add(4)?.checked_add(a.bytes.len())
300 })
301 .ok_or("preset te groot")?;
302 if size > MAX_FILE {
303 return Err("preset te groot".into());
304 }
305 Ok(size)
306 }
307 pub fn encode(&self) -> Result<Vec<u8>, String> {
308 self.validate()?;
309 let metadata = serde_json::to_vec(self).map_err(|e| e.to_string())?;
310 if metadata.len() > MAX_META {
311 return Err("presetmetadata is te groot".into());
312 }
313 let mut payload = metadata;
314 let meta_len = payload.len();
315 for asset in &self.assets {
316 payload.extend_from_slice(&(asset.bytes.len() as u32).to_le_bytes());
317 payload.extend_from_slice(&asset.bytes);
318 }
319 if payload.len() + 20 > MAX_FILE {
320 return Err("preset is te groot".into());
321 }
322 let mut out = MAGIC.to_vec();
323 out.extend_from_slice(&VERSION.to_le_bytes());
324 out.extend_from_slice(&(meta_len as u32).to_le_bytes());
325 out.extend_from_slice(&checksum(&payload).to_le_bytes());
326 out.extend(payload);
327 Ok(out)
328 }
329 pub fn decode(bytes: &[u8]) -> Result<Self, String> {
330 if bytes.len() < 20 || bytes.len() > MAX_FILE || &bytes[..8] != MAGIC {
331 return Err("ongeldig equipmentbestand of bestand te groot".into());
332 }
333 let number = |offset| u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
334 if !(1..=VERSION).contains(&number(8)) {
335 return Err("onbekende equipmentversie".into());
336 }
337 let meta_len = number(12) as usize;
338 if meta_len > MAX_META
339 || meta_len > bytes.len() - 20
340 || checksum(&bytes[20..]) != number(16)
341 {
342 return Err("beschadigde equipmentdata".into());
343 }
344 let version = number(8);
345 let raw: serde_json::Value =
346 serde_json::from_slice(&bytes[20..20 + meta_len]).map_err(|e| e.to_string())?;
347 let raw = if version < 4 {
348 migrate_preset_json(version, raw)?
349 } else {
350 raw
351 };
352 let mut preset: Self = serde_json::from_value(raw).map_err(|e| e.to_string())?;
353 if preset.assets.len() > MAX_ASSETS {
354 return Err("te veel assets".into());
355 }
356 let mut cursor = 20 + meta_len;
357 for asset in &mut preset.assets {
358 if cursor + 4 > bytes.len() {
359 return Err("afgebroken asset".into());
360 }
361 let len = number(cursor) as usize;
362 cursor += 4;
363 if len > MAX_ASSET || len > bytes.len() - cursor {
364 return Err("ongeldige assetlengte".into());
365 }
366 asset.bytes = bytes[cursor..cursor + len].to_vec();
367 cursor += len;
368 }
369 if cursor != bytes.len() {
370 return Err("onverwachte extra equipmentdata".into());
371 }
372 preset.validate()?;
373 Ok(preset)
374 }
375 pub fn read(path: &Path) -> Result<Self, String> {
376 if !path.is_file() {
377 return Err("kies een regulier presetbestand".into());
378 }
379 let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
380 let mut bytes = Vec::new();
381 file.take((MAX_FILE + 1) as u64)
382 .read_to_end(&mut bytes)
383 .map_err(|e| e.to_string())?;
384 Self::decode(&bytes)
385 }
386}
387
388/// Result of [`copy_gender_mapping`]: how many source mappings landed on the
389/// destination body, and how many had no bone of the same name.
390#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
391pub struct CopyGenderMapping {
392 pub mapped: usize,
393 pub unmatched: usize,
394}
395
396/// Last joint name in a hierarchical path (`"Hips[0]/Hand[12]"` → `"Hand"`).
397pub fn joint_leaf_name(path: &str) -> &str {
398 let last = path.rsplit('/').next().unwrap_or(path);
399 last.rsplit_once('[').map(|(name, _)| name).unwrap_or(last)
400}
401
402/// Joint names along a path, without the `[node-index]` suffixes.
403pub fn path_joint_names(path: &str) -> Vec<&str> {
404 path.split('/').map(joint_leaf_name).collect()
405}
406
407/// Destination joint whose name matches the source path, preferring a unique
408/// name and otherwise the longest shared ancestor-name chain.
409pub fn joint_by_name(body: &PreviewCharacter, source_path: &str) -> Option<usize> {
410 let src_names = path_joint_names(source_path);
411 let src_name = src_names.last().copied().filter(|n| !n.is_empty())?;
412 let dest_paths = joint_paths(body);
413 let mut matches: Vec<usize> = body
414 .joint_names
415 .iter()
416 .enumerate()
417 .filter(|(_, name)| *name == src_name)
418 .map(|(i, _)| i)
419 .collect();
420 if matches.is_empty() {
421 matches = dest_paths
422 .iter()
423 .enumerate()
424 .filter(|(_, p)| joint_leaf_name(p) == src_name)
425 .map(|(i, _)| i)
426 .collect();
427 }
428 if matches.is_empty() {
429 return None;
430 }
431 if matches.len() == 1 {
432 return Some(matches[0]);
433 }
434 matches.sort_by_key(|&i| {
435 let dest = path_joint_names(&dest_paths[i]);
436 let common = src_names
437 .iter()
438 .rev()
439 .zip(dest.iter().rev())
440 .take_while(|(a, b)| a == b)
441 .count();
442 std::cmp::Reverse((common, dest.len() == src_names.len()))
443 });
444 Some(matches[0])
445}
446
447/// Expand version 1–3 shared rigid offsets to a per-sex pair. The shared
448/// translation/rotation is applied to every enabled sex; a disabled sex keeps
449/// a zero offset (and whatever bone path it already had).
450pub fn migrate_preset_json(
451 version: u32,
452 mut value: serde_json::Value,
453) -> Result<serde_json::Value, String> {
454 if version >= 4 {
455 return Ok(value);
456 }
457 let applies = value.get("applies").cloned();
458 let enabled = |sex: usize| {
459 applies
460 .as_ref()
461 .and_then(|a| a.as_array())
462 .and_then(|a| a.get(sex))
463 .and_then(|v| v.as_bool())
464 .unwrap_or(true)
465 };
466 let Some(attachments) = value.get_mut("attachments").and_then(|a| a.as_array_mut()) else {
467 return Ok(value);
468 };
469 for attachment in attachments {
470 expand_vec3_per_sex(attachment, "translation", enabled)?;
471 expand_vec3_per_sex(attachment, "rotation_deg", enabled)?;
472 }
473 Ok(value)
474}
475
476fn expand_vec3_per_sex(
477 attachment: &mut serde_json::Value,
478 key: &str,
479 enabled: impl Fn(usize) -> bool,
480) -> Result<(), String> {
481 let Some(value) = attachment.get(key) else {
482 return Ok(());
483 };
484 if value
485 .as_array()
486 .is_some_and(|items| items.first().is_some_and(serde_json::Value::is_array))
487 {
488 return Ok(());
489 }
490 let vec3 = parse_vec3(value, key)?;
491 let zero = [0.0f32; 3];
492 attachment[key] = serde_json::json!([
493 if enabled(0) { vec3 } else { zero },
494 if enabled(1) { vec3 } else { zero },
495 ]);
496 Ok(())
497}
498
499fn parse_vec3(value: &serde_json::Value, key: &str) -> Result<[f32; 3], String> {
500 let items = value
501 .as_array()
502 .ok_or_else(|| format!("{key} is ongeldig"))?;
503 if items.len() != 3 {
504 return Err(format!("{key} is ongeldig"));
505 }
506 let mut out = [0.0f32; 3];
507 for (i, item) in items.iter().enumerate() {
508 out[i] = item.as_f64().ok_or_else(|| format!("{key} is ongeldig"))? as f32;
509 }
510 Ok(out)
511}
512
513/// Copy already-authored mappings from one sex onto a newly enabled sex.
514pub fn copy_gender_mapping(
515 preset: &mut Preset,
516 from_sex: usize,
517 to_sex: usize,
518 to_body: &PreviewCharacter,
519) -> Result<CopyGenderMapping, String> {
520 if from_sex > 1 || to_sex > 1 {
521 return Err("ongeldig geslacht".into());
522 }
523 if from_sex == to_sex {
524 return Ok(CopyGenderMapping::default());
525 }
526 if to_body.is_static() || to_body.joint_nodes.is_empty() {
527 return Err(
528 "kies een geskind lichaam voor dit geslacht om de botkoppeling over te nemen".into(),
529 );
530 }
531 let dest_paths = joint_paths(to_body);
532 let mut outcome = CopyGenderMapping::default();
533 let mut copied = false;
534 for index in 0..preset.attachments.len() {
535 if Preset::attachment_mapped(&preset.attachments[index], to_sex) {
536 continue;
537 }
538 if !Preset::attachment_mapped(&preset.attachments[index], from_sex) {
539 continue;
540 }
541 let item = &preset.attachments[index];
542 if item.skeleton.is_some() {
543 let mut links = Vec::new();
544 for link in &item.bone_links[from_sex] {
545 match joint_by_name(to_body, &link.target) {
546 Some(joint) => {
547 links.push(crate::equipment_rig::BoneLink {
548 bone: link.bone,
549 target: dest_paths[joint].clone(),
550 translation: link.translation,
551 rotation_deg: link.rotation_deg,
552 });
553 outcome.mapped += 1;
554 }
555 None => outcome.unmatched += 1,
556 }
557 }
558 if !links.is_empty() {
559 preset.attachments[index].bone_links[to_sex] = links;
560 copied = true;
561 }
562 } else if let Some(path) = item.bones[from_sex].clone() {
563 let translation = item.translation[from_sex];
564 let rotation_deg = item.rotation_deg[from_sex];
565 if let Some(joint) = joint_by_name(to_body, &path) {
566 let item = &mut preset.attachments[index];
567 item.bones[to_sex] = Some(dest_paths[joint].clone());
568 item.translation[to_sex] = translation;
569 item.rotation_deg[to_sex] = rotation_deg;
570 outcome.mapped += 1;
571 copied = true;
572 } else {
573 outcome.unmatched += 1;
574 }
575 }
576 }
577 if copied {
578 preset.rigs[to_sex] = Some(RigSignature::of(to_body));
579 for attachment in &mut preset.attachments {
580 attachment.bone_links[to_sex].retain(|l| dest_paths.contains(&l.target));
581 if attachment.bones[to_sex]
582 .as_ref()
583 .is_some_and(|p| !dest_paths.contains(p))
584 {
585 attachment.bones[to_sex] = None;
586 }
587 }
588 }
589 Ok(outcome)
590}
591
592pub fn valid_name(name: &str) -> bool {
593 !name.is_empty()
594 && name.len() <= 64
595 && name != "body"
596 && name
597 .bytes()
598 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-' || c == b'_')
599}
600
601pub fn checksum(bytes: &[u8]) -> u32 {
602 let mut crc = !0u32;
603 for byte in bytes {
604 crc ^= u32::from(*byte);
605 for _ in 0..8 {
606 crc = (crc >> 1) ^ (0xedb88320u32 & (0u32.wrapping_sub(crc & 1)));
607 }
608 }
609 !crc
610}
611
612/// Immutable snapshot publish: a hard link atomically creates the destination
613/// without overwriting an older saved preset. Failure leaves old data intact.
614pub fn write_snapshot(path: &Path, bytes: &[u8]) -> Result<(), String> {
615 if bytes.len() > MAX_FILE {
616 return Err("snapshot te groot".into());
617 }
618 if path.exists() {
619 if !path.is_file() {
620 return Err("snapshot is geen regulier bestand".into());
621 }
622 let mut current = Vec::new();
623 std::fs::File::open(path)
624 .map_err(|e| e.to_string())?
625 .take((MAX_FILE + 1) as u64)
626 .read_to_end(&mut current)
627 .map_err(|e| e.to_string())?;
628 return if current == bytes {
629 Ok(())
630 } else {
631 Err("snapshotnaam bestaat met andere inhoud".into())
632 };
633 }
634 let stamp = std::time::SystemTime::now()
635 .duration_since(std::time::UNIX_EPOCH)
636 .unwrap_or_default()
637 .as_nanos();
638 let temp = path.with_extension(format!("ppe.part-{}-{stamp}", std::process::id()));
639 let result = (|| {
640 let mut file = std::fs::OpenOptions::new()
641 .write(true)
642 .create_new(true)
643 .open(&temp)
644 .map_err(|e| e.to_string())?;
645 file.write_all(bytes)
646 .and_then(|_| file.sync_all())
647 .map_err(|e| e.to_string())?;
648 std::fs::hard_link(&temp, path).map_err(|e| e.to_string())?;
649 #[cfg(unix)]
650 std::fs::File::open(path.parent().ok_or("snapshotmap ontbreekt")?)
651 .and_then(|dir| dir.sync_all())
652 .map_err(|e| e.to_string())?;
653 Ok(())
654 })();
655 let _ = std::fs::remove_file(temp);
656 result
657}
658
659#[derive(Debug, Clone, PartialEq)]
660pub struct Texture {
661 pub content_hash: u64,
662 pub width: u32,
663 pub height: u32,
664 pub rgba: Vec<u8>,
665}
666fn decode_png(bytes: &[u8]) -> Result<Texture, String> {
667 let mut decoder = png::Decoder::new(Cursor::new(bytes));
668 decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
669 let mut reader = decoder.read_info().map_err(|e| e.to_string())?;
670 let info = reader.info();
671 if info.width == 0 || info.height == 0 || info.width > 2048 || info.height > 2048 {
672 return Err("texture is groter dan 2048×2048".into());
673 }
674 let size = reader
675 .output_buffer_size()
676 .ok_or("ongeldige texturegrootte")?;
677 if size > 2048 * 2048 * 8 {
678 return Err("texture is te groot".into());
679 }
680 let mut data = vec![0; size];
681 let info = reader.next_frame(&mut data).map_err(|e| e.to_string())?;
682 let channels = info.color_type.samples();
683 let mut rgba = Vec::with_capacity(info.width as usize * info.height as usize * 4);
684 for pixel in data[..info.buffer_size()].chunks_exact(channels) {
685 match info.color_type {
686 png::ColorType::Grayscale => {
687 rgba.extend_from_slice(&[pixel[0], pixel[0], pixel[0], 255])
688 }
689 png::ColorType::GrayscaleAlpha => {
690 rgba.extend_from_slice(&[pixel[0], pixel[0], pixel[0], pixel[1]])
691 }
692 png::ColorType::Rgb => rgba.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]),
693 png::ColorType::Rgba => rgba.extend_from_slice(pixel),
694 _ => return Err("onbekende texturecodering".into()),
695 }
696 }
697 use std::hash::{Hash, Hasher};
698 let mut hash = std::collections::hash_map::DefaultHasher::new();
699 rgba.hash(&mut hash);
700 Ok(Texture {
701 content_hash: hash.finish(),
702 width: info.width,
703 height: info.height,
704 rgba,
705 })
706}
707
708pub fn local_matrix(translation: [f32; 3], degrees: [f32; 3]) -> Mat4 {
709 let [x, y, z] = degrees.map(f32::to_radians);
710 let (sx, cx) = x.sin_cos();
711 let (sy, cy) = y.sin_cos();
712 let (sz, cz) = z.sin_cos();
713 let rx = [
714 1., 0., 0., 0., 0., cx, sx, 0., 0., -sx, cx, 0., 0., 0., 0., 1.,
715 ];
716 let ry = [
717 cy, 0., -sy, 0., 0., 1., 0., 0., sy, 0., cy, 0., 0., 0., 0., 1.,
718 ];
719 let rz = [
720 cz, sz, 0., 0., -sz, cz, 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
721 ];
722 let mut matrix = mul(rz, mul(ry, rx));
723 matrix[12] = translation[0];
724 matrix[13] = translation[1];
725 matrix[14] = translation[2];
726 matrix
727}
728
729#[derive(Debug, Clone)]
730pub struct Layer {
731 pub mesh: Mesh,
732 pub model: Mat4,
733 pub albedo: Option<std::sync::Arc<Texture>>,
734 pub shiny: Option<std::sync::Arc<Texture>>,
735 pub specular: f32,
736}
737
738#[derive(Default)]
739pub struct TextureCache {
740 entries: std::collections::VecDeque<(Vec<u8>, std::sync::Arc<Texture>)>,
741 bytes: usize,
742}
743impl TextureCache {
744 pub(crate) fn get(
745 &mut self,
746 preset: &Preset,
747 index: usize,
748 ) -> Result<std::sync::Arc<Texture>, String> {
749 let bytes = &preset.assets.get(index).ok_or("texture ontbreekt")?.bytes;
750 if let Some((_, texture)) = self.entries.iter().find(|(source, _)| source == bytes) {
751 return Ok(texture.clone());
752 }
753 let texture = std::sync::Arc::new(decode_png(bytes)?);
754 let cost = bytes.len() + texture.rgba.len();
755 while self.bytes + cost > 128 * 1024 * 1024 {
756 if let Some((source, image)) = self.entries.pop_front() {
757 self.bytes -= source.len() + image.rgba.len();
758 } else {
759 break;
760 }
761 }
762 self.bytes += cost;
763 self.entries.push_back((bytes.clone(), texture.clone()));
764 Ok(texture)
765 }
766}
767
768pub fn layers(
769 preset: &Preset,
770 sex: usize,
771 body: &PreviewCharacter,
772 controls: &PreviewControls,
773) -> Result<Vec<Layer>, String> {
774 layers_cached(preset, sex, body, controls, &mut TextureCache::default())
775}
776
777pub fn layers_cached(
778 preset: &Preset,
779 sex: usize,
780 body: &PreviewCharacter,
781 controls: &PreviewControls,
782 cache: &mut TextureCache,
783) -> Result<Vec<Layer>, String> {
784 if sex > 1 {
785 return Err("ongeldig geslacht".into());
786 }
787 if !preset.applies[sex] || preset.attachments.is_empty() {
788 return Ok(Vec::new());
789 }
790 let signature = RigSignature::of(body);
791 if preset.rigs[sex].as_ref() != Some(&signature) {
792 return Err("rig wijkt af: kies de botten opnieuw voor dit lichaam".into());
793 }
794 let paths = joint_paths(body);
795 let mut layers = Vec::new();
796 for item in &preset.attachments {
797 if let Some(skeleton) = &item.skeleton {
798 let models = crate::equipment_rig::bone_models(
799 skeleton,
800 &item.bone_links[sex],
801 &paths,
802 |joint| animated_joint_world(body, &controls.clip_name, controls.time, joint),
803 )?;
804 let shiny = item.shiny.map(|i| cache.get(preset, i)).transpose()?;
805 for mesh in &item.meshes {
806 layers.push(Layer {
807 mesh: mesh.clone(),
808 model: models[mesh.bone.unwrap_or(0)],
809 albedo: mesh.albedo.map(|i| cache.get(preset, i)).transpose()?,
810 shiny: shiny.clone(),
811 specular: item.specular,
812 });
813 }
814 continue;
815 }
816 let path = item.bones[sex]
817 .as_deref()
818 .ok_or("kies een bot voor dit onderdeel en geslacht")?;
819 let joint = paths
820 .iter()
821 .position(|p| p == path)
822 .ok_or("het gekoppelde bot ontbreekt")?;
823 let world = animated_joint_world(body, &controls.clip_name, controls.time, joint)
824 .ok_or("het gekoppelde bot ontbreekt")?;
825 let model = mul(
826 world,
827 local_matrix(item.translation[sex], item.rotation_deg[sex]),
828 );
829 let shiny = item.shiny.map(|i| cache.get(preset, i)).transpose()?;
830 for mesh in &item.meshes {
831 layers.push(Layer {
832 mesh: mesh.clone(),
833 model,
834 albedo: mesh.albedo.map(|i| cache.get(preset, i)).transpose()?,
835 shiny: shiny.clone(),
836 specular: item.specular,
837 });
838 }
839 }
840 Ok(layers)
841}
842
843/// Import an OBJ and its declared MTL/PNG dependencies into the preset itself.
844/// No extraction directories, absolute references or parent traversal allowed.
845pub fn import_obj(preset: &mut Preset, source: &Path) -> Result<usize, String> {
846 import_obj_limited(preset, source, MAX_VERTICES)
847}
848
849pub(crate) fn import_obj_limited(
850 preset: &mut Preset,
851 source: &Path,
852 max_vertices: usize,
853) -> Result<usize, String> {
854 if source
855 .extension()
856 .and_then(|x| x.to_str())
857 .map(|x| !x.eq_ignore_ascii_case("obj"))
858 .unwrap_or(true)
859 {
860 return Err("kies een rigide OBJ-model met MTL/PNG-bijbestanden".into());
861 }
862 let root = source
863 .parent()
864 .ok_or("modelmap ontbreekt")?
865 .canonicalize()
866 .map_err(|e| e.to_string())?;
867 let name = source
868 .file_name()
869 .and_then(|x| x.to_str())
870 .ok_or("ongeldige modelnaam")?;
871 let mut candidate = preset.clone();
872 if candidate.attachments.len() >= 32 {
873 return Err("te veel onderdelen".into());
874 }
875 compact_assets(&mut candidate)?;
876 let prefix = unique_prefix(&candidate, "part");
877 let obj_bytes = read_asset(&root, name)?;
878 let obj_text = std::str::from_utf8(&obj_bytes).map_err(|e| e.to_string())?;
879 let mut dependencies = BTreeSet::new();
880 dependencies.insert(name.to_string());
881 for line in obj_text.lines() {
882 if let Some(rest) = line.trim().strip_prefix("mtllib ") {
883 let mtl = rest.trim();
884 dependencies.insert(mtl.to_string());
885 let bytes = read_asset(&root, mtl)?;
886 let text = std::str::from_utf8(&bytes).map_err(|e| e.to_string())?;
887 for line in text.lines() {
888 let line = line.trim();
889 if line.starts_with("map_")
890 || line.starts_with("bump ")
891 || line.starts_with("disp ")
892 {
893 let (_, path) = line
894 .split_once(char::is_whitespace)
895 .ok_or("ongeldige textureverwijzing")?;
896 let relative = Path::new(mtl)
897 .parent()
898 .unwrap_or(Path::new(""))
899 .join(path.trim());
900 let relative = relative
901 .to_str()
902 .ok_or("ongeldig texturepad")?
903 .replace('\\', "/");
904 portable_relative(&relative)?;
905 dependencies.insert(relative);
906 }
907 }
908 }
909 }
910 if dependencies.len() + candidate.assets.len() > MAX_ASSETS {
911 return Err("te veel bijbestanden".into());
912 }
913 let mut asset_map = std::collections::BTreeMap::new();
914 let mut total_bytes: usize = candidate.assets.iter().map(|a| a.bytes.len()).sum();
915 for relative in dependencies {
916 let bytes = read_asset(&root, &relative)?;
917 total_bytes += bytes.len();
918 if total_bytes > MAX_FILE {
919 return Err("assets samen te groot".into());
920 }
921 let index = candidate.assets.len();
922 candidate.assets.push(EmbeddedAsset {
923 path: format!("{prefix}/{relative}"),
924 bytes,
925 });
926 asset_map.insert(relative, index);
927 }
928 // A checked callback prevents the parser from reading undeclared/unsafe files.
929 let mut input = std::io::BufReader::new(Cursor::new(obj_bytes));
930 let options = tobj::LoadOptions {
931 triangulate: true,
932 single_index: true,
933 ..Default::default()
934 };
935 let texture_paths = std::cell::RefCell::new(std::collections::BTreeMap::new());
936 let (models, materials) = tobj::load_obj_buf(&mut input, &options, |path| {
937 let relative = path.to_string_lossy().replace('\\', "/");
938 let index = asset_map
939 .get(&relative)
940 .ok_or(tobj::LoadError::OpenFileFailed)?;
941 let mut reader = std::io::BufReader::new(Cursor::new(&candidate.assets[*index].bytes));
942 let (materials, map) = tobj::load_mtl_buf(&mut reader)?;
943 for material in &materials {
944 if let Some(texture) = &material.diffuse_texture {
945 let relative = path
946 .parent()
947 .unwrap_or(Path::new(""))
948 .join(texture)
949 .to_string_lossy()
950 .replace('\\', "/");
951 texture_paths
952 .borrow_mut()
953 .insert(material.name.clone(), relative);
954 }
955 }
956 Ok((materials, map))
957 })
958 .map_err(|e| e.to_string())?;
959 let materials = materials.map_err(|e| e.to_string())?;
960 let texture_paths = texture_paths.into_inner();
961 let mut meshes = Vec::new();
962 let mut vertices: usize = candidate
963 .attachments
964 .iter()
965 .flat_map(|a| &a.meshes)
966 .map(|m| m.positions.len())
967 .sum();
968 for model in models {
969 let source = model.mesh;
970 vertices += source.positions.len() / 3;
971 if vertices > max_vertices || source.indices.len() > max_vertices * 6 {
972 return Err("equipmentmesh is te groot".into());
973 }
974 let positions: Vec<[f32; 3]> = source
975 .positions
976 .chunks_exact(3)
977 .map(|x| [x[0], x[1], x[2]])
978 .collect();
979 let mut normals: Vec<[f32; 3]> = source
980 .normals
981 .chunks_exact(3)
982 .map(|x| [x[0], x[1], x[2]])
983 .collect();
984 if normals.len() != positions.len() {
985 normals = compute_normals(&positions, &source.indices)?;
986 }
987 let mut uvs: Vec<[f32; 2]> = source
988 .texcoords
989 .chunks_exact(2)
990 .map(|x| [x[0], x[1]])
991 .collect();
992 if uvs.len() != positions.len() {
993 uvs = vec![[0.; 2]; positions.len()];
994 }
995 let material = source.material_id.and_then(|id| materials.get(id));
996 let color = material.and_then(|m| m.diffuse).unwrap_or([0.7, 0.7, 0.7]);
997 let albedo = material
998 .and_then(|m| texture_paths.get(&m.name))
999 .map(|path| {
1000 asset_map
1001 .get(path)
1002 .copied()
1003 .ok_or("ontbrekende diffuse texture")
1004 })
1005 .transpose()?;
1006 meshes.push(Mesh {
1007 positions,
1008 normals,
1009 uvs,
1010 indices: source.indices,
1011 color: [
1012 color[0],
1013 color[1],
1014 color[2],
1015 material.and_then(|m| m.dissolve).unwrap_or(1.),
1016 ],
1017 albedo,
1018 bone: None,
1019 });
1020 }
1021 if meshes.is_empty() {
1022 return Err("het OBJ-bestand heeft geen meshes".into());
1023 }
1024 candidate.attachments.push(Attachment {
1025 label: name.to_string(),
1026 source_assets: asset_map.values().copied().collect(),
1027 bones: [None, None],
1028 translation: [[0.; 3]; 2],
1029 rotation_deg: [[0.; 3]; 2],
1030 meshes,
1031 shiny: None,
1032 specular: 0.25,
1033 skeleton: None,
1034 bone_links: [Vec::new(), Vec::new()],
1035 });
1036 candidate.validate_with(max_vertices)?;
1037 *preset = candidate;
1038 Ok(preset.attachments.len() - 1)
1039}
1040
1041/// Import a rigid equipment model, choosing the reader by extension (OBJ+MTL or FBX).
1042pub fn import_model(preset: &mut Preset, source: &Path) -> Result<usize, String> {
1043 match source
1044 .extension()
1045 .and_then(|x| x.to_str())
1046 .map(|x| x.to_ascii_lowercase())
1047 .as_deref()
1048 {
1049 Some("obj") => import_obj(preset, source),
1050 Some("gltf" | "glb") => crate::equipment_rig::import_skinned(preset, source),
1051 Some("fbx") => match crate::equipment_rig::import_skinned(preset, source) {
1052 Err(error) if error == crate::equipment_rig::NOT_SKINNED => import_fbx(preset, source),
1053 other => other,
1054 },
1055 _ => Err("kies een OBJ- (met MTL/PNG), FBX- of geskind glTF/GLB-model".into()),
1056 }
1057}
1058
1059/// Import a rigid (unskinned) FBX into the preset. ufbx parses only the
1060/// in-memory bytes (no external file access); axis/unit normalisation to
1061/// right-handed Y-up metres happens once, here, exactly like `fbx_scene`.
1062/// Embedded PNG textures are copied into the preset; external PNG textures
1063/// must be relative files inside the model folder and are embedded too.
1064pub fn import_fbx(preset: &mut Preset, source: &Path) -> Result<usize, String> {
1065 import_fbx_limited(preset, source, MAX_VERTICES)
1066}
1067
1068pub(crate) fn import_fbx_limited(
1069 preset: &mut Preset,
1070 source: &Path,
1071 max_vertices: usize,
1072) -> Result<usize, String> {
1073 if source
1074 .extension()
1075 .and_then(|x| x.to_str())
1076 .map(|x| !x.eq_ignore_ascii_case("fbx"))
1077 .unwrap_or(true)
1078 {
1079 return Err("kies een rigide FBX-model".into());
1080 }
1081 let root = source
1082 .parent()
1083 .ok_or("modelmap ontbreekt")?
1084 .canonicalize()
1085 .map_err(|e| e.to_string())?;
1086 let name = source
1087 .file_name()
1088 .and_then(|x| x.to_str())
1089 .ok_or("ongeldige modelnaam")?;
1090 let mut candidate = preset.clone();
1091 if candidate.attachments.len() >= 32 {
1092 return Err("te veel onderdelen".into());
1093 }
1094 compact_assets(&mut candidate)?;
1095 let prefix = unique_prefix(&candidate, "part");
1096 let fbx_bytes = read_asset(&root, name)?;
1097 let opts = ufbx::LoadOpts {
1098 target_axes: ufbx::axes_right_handed_y_up(),
1099 target_unit_meters: 1.0,
1100 space_conversion: ufbx::SpaceConversion::ModifyGeometry,
1101 geometry_transform_handling: ufbx::GeometryTransformHandling::ModifyGeometry,
1102 generate_missing_normals: true,
1103 normalize_normals: true,
1104 load_external_files: false,
1105 ..Default::default()
1106 };
1107 let scene = ufbx::load_memory(&fbx_bytes, opts)
1108 .map_err(|e| format!("FBX laden mislukt: {}", e.description))?;
1109 if scene.metadata.geometry_ignored {
1110 return Err("FBX heeft geometrie genegeerd; bestand is niet tekenbaar".into());
1111 }
1112 let mut total_bytes: usize = candidate.assets.iter().map(|a| a.bytes.len()).sum();
1113 let mut push_asset =
1114 |candidate: &mut Preset, path: String, bytes: Vec<u8>| -> Result<usize, String> {
1115 total_bytes += bytes.len();
1116 if total_bytes > MAX_FILE || candidate.assets.len() >= MAX_ASSETS {
1117 return Err("assets samen te groot of te veel bijbestanden".into());
1118 }
1119 candidate.assets.push(EmbeddedAsset { path, bytes });
1120 Ok(candidate.assets.len() - 1)
1121 };
1122 let source_index = push_asset(&mut candidate, format!("{prefix}/{name}"), fbx_bytes)?;
1123 let mut source_assets = vec![source_index];
1124 // Texture identity (element id) -> embedded asset index, so shared textures are stored once.
1125 let mut texture_assets: std::collections::BTreeMap<u32, usize> = Default::default();
1126 let mut vertices: usize = candidate
1127 .attachments
1128 .iter()
1129 .flat_map(|a| &a.meshes)
1130 .map(|m| m.positions.len())
1131 .sum();
1132 let mut meshes = Vec::new();
1133 let mut tri = Vec::new();
1134 for node in &scene.nodes {
1135 let Some(mesh) = node.mesh.as_ref() else {
1136 continue;
1137 };
1138 if mesh.num_triangles == 0 {
1139 continue;
1140 }
1141 if !mesh.skin_deformers.is_empty() {
1142 return Err(format!(
1143 "FBX-mesh '{}' is geskinned; alleen rigide equipment wordt ondersteund",
1144 node.element.name.trim()
1145 ));
1146 }
1147 if !mesh.blend_deformers.is_empty() {
1148 return Err(format!(
1149 "FBX-mesh '{}' heeft morph targets; die worden niet ondersteund",
1150 node.element.name.trim()
1151 ));
1152 }
1153 let to_world = node.geometry_to_world;
1154 let for_normals = ufbx::matrix_for_normals(&to_world);
1155 let parts = mesh.material_parts.len().max(1);
1156 for part in 0..parts {
1157 let faces: Vec<u32> = if mesh.material_parts.is_empty() {
1158 (0..mesh.faces.len() as u32).collect()
1159 } else {
1160 mesh.material_parts[part]
1161 .face_indices
1162 .iter()
1163 .copied()
1164 .collect()
1165 };
1166 if faces.is_empty() {
1167 continue;
1168 }
1169 let material = if mesh.material_parts.is_empty() {
1170 None
1171 } else {
1172 mesh.materials.get(part)
1173 };
1174 let mut positions = Vec::new();
1175 let mut normals = Vec::new();
1176 let mut uvs = Vec::new();
1177 let mut indices = Vec::new();
1178 for face in faces {
1179 let face = mesh.faces[face as usize];
1180 if face.num_indices < 3 {
1181 continue;
1182 }
1183 let count = ufbx::triangulate_face_vec(&mut tri, mesh, face) as usize;
1184 for t in 0..count {
1185 let mut corners = [tri[t * 3], tri[t * 3 + 1], tri[t * 3 + 2]];
1186 if mesh.reversed_winding {
1187 corners.swap(1, 2);
1188 }
1189 for corner in corners {
1190 let corner = corner as usize;
1191 let p = ufbx::transform_position(&to_world, mesh.vertex_position[corner]);
1192 let n = if mesh.vertex_normal.exists {
1193 let n =
1194 ufbx::transform_direction(&for_normals, mesh.vertex_normal[corner]);
1195 let len = (n.x * n.x + n.y * n.y + n.z * n.z).sqrt();
1196 if len > 1e-12 {
1197 [(n.x / len) as f32, (n.y / len) as f32, (n.z / len) as f32]
1198 } else {
1199 [0., 1., 0.]
1200 }
1201 } else {
1202 [0., 1., 0.]
1203 };
1204 let uv = if mesh.vertex_uv.exists {
1205 let uv = mesh.vertex_uv[corner];
1206 [uv.x as f32, uv.y as f32]
1207 } else {
1208 [0., 0.]
1209 };
1210 indices.push(positions.len() as u32);
1211 positions.push([p.x as f32, p.y as f32, p.z as f32]);
1212 normals.push(n);
1213 uvs.push(uv);
1214 }
1215 }
1216 }
1217 if positions.is_empty() {
1218 continue;
1219 }
1220 vertices += positions.len();
1221 if vertices > max_vertices {
1222 return Err("equipmentmesh is te groot".into());
1223 }
1224 if positions
1225 .iter()
1226 .chain(&normals)
1227 .any(|v| v.iter().any(|x| !x.is_finite()))
1228 {
1229 return Err("FBX-mesh bevat niet-eindige posities of normalen".into());
1230 }
1231 if !mesh.vertex_normal.exists {
1232 normals = compute_normals(&positions, &indices)?;
1233 }
1234 let (mut color, texture) = match material {
1235 None => ([0.7, 0.7, 0.7, 1.0], None),
1236 Some(m) => {
1237 let map = if m.pbr.base_color.has_value {
1238 &m.pbr.base_color
1239 } else {
1240 &m.fbx.diffuse_color
1241 };
1242 let v = map.value_vec4;
1243 // Same alpha rule as the FBX body loader (`fbx_scene`).
1244 let alpha = if v.w == 0.0 { 1.0 } else { v.w as f32 };
1245 (
1246 [v.x as f32, v.y as f32, v.z as f32, alpha],
1247 map.texture
1248 .as_ref()
1249 .or(m.fbx.diffuse_color.texture.as_ref()),
1250 )
1251 }
1252 };
1253 if !color.iter().all(|c| c.is_finite()) {
1254 return Err("FBX-materiaal heeft een niet-eindige kleur".into());
1255 }
1256 let albedo = match texture {
1257 None => None,
1258 Some(texture) => {
1259 let id = texture.element.element_id;
1260 if let Some(index) = texture_assets.get(&id) {
1261 Some(*index)
1262 } else {
1263 let (path, bytes) = if texture.content.size > 0 {
1264 let label = Path::new(texture.relative_filename.trim())
1265 .file_name()
1266 .and_then(|n| n.to_str())
1267 .filter(|n| !n.is_empty())
1268 .map(|n| n.replace(['\\', ':'], "_"))
1269 .unwrap_or_else(|| format!("texture-{id}.png"));
1270 (
1271 format!("{prefix}/embedded/{id}-{label}"),
1272 texture.content.to_vec(),
1273 )
1274 } else {
1275 let relative = texture.relative_filename.trim().replace('\\', "/");
1276 if relative.is_empty() {
1277 return Err(
1278 "FBX-texture heeft geen relatief pad en is niet ingebed".into(),
1279 );
1280 }
1281 portable_relative(&relative)
1282 .map_err(|e| format!("FBX-texturepad geweigerd: {e}"))?;
1283 let bytes = read_asset(&root, &relative)
1284 .map_err(|e| format!("FBX-texture '{relative}': {e}"))?;
1285 (format!("{prefix}/{relative}"), bytes)
1286 };
1287 if bytes.len() > MAX_ASSET {
1288 return Err("FBX-texture is te groot".into());
1289 }
1290 decode_png(&bytes).map_err(|e| {
1291 format!("FBX-texture is geen bruikbare PNG (alleen PNG wordt ingesloten): {e}")
1292 })?;
1293 let index = push_asset(&mut candidate, path, bytes)?;
1294 texture_assets.insert(id, index);
1295 source_assets.push(index);
1296 Some(index)
1297 }
1298 }
1299 };
1300 if albedo.is_some() {
1301 // FBX semantics: a connected diffuse texture replaces the colour property.
1302 color = [1., 1., 1., color[3]];
1303 }
1304 meshes.push(Mesh {
1305 positions,
1306 normals,
1307 uvs,
1308 indices,
1309 color,
1310 albedo,
1311 bone: None,
1312 });
1313 }
1314 }
1315 if meshes.is_empty() {
1316 return Err("het FBX-bestand heeft geen tekenbare meshes".into());
1317 }
1318 source_assets.sort_unstable();
1319 source_assets.dedup();
1320 candidate.attachments.push(Attachment {
1321 label: name.to_string(),
1322 source_assets,
1323 bones: [None, None],
1324 translation: [[0.; 3]; 2],
1325 rotation_deg: [[0.; 3]; 2],
1326 meshes,
1327 shiny: None,
1328 specular: 0.25,
1329 skeleton: None,
1330 bone_links: [Vec::new(), Vec::new()],
1331 });
1332 candidate.validate_with(max_vertices)?;
1333 *preset = candidate;
1334 Ok(preset.attachments.len() - 1)
1335}
1336
1337pub fn import_shiny(preset: &mut Preset, attachment: usize, source: &Path) -> Result<(), String> {
1338 let root = source.parent().ok_or("texturemap ontbreekt")?;
1339 let name = source
1340 .file_name()
1341 .and_then(|x| x.to_str())
1342 .ok_or("ongeldige bestandsnaam")?;
1343 let bytes = read_asset(root, name)?;
1344 decode_png(&bytes)?;
1345 let mut candidate = preset.clone();
1346 let index = candidate.assets.len();
1347 candidate.assets.push(EmbeddedAsset {
1348 path: format!("{}/{name}", unique_prefix(&candidate, "shiny")),
1349 bytes,
1350 });
1351 candidate
1352 .attachments
1353 .get_mut(attachment)
1354 .ok_or("onderdeel ontbreekt")?
1355 .shiny = Some(index);
1356 compact_assets(&mut candidate)?;
1357 candidate.validate()?;
1358 *preset = candidate;
1359 Ok(())
1360}
1361
1362fn unique_prefix(preset: &Preset, kind: &str) -> String {
1363 (0..)
1364 .map(|i| format!("{kind}-{i}"))
1365 .find(|prefix| {
1366 !preset
1367 .assets
1368 .iter()
1369 .any(|a| a.path.split('/').next() == Some(prefix.as_str()))
1370 })
1371 .expect("finite asset list")
1372}
1373pub(crate) fn compact_assets(preset: &mut Preset) -> Result<(), String> {
1374 let used: BTreeSet<usize> = preset
1375 .attachments
1376 .iter()
1377 .flat_map(|a| {
1378 a.source_assets
1379 .iter()
1380 .copied()
1381 .chain(a.shiny)
1382 .chain(a.meshes.iter().filter_map(|m| m.albedo))
1383 })
1384 .collect();
1385 if used.iter().any(|i| *i >= preset.assets.len()) {
1386 return Err("ongeldige assetverwijzing".into());
1387 }
1388 let mut remap = vec![0; preset.assets.len()];
1389 let mut assets = Vec::new();
1390 for (i, asset) in preset.assets.iter().enumerate() {
1391 if used.contains(&i) {
1392 remap[i] = assets.len();
1393 assets.push(asset.clone());
1394 }
1395 }
1396 for a in &mut preset.attachments {
1397 for i in &mut a.source_assets {
1398 *i = remap[*i];
1399 }
1400 if let Some(i) = a.shiny.as_mut() {
1401 *i = remap[*i];
1402 }
1403 for mesh in &mut a.meshes {
1404 if let Some(i) = mesh.albedo.as_mut() {
1405 *i = remap[*i];
1406 }
1407 }
1408 }
1409 preset.assets = assets;
1410 Ok(())
1411}
1412/// Clear an override and release its no-longer-used embedded texture.
1413pub fn clear_shiny(preset: &mut Preset, index: usize) -> Result<(), String> {
1414 let mut candidate = preset.clone();
1415 candidate
1416 .attachments
1417 .get_mut(index)
1418 .ok_or("onderdeel ontbreekt")?
1419 .shiny = None;
1420 compact_assets(&mut candidate)?;
1421 *preset = candidate;
1422 Ok(())
1423}
1424/// Remove a part and its no-longer-used embedded assets, remapping all indices.
1425pub fn remove_attachment(preset: &mut Preset, index: usize) -> Result<(), String> {
1426 if index >= preset.attachments.len() {
1427 return Err("onderdeel ontbreekt".into());
1428 }
1429 let mut candidate = preset.clone();
1430 candidate.attachments.remove(index);
1431 compact_assets(&mut candidate)?;
1432 *preset = candidate;
1433 Ok(())
1434}
1435
1436fn read_asset(root: &Path, relative: &str) -> Result<Vec<u8>, String> {
1437 portable_relative(relative)?;
1438 let root = root.canonicalize().map_err(|e| e.to_string())?;
1439 let path = root
1440 .join(relative)
1441 .canonicalize()
1442 .map_err(|e| e.to_string())?;
1443 if !path.starts_with(&root) {
1444 return Err("asset verlaat de modelmap".into());
1445 }
1446 if !path.is_file() {
1447 return Err("asset is geen regulier bestand".into());
1448 }
1449 let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
1450 let mut bytes = Vec::new();
1451 file.take((MAX_ASSET + 1) as u64)
1452 .read_to_end(&mut bytes)
1453 .map_err(|e| e.to_string())?;
1454 if bytes.len() > MAX_ASSET {
1455 return Err("asset is te groot".into());
1456 }
1457 Ok(bytes)
1458}
1459fn compute_normals(positions: &[[f32; 3]], indices: &[u32]) -> Result<Vec<[f32; 3]>, String> {
1460 let mut normals = vec![[0.; 3]; positions.len()];
1461 for t in indices.chunks_exact(3) {
1462 let [a, b, c] = [t[0] as usize, t[1] as usize, t[2] as usize];
1463 if [a, b, c].iter().any(|i| *i >= positions.len()) {
1464 return Err("ongeldige OBJ-index".into());
1465 }
1466 let ab = std::array::from_fn::<_, 3, _>(|i| positions[b][i] - positions[a][i]);
1467 let ac = std::array::from_fn::<_, 3, _>(|i| positions[c][i] - positions[a][i]);
1468 let n = [
1469 ab[1] * ac[2] - ab[2] * ac[1],
1470 ab[2] * ac[0] - ab[0] * ac[2],
1471 ab[0] * ac[1] - ab[1] * ac[0],
1472 ];
1473 for vertex in [a, b, c] {
1474 for (i, value) in n.iter().enumerate() {
1475 normals[vertex][i] += value;
1476 }
1477 }
1478 }
1479 for normal in &mut normals {
1480 let length = normal.iter().map(|v| v * v).sum::<f32>().sqrt();
1481 if length > 1e-10 {
1482 for v in normal {
1483 *v /= length
1484 }
1485 }
1486 }
1487 Ok(normals)
1488}