Skip to main content

mmd_anim_runtime/
model_descriptor.rs

1//! C-layout-independent version 1 runtime model descriptors.
2//!
3//! This module is deliberately made up of ordinary Rust values (`Vec`,
4//! `Option`, and glam types).  FFI layers can copy their own records into this
5//! representation, while format importers can construct it without adopting a
6//! C ABI.  The compiler is the single normalization point for absolute PMX
7//! rest positions, metadata and offset tables.
8
9use std::fmt;
10
11use glam::{Quat, Vec3A};
12use thiserror::Error;
13
14use crate::{
15    AppendTransformInit, BoneIndex, BoneInit, BoneMorphOffset, GroupMorphOffset, IkAngleLimit,
16    IkLinkInit, IkSolverInit, LocalAxis, ModelArena, MorphIndex, MorphInit,
17};
18
19/// The only descriptor version understood by this crate.
20pub const RUNTIME_MODEL_DESCRIPTOR_VERSION_V1: u32 = 1;
21
22/// A host-independent snapshot of all runtime model data needed by v1.
23#[derive(Clone, Debug, PartialEq)]
24pub struct RuntimeModelDescriptorV1 {
25    /// Must be [`RUNTIME_MODEL_DESCRIPTOR_VERSION_V1`].
26    pub descriptor_version: u32,
27    pub bones: Vec<RuntimeBoneDescriptorV1>,
28    pub ik_solvers: Vec<RuntimeIkSolverDescriptorV1>,
29    pub append_transforms: Vec<RuntimeAppendTransformDescriptorV1>,
30    pub morphs: RuntimeMorphDescriptorV1,
31}
32
33impl Default for RuntimeModelDescriptorV1 {
34    fn default() -> Self {
35        Self {
36            descriptor_version: RUNTIME_MODEL_DESCRIPTOR_VERSION_V1,
37            bones: Vec::new(),
38            ik_solvers: Vec::new(),
39            append_transforms: Vec::new(),
40            morphs: RuntimeMorphDescriptorV1::default(),
41        }
42    }
43}
44
45impl RuntimeModelDescriptorV1 {
46    pub fn new(bones: Vec<RuntimeBoneDescriptorV1>) -> Self {
47        Self {
48            bones,
49            ..Self::default()
50        }
51    }
52}
53
54/// Absolute PMX/MMD-space rest position and per-bone metadata.
55#[derive(Clone, Debug, PartialEq)]
56pub struct RuntimeBoneDescriptorV1 {
57    pub parent: Option<BoneIndex>,
58    pub rest_position: Vec3A,
59    pub transform_order: i32,
60    pub transform_after_physics: bool,
61    pub fixed_axis: Option<Vec3A>,
62    pub local_axis: Option<LocalAxis>,
63}
64
65impl RuntimeBoneDescriptorV1 {
66    pub fn new(parent: Option<BoneIndex>, rest_position: Vec3A) -> Self {
67        Self {
68            parent,
69            rest_position,
70            transform_order: 0,
71            transform_after_physics: false,
72            fixed_axis: None,
73            local_axis: None,
74        }
75    }
76}
77
78/// One link in an IK chain.
79#[derive(Clone, Debug, PartialEq)]
80pub struct RuntimeIkLinkDescriptorV1 {
81    pub bone: BoneIndex,
82    pub angle_limit: Option<IkAngleLimit>,
83}
84
85impl RuntimeIkLinkDescriptorV1 {
86    pub fn new(bone: BoneIndex) -> Self {
87        Self {
88            bone,
89            angle_limit: None,
90        }
91    }
92}
93
94/// An IK solver attached to one IK bone.
95#[derive(Clone, Debug, PartialEq)]
96pub struct RuntimeIkSolverDescriptorV1 {
97    pub ik_bone: BoneIndex,
98    pub target_bone: BoneIndex,
99    pub links: Vec<RuntimeIkLinkDescriptorV1>,
100    pub iteration_count: u32,
101    pub limit_angle: f32,
102}
103
104impl RuntimeIkSolverDescriptorV1 {
105    pub fn new(
106        ik_bone: BoneIndex,
107        target_bone: BoneIndex,
108        links: Vec<RuntimeIkLinkDescriptorV1>,
109    ) -> Self {
110        Self {
111            ik_bone,
112            target_bone,
113            links,
114            iteration_count: 1,
115            limit_angle: 0.0,
116        }
117    }
118}
119
120/// An append transform (rotation and/or translation, optionally in local
121/// space).  A target may have at most one append transform.
122#[derive(Clone, Debug, PartialEq)]
123pub struct RuntimeAppendTransformDescriptorV1 {
124    pub target_bone: BoneIndex,
125    pub source_bone: BoneIndex,
126    pub ratio: f32,
127    pub affect_rotation: bool,
128    pub affect_translation: bool,
129    pub local: bool,
130}
131
132impl RuntimeAppendTransformDescriptorV1 {
133    pub fn new(target_bone: BoneIndex, source_bone: BoneIndex, ratio: f32) -> Self {
134        Self {
135            target_bone,
136            source_bone,
137            ratio,
138            affect_rotation: false,
139            affect_translation: false,
140            local: false,
141        }
142    }
143}
144
145/// Bone and group morph offsets.  Offsets are grouped by `morph_index` by the
146/// compiler and exposed in `ModelArena` through spans.
147#[derive(Clone, Debug, Default, PartialEq)]
148pub struct RuntimeMorphDescriptorV1 {
149    pub morph_count: u32,
150    pub bone_offsets: Vec<RuntimeBoneMorphOffsetDescriptorV1>,
151    pub group_offsets: Vec<RuntimeGroupMorphOffsetDescriptorV1>,
152}
153
154#[derive(Clone, Debug, PartialEq)]
155pub struct RuntimeBoneMorphOffsetDescriptorV1 {
156    pub morph_index: MorphIndex,
157    pub target_bone: BoneIndex,
158    pub position_offset: Vec3A,
159    pub rotation_offset: Quat,
160}
161
162#[derive(Clone, Debug, PartialEq)]
163pub struct RuntimeGroupMorphOffsetDescriptorV1 {
164    pub morph_index: MorphIndex,
165    pub child_morph: MorphIndex,
166    pub ratio: f32,
167}
168
169/// Detailed validation failure.  `path` always identifies the offending
170/// descriptor field, including its zero-based array index where applicable.
171#[derive(Clone, Debug, Error, PartialEq, Eq)]
172#[error("{path}: {kind}")]
173pub struct RuntimeModelDescriptorError {
174    pub path: String,
175    pub kind: RuntimeModelDescriptorErrorKind,
176}
177
178#[derive(Clone, Debug, Error, PartialEq, Eq)]
179pub enum RuntimeModelDescriptorErrorKind {
180    #[error("descriptor version must be {expected}, got {actual}")]
181    UnsupportedVersion { expected: u32, actual: u32 },
182    #[error("model must contain at least one bone")]
183    EmptyBones,
184    #[error("index {value} is out of range for length {length}")]
185    IndexOutOfRange { value: u32, length: usize },
186    #[error("parent cannot reference itself")]
187    SelfParent,
188    #[error("parent hierarchy contains a cycle")]
189    ParentCycle,
190    #[error("value is not finite")]
191    NonFinite,
192    #[error("axis is zero-length or otherwise degenerate")]
193    DegenerateAxis,
194    #[error("quaternion is zero-length or otherwise degenerate")]
195    DegenerateQuaternion,
196    #[error("minimum must not exceed maximum")]
197    InvalidRange,
198    #[error("iteration count must be greater than zero")]
199    InvalidIterationCount,
200    #[error("limit angle must be finite and non-negative")]
201    InvalidLimitAngle,
202    #[error("append ratio must be finite")]
203    InvalidAppendRatio,
204    #[error("append target is already used by another append transform")]
205    DuplicateAppendTarget,
206    #[error("append requires rotation and/or translation")]
207    InvalidAppendFlags,
208    #[error("morph count is zero but morph offsets are present")]
209    EmptyMorphSet,
210    #[error("morph group graph contains a cycle")]
211    GroupMorphCycle,
212    #[error("model arena rejected normalized payload: {0}")]
213    ModelBuild(String),
214    #[error("descriptor storage allocation failed")]
215    AllocationFailed,
216}
217
218impl RuntimeModelDescriptorError {
219    fn new(path: impl Into<String>, kind: RuntimeModelDescriptorErrorKind) -> Self {
220        Self {
221            path: path.into(),
222            kind,
223        }
224    }
225}
226
227/// Compile a validated v1 descriptor into the immutable runtime arena.
228pub fn compile_runtime_model_descriptor_v1(
229    descriptor: &RuntimeModelDescriptorV1,
230) -> Result<ModelArena, RuntimeModelDescriptorError> {
231    if descriptor.descriptor_version != RUNTIME_MODEL_DESCRIPTOR_VERSION_V1 {
232        return Err(RuntimeModelDescriptorError::new(
233            "descriptor_version",
234            RuntimeModelDescriptorErrorKind::UnsupportedVersion {
235                expected: RUNTIME_MODEL_DESCRIPTOR_VERSION_V1,
236                actual: descriptor.descriptor_version,
237            },
238        ));
239    }
240    if descriptor.bones.is_empty() {
241        return Err(RuntimeModelDescriptorError::new(
242            "bones",
243            RuntimeModelDescriptorErrorKind::EmptyBones,
244        ));
245    }
246
247    let bone_count = descriptor.bones.len();
248    validate_bones(&descriptor.bones)?;
249    validate_ik_solvers(&descriptor.ik_solvers, bone_count)?;
250    validate_append_transforms(&descriptor.append_transforms, bone_count)?;
251
252    let mut bones = Vec::with_capacity(bone_count);
253    let mut local_axes = Vec::with_capacity(bone_count);
254    for (bone_index, descriptor_bone) in descriptor.bones.iter().enumerate() {
255        let parent = descriptor_bone.parent;
256        let absolute_position = descriptor_bone.rest_position;
257        let rest_position = parent
258            .map(|index| absolute_position - descriptor.bones[index.as_usize()].rest_position)
259            .unwrap_or(absolute_position);
260        validate_vec3(format!("bones[{bone_index}].rest_position"), rest_position)?;
261        bones.push(BoneInit {
262            parent,
263            rest_position,
264            inverse_bind_matrix: glam::Mat4::from_translation((-absolute_position).into()),
265            transform_order: descriptor_bone.transform_order,
266            transform_after_physics: descriptor_bone.transform_after_physics,
267            fixed_axis: descriptor_bone.fixed_axis,
268            // PMX fixed axis is metadata and an IK constraint; ordinary local
269            // pose evaluation must not project rotations onto it.
270            enforce_fixed_axis: false,
271        });
272        local_axes.push(descriptor_bone.local_axis);
273    }
274
275    let ik_solvers = descriptor
276        .ik_solvers
277        .iter()
278        .map(|solver| IkSolverInit {
279            ik_bone: solver.ik_bone,
280            target_bone: solver.target_bone,
281            links: solver
282                .links
283                .iter()
284                .map(|link| IkLinkInit {
285                    bone: link.bone,
286                    angle_limit: link.angle_limit,
287                })
288                .collect(),
289            iteration_count: solver.iteration_count,
290            limit_angle: solver.limit_angle,
291        })
292        .collect();
293
294    let append_transforms = descriptor
295        .append_transforms
296        .iter()
297        .map(|append| AppendTransformInit {
298            target_bone: append.target_bone,
299            source_bone: append.source_bone,
300            ratio: append.ratio,
301            affect_rotation: append.affect_rotation,
302            affect_translation: append.affect_translation,
303            local: append.local,
304        })
305        .collect();
306
307    let morph = compile_morphs(&descriptor.morphs, bone_count)?;
308    let model = ModelArena::new_with_morphs(bones, ik_solvers, append_transforms, morph).map_err(
309        |error| match error {
310            crate::ModelBuildError::ParentCycle { bone } => RuntimeModelDescriptorError::new(
311                format!("bones[{bone}].parent"),
312                RuntimeModelDescriptorErrorKind::ParentCycle,
313            ),
314            crate::ModelBuildError::GroupMorphCycle { morph } => RuntimeModelDescriptorError::new(
315                format!("morphs.group_offsets[{morph}].child_morph"),
316                RuntimeModelDescriptorErrorKind::GroupMorphCycle,
317            ),
318            other => RuntimeModelDescriptorError::new(
319                "model",
320                RuntimeModelDescriptorErrorKind::ModelBuild(other.to_string()),
321            ),
322        },
323    )?;
324    Ok(model.with_local_axes(local_axes))
325}
326
327fn validate_bones(bones: &[RuntimeBoneDescriptorV1]) -> Result<(), RuntimeModelDescriptorError> {
328    for (bone_index, bone) in bones.iter().enumerate() {
329        if let Some(parent) = bone.parent {
330            if parent.as_usize() >= bones.len() {
331                return Err(RuntimeModelDescriptorError::new(
332                    format!("bones[{bone_index}].parent"),
333                    RuntimeModelDescriptorErrorKind::IndexOutOfRange {
334                        value: parent.0,
335                        length: bones.len(),
336                    },
337                ));
338            }
339            if parent.as_usize() == bone_index {
340                return Err(RuntimeModelDescriptorError::new(
341                    format!("bones[{bone_index}].parent"),
342                    RuntimeModelDescriptorErrorKind::SelfParent,
343                ));
344            }
345        }
346        validate_vec3(
347            format!("bones[{bone_index}].rest_position"),
348            bone.rest_position,
349        )?;
350        if let Some(axis) = bone.fixed_axis {
351            validate_axis(format!("bones[{bone_index}].fixed_axis"), axis)?;
352        }
353        if let Some(axis) = bone.local_axis {
354            validate_axis(format!("bones[{bone_index}].local_axis.x"), axis.x)?;
355            validate_axis(format!("bones[{bone_index}].local_axis.z"), axis.z)?;
356            if axis.basis_quat().is_none() {
357                return Err(RuntimeModelDescriptorError::new(
358                    format!("bones[{bone_index}].local_axis"),
359                    RuntimeModelDescriptorErrorKind::DegenerateAxis,
360                ));
361            }
362        }
363    }
364
365    Ok(())
366}
367
368fn validate_ik_solvers(
369    solvers: &[RuntimeIkSolverDescriptorV1],
370    bone_count: usize,
371) -> Result<(), RuntimeModelDescriptorError> {
372    for (solver_index, solver) in solvers.iter().enumerate() {
373        validate_bone_index(
374            format!("ik_solvers[{solver_index}].ik_bone"),
375            solver.ik_bone,
376            bone_count,
377        )?;
378        validate_bone_index(
379            format!("ik_solvers[{solver_index}].target_bone"),
380            solver.target_bone,
381            bone_count,
382        )?;
383        if solver.iteration_count == 0 {
384            return Err(RuntimeModelDescriptorError::new(
385                format!("ik_solvers[{solver_index}].iteration_count"),
386                RuntimeModelDescriptorErrorKind::InvalidIterationCount,
387            ));
388        }
389        if !solver.limit_angle.is_finite() || solver.limit_angle < 0.0 {
390            return Err(RuntimeModelDescriptorError::new(
391                format!("ik_solvers[{solver_index}].limit_angle"),
392                RuntimeModelDescriptorErrorKind::InvalidLimitAngle,
393            ));
394        }
395        for (link_index, link) in solver.links.iter().enumerate() {
396            validate_bone_index(
397                format!("ik_solvers[{solver_index}].links[{link_index}].bone"),
398                link.bone,
399                bone_count,
400            )?;
401            if let Some(limit) = link.angle_limit {
402                let min_path =
403                    format!("ik_solvers[{solver_index}].links[{link_index}].angle_limit.min");
404                let max_path =
405                    format!("ik_solvers[{solver_index}].links[{link_index}].angle_limit.max");
406                validate_vec3(min_path, limit.min)?;
407                validate_vec3(max_path, limit.max)?;
408                if (limit.min.cmple(limit.max)).bitmask() != 0b111 {
409                    return Err(RuntimeModelDescriptorError::new(
410                        format!("ik_solvers[{solver_index}].links[{link_index}].angle_limit"),
411                        RuntimeModelDescriptorErrorKind::InvalidRange,
412                    ));
413                }
414            }
415        }
416    }
417    Ok(())
418}
419
420fn validate_append_transforms(
421    appends: &[RuntimeAppendTransformDescriptorV1],
422    bone_count: usize,
423) -> Result<(), RuntimeModelDescriptorError> {
424    let mut targets = std::collections::HashSet::with_capacity(appends.len());
425    for (append_index, append) in appends.iter().enumerate() {
426        validate_bone_index(
427            format!("append_transforms[{append_index}].target_bone"),
428            append.target_bone,
429            bone_count,
430        )?;
431        validate_bone_index(
432            format!("append_transforms[{append_index}].source_bone"),
433            append.source_bone,
434            bone_count,
435        )?;
436        if !append.ratio.is_finite() {
437            return Err(RuntimeModelDescriptorError::new(
438                format!("append_transforms[{append_index}].ratio"),
439                RuntimeModelDescriptorErrorKind::InvalidAppendRatio,
440            ));
441        }
442        if !append.affect_rotation && !append.affect_translation {
443            return Err(RuntimeModelDescriptorError::new(
444                format!("append_transforms[{append_index}]"),
445                RuntimeModelDescriptorErrorKind::InvalidAppendFlags,
446            ));
447        }
448        if !targets.insert(append.target_bone) {
449            return Err(RuntimeModelDescriptorError::new(
450                format!("append_transforms[{append_index}].target_bone"),
451                RuntimeModelDescriptorErrorKind::DuplicateAppendTarget,
452            ));
453        }
454    }
455    Ok(())
456}
457
458fn compile_morphs(
459    descriptor: &RuntimeMorphDescriptorV1,
460    bone_count: usize,
461) -> Result<MorphInit, RuntimeModelDescriptorError> {
462    let morph_count = descriptor.morph_count as usize;
463    if morph_count == 0
464        && (!descriptor.bone_offsets.is_empty() || !descriptor.group_offsets.is_empty())
465    {
466        return Err(RuntimeModelDescriptorError::new(
467            "morphs.morph_count",
468            RuntimeModelDescriptorErrorKind::EmptyMorphSet,
469        ));
470    }
471
472    let mut bone_offsets = Vec::new();
473    bone_offsets
474        .try_reserve_exact(descriptor.bone_offsets.len())
475        .map_err(|_| allocation_error("morphs.bone_offsets"))?;
476    for (offset_index, offset) in descriptor.bone_offsets.iter().enumerate() {
477        validate_morph_index(
478            format!("morphs.bone_offsets[{offset_index}].morph_index"),
479            offset.morph_index,
480            morph_count,
481        )?;
482        validate_bone_index(
483            format!("morphs.bone_offsets[{offset_index}].target_bone"),
484            offset.target_bone,
485            bone_count,
486        )?;
487        validate_vec3(
488            format!("morphs.bone_offsets[{offset_index}].position_offset"),
489            offset.position_offset,
490        )?;
491        bone_offsets.push((
492            offset_index,
493            offset.morph_index,
494            BoneMorphOffset {
495                target_bone: offset.target_bone,
496                position_offset: offset.position_offset,
497                rotation_offset: validate_quaternion(
498                    format!("morphs.bone_offsets[{offset_index}].rotation_offset"),
499                    offset.rotation_offset,
500                )?,
501            },
502        ));
503    }
504
505    let mut group_offsets = Vec::new();
506    group_offsets
507        .try_reserve_exact(descriptor.group_offsets.len())
508        .map_err(|_| allocation_error("morphs.group_offsets"))?;
509    for (offset_index, offset) in descriptor.group_offsets.iter().enumerate() {
510        validate_morph_index(
511            format!("morphs.group_offsets[{offset_index}].morph_index"),
512            offset.morph_index,
513            morph_count,
514        )?;
515        validate_morph_index(
516            format!("morphs.group_offsets[{offset_index}].child_morph"),
517            offset.child_morph,
518            morph_count,
519        )?;
520        if !offset.ratio.is_finite() {
521            return Err(RuntimeModelDescriptorError::new(
522                format!("morphs.group_offsets[{offset_index}].ratio"),
523                RuntimeModelDescriptorErrorKind::NonFinite,
524            ));
525        }
526        group_offsets.push((
527            offset_index,
528            offset.morph_index,
529            GroupMorphOffset {
530                child_morph: offset.child_morph,
531                ratio: offset.ratio,
532            },
533        ));
534    }
535
536    crate::model::build_morph_init_from_indexed_offsets(
537        descriptor.morph_count,
538        bone_offsets,
539        group_offsets,
540    )
541    .map_err(|error| match error {
542        crate::ModelBuildError::GroupMorphCycleAt { offset, .. } => {
543            RuntimeModelDescriptorError::new(
544                format!("morphs.group_offsets[{offset}].child_morph"),
545                RuntimeModelDescriptorErrorKind::GroupMorphCycle,
546            )
547        }
548        crate::ModelBuildError::MorphCountZeroWithData => RuntimeModelDescriptorError::new(
549            "morphs.morph_count",
550            RuntimeModelDescriptorErrorKind::EmptyMorphSet,
551        ),
552        crate::ModelBuildError::MorphStorageAllocation => allocation_error("morphs"),
553        other => RuntimeModelDescriptorError::new(
554            "morphs",
555            RuntimeModelDescriptorErrorKind::ModelBuild(other.to_string()),
556        ),
557    })
558}
559
560fn allocation_error(path: impl Into<String>) -> RuntimeModelDescriptorError {
561    RuntimeModelDescriptorError::new(path, RuntimeModelDescriptorErrorKind::AllocationFailed)
562}
563
564fn validate_bone_index(
565    path: String,
566    value: BoneIndex,
567    length: usize,
568) -> Result<(), RuntimeModelDescriptorError> {
569    if value.as_usize() >= length {
570        return Err(RuntimeModelDescriptorError::new(
571            path,
572            RuntimeModelDescriptorErrorKind::IndexOutOfRange {
573                value: value.0,
574                length,
575            },
576        ));
577    }
578    Ok(())
579}
580
581fn validate_morph_index(
582    path: String,
583    value: MorphIndex,
584    length: usize,
585) -> Result<(), RuntimeModelDescriptorError> {
586    if value.as_usize() >= length {
587        return Err(RuntimeModelDescriptorError::new(
588            path,
589            RuntimeModelDescriptorErrorKind::IndexOutOfRange {
590                value: value.0,
591                length,
592            },
593        ));
594    }
595    Ok(())
596}
597
598fn validate_vec3(path: String, value: Vec3A) -> Result<(), RuntimeModelDescriptorError> {
599    if !value.is_finite() {
600        return Err(RuntimeModelDescriptorError::new(
601            path,
602            RuntimeModelDescriptorErrorKind::NonFinite,
603        ));
604    }
605    Ok(())
606}
607
608fn validate_axis(path: String, value: Vec3A) -> Result<(), RuntimeModelDescriptorError> {
609    validate_vec3(path.clone(), value)?;
610    let length_squared = value.length_squared();
611    if !length_squared.is_finite() || length_squared <= f32::EPSILON {
612        return Err(RuntimeModelDescriptorError::new(
613            path.clone(),
614            RuntimeModelDescriptorErrorKind::DegenerateAxis,
615        ));
616    }
617    let normalized = value.normalize();
618    if !normalized.is_finite()
619        || !normalized.length_squared().is_finite()
620        || normalized.length_squared() <= f32::EPSILON
621    {
622        return Err(RuntimeModelDescriptorError::new(
623            path,
624            RuntimeModelDescriptorErrorKind::DegenerateAxis,
625        ));
626    }
627    Ok(())
628}
629
630fn validate_quaternion(path: String, value: Quat) -> Result<Quat, RuntimeModelDescriptorError> {
631    if !value.is_finite() {
632        return Err(RuntimeModelDescriptorError::new(
633            path,
634            RuntimeModelDescriptorErrorKind::NonFinite,
635        ));
636    }
637    let length_squared = value.length_squared();
638    if !length_squared.is_finite() || length_squared <= f32::EPSILON {
639        return Err(RuntimeModelDescriptorError::new(
640            path,
641            RuntimeModelDescriptorErrorKind::DegenerateQuaternion,
642        ));
643    }
644    let normalized = value.normalize();
645    if !normalized.is_finite() || normalized.length_squared() <= f32::EPSILON {
646        return Err(RuntimeModelDescriptorError::new(
647            path,
648            RuntimeModelDescriptorErrorKind::DegenerateQuaternion,
649        ));
650    }
651    Ok(normalized)
652}
653
654impl fmt::Display for RuntimeModelDescriptorV1 {
655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656        f.debug_struct("RuntimeModelDescriptorV1")
657            .field("descriptor_version", &self.descriptor_version)
658            .field("bones", &self.bones.len())
659            .field("ik_solvers", &self.ik_solvers.len())
660            .field("append_transforms", &self.append_transforms.len())
661            .field("morph_count", &self.morphs.morph_count)
662            .finish()
663    }
664}