import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; import { BoxPrimitive, CapsulePrimitive, ConePrimitive, CylinderPrimitive, SpherePrimitive, TorusPrimitive, WedgePrimitive, HumanoidUnitAssembler, getHumanoidPartCatalog, getDefaultMaterialForSlot, MATERIAL_TYPE_PRESETS, MaterialSlot, type HumanoidPartDefinition, type ProceduralMesh } from '@procgen/proceduralGen/index.js'; import { getHumanoidVolumeRecipe, resolveHumanoidAnchorSpec, type HumanoidPlacement, type HumanoidAnchorSpec } from '@procgen/proceduralGen/humanoid/HumanoidRecipeLibrary.js'; import { getTemplateForUnit } from '@procgen/proceduralGen/UnitVolumeTemplates.js'; import { proceduralMeshToThreeGeometry } from './render/ProceduralMeshToThree'; import { sdfBox, sdfCapsule, sdfRoundedBox, sdfSphere } from '@procgen/proceduralGen/sdf/SdfPrimitives.js'; import { smoothUnion, subtract, union, translate } from '@procgen/proceduralGen/sdf/SdfOps.js'; import type { SdfFn } from '@procgen/proceduralGen/sdf/SdfTypes.js'; import { buildLeaperArcUnitEditorPreviewIntoLayers } from './home-hero-streamlined-target-decor'; import { LEAPER_ARC_EDITOR_CORE_NODES, LEAPER_ARC_EDITOR_SKELETON, LEAPER_ARC_SHARED_LEG_SHELL } from './leaper-arc-shared-anatomy'; import { buildLeaperArcAuthoredLegAttachmentDefs } from './leaper-arc-authored-leg-data'; import { sampleLeaperArcPreviewMotion, type LeaperArcPreviewMotionPreset, type LeaperArcPreviewMotionSample } from './leaper-arc-preview-motion'; import { LEAPER_ARC_TERRAIN_IK_DEFAULT_LIMITS, solveLeaperArcTerrainIk, type LeaperArcTerrainIkLegId } from './leaper-arc-terrain-ik-shared'; import { LEAPER_ARC_TERRAIN_IK_UNIT_EDITOR_PROFILE, buildLeaperArcTerrainIkUnitEditorConfig } from './leaper-arc-terrain-ik-unit-editor-profile'; import type { TargetAssemblyLayers } from './home-hero-streamlined-target-rigs'; import { getFlipTargetSharedVariant, type FlipTargetAnimationTrigger, type FlipTargetLightTrigger, type FlipTargetNode, type FlipTargetSharedVariant, type FlipTargetSoundTrigger, type FlipTargetSocket } from './targets/flipTargetShared'; const STORAGE_KEY = 'rts.unitEditorV2.recipe'; const STORAGE_BASE_PRESET_KEY = 'rts.unitEditorV2.basePreset'; const EDGE_LUT_SAMPLES = 160; const EDGE_FRAME_SAMPLES = 120; const DEFAULT_NODE_RADIUS = 0.05; const DEFAULT_ANCHOR_RADIUS = 0.04; const POSE_MANIFEST_URL = 'poses/poses.json'; const POSE_BASE_RIG_CANDIDATES: readonly string[] = ['Y Bot.fbx', 'y bot.fbx']; const WEAPON_ARCHETYPE_POSE_PREFERENCES: Record = { rifle: [ 'Rifle_Medium_mixamo.fbx', 'Rifle_Light_mixamo.fbx', 'Rifle_Heavy_mixamo.fbx', 'RifleFight_mixamo.fbx' ], marksman: [ 'Rifle_Medium_mixamo.fbx', 'Rifle_Light_mixamo.fbx', 'Rifle_Heavy_mixamo.fbx', 'RifleFight_mixamo.fbx' ], beam: [ 'Rifle_Light_mixamo.fbx', 'Pistol_Medium_mixamo.fbx', 'GunFu_JohnWick_mixamo.fbx' ], heavy: [ 'Rifle_Heavy_mixamo.fbx', 'ShotgunFight_mixamo.fbx', 'RPGLauncher_mixamo.fbx', 'Pistol_Heavy_mixamo.fbx' ], launcher: [ 'RPGLauncher_mixamo.fbx', 'Rifle_Heavy_mixamo.fbx', 'ShotgunFight_mixamo.fbx' ], 'forearm-pod': [ 'Pistol_Medium_mixamo.fbx', 'Pistol_Light_mixamo.fbx', 'GunFu_JohnWick_mixamo.fbx' ] }; const GLOBAL_POSE_FALLBACKS: readonly string[] = [ 'Y Bot@Rifle Idle.fbx', 'Y Bot@Rifle Idle (1).fbx', 'Y Bot@Firing Rifle.fbx', 'GunFu_JohnWick_mixamo.fbx', 'Execution_01_mixamo.fbx', 'Male1_A1_Stand.bvh' ]; const YBOT_POSE_ASSET_CANDIDATES: readonly string[] = [ 'Y Bot@Rifle Idle.fbx', 'Y Bot@Rifle Idle (1).fbx', 'Y Bot@Firing Rifle.fbx', 'Y Bot@Firing Rifle (1).fbx', 'Y Bot@Firing Rifle (2).fbx', 'Y Bot@Rifle Aim To Down.fbx', 'Y Bot.fbx' ]; const AAA_RIFLE_ROLE_ORDER = [ 'idle', 'aimIdle', 'fireShot', 'crouchAim', 'proneAim', 'walk', 'run', 'strafeLeft', 'strafeRight', 'turnLeft', 'turnRight', 'turnAround', 'reload', 'raise', 'lower' ] as const; type AaaRiflePoseRole = (typeof AAA_RIFLE_ROLE_ORDER)[number]; type AaaRiflePoseRoleSpec = { exact: readonly string[]; tokenSets: readonly (readonly string[])[]; }; type AaaRiflePoseLibrary = { roles: Partial>; missing: AaaRiflePoseRole[]; coverage: number; }; const AAA_RIFLE_ROLE_LABELS: Record = { idle: 'Idle', aimIdle: 'Aim Idle', fireShot: 'Fire', crouchAim: 'Crouch Aim', proneAim: 'Prone Aim', walk: 'Walk', run: 'Run', strafeLeft: 'Strafe Left', strafeRight: 'Strafe Right', turnLeft: 'Turn Left', turnRight: 'Turn Right', turnAround: 'Turn Around', reload: 'Reload', raise: 'Raise To Aim', lower: 'Lower From Aim' }; const AAA_RIFLE_ROLE_SPECS: Record = { idle: { exact: ['Rifle Idle.fbx', 'Y Bot@Rifle Idle.fbx', 'Y Bot@Rifle Idle (1).fbx', 'idle.fbx'], tokenSets: [['rifle', 'idle'], ['idle']] }, aimIdle: { exact: ['Rifle Aiming Idle.fbx', 'idle aiming.fbx', 'idle crouching aiming.fbx'], tokenSets: [['rifle', 'aim', 'idle'], ['aiming', 'idle'], ['aim', 'idle']] }, fireShot: { exact: ['Y Bot@Firing Rifle.fbx', 'Prone Firing Rifle.fbx', 'RifleFight_mixamo.fbx'], tokenSets: [['firing', 'rifle'], ['fire', 'rifle'], ['riflefight']] }, crouchAim: { exact: ['idle crouching aiming.fbx'], tokenSets: [['crouch', 'aim'], ['crouching', 'aim']] }, proneAim: { exact: ['Prone Firing Rifle.fbx'], tokenSets: [['prone', 'rifle'], ['prone', 'fire']] }, walk: { exact: ['Male1_B3_Walk.bvh'], tokenSets: [['walk']] }, run: { exact: ['Male1_C03_Run.bvh'], tokenSets: [['run']] }, strafeLeft: { exact: ['Male1_B22_SideStepLeft.bvh', 'Male1_C24_QuickSideStepLeft.bvh'], tokenSets: [['sidestep', 'left'], ['quicksidestep', 'left'], ['strafe', 'left']] }, strafeRight: { exact: ['Male1_B23_SideStepRight.bvh', 'Male1_C25_QuickSideStepRight.bvh'], tokenSets: [['sidestep', 'right'], ['quicksidestep', 'right'], ['strafe', 'right']] }, turnLeft: { exact: ['Male1_B10_WalkTurnLeft45.bvh', 'Male1_B9_WalkToTurnLeft90.bvh', 'Male1_C11_RunTurnLeft90.bvh'], tokenSets: [['turn', 'left'], ['left', '90'], ['left', '45']] }, turnRight: { exact: ['Male1_B12_WalkTurnRight90.bvh', 'Male1_B13_WalkTurnRight45.bvh', 'Male1_C14_RunTurnRight90.bvh'], tokenSets: [['turn', 'right'], ['right', '90'], ['right', '45']] }, turnAround: { exact: ['Male1_B15_WalkTurnAround.bvh', 'Male1_C17_RunTurnAround.bvh'], tokenSets: [['turnaround'], ['turn', 'around']] }, reload: { exact: [], tokenSets: [['reload', 'rifle'], ['reload']] }, raise: { exact: [], tokenSets: [['raise', 'rifle'], ['ready', 'rifle'], ['aim', 'up']] }, lower: { exact: [], tokenSets: [['lower', 'rifle'], ['aim', 'down']] } }; const EDGE_RENDER_SAMPLES = 64; const EDGE_RENDER_SAMPLES_DRAG = 20; const GENERATOR_AXIS_LENGTH = 0.08; const ATTACHMENT_AXIS_LENGTH = 0.06; const ATTACHMENT_LOD_SEGMENT_SCALE = 0.55; const ATTACHMENT_LOD_MIN_SEGMENTS = 6; const ATTACHMENT_LOD_MIN_RINGS = 4; const MAX_EDITOR_PIXEL_RATIO = 1.5; const IDLE_RENDER_KEEPALIVE_MS = 250; const DIRTY_RENDER_FRAMES = 2; const LAYER_NODE = 1; const LAYER_EDGE = 2; const LAYER_ANCHOR = 3; const LAYER_GENERATOR = 4; const LAYER_ATTACHMENT = 5; const LAYER_SCENE_PROP = 6; const UI_REFRESH_DEBOUNCE_MS = 16; const RECIPE_PREVIEW_DEBOUNCE_MS = 140; const IDLE_BOOT_TIMEOUT_MS = 120; const POSE_AVATAR_TARGET_HEIGHT = 1.8; const FLIP_TARGET_SHARED_TO_EDITOR_TARGET_ID: Record = { 'pivot-root': 'attach-target-face', 'target-face': 'attach-target-face', 'sensor-head': 'attach-target-sensor', 'sensor-mast': 'attach-sensor-mast', 'bull-mark': 'attach-target-bull', 'face-rib-top': 'attach-face-rib-top', 'face-rib-bottom': 'attach-face-rib-bottom', 'sensor-shroud': 'attach-sensor-shroud', 'face-lock-top': 'attach-face-lock-top', 'face-lock-bottom': 'attach-face-lock-bottom', 'yoke-left': 'attach-yoke-left', 'yoke-right': 'attach-yoke-right', 'beacon-arm-left': 'attach-beacon-arm-left', 'beacon-arm-right': 'attach-beacon-arm-right', 'beacon-pod-left': 'attach-beacon-pod-left', 'beacon-pod-right': 'attach-beacon-pod-right', 'counter-fin': 'attach-counter-fin', 'counterweight-main': 'attach-counterweight', 'counterweight-drum': 'attach-counterweight-drum', 'service-box': 'attach-face-backbox', 'actuator-link': 'attach-actuator-rod', 'face-visor-top': 'attach-face-visor-top', 'face-skirt-bottom': 'attach-face-skirt-bottom', 'armored-cheek-left': 'attach-armored-cheek-left', 'armored-cheek-right': 'attach-armored-cheek-right', 'visor-cheek-link-left': 'attach-visor-cheek-link-left', 'visor-cheek-link-right': 'attach-visor-cheek-link-right', 'skirt-lock-left': 'attach-skirt-lock-left', 'skirt-lock-right': 'attach-skirt-lock-right', 'counterweight-left': 'attach-counterweight-left', 'counterweight-right': 'attach-counterweight-right', 'counterweight-bridge': 'attach-counterweight-bridge', 'breach-bar-top': 'attach-breach-bar-top', 'breach-bar-mid': 'attach-breach-bar-mid', 'breach-bar-bottom': 'attach-breach-bar-bottom', 'lock-dog-left': 'attach-lock-dog-left', 'lock-dog-right': 'attach-lock-dog-right', 'warning-beacon': 'attach-warning-beacon' }; const FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID: Record = { pivot: 'anchor-pivot', 'base-top': 'anchor-base-top', 'face-front': 'anchor-face-front', 'face-back': 'anchor-face-back', 'face-left': 'anchor-face-left', 'face-right': 'anchor-face-right', 'sensor-top': 'anchor-sensor-top' }; const flipTargetNodeRotationToDegrees = (node?: FlipTargetNode): Vec3Like | undefined => { if (!node?.rotation) return undefined; return { x: THREE.MathUtils.radToDeg(node.rotation[0]), y: THREE.MathUtils.radToDeg(node.rotation[1]), z: THREE.MathUtils.radToDeg(node.rotation[2]) }; }; const getFlipTargetSharedNode = ( variantId: FlipTargetSharedVariant['id'], nodeId: string ): FlipTargetNode => { const variant = getFlipTargetSharedVariant(variantId); const node = variant.nodes.find((entry) => entry.id === nodeId); if (!node) { throw new Error(`Missing shared flip-target node ${nodeId} for ${variantId}.`); } return node; }; const getFlipTargetSharedSocket = ( variantId: FlipTargetSharedVariant['id'], socketId: string ): FlipTargetSocket => { const variant = getFlipTargetSharedVariant(variantId); const socket = variant.sockets.find((entry) => entry.id === socketId); if (!socket) { throw new Error(`Missing shared flip-target socket ${socketId} for ${variantId}.`); } return socket; }; const mapSharedFlipTargetAnimationTrigger = ( trigger: FlipTargetAnimationTrigger ): RuntimeTargetAnimationTrigger | null => { const targetId = FLIP_TARGET_SHARED_TO_EDITOR_TARGET_ID[trigger.targetId]; if (!targetId) return null; return { id: trigger.id, event: trigger.event as RuntimeTargetTriggerEvent, action: trigger.action as RuntimeTargetTriggerAction, targetId, axis: trigger.axis, amplitude: trigger.amplitude, durationMs: trigger.durationMs, delayMs: trigger.delayMs, profile: trigger.profile as RuntimeTargetMotionProfile | undefined }; }; const mapSharedFlipTargetLightTrigger = ( trigger: FlipTargetLightTrigger ): RuntimeTargetLightTrigger | null => { const socketId = FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID[trigger.socketId]; if (!socketId) return null; return { id: trigger.id, event: trigger.event as RuntimeTargetTriggerEvent, socketId, color: trigger.color, intensity: trigger.intensity, radius: trigger.radius, durationMs: trigger.durationMs, delayMs: trigger.delayMs, flicker: trigger.flicker }; }; const mapSharedFlipTargetSoundTrigger = ( trigger: FlipTargetSoundTrigger ): RuntimeTargetSoundTrigger => ({ id: trigger.id, event: trigger.event as RuntimeTargetTriggerEvent, cue: trigger.cue as RuntimeTargetSoundCue, volume: trigger.volume, durationMs: trigger.durationMs, delayMs: trigger.delayMs, frequencyHz: trigger.frequencyHz }); type IdleTaskDeadline = { didTimeout: boolean; timeRemaining: () => number; }; function scheduleIdleTask(task: (deadline: IdleTaskDeadline) => void, timeout = IDLE_BOOT_TIMEOUT_MS): void { const idleWindow = window as Window & { requestIdleCallback?: ( callback: (deadline: IdleTaskDeadline) => void, options?: { timeout?: number } ) => number; }; if (typeof idleWindow.requestIdleCallback === 'function') { idleWindow.requestIdleCallback(task, { timeout }); return; } window.setTimeout(() => { task({ didTimeout: false, timeRemaining: () => 0 }); }, 16); } type MarksmanIterationModule = typeof import('./unit-editor/marksman/MarksmanIterationCore.js'); type RifleIterationModule = typeof import('./unit-editor/rifle/RifleIterationCore.js'); type HeavyIterationModule = typeof import('./unit-editor/heavy/HeavyIterationCore.js'); type WeaponIterationModules = { marksman: MarksmanIterationModule; rifle: RifleIterationModule; heavy: HeavyIterationModule; }; let weaponIterationModules: WeaponIterationModules | null = null; let weaponIterationModulesPromise: Promise | null = null; let weaponIterationHydrationQueued = false; type SdfMesherModule = typeof import('@procgen/proceduralGen/sdf/SdfMesher.js'); let sdfMesherModule: SdfMesherModule | null = null; let sdfMesherPromise: Promise | null = null; let sdfMesherHydrationQueued = false; async function ensureWeaponIterationModulesLoaded(): Promise { if (weaponIterationModules) { return weaponIterationModules; } if (!weaponIterationModulesPromise) { weaponIterationModulesPromise = Promise.all([ import('./unit-editor/marksman/MarksmanIterationCore.js'), import('./unit-editor/rifle/RifleIterationCore.js'), import('./unit-editor/heavy/HeavyIterationCore.js') ]).then(([marksman, rifle, heavy]) => ({ marksman, rifle, heavy })); } weaponIterationModules = await weaponIterationModulesPromise; return weaponIterationModules; } function queueWeaponIterationHydration(): void { if (weaponIterationHydrationQueued) return; weaponIterationHydrationQueued = true; scheduleIdleTask(() => { void ensureWeaponIterationModulesLoaded() .then((modules) => { hydratePromotedWeaponSpecs(modules); if (!bootHydrationInProgress) { refreshUi(); } }) .catch((error) => { console.warn('[UnitEditorV2] Deferred weapon iteration module load failed.', error); }) .finally(() => { weaponIterationHydrationQueued = false; }); }); } async function ensureSdfMesherLoaded(): Promise { if (sdfMesherModule) { return sdfMesherModule; } if (!sdfMesherPromise) { sdfMesherPromise = import('@procgen/proceduralGen/sdf/SdfMesher.js'); } sdfMesherModule = await sdfMesherPromise; return sdfMesherModule; } function queueSdfMesherHydration(): void { if (sdfMesherHydrationQueued || sdfMesherModule) return; sdfMesherHydrationQueued = true; scheduleIdleTask(() => { void ensureSdfMesherLoaded() .then(() => { if (!bootHydrationInProgress) { scheduleIdleTask(() => { rebuildDerivedGroups(); }); } }) .catch((error) => { console.warn('[UnitEditorV2] Deferred SDF mesher load failed.', error); }) .finally(() => { sdfMesherHydrationQueued = false; }); }); } function tryMeshSdf( sdf: SdfFn, bounds: { min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } }, options: Record ): ProceduralMesh | null { if (!sdfMesherModule) { return null; } return sdfMesherModule.meshSdf(sdf, bounds, options as any); } type Vec3Like = { x: number; y: number; z: number }; type EdgeCurveType = 'catmull' | 'line'; type OrientationRule = | 'alongEdge' | 'perpendicular' | 'fixedAngle' | 'surfaceNormal' | 'lookAt' | 'hybrid'; type AnchorFace = 'center' | 'top' | 'bottom' | 'left' | 'right' | 'front' | 'back'; type PortProfile = 'any' | 'male' | 'female' | 'rail'; type GeneratorType = 'alongEdge' | 'radial' | 'mirror'; type EditorMode = 'select' | 'add-node' | 'add-edge' | 'surface'; type PartPrimitive = HumanoidPartDefinition['primitive']; type PartSize = HumanoidPartDefinition['size']; type SculptPrimitive = 'box' | 'roundedBox' | 'sphere' | 'capsule' | 'armorPad'; type ShapeProfileKind = 'none' | 'torso' | 'limb' | 'mechLimb' | 'plate' | 'block' | 'hardSurface'; type HumanoidWeaponArchetype = 'rifle' | 'marksman' | 'beam' | 'heavy' | 'launcher' | 'forearm-pod'; type WeaponDesignSectionId = 'mount' | 'rear' | 'core' | 'front' | 'top' | 'bottom' | 'port' | 'starboard' | 'support'; type WeaponQaSubsystem = 'action' | 'optic' | 'magazine'; interface WeaponDesignDoctrine { role: string; readProfile: 'compact' | 'balanced' | 'long'; balance: 'rear-heavy' | 'center-balanced' | 'front-heavy'; visualLanguage: 'service' | 'precision' | 'industrial' | 'energy' | 'integrated'; signature: string[]; } interface WeaponDesignSection { anchorId?: string; origin: Vec3Like; rotation?: Vec3Like; } interface WeaponDesignPlacement { id: string; moduleId: string; section: WeaponDesignSectionId; offset?: Vec3Like; scale?: Vec3Like; rotation?: Vec3Like; planId?: string; plannedProfile?: WeaponPlannedProfile; plannedDimensions?: Vec3Like; plannedCenter?: Vec3Like; silhouetteMaxDimensions?: Vec3Like; } type WeaponPlannedProfile = | 'precision_cradle' | 'precision_stock' | 'precision_receiver_upper' | 'precision_receiver_lower' | 'precision_rail' | 'precision_magwell' | 'precision_magwell_throat' | 'box_magazine' | 'precision_barrel' | 'precision_shroud' | 'precision_cooling_jacket' | 'precision_muzzle' | 'precision_muzzle_brake' | 'precision_optic_tube' | 'precision_optic_clamp' | 'skeletal_optic_stanchion' | 'marksman_cradle' | 'marksman_stock' | 'marksman_rail' | 'marksman_magwell' | 'marksman_magwell_throat' | 'marksman_magazine' | 'marksman_shroud' | 'marksman_cooling_jacket' | 'marksman_optic_clamp' | 'marksman_optic_stanchion' | 'marksman_shoulder_pad' | 'marksman_stock_bridge' | 'marksman_action_cover' | 'marksman_action_spine' | 'marksman_breach' | 'marksman_jacket' | 'marksman_barrel' | 'marksman_receiver_upper' | 'marksman_receiver_lower' | 'marksman_mount_block' | 'marksman_bridge' | 'marksman_saddle' | 'marksman_collar' | 'marksman_throat_collar' | 'marksman_muzzle_collar' | 'marksman_fairing' | 'marksman_brace' | 'marksman_shroud_clamp' | 'marksman_gas_block' | 'marksman_gas_block_saddle' | 'marksman_optic_tube' | 'marksman_optic_ring' | 'marksman_front_sight_base' | 'marksman_front_sight_post' | 'marksman_front_sight' | 'marksman_rear_sight' | 'marksman_optic_lens_front' | 'marksman_optic_lens_rear' | 'marksman_action_port_cover' | 'marksman_recoil_spring' | 'marksman_muzzle' | 'marksman_muzzle_brake' | 'marksman_muzzle_baffle' | 'marksman_muzzle_vent_insert' | 'marksman_counterweight' | 'marksman_rail_clamp' | 'marksman_rear_sight_base' | 'marksman_bolt_housing' | 'marksman_mag_brace' | 'marksman_mag_latch'; type LocalMarksmanWeaponPlannedProfile = | 'marksman_cradle' | 'marksman_stock' | 'marksman_rail' | 'marksman_magwell' | 'marksman_magwell_throat' | 'marksman_magazine' | 'marksman_shroud' | 'marksman_cooling_jacket' | 'marksman_optic_clamp' | 'marksman_optic_stanchion' | 'marksman_shoulder_pad' | 'marksman_stock_bridge' | 'marksman_action_cover' | 'marksman_action_spine' | 'marksman_breach' | 'marksman_jacket' | 'marksman_barrel' | 'marksman_receiver_upper' | 'marksman_receiver_lower' | 'marksman_mount_block' | 'marksman_bridge' | 'marksman_saddle' | 'marksman_collar' | 'marksman_throat_collar' | 'marksman_muzzle_collar' | 'marksman_fairing' | 'marksman_brace' | 'marksman_shroud_clamp' | 'marksman_gas_block' | 'marksman_gas_block_saddle' | 'marksman_optic_tube' | 'marksman_optic_ring' | 'marksman_front_sight_base' | 'marksman_front_sight_post' | 'marksman_front_sight' | 'marksman_rear_sight' | 'marksman_optic_lens_front' | 'marksman_optic_lens_rear' | 'marksman_action_port_cover' | 'marksman_recoil_spring' | 'marksman_muzzle' | 'marksman_muzzle_brake' | 'marksman_muzzle_baffle' | 'marksman_muzzle_vent_insert' | 'marksman_counterweight' | 'marksman_rail_clamp' | 'marksman_rear_sight_base' | 'marksman_bolt_housing' | 'marksman_mag_brace' | 'marksman_mag_latch'; interface WeaponPartPlan { id: string; moduleId: string; section: WeaponDesignSectionId; profile: WeaponPlannedProfile; stationStart: number; stationEnd: number; centerY: number; width?: number; height?: number; centerX?: number; rotation?: Vec3Like; targetDimensions?: Vec3Like; silhouetteMaxDimensions?: Vec3Like; } interface WeaponPlan { id: string; spineStart: number; spineEnd: number; parts: WeaponPartPlan[]; } interface WeaponSubMassRule { id: string; label: string; section: WeaponDesignSectionId; emphasis: 'primary' | 'secondary' | 'support'; expectedPlacementIds: string[]; } interface WeaponTransitionRule { id: string; label: string; section: WeaponDesignSectionId; moduleId: string; style: 'bridge' | 'collar' | 'saddle' | 'fairing' | 'brace'; offset?: Vec3Like; scale?: Vec3Like; rotation?: Vec3Like; influence?: { targetPlacementIds: string[]; scaleMultiplier?: Vec3Like; offsetShift?: Vec3Like; }; } interface WeaponHoldDirectionProfile { leftLateral: number; rightLateral: number; vertical: number; forward: number; } interface WeaponHoldPoleProfile { lateral: number; vertical: number; forward: number; biasLateral: number; biasVertical: number; biasForward: number; } interface WeaponHoldTorsoClearanceProfile { radiiScale: Vec3Like; minRadii: Vec3Like; centerOffsetScale: Vec3Like; padding: number; elbowLateralRatio: number; elbowForwardRatio: number; wristLateralRatio: number; wristForwardRatio: number; } interface WeaponHoldProfile { handDirection: WeaponHoldDirectionProfile; supportPole: WeaponHoldPoleProfile; shoulderOffset: Vec3Like; torsoClearance: WeaponHoldTorsoClearanceProfile; } interface WeaponDesignSpec { archetype: HumanoidWeaponArchetype; doctrine: WeaponDesignDoctrine; holdProfile: WeaponHoldProfile; sections: Record; plan?: WeaponPlan; placements: WeaponDesignPlacement[]; subMassRules: WeaponSubMassRule[]; transitionRules: WeaponTransitionRule[]; } interface WeaponSectionEnvelope { count: number; min: Vec3Like; max: Vec3Like; size: Vec3Like; center: Vec3Like; } interface WeaponArchetypeQaScore { archetype: HumanoidWeaponArchetype; score: number; overCorrection: number; instability: number; changedSections: number; multiHitPlacements: number; } interface NodeEntry { id: string; position: Vec3Like; rotation?: Vec3Like; mode?: 'static' | 'relative'; parentId?: string; poseJoint?: string; tags: string[]; } interface EdgeEntry { id: string; a: string; b: string; radius: number; curve: EdgeCurveType; controlOffset: Vec3Like; } interface AnchorEntry { id: string; type: 'node' | 'edge' | 'surface' | 'attachment'; nodeId?: string; edgeId?: string; attachmentId?: string; attachmentFace?: AnchorFace; s?: number; len?: number; orientation: OrientationRule; radialAngle: number; offset?: Vec3Like; portProfile?: PortProfile; exportSocket?: boolean; tags: string[]; accepts: string[]; surface?: { point: Vec3Like; normal: Vec3Like; parentAttachmentId?: string; localPoint?: Vec3Like; localNormal?: Vec3Like; }; lookAtNodeId?: string; proximalNodeId?: string; distalNodeId?: string; upNodeId?: string; } interface GeneratorParamsAlong { mode: 'count' | 'spacing'; count: number; spacing: number; startLen: number; endLen: number; } interface GeneratorParamsRadial { count: number; radius: number; startAngleDeg: number; axis: 'x' | 'y' | 'z'; } interface GeneratorParamsMirror { axis: 'x' | 'y' | 'z'; offset: number; } type GeneratorParams = GeneratorParamsAlong | GeneratorParamsRadial | GeneratorParamsMirror; interface GeneratorEntry { id: string; type: GeneratorType; baseAnchorId: string; params: GeneratorParams; } interface AttachmentEntry { id: string; anchorId: string; moduleId: string; portId: string; generatorId?: string; generatorIndex?: number; mirrored?: boolean; offset?: Vec3Like; rotation?: Vec3Like; scale?: Vec3Like; shape?: { primitive?: PartPrimitive; plannedWeaponProfile?: WeaponPlannedProfile; plannedWeaponExtrusion?: { points: { u: number; v: number }[]; bevelScale: number; bevelThicknessScale: number; }; size?: PartSize; taper?: { xTop?: number; xBottom?: number; zTop?: number; zBottom?: number; }; chamfer?: { edge?: number; corner?: number; }; profile?: { kind?: ShapeProfileKind; intensity?: number; }; }; sculpt?: { enabled: boolean; primitive: SculptPrimitive; size: PartSize; roundness: number; bulge: { enabled: boolean; radius: number; smooth: number; offset: Vec3Like; }; cut: { enabled: boolean; radius: number; offset: Vec3Like; }; }; } interface Frame { origin: THREE.Vector3; right: THREE.Vector3; up: THREE.Vector3; forward: THREE.Vector3; } interface SocketExport { id: string; frame: { origin: Vec3Like; right: Vec3Like; up: Vec3Like; forward: Vec3Like; }; tags: string[]; } type RuntimeTargetTriggerEvent = 'idle' | 'hit' | 'recover' | 'activate'; type RuntimeTargetTriggerAction = 'rotate' | 'translate' | 'lightPulse'; type RuntimeTargetMotionProfile = 'snap' | 'heavy' | 'servo' | 'rebound'; interface RuntimeTargetAnimationTrigger { id: string; event: RuntimeTargetTriggerEvent; action: RuntimeTargetTriggerAction; targetId: string; axis?: 'x' | 'y' | 'z'; amplitude?: number; durationMs?: number; delayMs?: number; profile?: RuntimeTargetMotionProfile; loop?: boolean; } interface RuntimeTargetLightTrigger { id: string; event: RuntimeTargetTriggerEvent; socketId: string; color: string; intensity: number; radius: number; durationMs: number; delayMs?: number; flicker?: boolean; } type RuntimeTargetSoundCue = 'beep' | 'servo' | 'thump' | 'alarm'; interface RuntimeTargetSoundTrigger { id: string; event: RuntimeTargetTriggerEvent; cue: RuntimeTargetSoundCue; volume: number; durationMs: number; delayMs?: number; frequencyHz?: number; flicker?: boolean; } interface RuntimeTargetMetadata { id: string; category: 'flip-target'; variant: string; hitSurfaceSocketId: string; recoverSocketId: string; pivotSocketId: string; serviceSocketId?: string; lightSocketIds?: string[]; animationTriggers: RuntimeTargetAnimationTrigger[]; lightTriggers: RuntimeTargetLightTrigger[]; soundTriggers: RuntimeTargetSoundTrigger[]; } interface RecipeMetadata { runtimeTarget?: RuntimeTargetMetadata; } interface RuntimeTargetExportPackage { recipeId: string; basePreset: string; sockets: SocketExport[]; metadata: RuntimeTargetMetadata; } interface RuntimeTargetPreviewTriggerState { key: string; event: RuntimeTargetTriggerEvent; action: Exclude; axis: 'x' | 'y' | 'z'; targetId: string; target: THREE.Object3D; basePosition: THREE.Vector3; baseRotation: THREE.Euler; startValue: number; currentValue: number; targetValue: number; delayMs: number; elapsedMs: number; durationMs: number; profile: RuntimeTargetMotionProfile; } interface RuntimeTargetPreviewLightState { key: string; event: RuntimeTargetTriggerEvent; socketId: string; light: THREE.PointLight; marker: THREE.Mesh; color: THREE.Color; intensity: number; durationMs: number; delayMs: number; elapsedMs: number; flicker: boolean; } type RuntimeTargetTriggerListEntry = | { key: string; kind: 'animation'; trigger: RuntimeTargetAnimationTrigger } | { key: string; kind: 'light'; trigger: RuntimeTargetLightTrigger } | { key: string; kind: 'sound'; trigger: RuntimeTargetSoundTrigger }; interface DragState { nodeId: string; plane: THREE.Plane; offset: THREE.Vector3; pointerId: number; } interface AnchorDragState { anchorId: string; pointerId: number; mode: 'edge' | 'normal'; baseFrame: Frame; baseOffset: Vec3Like; } interface GeneratorDragState { generatorId: string; handle: 'start' | 'end' | 'spacing' | 'radius' | 'mirror'; pointerId: number; axis: THREE.Vector3; basePoint: THREE.Vector3; baseValue: number; } interface EdgeCache { curve: THREE.Curve; lengths: number[]; totalLength: number; frames: { tangents: THREE.Vector3[]; normals: THREE.Vector3[]; binormals: THREE.Vector3[]; }; } interface ModuleEntry { id: string; label: string; def: HumanoidPartDefinition; portType: string; portProfile?: PortProfile; group: string; purpose: string; } type ScenePropKind = 'ground' | 'terrain' | 'ramp' | 'box' | 'steps' | 'rock'; type TerrainPatchPreset = 'rolling' | 'ridge' | 'crater' | 'side-slope' | 'custom'; interface ScenePropTerrainSettings { preset: TerrainPatchPreset; amplitude: number; frequency: number; resolution: number; } interface ScenePropEntry { id: string; kind: ScenePropKind; position: Vec3Like; rotation?: Vec3Like; scale: Vec3Like; terrain?: ScenePropTerrainSettings; } type SelectionKind = 'node' | 'edge' | 'anchor' | 'attachment' | 'generator' | 'scene-prop' | null; interface SelectionState { kind: SelectionKind; id?: string; } interface PoseMapping { node: NodeEntry; bone: THREE.Bone; rotationOffset: THREE.Quaternion; rotationMode?: 'yawOnly' | 'noRoll'; } interface DerivedPoseMapping { node: NodeEntry; parentBone: THREE.Bone; childBone: THREE.Bone; ratio: number; rotationOffset: THREE.Quaternion; rotationMode?: 'yawOnly' | 'noRoll'; } interface PoseSnapshot { position: Vec3Like; rotation?: Vec3Like; mode?: 'static' | 'relative'; parentId?: string; } interface WeaponArmTargets { elbowTarget: THREE.Vector3; wristTarget: THREE.Vector3; handTarget: THREE.Vector3; } interface TorsoClearanceVolume { frame: Frame; radii: THREE.Vector3; } interface WeaponHoldDebugState { gripFrame: Frame | null; supportFrame: Frame | null; shoulderFrame: Frame | null; supportPoleHint: THREE.Vector3 | null; supportTargets: WeaponArmTargets | null; torsoClearance: TorsoClearanceVolume | null; supportClearanceApplied: boolean; } interface LiveRebuildOptions { skipVisualState?: boolean; } interface PosePlaybackState { file: string | null; clip: THREE.AnimationClip | null; mixer: THREE.AnimationMixer | null; action: THREE.AnimationAction | null; root: THREE.Object3D | null; rootBone: THREE.Bone | null; duration: number; time: number; playing: boolean; loop: boolean; speed: number; rootMotion: boolean; boneMap: Map; mappings: PoseMapping[]; derivedMappings: DerivedPoseMapping[]; snapshot: Map; scale: number; offset: THREE.Vector3; baseRoot: THREE.Vector3; forwardAlignmentQuat: THREE.Quaternion; forwardAlignmentPivot: THREE.Vector3; forwardAlignmentRad: number; influence: number; upperBodyInfluence: number; weaponHoldEnabled: boolean; weaponHoldHandBlend: number; weaponHoldShoulderBlend: number; weaponHoldSupportElbowBias: number; } type CharacterSandboxPosePreset = 'idle' | 'aim' | 'fire'; type LeaperSandboxMotionPreset = LeaperArcPreviewMotionPreset; type LeaperSandboxMotionSample = LeaperArcPreviewMotionSample; interface CharacterSandboxState { enabled: boolean; posePreset: CharacterSandboxPosePreset; leaperMotionPreset: LeaperSandboxMotionPreset; leaperMotionStartedAtMs: number; aimYawDeg: number; aimPitchDeg: number; adsBlend: number; autoFire: boolean; fireRateHz: number; nextAutoFireAtMs: number; muzzleFlashTimer: number; recoilKick: number; infoNextUpdateAtMs: number; } interface CharacterSandboxIkProfile { handWeight: number; shoulderWeight: number; supportElbowBias: number; } type PoseAssetFormat = 'bvh' | 'fbx'; interface LoadedPoseAsset { format: PoseAssetFormat; root: THREE.Object3D; rootBone: THREE.Bone; clip: THREE.AnimationClip; bones: THREE.Bone[]; } interface LoadedPoseRig { format: PoseAssetFormat; root: THREE.Object3D; rootBone: THREE.Bone; bones: THREE.Bone[]; } interface CanonicalPoseRig { file: string; root: THREE.Object3D; rootBone: THREE.Bone; bones: THREE.Bone[]; boneMap: Map; } interface EditorState { nodes: NodeEntry[]; edges: EdgeEntry[]; anchors: AnchorEntry[]; generators: GeneratorEntry[]; attachments: AttachmentEntry[]; sceneProps: ScenePropEntry[]; recipeMetadata: RecipeMetadata | null; selection: SelectionState; hover: SelectionState; attachmentInstanceIndex: number | null; proxyMesh: ProceduralMesh | null; proxyObject: THREE.Object3D | null; surfacePickMode: boolean; mode: EditorMode; edgeDraftStart: string | null; dragState: DragState | null; anchorDragState: AnchorDragState | null; generatorDragState: GeneratorDragState | null; pendingDeselect: boolean; showGrid: boolean; showEdges: boolean; showAnchors: boolean; showGenerators: boolean; showModules: boolean; showProxy: boolean; showSceneProps: boolean; enableLeaperTerrainIk: boolean; leaperIkForeAftLimitDeg: number; leaperIkLateralLimitDeg: number; showAttachmentAxes: boolean; profileGeneration: boolean; showSkeleton: boolean; showWeaponQa: boolean; showFootProbes: boolean; showFootProbeLabels: boolean; mirrorAttachments: boolean; gizmoEnabled: boolean; gizmoMode: 'translate' | 'rotate' | 'scale'; edgeCache: Map; nextIds: { node: number; edge: number; anchor: number; generator: number; attachment: number; sceneProp: number; }; } interface NodePreviewOverride { position?: THREE.Vector3; quaternion?: THREE.Quaternion; } interface LeaperTerrainIkLegDebug { label: string; hit: boolean; contactY: number; deltaY: number; reachRatio: number; normalAngleDeg: number; } interface LeaperTerrainIkDebugSnapshot { active: boolean; bodyLift: number; bodyTiltDeg: number; foreAftTiltDeg: number; lateralTiltDeg: number; foreAftClamped: boolean; lateralClamped: boolean; legRows: LeaperTerrainIkLegDebug[]; } function getEl(id: string): T { const el = document.getElementById(id); if (!el) { throw new Error(`Missing element #${id}`); } return el as T; } function getOptionalEl(id: string): T | null { return document.getElementById(id) as T | null; } const controls = { canvas: getEl('unit-editor-canvas'), status: getEl('ue-status'), selection: getEl('ue-selection'), hover: getEl('ue-hover'), modeSelect: getEl('ue-mode-select'), modeAddNode: getEl('ue-mode-add-node'), modeAddEdge: getEl('ue-mode-add-edge'), modeSurface: getEl('ue-mode-surface'), toggleGrid: getEl('ue-toggle-grid'), toggleEdges: getEl('ue-toggle-edges'), toggleAnchors: getEl('ue-toggle-anchors'), toggleGenerators: getEl('ue-toggle-generators'), toggleModules: getEl('ue-toggle-modules'), toggleProxy: getEl('ue-toggle-proxy'), toggleSkeleton: getOptionalEl('ue-toggle-skeleton'), toggleGizmo: getEl('ue-toggle-gizmo'), gizmoTranslate: getEl('ue-gizmo-translate'), gizmoRotate: getEl('ue-gizmo-rotate'), gizmoScale: getEl('ue-gizmo-scale'), presetSelect: getEl('ue-preset-select'), nodeX: getEl('ue-node-x'), nodeY: getEl('ue-node-y'), nodeZ: getEl('ue-node-z'), nodeMode: getEl('ue-node-mode'), nodePose: getEl('ue-node-pose'), nodeParent: getEl('ue-node-parent'), addNode: getEl('ue-add-node'), nodeList: getEl('ue-node-list'), focusNode: getEl('ue-focus-node'), deleteNode: getEl('ue-delete-node'), edgeStart: getEl('ue-edge-start'), edgeEnd: getEl('ue-edge-end'), edgeRadius: getEl('ue-edge-radius'), edgeCurve: getEl('ue-edge-curve'), edgeControlX: getEl('ue-edge-control-x'), edgeControlY: getEl('ue-edge-control-y'), edgeControlZ: getEl('ue-edge-control-z'), addEdge: getEl('ue-add-edge'), edgeList: getEl('ue-edge-list'), focusEdge: getEl('ue-focus-edge'), deleteEdge: getEl('ue-delete-edge'), edgeInfo: getEl('ue-edge-info'), anchorType: getEl('ue-anchor-type'), anchorNode: getEl('ue-anchor-node'), anchorEdge: getEl('ue-anchor-edge'), anchorAttachment: getEl('ue-anchor-attachment'), anchorAttachmentFace: getEl('ue-anchor-attachment-face'), anchorS: getEl('ue-anchor-s'), anchorLen: getEl('ue-anchor-len'), anchorRule: getEl('ue-anchor-rule'), anchorAngle: getEl('ue-anchor-angle'), anchorOffsetX: getEl('ue-anchor-offset-x'), anchorOffsetY: getEl('ue-anchor-offset-y'), anchorOffsetZ: getEl('ue-anchor-offset-z'), anchorTags: getEl('ue-anchor-tags'), anchorAccepts: getEl('ue-anchor-accepts'), anchorProfile: getOptionalEl('ue-anchor-profile'), anchorExportSocket: getOptionalEl('ue-anchor-export'), addAnchor: getEl('ue-add-anchor'), pickSurface: getEl('ue-pick-surface'), anchorList: getEl('ue-anchor-list'), focusAnchor: getEl('ue-focus-anchor'), deleteAnchor: getEl('ue-delete-anchor'), detachAnchorParent: getEl('ue-anchor-detach-parent'), generatorBase: getEl('ue-generator-base'), generatorType: getEl('ue-generator-type'), generatorMode: getEl('ue-generator-mode'), generatorCount: getEl('ue-generator-count'), generatorSpacing: getEl('ue-generator-spacing'), generatorStart: getEl('ue-generator-start'), generatorEnd: getEl('ue-generator-end'), generatorRadius: getEl('ue-generator-radius'), generatorAngle: getEl('ue-generator-angle'), generatorPlane: getEl('ue-generator-plane'), generatorPlaneOffset: getEl('ue-generator-plane-offset'), addGenerator: getEl('ue-add-generator'), showGenerators: getEl('ue-show-generators'), generatorList: getEl('ue-generator-list'), deleteGenerator: getEl('ue-delete-generator'), poseSelect: getEl('ue-pose-select'), poseLoad: getEl('ue-pose-load'), poseLoadYBot: getEl('ue-pose-load-ybot'), posePlay: getEl('ue-pose-play'), poseStop: getEl('ue-pose-stop'), poseWeaponHold: getEl('ue-pose-weapon-hold'), poseInfluence: getEl('ue-pose-influence'), poseUpperBodyInfluence: getEl('ue-pose-upper-body'), poseWeaponHoldHandBlend: getEl('ue-pose-weapon-hold-hand'), poseWeaponHoldShoulderBlend: getEl('ue-pose-weapon-hold-shoulder'), poseWeaponHoldSupportElbowBias: getEl('ue-pose-weapon-hold-support-elbow'), poseLoop: getEl('ue-pose-loop'), poseRoot: getEl('ue-pose-root'), poseSpeed: getEl('ue-pose-speed'), poseTime: getEl('ue-pose-time'), poseInfo: getEl('ue-pose-info'), poseDebug: getEl('ue-pose-debug'), charSandboxEnable: getEl('ue-char-sandbox-enable'), charAnimIdle: getEl('ue-char-anim-idle'), charAnimAim: getEl('ue-char-anim-aim'), charAnimFire: getEl('ue-char-anim-fire'), charAimYaw: getEl('ue-char-aim-yaw'), charAimPitch: getEl('ue-char-aim-pitch'), charAimAds: getEl('ue-char-aim-ads'), charFireOnce: getEl('ue-char-fire-once'), charFireLoop: getEl('ue-char-fire-loop'), charFireRate: getEl('ue-char-fire-rate'), charSandboxInfo: getEl('ue-char-sandbox-info'), weaponQaToggle: getEl('ue-weapon-qa-toggle'), weaponQaInfo: getEl('ue-weapon-qa-info'), attachmentAnchor: getEl('ue-attachment-anchor'), attachmentGenerator: getOptionalEl('ue-attachment-generator'), attachmentGeneratorIndex: getOptionalEl('ue-attachment-generator-index'), moduleFilter: getEl('ue-module-filter'), showIncompatible: getEl('ue-show-incompatible'), mirrorAttachments: getOptionalEl('ue-mirror-attachments'), moduleCompat: getEl('ue-module-compat'), moduleProfileBadges: getOptionalEl('ue-module-profile-badges'), moduleList: getEl('ue-module-list'), attachModule: getEl('ue-attach-module'), detachModule: getEl('ue-detach-module'), attachmentList: getEl('ue-attachment-list'), attachmentOffsetX: getEl('ue-attachment-offset-x'), attachmentOffsetY: getEl('ue-attachment-offset-y'), attachmentOffsetZ: getEl('ue-attachment-offset-z'), attachmentRotX: getEl('ue-attachment-rot-x'), attachmentRotY: getEl('ue-attachment-rot-y'), attachmentRotZ: getEl('ue-attachment-rot-z'), attachmentScaleX: getEl('ue-attachment-scale-x'), attachmentScaleY: getEl('ue-attachment-scale-y'), attachmentScaleZ: getEl('ue-attachment-scale-z'), attachmentInfo: getEl('ue-attachment-info'), attachmentPrimitive: getEl('ue-attachment-primitive'), attachmentSizeX: getEl('ue-attachment-size-x'), attachmentSizeY: getEl('ue-attachment-size-y'), attachmentSizeZ: getEl('ue-attachment-size-z'), attachmentSizeMinor: getEl('ue-attachment-size-minor'), attachmentSizeSegments: getEl('ue-attachment-size-segments'), attachmentSizeRings: getEl('ue-attachment-size-rings'), attachmentTaperXTop: getOptionalEl('ue-attachment-taper-x-top'), attachmentTaperXBottom: getOptionalEl('ue-attachment-taper-x-bottom'), attachmentTaperZTop: getOptionalEl('ue-attachment-taper-z-top'), attachmentTaperZBottom: getOptionalEl('ue-attachment-taper-z-bottom'), attachmentChamferEdge: getOptionalEl('ue-attachment-chamfer-edge'), attachmentChamferCorner: getOptionalEl('ue-attachment-chamfer-corner'), attachmentProfileKind: getOptionalEl('ue-attachment-profile-kind'), attachmentProfileIntensity: getOptionalEl('ue-attachment-profile-intensity'), attachmentRotateYawNeg: getEl('ue-attachment-rotate-yaw-neg'), attachmentRotateYawPos: getEl('ue-attachment-rotate-yaw-pos'), attachmentRotatePitchNeg: getEl('ue-attachment-rotate-pitch-neg'), attachmentRotatePitchPos: getEl('ue-attachment-rotate-pitch-pos'), attachmentRotateRollNeg: getEl('ue-attachment-rotate-roll-neg'), attachmentRotateRollPos: getEl('ue-attachment-rotate-roll-pos'), attachmentTransformReset: getEl('ue-attachment-transform-reset'), attachmentShapeReset: getEl('ue-attachment-shape-reset'), sculptToggle: getEl('ue-sculpt-toggle'), sculptPanel: getEl('ue-sculpt-panel'), sculptEnable: getEl('ue-sculpt-enable'), sculptPrimitive: getEl('ue-sculpt-primitive'), sculptSizeX: getEl('ue-sculpt-size-x'), sculptSizeY: getEl('ue-sculpt-size-y'), sculptSizeZ: getEl('ue-sculpt-size-z'), sculptRoundness: getEl('ue-sculpt-roundness'), sculptBulgeEnable: getEl('ue-sculpt-bulge-enable'), sculptBulgeRadius: getEl('ue-sculpt-bulge-radius'), sculptBulgeSmooth: getEl('ue-sculpt-bulge-smooth'), sculptBulgeOffsetX: getEl('ue-sculpt-bulge-offset-x'), sculptBulgeOffsetY: getEl('ue-sculpt-bulge-offset-y'), sculptBulgeOffsetZ: getEl('ue-sculpt-bulge-offset-z'), sculptCutEnable: getEl('ue-sculpt-cut-enable'), sculptCutRadius: getEl('ue-sculpt-cut-radius'), sculptCutOffsetX: getEl('ue-sculpt-cut-offset-x'), sculptCutOffsetY: getEl('ue-sculpt-cut-offset-y'), sculptCutOffsetZ: getEl('ue-sculpt-cut-offset-z'), sculptReset: getEl('ue-sculpt-reset'), bakeResolution: getEl('ue-bake-resolution'), bakeProxy: getEl('ue-bake-proxy'), showProxy: getEl('ue-show-proxy'), showAttachmentAxes: getEl('ue-show-attachment-axes'), profileGeneration: getEl('ue-profile-generation'), exportObj: getEl('ue-export-obj'), exportSockets: getOptionalEl('ue-export-sockets'), exportRuntimeTarget: getOptionalEl('ue-export-runtime-target'), runtimeTargetActivate: getOptionalEl('ue-runtime-target-activate'), runtimeTargetHit: getOptionalEl('ue-runtime-target-hit'), runtimeTargetRecover: getOptionalEl('ue-runtime-target-recover'), runtimeTargetAutoRecover: getOptionalEl('ue-runtime-target-auto-recover'), runtimeTargetPanel: getOptionalEl('ue-runtime-target-panel'), runtimeTargetTriggerEditor: getOptionalEl('ue-runtime-target-trigger-editor'), runtimeTargetTriggerEditorToggle: getOptionalEl('ue-runtime-target-trigger-editor-toggle'), runtimeTargetTriggerSummary: getOptionalEl('ue-runtime-target-trigger-summary'), runtimeTargetTriggerAmplitudeLabel: getOptionalEl('ue-runtime-target-trigger-amplitude-label'), runtimeTargetMotionProfileGroup: getOptionalEl('ue-runtime-target-motion-profile-group'), runtimeTargetSoundCueGroup: getOptionalEl('ue-runtime-target-sound-cue-group'), runtimeTargetSoundFrequencyGroup: getOptionalEl('ue-runtime-target-sound-frequency-group'), runtimeTargetTriggerList: getOptionalEl('ue-runtime-target-trigger-list'), runtimeTargetTriggerEvent: getOptionalEl('ue-runtime-target-trigger-event'), runtimeTargetTriggerAmplitude: getOptionalEl('ue-runtime-target-trigger-amplitude'), runtimeTargetTriggerDelay: getOptionalEl('ue-runtime-target-trigger-delay'), runtimeTargetTriggerDuration: getOptionalEl('ue-runtime-target-trigger-duration'), runtimeTargetMotionProfile: getOptionalEl('ue-runtime-target-motion-profile'), runtimeTargetTriggerFlicker: getOptionalEl('ue-runtime-target-trigger-flicker'), runtimeTargetSoundCue: getOptionalEl('ue-runtime-target-sound-cue'), runtimeTargetSoundFrequency: getOptionalEl('ue-runtime-target-sound-frequency'), runtimeTargetSequencePlay: getOptionalEl('ue-runtime-target-sequence-play'), runtimeTargetSequenceStop: getOptionalEl('ue-runtime-target-sequence-stop'), runtimeTargetTimeline: getOptionalEl('ue-runtime-target-timeline'), runtimeTargetPreviewInfo: getOptionalEl('ue-runtime-target-preview-info'), runtimeTargetTimelineInfo: getOptionalEl('ue-runtime-target-timeline-info'), scenePropKind: getEl('ue-scene-prop-kind'), scenePropList: getEl('ue-scene-prop-list'), scenePropSpawnUnder: getEl('ue-scene-prop-spawn-under'), scenePropSpawnCenter: getEl('ue-scene-prop-spawn-center'), scenePropFocus: getEl('ue-scene-prop-focus'), scenePropDelete: getEl('ue-scene-prop-delete'), scenePropClear: getEl('ue-scene-prop-clear'), showSceneProps: getEl('ue-show-scene-props'), enableLeaperTerrainIk: getEl('ue-enable-leaper-terrain-ik'), leaperIkForeAftLimit: getEl('ue-leaper-ik-foreaft-limit'), leaperIkForeAftLimitLabel: getEl('ue-leaper-ik-foreaft-limit-label'), leaperIkLateralLimit: getEl('ue-leaper-ik-lateral-limit'), leaperIkLateralLimitLabel: getEl('ue-leaper-ik-lateral-limit-label'), scenePropInfo: getEl('ue-scene-prop-info'), leaperIkInfo: getEl('ue-leaper-ik-info'), sceneTerrainPreset: getEl('ue-scene-terrain-preset'), sceneTerrainAmplitude: getEl('ue-scene-terrain-amplitude'), sceneTerrainFrequency: getEl('ue-scene-terrain-frequency'), sceneTerrainResolution: getEl('ue-scene-terrain-resolution'), sceneTerrainApply: getEl('ue-scene-terrain-apply'), showFootProbes: getEl('ue-show-foot-probes'), showFootProbeLabels: getEl('ue-show-foot-probe-labels'), socketIncludeGenerators: getOptionalEl('ue-socket-include-generators'), socketTagFilter: getOptionalEl('ue-socket-tag-filter'), bakeStats: getEl('ue-bake-stats'), saveJson: getEl('ue-save-json'), reloadPreset: getEl('ue-reload-preset'), resetSkeleton: getEl('ue-reset-skeleton'), reset: getEl('ue-reset'), loadJson: getEl('ue-load-json'), recipePreview: getEl('ue-recipe-preview') }; const panels = { structure: getEl('ue-panel-structure'), poses: getEl('ue-panel-poses'), characterSandbox: getEl('ue-panel-character-sandbox'), weaponQa: getEl('ue-panel-weapon-qa'), anchors: getEl('ue-panel-anchors'), generators: getEl('ue-panel-generators'), modules: getEl('ue-panel-modules'), bake: getEl('ue-panel-bake'), sceneProps: getEl('ue-panel-scene-props'), io: getEl('ue-panel-io') }; type SidebarCapability = | 'structure' | 'poses' | 'sandbox' | 'weaponQa' | 'anchors' | 'generators' | 'modules' | 'bake' | 'terrain' | 'io'; type SidebarCapabilityAvailability = Record; const capabilityPanels: Record = { structure: [panels.structure], poses: [panels.poses], sandbox: [panels.characterSandbox], weaponQa: [panels.weaponQa], anchors: [panels.anchors], generators: [panels.generators], modules: [panels.modules], bake: [panels.bake], terrain: [panels.sceneProps], io: [panels.io] }; const TERRAIN_PATCH_PRESETS: Record, Omit> = { rolling: { amplitude: 0.12, frequency: 1.35, resolution: 24 }, ridge: { amplitude: 0.22, frequency: 0.95, resolution: 32 }, crater: { amplitude: 0.2, frequency: 1.15, resolution: 32 }, 'side-slope': { amplitude: 0.24, frequency: 1.0, resolution: 20 } }; const sidebarLayout = getOptionalEl('ue-layout'); const sidebarRoot = getOptionalEl('ue-sidebar'); const sidebarCapabilityButtons = Array.from( document.querySelectorAll('[data-ue-capability]') ); let activeSidebarCapability: SidebarCapability | null = 'structure'; let nodePreviewOverrideMap = new Map(); let leaperTerrainIkDebugSnapshot: LeaperTerrainIkDebugSnapshot = { active: false, bodyLift: 0, bodyTiltDeg: 0, foreAftTiltDeg: 0, lateralTiltDeg: 0, foreAftClamped: false, lateralClamped: false, legRows: [] }; function resolveSidebarCapabilityAvailability(): SidebarCapabilityAvailability { const hasStructure = state.nodes.length > 0 || state.edges.length > 0; const hasAnchors = state.anchors.length > 0; const hasGenerators = state.generators.length > 0; const hasAttachments = state.attachments.length > 0; const hasBakeable = hasStructure || hasAttachments || Boolean(state.proxyMesh); const hasWeaponQa = Boolean(getActiveWeaponDesignSpec()); return { structure: true, poses: hasStructure, sandbox: hasStructure, weaponQa: hasWeaponQa, anchors: hasStructure || hasAnchors || state.mode === 'surface', generators: hasAnchors || hasGenerators, modules: hasAnchors || hasAttachments, bake: hasBakeable, terrain: true, io: true }; } function setActiveSidebarCapability( capability: SidebarCapability | null, options?: { forceOpen?: boolean } ): void { if (!capability) return; activeSidebarCapability = capability; updatePanelVisibility(); } function getCapabilityForSelection(kind: SelectionKind): SidebarCapability | null { switch (kind) { case 'node': case 'edge': return 'structure'; case 'anchor': return 'anchors'; case 'generator': return 'generators'; case 'attachment': return 'modules'; case 'scene-prop': return null; default: return null; } } function getCapabilityForMode(mode: EditorMode): SidebarCapability | null { switch (mode) { case 'add-node': case 'add-edge': return 'structure'; case 'surface': return 'anchors'; case 'select': default: return null; } } const poseState: PosePlaybackState = { file: null, clip: null, mixer: null, action: null, root: null, rootBone: null, duration: 0, time: 0, playing: false, loop: true, speed: 1, rootMotion: false, boneMap: new Map(), mappings: [], derivedMappings: [], snapshot: new Map(), scale: 1, offset: new THREE.Vector3(), baseRoot: new THREE.Vector3(), forwardAlignmentQuat: new THREE.Quaternion(), forwardAlignmentPivot: new THREE.Vector3(), forwardAlignmentRad: 0, influence: 1, upperBodyInfluence: 1, weaponHoldEnabled: false, weaponHoldHandBlend: 0.35, weaponHoldShoulderBlend: 0.12, weaponHoldSupportElbowBias: 0.28 }; const characterSandboxState: CharacterSandboxState = { enabled: true, posePreset: 'aim', leaperMotionPreset: 'idle', leaperMotionStartedAtMs: 0, aimYawDeg: 0, aimPitchDeg: 0, adsBlend: 0.7, autoFire: false, fireRateHz: 6, nextAutoFireAtMs: 0, muzzleFlashTimer: 0, recoilKick: 0, infoNextUpdateAtMs: 0 }; let lastAutoPoseArchetype: HumanoidWeaponArchetype | null = null; let lastAutoPoseFile: string | null = null; const torsoPoseYawEuler = new THREE.Euler(); const torsoPoseYawQuat = new THREE.Quaternion(); const poseNoRollEuler = new THREE.Euler(); const poseNoRollQuat = new THREE.Quaternion(); const poseWorldUp = new THREE.Vector3(0, 1, 0); const poseClock = new THREE.Clock(); type PoseLoaderBundle = { bvhLoader: { loadAsync: (url: string) => Promise<{ skeleton: { bones: THREE.Bone[] }; clip: THREE.AnimationClip }>; }; fbxLoader: { loadAsync: (url: string) => Promise; }; }; let poseLoaderBundlePromise: Promise | null = null; let canonicalPoseRig: CanonicalPoseRig | null = null; let canonicalPoseRigPromise: Promise | null = null; async function getPoseLoaderBundle(): Promise { if (!poseLoaderBundlePromise) { poseLoaderBundlePromise = Promise.all([ import('three/examples/jsm/loaders/BVHLoader.js'), import('three/examples/jsm/loaders/FBXLoader.js') ]).then(([bvhModule, fbxModule]) => ({ bvhLoader: new bvhModule.BVHLoader(), fbxLoader: new fbxModule.FBXLoader() })); } return poseLoaderBundlePromise; } const weaponHoldDebugState: WeaponHoldDebugState = { gripFrame: null, supportFrame: null, shoulderFrame: null, supportPoleHint: null, supportTargets: null, torsoClearance: null, supportClearanceApplied: false }; const renderer = new THREE.WebGLRenderer({ canvas: controls.canvas, antialias: true }); renderer.setPixelRatio(getEditorPixelRatio()); renderer.setClearColor(0x0e1116, 1); const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0e1116); const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 200); camera.position.set(1.4, 1.2, 1.6); camera.layers.enable(LAYER_NODE); camera.layers.enable(LAYER_EDGE); camera.layers.enable(LAYER_ANCHOR); camera.layers.enable(LAYER_GENERATOR); camera.layers.enable(LAYER_ATTACHMENT); camera.layers.enable(LAYER_SCENE_PROP); const orbit = new OrbitControls(camera, renderer.domElement); orbit.enableDamping = true; orbit.dampingFactor = 0.08; orbit.target.set(0, 0.2, 0); const transform = new TransformControls(camera, renderer.domElement); transform.setMode('translate'); transform.setSize(0.8); scene.add(transform); const ambient = new THREE.AmbientLight(0xffffff, 0.4); scene.add(ambient); const dirLight = new THREE.DirectionalLight(0xffffff, 0.9); dirLight.position.set(2.8, 4, 2.6); scene.add(dirLight); const grid = new THREE.GridHelper(8, 32, 0x2c2f36, 0x21242b); (grid.material as THREE.Material).transparent = true; (grid.material as THREE.Material).opacity = 0.8; scene.add(grid); const nodeGroup = new THREE.Group(); const edgeGroup = new THREE.Group(); const anchorGroup = new THREE.Group(); const skeletonGroup = new THREE.Group(); const generatorGroup = new THREE.Group(); const generatorGizmoGroup = new THREE.Group(); const moduleGroup = new THREE.Group(); const attachmentAxisGroup = new THREE.Group(); const scenePropGroup = new THREE.Group(); const footProbeGroup = new THREE.Group(); const proxyGroup = new THREE.Group(); const previewGroup = new THREE.Group(); const runtimeTargetPreviewGroup = new THREE.Group(); const dronePresetPreviewGroup = new THREE.Group(); const leaperRuntimePreviewGroup = new THREE.Group(); const leaperRuntimePreviewBaseGroup = new THREE.Group(); const leaperRuntimePreviewDetailGroup = new THREE.Group(); const leaperRuntimePreviewHighlightGroup = new THREE.Group(); const leaperRuntimePreviewWeaponGroup = new THREE.Group(); const poseDebugGroup = new THREE.Group(); const poseAvatarGroup = new THREE.Group(); const characterSandboxFxGroup = new THREE.Group(); leaperRuntimePreviewGroup.add( leaperRuntimePreviewBaseGroup, leaperRuntimePreviewDetailGroup, leaperRuntimePreviewHighlightGroup, leaperRuntimePreviewWeaponGroup ); scene.add( nodeGroup, edgeGroup, anchorGroup, skeletonGroup, generatorGroup, generatorGizmoGroup, moduleGroup, attachmentAxisGroup, scenePropGroup, footProbeGroup, proxyGroup, previewGroup, runtimeTargetPreviewGroup, dronePresetPreviewGroup, leaperRuntimePreviewGroup, poseDebugGroup, poseAvatarGroup, characterSandboxFxGroup ); const nodeMaterial = new THREE.MeshStandardMaterial({ color: 0x8f9ba8, metalness: 0.3, roughness: 0.6 }); const nodeSelectedMaterial = new THREE.MeshStandardMaterial({ color: 0x4ea1ff, metalness: 0.2, roughness: 0.4 }); const nodeStaticMaterial = new THREE.MeshStandardMaterial({ color: 0x7b8c9a, metalness: 0.25, roughness: 0.5 }); const nodeRelativeMaterial = new THREE.MeshStandardMaterial({ color: 0xd28b4f, metalness: 0.2, roughness: 0.45 }); const nodeSubtleMaterial = new THREE.MeshStandardMaterial({ color: 0x303943, metalness: 0.16, roughness: 0.84, transparent: true, opacity: 0.22, depthWrite: false }); const edgeMaterial = new THREE.LineBasicMaterial({ color: 0x8da2b8 }); const edgeSelectedMaterial = new THREE.LineBasicMaterial({ color: 0x4ea1ff }); const edgePreviewMaterial = new THREE.LineDashedMaterial({ color: 0x57e7ff, dashSize: 0.08, gapSize: 0.05 }); const snapMaterial = new THREE.MeshStandardMaterial({ color: 0x57e7ff, emissive: 0x1a5e70, roughness: 0.4, metalness: 0.2 }); const edgeHoverMaterial = new THREE.LineBasicMaterial({ color: 0xffd166 }); const skeletonLineMaterial = new THREE.LineBasicMaterial({ color: 0x4a5a6b }); const nodeHoverMaterial = new THREE.MeshStandardMaterial({ color: 0xffd166, metalness: 0.2, roughness: 0.4 }); const scenePropMaterial = new THREE.MeshStandardMaterial({ color: 0x586675, metalness: 0.18, roughness: 0.82 }); const scenePropHoverMaterial = new THREE.MeshStandardMaterial({ color: 0xcaa15a, emissive: 0x372308, metalness: 0.16, roughness: 0.66 }); const scenePropSelectedMaterial = new THREE.MeshStandardMaterial({ color: 0x59a9ff, emissive: 0x123a63, metalness: 0.16, roughness: 0.58 }); const footProbeLineMaterial = new THREE.LineBasicMaterial({ color: 0x55d7ff }); const footProbeMissMaterial = new THREE.LineBasicMaterial({ color: 0xff7b55 }); const footProbeNormalMaterial = new THREE.LineBasicMaterial({ color: 0x8ef0a8 }); const footProbeStartMaterial = new THREE.MeshStandardMaterial({ color: 0x2f89ff, emissive: 0x0d2b57, metalness: 0.18, roughness: 0.42 }); const footProbeHitMaterial = new THREE.MeshStandardMaterial({ color: 0x8ef0a8, emissive: 0x143d20, metalness: 0.16, roughness: 0.42 }); const footProbeMissMarkerMaterial = new THREE.MeshStandardMaterial({ color: 0xff7b55, emissive: 0x4f1f12, metalness: 0.14, roughness: 0.44 }); const footProbeMarkerGeometry = new THREE.SphereGeometry(0.018, 12, 10); const footProbeLabelTextureCache = new Map(); const anchorSelectedMaterial = new THREE.MeshStandardMaterial({ color: 0x57e7ff, emissive: 0x143b49, metalness: 0.2, roughness: 0.4 }); const anchorHoverMaterial = new THREE.MeshStandardMaterial({ color: 0xffd166, emissive: 0x4f3a00, metalness: 0.2, roughness: 0.4 }); const anchorMaterial = new THREE.MeshStandardMaterial({ color: 0xf0b429, emissive: 0x3b2a00, metalness: 0.2, roughness: 0.4 }); const anchorSubtleMaterial = new THREE.MeshStandardMaterial({ color: 0x5c4720, emissive: 0x120d04, metalness: 0.12, roughness: 0.78, transparent: true, opacity: 0.2, depthWrite: false }); const generatorMaterial = new THREE.MeshStandardMaterial({ color: 0x6fcf97, metalness: 0.2, roughness: 0.4 }); const generatorLineMaterial = new THREE.LineBasicMaterial({ color: 0x6fcf97 }); const attachmentAxisUpMaterial = new THREE.LineBasicMaterial({ color: 0x6fcf97, transparent: true, opacity: 0.85 }); const attachmentAxisForwardMaterial = new THREE.LineBasicMaterial({ color: 0x4ea1ff, transparent: true, opacity: 0.85 }); const poseDebugGripMaterial = new THREE.LineBasicMaterial({ color: 0x4ea1ff }); const poseDebugSupportMaterial = new THREE.LineBasicMaterial({ color: 0x6fcf97 }); const poseDebugShoulderMaterial = new THREE.LineBasicMaterial({ color: 0xffb454 }); const poseDebugTorsoMaterial = new THREE.LineBasicMaterial({ color: 0xff6b6b, transparent: true, opacity: 0.55 }); const poseDebugGripMarkerMaterial = new THREE.MeshStandardMaterial({ color: 0x4ea1ff, emissive: 0x12304f, roughness: 0.35, metalness: 0.15 }); const poseDebugSupportMarkerMaterial = new THREE.MeshStandardMaterial({ color: 0x6fcf97, emissive: 0x163725, roughness: 0.4, metalness: 0.1 }); const poseDebugShoulderMarkerMaterial = new THREE.MeshStandardMaterial({ color: 0xffb454, emissive: 0x4d2c00, roughness: 0.4, metalness: 0.1 }); const poseDebugMarkerGeometry = new THREE.SphereGeometry(0.018, 10, 10); const characterSandboxMuzzleCoreMaterial = new THREE.MeshBasicMaterial({ color: 0xffefb4, transparent: true, opacity: 0.95, depthWrite: false }); const characterSandboxMuzzleHaloMaterial = new THREE.MeshBasicMaterial({ color: 0x7ecbff, transparent: true, opacity: 0.65, depthWrite: false, side: THREE.DoubleSide }); const characterSandboxMuzzleCore = new THREE.Mesh( new THREE.ConeGeometry(0.018, 0.1, 16, 1, true), characterSandboxMuzzleCoreMaterial ); characterSandboxMuzzleCore.geometry.rotateX(Math.PI / 2); const characterSandboxMuzzleHalo = new THREE.Mesh( new THREE.RingGeometry(0.014, 0.04, 24), characterSandboxMuzzleHaloMaterial ); const characterSandboxMuzzleLight = new THREE.PointLight(0x9ad8ff, 0, 1.1, 2.2); characterSandboxMuzzleCore.visible = false; characterSandboxMuzzleHalo.visible = false; characterSandboxMuzzleLight.visible = false; characterSandboxFxGroup.add(characterSandboxMuzzleCore, characterSandboxMuzzleHalo, characterSandboxMuzzleLight); const generatorLinkMaterial = new THREE.LineDashedMaterial({ color: 0x4f9f7a, dashSize: 0.06, gapSize: 0.04 }); const generatorHandleMaterial = new THREE.MeshStandardMaterial({ color: 0x57e7ff, metalness: 0.2, roughness: 0.4 }); const generatorHandleActiveMaterial = new THREE.MeshStandardMaterial({ color: 0xffd166, metalness: 0.2, roughness: 0.4 }); const proxyMaterial = new THREE.MeshStandardMaterial({ color: 0x9aa7b4, metalness: 0.1, roughness: 0.75, transparent: true, opacity: 0.35 }); const nodeGeometry = new THREE.SphereGeometry(DEFAULT_NODE_RADIUS, 18, 16); const anchorGeometry = new THREE.SphereGeometry(DEFAULT_ANCHOR_RADIUS, 12, 10); const generatorGeometry = new THREE.SphereGeometry(DEFAULT_ANCHOR_RADIUS * 0.8, 10, 8); const snapGeometry = new THREE.SphereGeometry(DEFAULT_ANCHOR_RADIUS * 0.9, 12, 10); const generatorHandleGeometry = new THREE.SphereGeometry(DEFAULT_ANCHOR_RADIUS * 1.05, 12, 10); const edgePreviewLine = new THREE.Line(new THREE.BufferGeometry(), edgePreviewMaterial); edgePreviewLine.visible = false; const edgeSnapMarker = new THREE.Mesh(snapGeometry, snapMaterial); edgeSnapMarker.visible = false; previewGroup.add(edgePreviewLine, edgeSnapMarker); const dronePreviewLineMaterial = new THREE.LineBasicMaterial({ color: 0xff6b3d, transparent: true, opacity: 0.9 }); const dronePreviewConeMaterial = new THREE.MeshBasicMaterial({ color: 0xff7043, transparent: true, opacity: 0.18, depthWrite: false, side: THREE.DoubleSide }); const dronePreviewSweepMaterial = new THREE.MeshBasicMaterial({ color: 0x6ee7ff, transparent: true, opacity: 0.16, depthWrite: false, side: THREE.DoubleSide }); const dronePreviewBeaconMaterial = new THREE.MeshBasicMaterial({ color: 0x8af0ff, transparent: true, opacity: 0.72, depthWrite: false }); const dronePreviewRay = new THREE.Line( new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3(0, 0, 1)]), dronePreviewLineMaterial ); const dronePreviewCone = new THREE.Mesh( new THREE.ConeGeometry(0.32, 1, 24, 1, true), dronePreviewConeMaterial ); const dronePreviewScoutSweep = new THREE.Mesh( new THREE.ConeGeometry(0.42, 1.2, 24, 1, true), dronePreviewSweepMaterial ); const dronePreviewBeaconA = new THREE.Mesh( new THREE.SphereGeometry(0.028, 12, 10), dronePreviewBeaconMaterial ); const dronePreviewBeaconB = dronePreviewBeaconA.clone(); dronePreviewCone.visible = false; dronePreviewScoutSweep.visible = false; dronePreviewBeaconA.visible = false; dronePreviewBeaconB.visible = false; dronePresetPreviewGroup.add( dronePreviewRay, dronePreviewCone, dronePreviewScoutSweep, dronePreviewBeaconA, dronePreviewBeaconB ); const dronePreviewOrigin = new THREE.Vector3(); const dronePreviewForward = new THREE.Vector3(); const dronePreviewTarget = new THREE.Vector3(); const dronePreviewMidpoint = new THREE.Vector3(); const dronePreviewRight = new THREE.Vector3(); const dronePreviewQuat = new THREE.Quaternion(); const dronePreviewXAxis = new THREE.Vector3(1, 0, 0); const dronePreviewSensorUp = new THREE.Vector3(0, 1, 0); const dronePreviewSensorFallbackForward = new THREE.Vector3(0, 0, 1); const dronePreviewRotorIdsHeavy = [ 'attach-heavy-rotor-front-left', 'attach-heavy-rotor-front-right', 'attach-heavy-rotor-back-left', 'attach-heavy-rotor-back-right' ] as const; const dronePreviewRotorIdsScout = [ 'attach-scout-rotor-front-left', 'attach-scout-rotor-front-right', 'attach-scout-rotor-back-left', 'attach-scout-rotor-back-right' ] as const; const MODULE_BASE_ROTATION_DEG_BY_ID: Readonly> = { // Leaper leg/body structural parts that are modeled around the catalog Y axis // but are mounted as forward-running shells in the unit editor. core_shoulder_yoke: [90, 0, 0], core_ankle_joint: [90, 0, 0], detail_knee_plate: [90, 0, 0], core_foot_block: [90, 0, 0] }; const LEAPER_RUNTIME_VISIBLE_BODY_ATTACHMENT_IDS: ReadonlySet = new Set([ 'attach-leaper-bridge-left', 'attach-leaper-bridge-right', 'attach-leaper-shoulder-throat-left', 'attach-leaper-shoulder-throat-right', 'attach-leaper-side-skirt-left', 'attach-leaper-side-skirt-right', 'attach-leaper-shoulder-shell-left', 'attach-leaper-shoulder-shell-right', 'attach-leaper-shoulder-collar-left', 'attach-leaper-shoulder-collar-right', 'attach-leaper-shoulder-gusset-left', 'attach-leaper-shoulder-gusset-right', 'attach-leaper-haunch-left', 'attach-leaper-haunch-right', 'attach-leaper-hip-saddle-left', 'attach-leaper-hip-saddle-right', 'attach-leaper-hip-collar-left', 'attach-leaper-hip-collar-right', 'attach-leaper-hip-gusset-left', 'attach-leaper-hip-gusset-right' ]); const leaperRuntimePreviewLayers: TargetAssemblyLayers = { base: leaperRuntimePreviewBaseGroup, details: leaperRuntimePreviewDetailGroup, highlights: leaperRuntimePreviewHighlightGroup, weapons: leaperRuntimePreviewWeaponGroup }; let leaperRuntimePreviewRig: THREE.Group | null = null; let leaperRuntimePreviewBaseFrame: Frame | null = null; let leaperRuntimePreviewBasePosition: THREE.Vector3 | null = null; let leaperRuntimePreviewBaseQuaternion: THREE.Quaternion | null = null; function setDronePreviewLine(start: THREE.Vector3, end: THREE.Vector3): void { (dronePreviewRay.geometry as THREE.BufferGeometry).setFromPoints([start.clone(), end.clone()]); } function updateDronePresetPreview(now: number): boolean { const presetName = currentBasePresetName; const isHeavyDrone = presetName === 'drone-heavy-quadcannon'; const isScoutDrone = presetName === 'drone-scout-recon'; dronePresetPreviewGroup.visible = isHeavyDrone || isScoutDrone; dronePreviewRay.visible = isHeavyDrone || isScoutDrone; dronePreviewCone.visible = isHeavyDrone; dronePreviewScoutSweep.visible = isScoutDrone; dronePreviewBeaconA.visible = isHeavyDrone || isScoutDrone; dronePreviewBeaconB.visible = isHeavyDrone || isScoutDrone; if (!isHeavyDrone && !isScoutDrone) { return false; } const t = now * 0.001; const sensorMesh = isHeavyDrone ? getAttachmentSelectionMesh('attach-heavy-sensor-eye', null) ?? getAttachmentSelectionMesh('attach-heavy-sensor-head', null) : getAttachmentSelectionMesh('attach-scout-sensor-eye', null) ?? getAttachmentSelectionMesh('attach-scout-sensor-head', null); if (!sensorMesh) { return false; } sensorMesh.updateMatrixWorld(true); sensorMesh.getWorldPosition(dronePreviewOrigin); sensorMesh.getWorldQuaternion(dronePreviewQuat); dronePreviewForward.copy(dronePreviewSensorFallbackForward).applyQuaternion(dronePreviewQuat).normalize(); if (dronePreviewForward.lengthSq() < 1e-4) { dronePreviewForward.set(0, 0, 1); } if (isHeavyDrone) { const lockAlpha = 0.5 + 0.5 * Math.sin(t * 0.85); const lockDistance = THREE.MathUtils.lerp(2.8, 1.1, lockAlpha); const lockRadius = THREE.MathUtils.lerp(0.72, 0.18, lockAlpha); dronePreviewTarget.copy(dronePreviewOrigin).addScaledVector(dronePreviewForward, lockDistance); setDronePreviewLine(dronePreviewOrigin, dronePreviewTarget); dronePreviewMidpoint.copy(dronePreviewOrigin).add(dronePreviewTarget).multiplyScalar(0.5); dronePreviewCone.position.copy(dronePreviewMidpoint); dronePreviewCone.quaternion.setFromUnitVectors(dronePreviewSensorUp, dronePreviewForward); dronePreviewCone.scale.set(lockRadius, lockDistance, lockRadius); (dronePreviewCone.material as THREE.MeshBasicMaterial).opacity = THREE.MathUtils.lerp(0.12, 0.24, lockAlpha); const rotorPulse = 0.75 + 0.25 * Math.sin(t * 7.5); const rotorLeft = getAttachmentSelectionMesh(dronePreviewRotorIdsHeavy[0], null); const rotorRight = getAttachmentSelectionMesh(dronePreviewRotorIdsHeavy[1], null); if (rotorLeft) { rotorLeft.updateMatrixWorld(true); rotorLeft.getWorldPosition(dronePreviewBeaconA.position); dronePreviewBeaconA.scale.setScalar(rotorPulse); } if (rotorRight) { rotorRight.updateMatrixWorld(true); rotorRight.getWorldPosition(dronePreviewBeaconB.position); dronePreviewBeaconB.scale.setScalar(rotorPulse); } (dronePreviewBeaconA.material as THREE.MeshBasicMaterial).color.setHex(0xff8a5b); (dronePreviewBeaconB.material as THREE.MeshBasicMaterial).color.setHex(0xffd1a3); } else { const sweepYaw = Math.sin(t * 1.6) * 0.42; dronePreviewRight.crossVectors(dronePreviewForward, new THREE.Vector3(0, 1, 0)); if (dronePreviewRight.lengthSq() < 1e-4) { dronePreviewRight.set(1, 0, 0); } dronePreviewRight.normalize(); dronePreviewTarget .copy(dronePreviewOrigin) .addScaledVector(dronePreviewForward, 2.2) .addScaledVector(dronePreviewRight, sweepYaw); setDronePreviewLine(dronePreviewOrigin, dronePreviewTarget); dronePreviewMidpoint.copy(dronePreviewOrigin).add(dronePreviewTarget).multiplyScalar(0.5); dronePreviewForward.copy(dronePreviewTarget).sub(dronePreviewOrigin).normalize(); dronePreviewScoutSweep.position.copy(dronePreviewMidpoint); dronePreviewScoutSweep.quaternion.setFromUnitVectors(dronePreviewSensorUp, dronePreviewForward); dronePreviewScoutSweep.scale.set(0.56, 1.7, 0.56); (dronePreviewScoutSweep.material as THREE.MeshBasicMaterial).opacity = 0.09 + Math.abs(Math.sin(t * 1.6)) * 0.14; const beaconPulse = 0.62 + 0.38 * Math.sin(t * 4.4); const beaconLeft = getAttachmentSelectionMesh(dronePreviewRotorIdsScout[0], null); const beaconRight = getAttachmentSelectionMesh(dronePreviewRotorIdsScout[1], null); if (beaconLeft) { beaconLeft.updateMatrixWorld(true); beaconLeft.getWorldPosition(dronePreviewBeaconA.position); dronePreviewBeaconA.scale.setScalar(beaconPulse); } if (beaconRight) { beaconRight.updateMatrixWorld(true); beaconRight.getWorldPosition(dronePreviewBeaconB.position); dronePreviewBeaconB.scale.setScalar(beaconPulse); } (dronePreviewBeaconA.material as THREE.MeshBasicMaterial).color.setHex(0x78e8ff); (dronePreviewBeaconB.material as THREE.MeshBasicMaterial).color.setHex(0xb6fbff); } return true; } function isLeaperRuntimeBodyPreviewActive(): boolean { return currentBasePresetName === 'leaper-arc' && state.showModules; } function isLeaperRuntimeBodyAttachment(attachment: AttachmentEntry): boolean { return attachment.id.startsWith('attach-leaper-') && !LEAPER_RUNTIME_VISIBLE_BODY_ATTACHMENT_IDS.has(attachment.id); } function getLeaperRuntimeBodyFrame(usePreviewOverrides = true): Frame | null { const coreFront = getNodeById('node-core-front'); const coreBack = getNodeById('node-core-back'); const dorsal = getNodeById('node-dorsal'); if (!coreFront || !coreBack || !dorsal) return null; const front = resolveNodeWorldTransform(coreFront, new Set(), usePreviewOverrides); const back = resolveNodeWorldTransform(coreBack, new Set(), usePreviewOverrides); const dorsalWorld = resolveNodeWorldTransform(dorsal, new Set(), usePreviewOverrides); const origin = front.position.clone().lerp(back.position, 0.5); const forward = front.position.clone().sub(back.position); if (forward.lengthSq() <= 1e-6) return null; forward.normalize(); const dorsalOffset = dorsalWorld.position.clone().sub(origin).projectOnPlane(forward); let up = dorsalOffset.lengthSq() > 1e-6 ? dorsalOffset.normalize() : new THREE.Vector3(0, 1, 0).applyQuaternion(front.quaternion).projectOnPlane(forward); if (up.lengthSq() <= 1e-6) { up = new THREE.Vector3(0, 1, 0); } else { up.normalize(); } return makeFrame(origin, forward, up); } function clearLeaperRuntimePreview(): void { clearGroup(leaperRuntimePreviewBaseGroup, true, true); clearGroup(leaperRuntimePreviewDetailGroup, true, true); clearGroup(leaperRuntimePreviewHighlightGroup, true, true); clearGroup(leaperRuntimePreviewWeaponGroup, true, true); leaperRuntimePreviewRig = null; leaperRuntimePreviewBaseFrame = null; leaperRuntimePreviewBasePosition = null; leaperRuntimePreviewBaseQuaternion = null; } function syncLeaperRuntimePreviewBody(): void { if (!leaperRuntimePreviewRig || !leaperRuntimePreviewBaseFrame || !leaperRuntimePreviewBasePosition || !leaperRuntimePreviewBaseQuaternion) { return; } const currentFrame = getLeaperRuntimeBodyFrame(true); if (!currentFrame) return; const baseQuaternion = frameToQuaternion(leaperRuntimePreviewBaseFrame); const currentQuaternion = frameToQuaternion(currentFrame); const deltaQuaternion = currentQuaternion.clone().multiply(baseQuaternion.clone().invert()); const relativePosition = leaperRuntimePreviewBasePosition .clone() .sub(leaperRuntimePreviewBaseFrame.origin) .applyQuaternion(deltaQuaternion); leaperRuntimePreviewRig.position.copy(currentFrame.origin).add(relativePosition); leaperRuntimePreviewRig.quaternion.copy(deltaQuaternion.multiply(leaperRuntimePreviewBaseQuaternion.clone())); leaperRuntimePreviewRig.updateMatrixWorld(true); } function rebuildLeaperRuntimePreview(): void { clearLeaperRuntimePreview(); if (currentBasePresetName !== 'leaper-arc') return; const preview = buildLeaperArcUnitEditorPreviewIntoLayers(leaperRuntimePreviewLayers); if (!preview) return; leaperRuntimePreviewRig = preview.rig; leaperRuntimePreviewBaseFrame = getLeaperRuntimeBodyFrame(false); leaperRuntimePreviewBasePosition = preview.rig.position.clone(); leaperRuntimePreviewBaseQuaternion = preview.rig.quaternion.clone(); syncLeaperRuntimePreviewBody(); } const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2(); let gizmoPointerActive = false; let pendingPointerMove: PointerEvent | null = null; let pointerMoveScheduled = false; let liveRebuildScheduled = false; let pendingLiveRebuild = false; let pendingTransformUiRefresh = false; let pendingUiRefreshOnPointerUp = false; let pendingAttachmentRebuild = false; let renderDirtyFrames = DIRTY_RENDER_FRAMES; let lastRenderTimeMs = performance.now(); let orbitInteracting = false; let pageVisible = document.visibilityState !== 'hidden'; let activePoseAvatarRoot: THREE.Object3D | null = null; let bootHydrationInProgress = true; let hasUserEditedRecipe = false; let refreshUiTimer: number | null = null; let refreshUiPending = false; let recipePreviewTimer: number | null = null; let recipePreviewPending = false; let firstInteractiveFrameSeen = false; let postInteractiveTasks: Array<() => void> = []; function markFirstInteractiveFrame(): void { if (firstInteractiveFrameSeen) return; firstInteractiveFrameSeen = true; if (postInteractiveTasks.length <= 0) return; const pending = postInteractiveTasks; postInteractiveTasks = []; for (const task of pending) { scheduleIdleTask(() => { task(); }); } } function scheduleAfterFirstInteractiveFrame(task: () => void): void { if (firstInteractiveFrameSeen) { scheduleIdleTask(() => { task(); }); return; } postInteractiveTasks.push(task); } raycaster.params.Line.threshold = 0.04; orbit.addEventListener('start', () => { markFirstInteractiveFrame(); orbitInteracting = true; markRenderDirty(2); }); orbit.addEventListener('end', () => { orbitInteracting = false; markRenderDirty(2); }); orbit.addEventListener('change', () => { markRenderDirty(2); }); transform.addEventListener('change', () => { markRenderDirty(2); }); document.addEventListener('visibilitychange', () => { pageVisible = document.visibilityState !== 'hidden'; if (pageVisible) { poseClock.getDelta(); markRenderDirty(3); } }); const moduleCatalog = buildModuleCatalog(); const state: EditorState = { nodes: [], edges: [], anchors: [], generators: [], attachments: [], sceneProps: [], recipeMetadata: null, selection: { kind: null }, hover: { kind: null }, attachmentInstanceIndex: null, proxyMesh: null, proxyObject: null, surfacePickMode: false, mode: 'select', edgeDraftStart: null, dragState: null, anchorDragState: null, generatorDragState: null, pendingDeselect: false, showGrid: true, showEdges: true, showAnchors: true, showGenerators: true, showModules: true, showProxy: true, showSceneProps: true, enableLeaperTerrainIk: true, leaperIkForeAftLimitDeg: LEAPER_ARC_TERRAIN_IK_DEFAULT_LIMITS.foreAftTiltLimitDeg, leaperIkLateralLimitDeg: LEAPER_ARC_TERRAIN_IK_DEFAULT_LIMITS.lateralTiltLimitDeg, showAttachmentAxes: false, profileGeneration: false, showSkeleton: false, showWeaponQa: false, showFootProbes: true, showFootProbeLabels: true, mirrorAttachments: true, gizmoEnabled: true, gizmoMode: 'translate', edgeCache: new Map(), nextIds: { node: 1, edge: 1, anchor: 1, generator: 1, attachment: 1, sceneProp: 1 } }; const SLOT_COLORS: Record = { [MaterialSlot.BASE]: 0x9aa7b4, [MaterialSlot.TEAM_PRIMARY]: 0x4e79a7, [MaterialSlot.TEAM_SECONDARY]: 0xf28e2b, [MaterialSlot.ACCENT]: 0xe15759 }; const moduleMeshCache = new Map(); const attachmentMeshCache = new Map(); const plannedWeaponGeometryVersion = 'planned-v18'; const sculptMeshCache = new Map(); let lastSocketExport: SocketExport[] = []; const runtimeTargetPreviewTriggerStates: RuntimeTargetPreviewTriggerState[] = []; const runtimeTargetPreviewLightStates: RuntimeTargetPreviewLightState[] = []; let runtimeTargetPreviewRecoverAtMs = 0; const runtimeTargetPreviewMarkerGeometry = new THREE.SphereGeometry(0.025, 10, 8); const runtimeTargetPreviewBaseTransformMap = new Map(); let runtimeTargetSelectedTriggerKey: string | null = null; let runtimeTargetTriggerEditorExpanded = false; let runtimeTargetTimelinePlaying = false; let runtimeTargetTimelineMs = 0; let runtimeTargetTimelineLastMs = 0; let runtimeTargetPreviewAudioContext: AudioContext | null = null; let runtimeTargetPreviewAudioUnlockPending = false; const runtimeTargetPreviewSoundTimeouts = new Set(); type GenerationProfileEntry = { ms: number; count: number }; type GenerationProfile = { label: string; startedAt: number; stages: Map; meshes: Map; counters: Map; slow: Array<{ name: string; ms: number }>; }; const GENERATION_PROFILE_SLOW_THRESHOLD_MS = 4; let activeGenerationProfile: GenerationProfile | null = null; function createGenerationProfile(label: string): GenerationProfile { return { label, startedAt: performance.now(), stages: new Map(), meshes: new Map(), counters: new Map(), slow: [] }; } function bumpGenerationCounter(profile: GenerationProfile, key: string, delta = 1): void { profile.counters.set(key, (profile.counters.get(key) ?? 0) + delta); } function recordGenerationEntry( map: Map, key: string, ms: number ): void { const entry = map.get(key); if (entry) { entry.ms += ms; entry.count += 1; } else { map.set(key, { ms, count: 1 }); } } function recordGenerationStage(profile: GenerationProfile, key: string, ms: number): void { recordGenerationEntry(profile.stages, key, ms); } function recordGenerationMesh(profile: GenerationProfile, key: string, ms: number): void { recordGenerationEntry(profile.meshes, key, ms); if (ms >= GENERATION_PROFILE_SLOW_THRESHOLD_MS) { profile.slow.push({ name: key, ms }); } } function finishGenerationProfile(profile: GenerationProfile): void { const totalMs = performance.now() - profile.startedAt; const stageRows = [...profile.stages.entries()] .map(([name, entry]) => ({ stage: name, ms: Number(entry.ms.toFixed(2)), count: entry.count })) .sort((a, b) => b.ms - a.ms); const meshRows = [...profile.meshes.entries()] .map(([name, entry]) => ({ mesh: name, ms: Number(entry.ms.toFixed(2)), count: entry.count, avgMs: Number((entry.ms / Math.max(1, entry.count)).toFixed(2)) })) .sort((a, b) => b.ms - a.ms) .slice(0, 12); const slowRows = profile.slow .sort((a, b) => b.ms - a.ms) .slice(0, 12) .map((entry) => ({ mesh: entry.name, ms: Number(entry.ms.toFixed(2)) })); const counters = Object.fromEntries(profile.counters.entries()); console.groupCollapsed(`[WeaponGen] ${profile.label} ${totalMs.toFixed(2)}ms`); if (stageRows.length > 0) console.table(stageRows); if (meshRows.length > 0) console.table(meshRows); if (slowRows.length > 0) console.table(slowRows); if (Object.keys(counters).length > 0) console.info('[WeaponGen] Counters', counters); console.groupEnd(); } function parseNumber(value: string, fallback: number): number { const n = Number.parseFloat(value); return Number.isFinite(n) ? n : fallback; } function clamp01(value: number): number { return Math.max(0, Math.min(1, value)); } function smoothstep(edge0: number, edge1: number, value: number): number { const t = clamp01((value - edge0) / Math.max(1e-6, edge1 - edge0)); return t * t * (3 - 2 * t); } function gaussianBand(value: number, center: number, width: number): number { const sigma = Math.max(0.001, width); const delta = (value - center) / sigma; return Math.exp(-(delta * delta)); } function parseOptionalInputValue(input: HTMLInputElement | null, fallback: number): number { return input ? parseNumber(input.value, fallback) : fallback; } function setOptionalInputValue(input: HTMLInputElement | null, value: number): void { if (!input) return; input.value = value.toFixed(2); } function snapValue(value: number, step: number): number { if (step <= 0) return value; return Math.round(value / step) * step; } function parseCsv(value: string): string[] { return value .split(',') .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); } function getEditorPixelRatio(): number { return Math.min(window.devicePixelRatio || 1, MAX_EDITOR_PIXEL_RATIO); } function isInteractionActive(): boolean { return Boolean(state.dragState || state.anchorDragState || state.generatorDragState || gizmoPointerActive); } function getEdgeRenderSamples(): number { return isInteractionActive() ? EDGE_RENDER_SAMPLES_DRAG : EDGE_RENDER_SAMPLES; } type PendingPresetLodRestore = { token: number; presetName: string; }; function setAttachmentPreviewLod(enabled: boolean): void { if (useAttachmentPreviewLod === enabled) return; useAttachmentPreviewLod = enabled; rebuildDerivedGroups(); } function isMarksmanPreset(name: string): boolean { return name === 'humanoid-marksman' || name === 'weapon-marksman-standalone'; } function shouldUsePreviewLodForPreset(name: string, doc: RecipeDocument): boolean { if (isMarksmanPreset(name)) return true; const attachmentCount = doc.attachments.length; const edgeCount = doc.edges.length; const nodeCount = doc.nodes.length; return attachmentCount >= 12 || (attachmentCount >= 8 && (edgeCount + nodeCount) >= 18); } function schedulePresetLodRestore(presetName: string, token: number): void { const runRestore = (): void => { if (!pendingPresetLodRestore) return; if (pendingPresetLodRestore && presetLodRestore?.token !== token) return; if (currentBasePresetName !== presetName) return; pendingPresetLodRestore = false; presetLodRestore = null; if (useAttachmentPreviewLod) { setAttachmentPreviewLod(false); } }; requestAnimationFrame(() => { requestAnimationFrame(() => { const win = window as | (Window & { requestIdleCallback?: (callback: () => void, options?: { timeout?: number }) => number; }) | undefined; if (win?.requestIdleCallback) { win.requestIdleCallback(runRestore, { timeout: 1000 }); } else { setTimeout(runRestore, 30); } }); }); } function applyPreviewLodToDef(def: HumanoidPartDefinition, enabled: boolean): HumanoidPartDefinition { if (!enabled || !def.size) return def; const size = { ...def.size }; if (typeof size.segments === 'number') { size.segments = Math.max(ATTACHMENT_LOD_MIN_SEGMENTS, Math.floor(size.segments * ATTACHMENT_LOD_SEGMENT_SCALE)); } if (typeof size.rings === 'number') { size.rings = Math.max(ATTACHMENT_LOD_MIN_RINGS, Math.floor(size.rings * ATTACHMENT_LOD_SEGMENT_SCALE)); } return { ...def, size }; } function updateRaycasterLayers(): void { raycaster.layers.mask = 0; raycaster.layers.enable(LAYER_NODE); if (state.showEdges) raycaster.layers.enable(LAYER_EDGE); if (state.showAnchors) raycaster.layers.enable(LAYER_ANCHOR); if (state.showGenerators) raycaster.layers.enable(LAYER_GENERATOR); if (state.showModules) raycaster.layers.enable(LAYER_ATTACHMENT); if (state.showSceneProps) raycaster.layers.enable(LAYER_SCENE_PROP); } function readAnchorOffset(): Vec3Like | undefined { const x = parseNumber(controls.anchorOffsetX.value, 0); const y = parseNumber(controls.anchorOffsetY.value, 0); const z = parseNumber(controls.anchorOffsetZ.value, 0); if (Math.abs(x) < 1e-5 && Math.abs(y) < 1e-5 && Math.abs(z) < 1e-5) { return undefined; } return vec3(x, y, z); } function readAttachmentOffset(): Vec3Like | undefined { const x = parseNumber(controls.attachmentOffsetX.value, 0); const y = parseNumber(controls.attachmentOffsetY.value, 0); const z = parseNumber(controls.attachmentOffsetZ.value, 0); if (Math.abs(x) < 1e-5 && Math.abs(y) < 1e-5 && Math.abs(z) < 1e-5) { return undefined; } return vec3(x, y, z); } function readAttachmentRotation(): Vec3Like | undefined { const x = parseNumber(controls.attachmentRotX.value, 0); const y = parseNumber(controls.attachmentRotY.value, 0); const z = parseNumber(controls.attachmentRotZ.value, 0); if (Math.abs(x) < 1e-5 && Math.abs(y) < 1e-5 && Math.abs(z) < 1e-5) { return undefined; } return vec3(x, y, z); } function readAttachmentScale(): Vec3Like | undefined { const x = parseNumber(controls.attachmentScaleX.value, 1); const y = parseNumber(controls.attachmentScaleY.value, 1); const z = parseNumber(controls.attachmentScaleZ.value, 1); if (Math.abs(x - 1) < 1e-5 && Math.abs(y - 1) < 1e-5 && Math.abs(z - 1) < 1e-5) { return undefined; } return vec3(x, y, z); } function readSculptOffset(xEl: HTMLInputElement, yEl: HTMLInputElement, zEl: HTMLInputElement): Vec3Like { return vec3(parseNumber(xEl.value, 0), parseNumber(yEl.value, 0), parseNumber(zEl.value, 0)); } function isUserTyping(): boolean { const el = document.activeElement as HTMLElement | null; if (!el) return false; const tag = el.tagName.toLowerCase(); return tag === 'input' || tag === 'textarea' || tag === 'select'; } function updateHoverText(text: string): void { controls.hover.textContent = text; } function markRenderDirty(frames = DIRTY_RENDER_FRAMES): void { renderDirtyFrames = Math.max(renderDirtyFrames, frames); } function clearRuntimeTargetPreviewSoundTimers(): void { for (const timeoutId of runtimeTargetPreviewSoundTimeouts) { window.clearTimeout(timeoutId); } runtimeTargetPreviewSoundTimeouts.clear(); } function ensureRuntimeTargetPreviewAudioContext(): AudioContext | null { const AudioCtor = window.AudioContext || (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (!AudioCtor) return null; if (!runtimeTargetPreviewAudioContext) { runtimeTargetPreviewAudioContext = new AudioCtor(); } if (runtimeTargetPreviewAudioContext.state === 'suspended' && !runtimeTargetPreviewAudioUnlockPending) { runtimeTargetPreviewAudioUnlockPending = true; void runtimeTargetPreviewAudioContext.resume().finally(() => { runtimeTargetPreviewAudioUnlockPending = false; }); } return runtimeTargetPreviewAudioContext; } function playRuntimeTargetPreviewSoundCue(trigger: RuntimeTargetSoundTrigger): void { const ctx = ensureRuntimeTargetPreviewAudioContext(); if (!ctx) return; const now = ctx.currentTime; const duration = Math.max(0.04, (trigger.durationMs ?? 180) * 0.001); const volume = THREE.MathUtils.clamp(trigger.volume ?? 0.18, 0, 1); const frequency = Math.max(40, trigger.frequencyHz ?? (trigger.cue === 'thump' ? 92 : trigger.cue === 'servo' ? 180 : trigger.cue === 'alarm' ? 620 : 520)); const osc = ctx.createOscillator(); const gain = ctx.createGain(); let waveform: OscillatorType = 'sine'; if (trigger.cue === 'servo') waveform = 'sawtooth'; if (trigger.cue === 'thump') waveform = 'triangle'; if (trigger.cue === 'alarm') waveform = 'square'; osc.type = waveform; osc.frequency.setValueAtTime(frequency, now); if (trigger.cue === 'servo') { osc.frequency.exponentialRampToValueAtTime(Math.max(70, frequency * 0.62), now + duration); } else if (trigger.cue === 'alarm') { osc.frequency.linearRampToValueAtTime(frequency * 1.18, now + duration * 0.45); osc.frequency.linearRampToValueAtTime(frequency * 0.92, now + duration); } else if (trigger.cue === 'thump') { osc.frequency.exponentialRampToValueAtTime(Math.max(42, frequency * 0.55), now + duration); } gain.gain.setValueAtTime(0.0001, now); gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, volume), now + Math.min(0.03, duration * 0.2)); gain.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc.connect(gain); gain.connect(ctx.destination); osc.start(now); osc.stop(now + duration + 0.02); } function getRuntimeTargetMotionProfileLabel(profile: RuntimeTargetMotionProfile | undefined): string { switch (profile ?? 'servo') { case 'snap': return 'snap'; case 'heavy': return 'heavy'; case 'rebound': return 'rebound'; case 'servo': default: return 'servo'; } } function easeOutCubic(t: number): number { const inv = 1 - t; return 1 - inv * inv * inv; } function easeInOutCubic(t: number): number { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } function easeOutBack(t: number): number { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); } function evaluateRuntimeTargetMotionValue( startValue: number, targetValue: number, rawAlpha: number, profile: RuntimeTargetMotionProfile | undefined ): number { const alpha = THREE.MathUtils.clamp(rawAlpha, 0, 1); let shaped = alpha; switch (profile ?? 'servo') { case 'snap': shaped = easeOutCubic(alpha); break; case 'heavy': shaped = easeInOutCubic(alpha); break; case 'rebound': shaped = easeOutBack(alpha); break; case 'servo': default: shaped = THREE.MathUtils.smootherstep(alpha, 0, 1); break; } return THREE.MathUtils.lerp(startValue, targetValue, shaped); } function scheduleRuntimeTargetPreviewSounds(event: RuntimeTargetTriggerEvent): void { const metadata = getRuntimeTargetMetadata(); if (!metadata) return; for (const trigger of metadata.soundTriggers) { if (trigger.event !== event) continue; const delayMs = Math.max(0, trigger.delayMs ?? 0); if (delayMs > 0) { const timeoutId = window.setTimeout(() => { runtimeTargetPreviewSoundTimeouts.delete(timeoutId); playRuntimeTargetPreviewSoundCue(trigger); }, delayMs); runtimeTargetPreviewSoundTimeouts.add(timeoutId); } else { playRuntimeTargetPreviewSoundCue(trigger); } } } function getRuntimeTargetTriggerEntries(metadata: RuntimeTargetMetadata | null): RuntimeTargetTriggerListEntry[] { if (!metadata) return []; return [ ...metadata.animationTriggers.map((trigger) => ({ key: `animation:${trigger.id}`, kind: 'animation' as const, trigger })), ...metadata.lightTriggers.map((trigger) => ({ key: `light:${trigger.id}`, kind: 'light' as const, trigger })), ...metadata.soundTriggers.map((trigger) => ({ key: `sound:${trigger.id}`, kind: 'sound' as const, trigger })) ]; } function getSelectedRuntimeTargetTriggerEntry(): RuntimeTargetTriggerListEntry | null { const metadata = getRuntimeTargetMetadata(); const entries = getRuntimeTargetTriggerEntries(metadata); if (!entries.length) return null; const selectedKey = controls.runtimeTargetTriggerList?.value || runtimeTargetSelectedTriggerKey; const entry = entries.find((candidate) => candidate.key === selectedKey) ?? entries[0]; runtimeTargetSelectedTriggerKey = entry.key; return entry; } function updateRuntimeTargetTriggerEditorLayout(): void { const metadata = getRuntimeTargetMetadata(); const hasMetadata = Boolean(metadata); if (!hasMetadata) { runtimeTargetTriggerEditorExpanded = false; } if (controls.runtimeTargetPanel) controls.runtimeTargetPanel.hidden = !hasMetadata; const entry = getSelectedRuntimeTargetTriggerEntry(); const hasEntry = Boolean(entry); const canEdit = hasMetadata && hasEntry; const editorVisible = canEdit && runtimeTargetTriggerEditorExpanded; if (controls.runtimeTargetTriggerEditor) { controls.runtimeTargetTriggerEditor.hidden = !editorVisible; } if (controls.runtimeTargetTriggerEditorToggle) { controls.runtimeTargetTriggerEditorToggle.disabled = !canEdit; controls.runtimeTargetTriggerEditorToggle.textContent = editorVisible ? 'Hide Trigger' : 'Edit Trigger'; controls.runtimeTargetTriggerEditorToggle.setAttribute('aria-expanded', editorVisible ? 'true' : 'false'); } if (controls.runtimeTargetTriggerSummary) { if (!hasMetadata) { controls.runtimeTargetTriggerSummary.textContent = 'No runtime metadata on this preset.'; } else if (!entry) { controls.runtimeTargetTriggerSummary.textContent = 'No runtime trigger selected.'; } else { const suffix = entry.kind === 'animation' ? ` | ${getRuntimeTargetMotionProfileLabel(entry.trigger.profile)}` : ''; controls.runtimeTargetTriggerSummary.textContent = `Selected ${entry.kind} trigger: ${entry.trigger.id} (${entry.trigger.event})${suffix}`; } } if (controls.runtimeTargetTriggerAmplitudeLabel) { controls.runtimeTargetTriggerAmplitudeLabel.textContent = entry?.kind === 'light' ? 'Intensity' : entry?.kind === 'sound' ? 'Volume' : 'Amplitude'; } const showSoundControls = editorVisible && entry?.kind === 'sound'; const showMotionProfile = editorVisible && entry?.kind === 'animation'; if (controls.runtimeTargetMotionProfileGroup) controls.runtimeTargetMotionProfileGroup.hidden = !showMotionProfile; if (controls.runtimeTargetSoundCueGroup) controls.runtimeTargetSoundCueGroup.hidden = !showSoundControls; if (controls.runtimeTargetSoundFrequencyGroup) controls.runtimeTargetSoundFrequencyGroup.hidden = !showSoundControls; } function updateRuntimeTargetTriggerOptions(): void { const select = controls.runtimeTargetTriggerList; if (!select) { updateRuntimeTargetTriggerEditorLayout(); return; } const metadata = getRuntimeTargetMetadata(); const entries = getRuntimeTargetTriggerEntries(metadata); const current = select.value || runtimeTargetSelectedTriggerKey; select.innerHTML = ''; if (!entries.length) { const option = document.createElement('option'); option.value = ''; option.textContent = 'No runtime triggers'; select.appendChild(option); select.disabled = true; runtimeTargetSelectedTriggerKey = null; updateRuntimeTargetTriggerEditorLayout(); return; } select.disabled = false; for (const entry of entries) { const option = document.createElement('option'); option.value = entry.key; option.textContent = `${entry.kind}: ${entry.trigger.id}`; select.appendChild(option); } const resolved = entries.find((entry) => entry.key === current)?.key ?? entries[0].key; select.value = resolved; runtimeTargetSelectedTriggerKey = resolved; updateRuntimeTargetTriggerEditorLayout(); } function updateRuntimeTargetTriggerInputs(): void { const entry = getSelectedRuntimeTargetTriggerEntry(); const eventSelect = controls.runtimeTargetTriggerEvent; const amplitudeInput = controls.runtimeTargetTriggerAmplitude; const delayInput = controls.runtimeTargetTriggerDelay; const durationInput = controls.runtimeTargetTriggerDuration; const motionProfileSelect = controls.runtimeTargetMotionProfile; const flickerInput = controls.runtimeTargetTriggerFlicker; const cueSelect = controls.runtimeTargetSoundCue; const frequencyInput = controls.runtimeTargetSoundFrequency; const hasEntry = Boolean(entry); if (eventSelect) eventSelect.disabled = !hasEntry; if (amplitudeInput) amplitudeInput.disabled = !hasEntry; if (delayInput) delayInput.disabled = !hasEntry; if (durationInput) durationInput.disabled = !hasEntry; if (motionProfileSelect) motionProfileSelect.disabled = entry?.kind !== 'animation'; if (flickerInput) flickerInput.disabled = !hasEntry; const isSound = entry?.kind === 'sound'; if (cueSelect) cueSelect.disabled = !isSound; if (frequencyInput) frequencyInput.disabled = !isSound; if (!entry) { if (eventSelect) eventSelect.value = 'activate'; if (amplitudeInput) amplitudeInput.value = '0'; if (delayInput) delayInput.value = '0'; if (durationInput) durationInput.value = '0'; if (motionProfileSelect) motionProfileSelect.value = 'servo'; if (flickerInput) flickerInput.checked = false; if (cueSelect) cueSelect.value = 'beep'; if (frequencyInput) frequencyInput.value = '440'; updateRuntimeTargetTriggerEditorLayout(); return; } if (eventSelect) eventSelect.value = entry.trigger.event; if (entry.kind === 'animation') { if (amplitudeInput) amplitudeInput.value = String(entry.trigger.amplitude ?? 0); if (delayInput) delayInput.value = String(entry.trigger.delayMs ?? 0); if (durationInput) durationInput.value = String(entry.trigger.durationMs ?? 0); if (motionProfileSelect) motionProfileSelect.value = entry.trigger.profile ?? 'servo'; if (flickerInput) flickerInput.checked = Boolean(entry.trigger.loop); if (cueSelect) cueSelect.value = 'beep'; if (frequencyInput) frequencyInput.value = '440'; } else if (entry.kind === 'light') { if (amplitudeInput) amplitudeInput.value = String(entry.trigger.intensity ?? 0); if (delayInput) delayInput.value = String(entry.trigger.delayMs ?? 0); if (durationInput) durationInput.value = String(entry.trigger.durationMs ?? 0); if (motionProfileSelect) motionProfileSelect.value = 'servo'; if (flickerInput) flickerInput.checked = Boolean(entry.trigger.flicker); if (cueSelect) cueSelect.value = 'beep'; if (frequencyInput) frequencyInput.value = '440'; } else { if (amplitudeInput) amplitudeInput.value = String(entry.trigger.volume ?? 0); if (delayInput) delayInput.value = String(entry.trigger.delayMs ?? 0); if (durationInput) durationInput.value = String(entry.trigger.durationMs ?? 0); if (motionProfileSelect) motionProfileSelect.value = 'servo'; if (flickerInput) flickerInput.checked = Boolean(entry.trigger.flicker); if (cueSelect) cueSelect.value = entry.trigger.cue; if (frequencyInput) frequencyInput.value = String(entry.trigger.frequencyHz ?? 440); } updateRuntimeTargetTriggerEditorLayout(); } function getRuntimeTargetTimelineDurationMs(metadata: RuntimeTargetMetadata | null): number { if (!metadata) return 2000; const animationEnd = metadata.animationTriggers.reduce((acc, trigger) => { const start = getRuntimeTargetEventStartMs(metadata, trigger.event) + (trigger.delayMs ?? 0); return Math.max(acc, start + (trigger.durationMs ?? 0)); }, 0); const lightEnd = metadata.lightTriggers.reduce((acc, trigger) => { const start = getRuntimeTargetEventStartMs(metadata, trigger.event) + (trigger.delayMs ?? 0); return Math.max(acc, start + (trigger.durationMs ?? 0)); }, 0); const soundEnd = metadata.soundTriggers.reduce((acc, trigger) => { const start = getRuntimeTargetEventStartMs(metadata, trigger.event) + (trigger.delayMs ?? 0); return Math.max(acc, start + (trigger.durationMs ?? 0)); }, 0); return Math.max(800, animationEnd, lightEnd, soundEnd, 1200); } function getRuntimeTargetEventStartMs(metadata: RuntimeTargetMetadata, event: RuntimeTargetTriggerEvent): number { void metadata; void event; return 0; } function updateRuntimeTargetTimelineInfo(): void { if (!controls.runtimeTargetTimelineInfo) return; const metadata = getRuntimeTargetMetadata(); if (!metadata) { controls.runtimeTargetTimelineInfo.textContent = 'Timeline idle.'; return; } const duration = getRuntimeTargetTimelineDurationMs(metadata); const lines = [ `Timeline: ${Math.round(runtimeTargetTimelineMs)} / ${duration} ms`, `Mode: ${runtimeTargetTimelinePlaying ? 'playing' : 'stopped'}`, `Sound hooks: ${metadata.soundTriggers.length}` ]; controls.runtimeTargetTimelineInfo.textContent = lines.join('\n'); } function captureRuntimeTargetPreviewBaseTransforms(): void { runtimeTargetPreviewBaseTransformMap.clear(); const metadata = getRuntimeTargetMetadata(); if (!metadata) return; for (const trigger of metadata.animationTriggers) { const target = attachmentMeshMap.get(trigger.targetId); if (!target || runtimeTargetPreviewBaseTransformMap.has(trigger.targetId)) continue; runtimeTargetPreviewBaseTransformMap.set(trigger.targetId, { position: target.position.clone(), rotation: target.rotation.clone() }); } } function applySelectedRuntimeTargetTriggerInputs(): void { const entry = getSelectedRuntimeTargetTriggerEntry(); if (!entry) return; const eventValue = (controls.runtimeTargetTriggerEvent?.value ?? entry.trigger.event) as RuntimeTargetTriggerEvent; const amplitude = parseNumber(controls.runtimeTargetTriggerAmplitude?.value ?? '0', 0); const delayMs = Math.max(0, parseNumber(controls.runtimeTargetTriggerDelay?.value ?? '0', 0)); const durationMs = Math.max(1, parseNumber(controls.runtimeTargetTriggerDuration?.value ?? '1', 1)); const motionProfile = (controls.runtimeTargetMotionProfile?.value ?? 'servo') as RuntimeTargetMotionProfile; const flicker = controls.runtimeTargetTriggerFlicker?.checked ?? false; entry.trigger.event = eventValue; if (entry.kind === 'animation') { entry.trigger.amplitude = amplitude; entry.trigger.delayMs = delayMs; entry.trigger.durationMs = durationMs; entry.trigger.profile = motionProfile; entry.trigger.loop = flicker; } else if (entry.kind === 'light') { entry.trigger.intensity = amplitude; entry.trigger.delayMs = delayMs; entry.trigger.durationMs = durationMs; entry.trigger.flicker = flicker; } else { entry.trigger.volume = THREE.MathUtils.clamp(amplitude, 0, 1); entry.trigger.delayMs = delayMs; entry.trigger.durationMs = durationMs; entry.trigger.flicker = flicker; entry.trigger.cue = (controls.runtimeTargetSoundCue?.value ?? entry.trigger.cue) as RuntimeTargetSoundCue; entry.trigger.frequencyHz = Math.max(40, parseNumber(controls.runtimeTargetSoundFrequency?.value ?? '440', 440)); } markPresetCustom(); captureRuntimeTargetPreviewBaseTransforms(); updateRuntimeTargetTriggerOptions(); updateRuntimeTargetTriggerInputs(); updateRuntimeTargetPreviewInfo(); updateRuntimeTargetTimelineInfo(); updateRecipePreview(); if (controls.runtimeTargetTimeline) { runtimeTargetTimelineMs = parseNumber(controls.runtimeTargetTimeline.value, runtimeTargetTimelineMs); } markRenderDirty(4); } function updateRuntimeTargetTimelineControls(): void { const metadata = getRuntimeTargetMetadata(); const enabled = Boolean(metadata); const duration = getRuntimeTargetTimelineDurationMs(metadata); if (controls.runtimeTargetTimeline) { controls.runtimeTargetTimeline.disabled = !enabled; controls.runtimeTargetTimeline.max = String(duration); controls.runtimeTargetTimeline.value = String(Math.min(duration, Math.max(0, runtimeTargetTimelineMs))); } if (controls.runtimeTargetSequencePlay) controls.runtimeTargetSequencePlay.disabled = !enabled; if (controls.runtimeTargetSequenceStop) controls.runtimeTargetSequenceStop.disabled = !enabled; } function ensureRuntimeTargetPreviewLightState(trigger: RuntimeTargetLightTrigger): RuntimeTargetPreviewLightState { const existingState = runtimeTargetPreviewLightStates.find((candidate) => candidate.key === `${trigger.id}:${trigger.socketId}`); if (existingState) { return existingState; } const light = new THREE.PointLight(0xffffff, 0, trigger.radius, 2); light.castShadow = false; const markerMaterial = new THREE.MeshStandardMaterial({ color: new THREE.Color(trigger.color), emissive: new THREE.Color(trigger.color), emissiveIntensity: 0, roughness: 0.3, metalness: 0.08 }); const marker = new THREE.Mesh(runtimeTargetPreviewMarkerGeometry, markerMaterial); marker.castShadow = false; runtimeTargetPreviewGroup.add(light, marker); const createdState: RuntimeTargetPreviewLightState = { key: `${trigger.id}:${trigger.socketId}`, event: trigger.event, socketId: trigger.socketId, light, marker, color: new THREE.Color(trigger.color), intensity: trigger.intensity, durationMs: Math.max(1, trigger.durationMs), delayMs: Math.max(0, trigger.delayMs ?? 0), elapsedMs: 0, flicker: Boolean(trigger.flicker) }; runtimeTargetPreviewLightStates.push(createdState); return createdState; } function evaluateRuntimeTargetTimeline(timeMs: number, playSounds = false): void { const metadata = getRuntimeTargetMetadata(); if (!metadata) { resetRuntimeTargetPreview(); return; } captureRuntimeTargetPreviewBaseTransforms(); clearRuntimeTargetPreviewSoundTimers(); runtimeTargetTimelineMs = THREE.MathUtils.clamp(timeMs, 0, getRuntimeTargetTimelineDurationMs(metadata)); const activeTargetKeys = new Set(); for (const [targetId, base] of runtimeTargetPreviewBaseTransformMap.entries()) { const target = attachmentMeshMap.get(targetId); if (!target) continue; target.position.copy(base.position); target.rotation.copy(base.rotation); target.updateMatrix(); } const animationEntries = metadata.animationTriggers .filter((trigger) => trigger.event === 'hit' || trigger.event === 'recover') .sort((a, b) => { const aStart = a.delayMs ?? 0; const bStart = b.delayMs ?? 0; return aStart - bStart; }); const groupedAnimations = new Map(); for (const trigger of animationEntries) { if (!trigger.axis) continue; const key = `${trigger.action}:${trigger.targetId}:${trigger.axis}`; const list = groupedAnimations.get(key) ?? []; list.push(trigger); groupedAnimations.set(key, list); } for (const [key, triggers] of groupedAnimations.entries()) { const trigger = [...triggers] .reverse() .find((candidate) => { const start = getRuntimeTargetEventStartMs(metadata, candidate.event) + (candidate.delayMs ?? 0); return runtimeTargetTimelineMs >= start; }); if (!trigger || !trigger.axis) continue; const target = attachmentMeshMap.get(trigger.targetId); const base = runtimeTargetPreviewBaseTransformMap.get(trigger.targetId); if (!target || !base) continue; const start = getRuntimeTargetEventStartMs(metadata, trigger.event) + (trigger.delayMs ?? 0); const end = start + Math.max(1, trigger.durationMs ?? 1); const localAlpha = THREE.MathUtils.clamp((runtimeTargetTimelineMs - start) / Math.max(1, end - start), 0, 1); const value = evaluateRuntimeTargetMotionValue( trigger.event === 'recover' ? (triggers[0]?.amplitude ?? 0) : 0, trigger.amplitude ?? 0, localAlpha, trigger.profile ); if (trigger.action === 'rotate') { target.rotation.copy(base.rotation); target.rotation[trigger.axis] = base.rotation[trigger.axis] + THREE.MathUtils.degToRad(value); } else { target.position.copy(base.position); target.position[trigger.axis] = base.position[trigger.axis] + value; } activeTargetKeys.add(key); } const activeLightKeys = new Set(); for (const trigger of metadata.lightTriggers.filter((candidate) => candidate.event === 'hit' || candidate.event === 'recover')) { const start = getRuntimeTargetEventStartMs(metadata, trigger.event) + (trigger.delayMs ?? 0); const end = start + Math.max(1, trigger.durationMs); if (runtimeTargetTimelineMs < start || runtimeTargetTimelineMs > end) continue; const anchor = getAnchorById(trigger.socketId); const frame = anchor ? resolveAnchorFrame(anchor) : null; if (!frame) continue; const state = ensureRuntimeTargetPreviewLightState(trigger); const alpha = THREE.MathUtils.clamp((runtimeTargetTimelineMs - start) / Math.max(1, trigger.durationMs), 0, 1); const flicker = trigger.flicker ? 0.88 + Math.sin(runtimeTargetTimelineMs * 0.05 + alpha * 6) * 0.12 : 1; const intensity = (1 - alpha) * trigger.intensity * flicker; state.light.position.copy(frame.origin); state.light.intensity = intensity; state.light.distance = trigger.radius; state.marker.position.copy(frame.origin); state.marker.scale.setScalar(0.72 + intensity * 0.12); const markerMaterial = state.marker.material; if (markerMaterial instanceof THREE.MeshStandardMaterial) { markerMaterial.color.set(trigger.color); markerMaterial.emissive.set(trigger.color); markerMaterial.emissiveIntensity = intensity * 0.8; } activeLightKeys.add(state.key); } for (let i = runtimeTargetPreviewLightStates.length - 1; i >= 0; i -= 1) { const state = runtimeTargetPreviewLightStates[i]; if (activeLightKeys.has(state.key)) continue; runtimeTargetPreviewGroup.remove(state.light, state.marker); runtimeTargetPreviewLightStates.splice(i, 1); } if (playSounds) { for (const trigger of metadata.soundTriggers.filter((candidate) => candidate.event === 'hit' || candidate.event === 'recover')) { const start = getRuntimeTargetEventStartMs(metadata, trigger.event) + (trigger.delayMs ?? 0); if (runtimeTargetTimelineLastMs < start && runtimeTargetTimelineMs >= start) { playRuntimeTargetPreviewSoundCue(trigger); } } } runtimeTargetTimelineLastMs = runtimeTargetTimelineMs; if (controls.runtimeTargetTimeline) { controls.runtimeTargetTimeline.value = String(runtimeTargetTimelineMs); } updateRuntimeTargetPreviewInfo(); updateRuntimeTargetTimelineInfo(); markRenderDirty(2); } function getRuntimeTargetMetadata(): RuntimeTargetMetadata | null { return state.recipeMetadata?.runtimeTarget ?? null; } function updateRuntimeTargetPreviewInfo(): void { if (!controls.runtimeTargetPreviewInfo) return; const metadata = getRuntimeTargetMetadata(); if (!metadata) { controls.runtimeTargetPreviewInfo.textContent = 'Runtime target preview unavailable.'; return; } const lines = [ `Runtime target: ${metadata.id} (${metadata.variant})`, `Animations: ${metadata.animationTriggers.length} | lights: ${metadata.lightTriggers.length} | sounds: ${metadata.soundTriggers.length}`, `Hit socket: ${metadata.hitSurfaceSocketId}`, `Recover socket: ${metadata.recoverSocketId}`, `Active anims: ${runtimeTargetPreviewTriggerStates.length} | active lights: ${runtimeTargetPreviewLightStates.length}` ]; controls.runtimeTargetPreviewInfo.textContent = lines.join('\n'); } function resetRuntimeTargetPreview(): void { for (const state of runtimeTargetPreviewTriggerStates) { if (state.action === 'rotate') { state.target.rotation.copy(state.baseRotation); } else { state.target.position.copy(state.basePosition); } state.target.updateMatrix(); } runtimeTargetPreviewTriggerStates.length = 0; runtimeTargetPreviewLightStates.length = 0; runtimeTargetPreviewRecoverAtMs = 0; runtimeTargetTimelinePlaying = false; clearRuntimeTargetPreviewSoundTimers(); runtimeTargetPreviewGroup.clear(); updateRuntimeTargetPreviewInfo(); updateRuntimeTargetTimelineInfo(); markRenderDirty(2); } function triggerRuntimeTargetPreview(event: RuntimeTargetTriggerEvent): void { const metadata = getRuntimeTargetMetadata(); if (!metadata) { updateRuntimeTargetPreviewInfo(); return; } captureRuntimeTargetPreviewBaseTransforms(); for (const trigger of metadata.animationTriggers) { if (trigger.event !== event || trigger.action === 'lightPulse' || !trigger.axis) continue; const target = attachmentMeshMap.get(trigger.targetId); if (!target) continue; const key = `${trigger.action}:${trigger.targetId}:${trigger.axis}`; const existing = runtimeTargetPreviewTriggerStates.find((state) => state.key === key); const currentValue = existing?.currentValue ?? 0; const nextState: RuntimeTargetPreviewTriggerState = existing ?? { key, event, action: trigger.action, axis: trigger.axis, targetId: trigger.targetId, target, basePosition: target.position.clone(), baseRotation: target.rotation.clone(), startValue: currentValue, currentValue, targetValue: trigger.amplitude ?? 0, delayMs: trigger.delayMs ?? 0, elapsedMs: 0, durationMs: Math.max(1, trigger.durationMs ?? 240), profile: trigger.profile ?? 'servo' }; nextState.event = event; nextState.action = trigger.action; nextState.axis = trigger.axis; nextState.target = target; nextState.basePosition.copy(target.position); nextState.baseRotation.copy(target.rotation); nextState.startValue = currentValue; nextState.currentValue = currentValue; nextState.targetValue = trigger.amplitude ?? 0; nextState.delayMs = trigger.delayMs ?? 0; nextState.elapsedMs = 0; nextState.durationMs = Math.max(1, trigger.durationMs ?? 240); nextState.profile = trigger.profile ?? 'servo'; if (!existing) { runtimeTargetPreviewTriggerStates.push(nextState); } } for (const trigger of metadata.lightTriggers) { if (trigger.event !== event) continue; const anchor = getAnchorById(trigger.socketId); const frame = anchor ? resolveAnchorFrame(anchor) : null; if (!frame) continue; const key = `${trigger.id}:${trigger.socketId}`; let state = runtimeTargetPreviewLightStates.find((candidate) => candidate.key === key); if (!state) { const light = new THREE.PointLight(0xffffff, 0, trigger.radius, 2); light.castShadow = false; const markerMaterial = new THREE.MeshStandardMaterial({ color: new THREE.Color(trigger.color), emissive: new THREE.Color(trigger.color), emissiveIntensity: 0.1, roughness: 0.3, metalness: 0.08 }); const marker = new THREE.Mesh(runtimeTargetPreviewMarkerGeometry, markerMaterial); marker.castShadow = false; runtimeTargetPreviewGroup.add(light, marker); state = { key, event, socketId: trigger.socketId, light, marker, color: new THREE.Color(trigger.color), intensity: trigger.intensity, durationMs: Math.max(1, trigger.durationMs), delayMs: Math.max(0, trigger.delayMs ?? 0), elapsedMs: 0, flicker: Boolean(trigger.flicker) }; runtimeTargetPreviewLightStates.push(state); } state.event = event; state.socketId = trigger.socketId; state.color.set(trigger.color); state.intensity = trigger.intensity; state.durationMs = Math.max(1, trigger.durationMs); state.delayMs = Math.max(0, trigger.delayMs ?? 0); state.elapsedMs = 0; state.flicker = Boolean(trigger.flicker); state.light.color.copy(state.color); state.light.distance = trigger.radius; state.light.position.copy(frame.origin); state.marker.position.copy(frame.origin); const markerMaterial = state.marker.material; if (markerMaterial instanceof THREE.MeshStandardMaterial) { markerMaterial.color.copy(state.color); markerMaterial.emissive.copy(state.color); markerMaterial.emissiveIntensity = 0.1; } } if (event === 'hit' && (controls.runtimeTargetAutoRecover?.checked ?? true)) { const maxDelay = metadata.animationTriggers .filter((trigger) => trigger.event === 'recover') .reduce((acc, trigger) => Math.max(acc, trigger.delayMs ?? 0), 700); runtimeTargetPreviewRecoverAtMs = performance.now() + Math.max(120, maxDelay); } else if (event === 'recover') { runtimeTargetPreviewRecoverAtMs = 0; } scheduleRuntimeTargetPreviewSounds(event); updateRuntimeTargetPreviewInfo(); markRenderDirty(6); } function updateRuntimeTargetPreview(dtMs: number, nowMs: number): boolean { const metadata = getRuntimeTargetMetadata(); if (!metadata) { if (runtimeTargetPreviewTriggerStates.length > 0 || runtimeTargetPreviewLightStates.length > 0) { resetRuntimeTargetPreview(); } return false; } if (runtimeTargetPreviewRecoverAtMs > 0 && nowMs >= runtimeTargetPreviewRecoverAtMs) { triggerRuntimeTargetPreview('recover'); } let active = false; for (let i = runtimeTargetPreviewTriggerStates.length - 1; i >= 0; i -= 1) { const state = runtimeTargetPreviewTriggerStates[i]; if (!attachmentMeshMap.has(state.targetId)) { runtimeTargetPreviewTriggerStates.splice(i, 1); continue; } const target = attachmentMeshMap.get(state.targetId); if (!target) { runtimeTargetPreviewTriggerStates.splice(i, 1); continue; } state.target = target; if (state.delayMs > 0) { state.delayMs = Math.max(0, state.delayMs - dtMs); active = true; continue; } state.elapsedMs += dtMs; const alpha = THREE.MathUtils.clamp(state.elapsedMs / Math.max(1, state.durationMs), 0, 1); state.currentValue = evaluateRuntimeTargetMotionValue( state.startValue, state.targetValue, alpha, state.profile ); if (state.action === 'rotate') { state.target.rotation.copy(state.baseRotation); state.target.rotation[state.axis] = state.baseRotation[state.axis] + THREE.MathUtils.degToRad(state.currentValue); } else { state.target.position.copy(state.basePosition); state.target.position[state.axis] = state.basePosition[state.axis] + state.currentValue; } const settled = alpha >= 1; if (settled && state.event === 'recover' && Math.abs(state.targetValue) <= 1e-3) { if (state.action === 'rotate') { state.target.rotation.copy(state.baseRotation); } else { state.target.position.copy(state.basePosition); } runtimeTargetPreviewTriggerStates.splice(i, 1); continue; } if (settled) { state.currentValue = state.targetValue; } active = true; } for (let i = runtimeTargetPreviewLightStates.length - 1; i >= 0; i -= 1) { const state = runtimeTargetPreviewLightStates[i]; const anchor = getAnchorById(state.socketId); const frame = anchor ? resolveAnchorFrame(anchor) : null; if (!frame) { runtimeTargetPreviewGroup.remove(state.light, state.marker); runtimeTargetPreviewLightStates.splice(i, 1); continue; } state.light.position.copy(frame.origin); state.marker.position.copy(frame.origin); if (state.delayMs > 0) { state.delayMs = Math.max(0, state.delayMs - dtMs); state.light.intensity = 0; state.marker.visible = false; active = true; continue; } state.elapsedMs += dtMs; const alpha = THREE.MathUtils.clamp(state.elapsedMs / Math.max(1, state.durationMs), 0, 1); const flicker = state.flicker ? 0.88 + Math.sin(nowMs * 0.05 + i) * 0.12 : 1; const intensity = (1 - alpha) * state.intensity * flicker; state.light.intensity = intensity; state.marker.scale.setScalar(0.7 + intensity * 0.12); state.marker.visible = intensity > 0.01; const markerMaterial = state.marker.material; if (markerMaterial instanceof THREE.MeshStandardMaterial) { markerMaterial.emissiveIntensity = intensity * 0.85; } if (alpha >= 1) { runtimeTargetPreviewGroup.remove(state.light, state.marker); runtimeTargetPreviewLightStates.splice(i, 1); continue; } active = true; } if (active) { markRenderDirty(2); } updateRuntimeTargetPreviewInfo(); return active; } function setHover(kind: SelectionKind, id?: string): void { const same = state.hover.kind === kind && state.hover.id === id; state.hover = { kind, id }; if (!same) { applyVisualState(); } } function applyVisualState(): void { const subtleNodeOverlay = currentBasePresetName === 'leaper-arc' && !state.showSkeleton; const subtleAnchorOverlay = currentBasePresetName === 'leaper-arc'; for (const [id, mesh] of nodeMeshMap.entries()) { const isSelected = state.selection.kind === 'node' && state.selection.id === id; const isHovered = state.hover.kind === 'node' && state.hover.id === id; if (isSelected) { mesh.material = nodeSelectedMaterial; mesh.scale.setScalar(subtleNodeOverlay ? 0.72 : 1); } else if (isHovered) { mesh.material = nodeHoverMaterial; mesh.scale.setScalar(subtleNodeOverlay ? 0.64 : 1); } else if (subtleNodeOverlay) { mesh.material = nodeSubtleMaterial; mesh.scale.setScalar(0.34); } else if (state.showSkeleton) { const node = getNodeById(id); mesh.material = node?.mode === 'relative' ? nodeRelativeMaterial : nodeStaticMaterial; mesh.scale.setScalar(1); } else { mesh.material = nodeMaterial; mesh.scale.setScalar(1); } } for (const [id, mesh] of anchorMeshMap.entries()) { const isSelected = state.selection.kind === 'anchor' && state.selection.id === id; const isHovered = state.hover.kind === 'anchor' && state.hover.id === id; if (isSelected) { mesh.material = anchorSelectedMaterial; mesh.scale.setScalar(subtleAnchorOverlay ? 0.66 : 1); } else if (isHovered) { mesh.material = anchorHoverMaterial; mesh.scale.setScalar(subtleAnchorOverlay ? 0.56 : 1); } else if (subtleAnchorOverlay) { mesh.material = anchorSubtleMaterial; mesh.scale.setScalar(0.3); } else { mesh.material = anchorMaterial; mesh.scale.setScalar(1); } const axes = anchorAxesMap.get(id); if (axes) { axes.visible = state.showAnchors && (!subtleAnchorOverlay || isSelected || isHovered); } } for (const [id, line] of edgeLineMap.entries()) { const isSelected = state.selection.kind === 'edge' && state.selection.id === id; const isHovered = state.hover.kind === 'edge' && state.hover.id === id; line.material = isSelected ? edgeSelectedMaterial : (isHovered ? edgeHoverMaterial : edgeMaterial); } for (const [id, object] of scenePropMeshMap.entries()) { const isSelected = state.selection.kind === 'scene-prop' && state.selection.id === id; const isHovered = state.hover.kind === 'scene-prop' && state.hover.id === id; const material = isSelected ? scenePropSelectedMaterial : (isHovered ? scenePropHoverMaterial : scenePropMaterial); object.traverse((child) => { const mesh = child as THREE.Mesh; if (mesh.isMesh) { mesh.material = material; } }); } markRenderDirty(); } function vec3(x: number, y: number, z: number): Vec3Like { return { x, y, z }; } const DEFAULT_WEAPON_HOLD_PROFILE: WeaponHoldProfile = { handDirection: { leftLateral: -0.16, rightLateral: 0.12, vertical: -0.3, forward: 0.94 }, supportPole: { lateral: 1, vertical: 0.45, forward: 0, biasLateral: 0.85, biasVertical: -0.65, biasForward: -0.18 }, shoulderOffset: vec3(0.06, 0.03, 0), torsoClearance: { radiiScale: vec3(0.34, 0.25, 0.28), minRadii: vec3(0.12, 0.16, 0.1), centerOffsetScale: vec3(0, 0.16, 0.08), padding: 0.03, elbowLateralRatio: 0.82, elbowForwardRatio: 0.16, wristLateralRatio: 0.68, wristForwardRatio: 0.1 } }; const RIFLE_WEAPON_HOLD_PROFILE: WeaponHoldProfile = { handDirection: { leftLateral: -0.18, rightLateral: 0.12, vertical: -0.28, forward: 0.96 }, supportPole: { lateral: 1, vertical: 0.48, forward: -0.02, biasLateral: 0.84, biasVertical: -0.58, biasForward: -0.12 }, shoulderOffset: vec3(0.058, 0.028, -0.004), torsoClearance: { radiiScale: vec3(0.35, 0.25, 0.29), minRadii: vec3(0.12, 0.16, 0.11), centerOffsetScale: vec3(0, 0.15, 0.09), padding: 0.032, elbowLateralRatio: 0.86, elbowForwardRatio: 0.18, wristLateralRatio: 0.7, wristForwardRatio: 0.11 } }; const MARKSMAN_WEAPON_HOLD_PROFILE: WeaponHoldProfile = { handDirection: { leftLateral: -0.22, rightLateral: 0.12, vertical: -0.24, forward: 0.98 }, supportPole: { lateral: 1.08, vertical: 0.52, forward: -0.04, biasLateral: 0.92, biasVertical: -0.74, biasForward: -0.2 }, shoulderOffset: vec3(0.048, 0.024, -0.012), torsoClearance: { radiiScale: vec3(0.36, 0.24, 0.31), minRadii: vec3(0.125, 0.16, 0.11), centerOffsetScale: vec3(0, 0.14, 0.1), padding: 0.034, elbowLateralRatio: 0.94, elbowForwardRatio: 0.22, wristLateralRatio: 0.76, wristForwardRatio: 0.14 } }; const BEAM_WEAPON_HOLD_PROFILE: WeaponHoldProfile = { handDirection: { leftLateral: -0.14, rightLateral: 0.1, vertical: -0.24, forward: 0.95 }, supportPole: { lateral: 0.9, vertical: 0.42, forward: 0.02, biasLateral: 0.72, biasVertical: -0.5, biasForward: -0.1 }, shoulderOffset: vec3(0.05, 0.026, -0.002), torsoClearance: { radiiScale: vec3(0.33, 0.24, 0.27), minRadii: vec3(0.115, 0.155, 0.095), centerOffsetScale: vec3(0, 0.16, 0.07), padding: 0.028, elbowLateralRatio: 0.78, elbowForwardRatio: 0.12, wristLateralRatio: 0.64, wristForwardRatio: 0.08 } }; const HEAVY_WEAPON_HOLD_PROFILE: WeaponHoldProfile = { handDirection: { leftLateral: -0.25, rightLateral: 0.14, vertical: -0.34, forward: 0.9 }, supportPole: { lateral: 1.18, vertical: 0.58, forward: -0.06, biasLateral: 1.02, biasVertical: -0.82, biasForward: -0.24 }, shoulderOffset: vec3(0.074, 0.032, 0), torsoClearance: { radiiScale: vec3(0.4, 0.27, 0.33), minRadii: vec3(0.13, 0.17, 0.12), centerOffsetScale: vec3(0, 0.17, 0.11), padding: 0.038, elbowLateralRatio: 1.02, elbowForwardRatio: 0.24, wristLateralRatio: 0.82, wristForwardRatio: 0.16 } }; const LAUNCHER_WEAPON_HOLD_PROFILE: WeaponHoldProfile = { handDirection: { leftLateral: -0.27, rightLateral: 0.14, vertical: -0.22, forward: 0.9 }, supportPole: { lateral: 1.16, vertical: 0.38, forward: 0.08, biasLateral: 0.96, biasVertical: -0.62, biasForward: -0.06 }, shoulderOffset: vec3(0.032, 0.052, -0.024), torsoClearance: { radiiScale: vec3(0.41, 0.28, 0.35), minRadii: vec3(0.14, 0.175, 0.12), centerOffsetScale: vec3(0, 0.18, 0.12), padding: 0.04, elbowLateralRatio: 1.06, elbowForwardRatio: 0.26, wristLateralRatio: 0.84, wristForwardRatio: 0.18 } }; const FOREARM_POD_WEAPON_HOLD_PROFILE: WeaponHoldProfile = { handDirection: { leftLateral: -0.1, rightLateral: 0.08, vertical: -0.18, forward: 0.96 }, supportPole: { lateral: 0.78, vertical: 0.36, forward: 0.04, biasLateral: 0.58, biasVertical: -0.38, biasForward: -0.04 }, shoulderOffset: vec3(0.02, 0.012, -0.008), torsoClearance: { radiiScale: vec3(0.3, 0.22, 0.24), minRadii: vec3(0.11, 0.15, 0.09), centerOffsetScale: vec3(0, 0.14, 0.06), padding: 0.024, elbowLateralRatio: 0.72, elbowForwardRatio: 0.1, wristLateralRatio: 0.58, wristForwardRatio: 0.06 } }; function toThree(v: Vec3Like): THREE.Vector3 { return new THREE.Vector3(v.x, v.y, v.z); } function fromThree(v: THREE.Vector3): Vec3Like { return { x: v.x, y: v.y, z: v.z }; } function setStatus(message: string): void { controls.status.textContent = message; } function updatePoseInfo(message: string): void { controls.poseInfo.textContent = message; } function formatDiagVec3(value: Vec3Like, digits = 2): string { return `${value.x.toFixed(digits)} ${value.y.toFixed(digits)} ${value.z.toFixed(digits)}`; } function formatDiagQuatDegrees(quat: THREE.Quaternion, order: THREE.EulerOrder = 'XYZ', digits = 1): string { const euler = new THREE.Euler().setFromQuaternion(quat, order); return [ THREE.MathUtils.radToDeg(euler.x).toFixed(digits), THREE.MathUtils.radToDeg(euler.y).toFixed(digits), THREE.MathUtils.radToDeg(euler.z).toFixed(digits) ].join(' '); } function getPoseDiagnosticNode(): NodeEntry | undefined { if (state.selection.kind === 'node' && state.selection.id) { return getNodeById(state.selection.id); } return getNodeById('node-torso') ?? state.nodes[0]; } function formatDiagThreeVec(value: THREE.Vector3, digits = 2): string { return `${value.x.toFixed(digits)} ${value.y.toFixed(digits)} ${value.z.toFixed(digits)}`; } function buildWeaponHoldDebugLines(): string[] { if (!poseState.weaponHoldEnabled) { return ['Weapon hold: disabled']; } const spec = getActiveWeaponDesignSpec(); const holdProfile = getActiveWeaponHoldProfile(); const lines = [ `Hold profile: ${spec?.archetype ?? 'default'}`, `Pose influence: ${poseState.influence.toFixed(2)}`, `Upper-body influence: ${poseState.upperBodyInfluence.toFixed(2)}`, `Weapon hand blend: ${poseState.weaponHoldHandBlend.toFixed(2)}`, `Weapon shoulder bias: ${poseState.weaponHoldShoulderBlend.toFixed(2)}`, `Support elbow bias: ${poseState.weaponHoldSupportElbowBias.toFixed(2)}`, `Support spread: ${holdProfile.handDirection.leftLateral.toFixed(2)} lat | ${holdProfile.handDirection.forward.toFixed(2)} fwd`, `Torso keep-out: elbow ${holdProfile.torsoClearance.elbowLateralRatio.toFixed(2)}/${holdProfile.torsoClearance.elbowForwardRatio.toFixed(2)} | wrist ${holdProfile.torsoClearance.wristLateralRatio.toFixed(2)}/${holdProfile.torsoClearance.wristForwardRatio.toFixed(2)}` ]; if (weaponHoldDebugState.gripFrame) { lines.push(`Grip anchor: ${formatDiagThreeVec(weaponHoldDebugState.gripFrame.origin)}`); } if (weaponHoldDebugState.supportFrame) { lines.push(`Support anchor: ${formatDiagThreeVec(weaponHoldDebugState.supportFrame.origin)}`); } if (weaponHoldDebugState.supportPoleHint) { lines.push(`Support pole hint: ${formatDiagThreeVec(weaponHoldDebugState.supportPoleHint, 2)}`); } if (weaponHoldDebugState.supportTargets) { lines.push(`Support elbow target: ${formatDiagThreeVec(weaponHoldDebugState.supportTargets.elbowTarget)}`); lines.push(`Support wrist target: ${formatDiagThreeVec(weaponHoldDebugState.supportTargets.wristTarget)}`); } if (weaponHoldDebugState.supportClearanceApplied) { lines.push('Torso clearance: support arm pushed out of torso volume.'); } else if (weaponHoldDebugState.torsoClearance) { lines.push('Torso clearance: support arm remains outside torso volume.'); } return lines; } function updatePoseDebugInfo(): void { if (!poseState.clip) { controls.poseDebug.textContent = 'No pose loaded.'; return; } const forwardAlignmentDeg = THREE.MathUtils.radToDeg(poseState.forwardAlignmentRad); const forwardAlignmentLine = `Forward alignment yaw deg: ${forwardAlignmentDeg.toFixed(1)}`; const node = getPoseDiagnosticNode(); if (!node) { controls.poseDebug.textContent = 'No node available for pose diagnostics.'; return; } const joint = getPoseJointName(node) ?? '(unmapped)'; const directMapping = poseState.mappings.find((entry) => entry.node.id === node.id); const derivedMapping = poseState.derivedMappings.find((entry) => entry.node.id === node.id); if (directMapping) { const lines = [ `Node: ${node.id}`, `Pose joint: ${joint}`, `BVH bone: ${directMapping.bone.name}`, `Bind correction XYZ deg: ${formatDiagQuatDegrees(directMapping.rotationOffset)}`, `Rotation apply mode: ${directMapping.rotationMode ?? 'full'}`, forwardAlignmentLine ]; if (node.id === 'node-torso') { lines.push('Torso issue: the mapped BVH spine/chest bone carries pitch and roll during turns in its local frame.'); lines.push('Why it looks wrong here: the editor torso behaves like an upright body block, so that pitch/roll reads as a forward lean.'); lines.push(directMapping.rotationMode === 'yawOnly' ? 'Current fix: torso keeps yaw only, dropping pitch and roll.' : 'Fix direction: add an explicit torso axis remap or constrain torso to yaw-dominant motion.'); } lines.push(...buildWeaponHoldDebugLines()); controls.poseDebug.textContent = lines.join('\n'); return; } if (derivedMapping) { controls.poseDebug.textContent = [ `Node: ${node.id}`, `Pose joint: ${joint}`, `Derived from: ${derivedMapping.parentBone.name} -> ${derivedMapping.childBone.name}`, `Blend ratio: ${derivedMapping.ratio.toFixed(2)}`, `Bind correction XYZ deg: ${formatDiagQuatDegrees(derivedMapping.rotationOffset)}`, `Rotation apply mode: ${derivedMapping.rotationMode ?? 'full'}`, forwardAlignmentLine, ...buildWeaponHoldDebugLines() ].join('\n'); return; } controls.poseDebug.textContent = [ `Node: ${node.id}`, `Pose joint: ${joint}`, forwardAlignmentLine, 'Mapping status: no BVH bone matched this node.', 'Fix: set an explicit Pose Joint on the node or remap it to a bone name that exists in the loaded BVH.', ...buildWeaponHoldDebugLines() ].join('\n'); } function updateAttachmentDiagnosticInfo(attachment?: AttachmentEntry | null): void { if (!attachment) { controls.attachmentInfo.textContent = [ 'Select an attachment from the list or click a module mesh.', 'Transforms are edited in anchor-local space: +X right, +Y up, +Z forward.', 'The attachment gizmo uses local space.' ].join('\n'); return; } const moduleEntry = getModuleById(attachment.moduleId); const baseQuat = getAttachmentBaseQuaternion(attachment, moduleEntry); const weaponHandForwardCorrectionQuat = getWeaponHandForwardCorrectionQuaternion(attachment); const frame = getAttachmentFrames(attachment, state.attachmentInstanceIndex)[0]; const lines = [ `Attachment: ${attachment.id}`, `Anchor: ${attachment.anchorId}`, 'Editing space: anchor-local (+X right, +Y up, +Z forward)', `Offset: ${formatDiagVec3(attachment.offset ?? vec3(0, 0, 0))}`, `Rotation XYZ deg: ${formatDiagVec3(attachment.rotation ?? vec3(0, 0, 0), 1)}`, `Scale: ${formatDiagVec3(attachment.scale ?? vec3(1, 1, 1))}` ]; if (attachment.generatorId) { lines.push(`Generator binding: ${attachment.generatorId}${attachment.generatorIndex !== undefined ? `#${attachment.generatorIndex}` : ' (all instances)'}`); } if (moduleEntry) { lines.push(`Module: ${moduleEntry.id} (${moduleEntry.purpose})`); } const weaponQa = getWeaponQaContext(attachment); if (weaponQa) { lines.push(`Weapon section: ${formatWeaponSectionLabel(weaponQa.section)}`); if (weaponQa.subsystem) { lines.push(`Weapon subsystem: ${formatWeaponQaSubsystemLabel(weaponQa.subsystem)}`); } lines.push(`Weapon role: ${weaponQa.spec.doctrine.role}`); if (weaponQa.placement) { const reshapers = weaponQa.spec.transitionRules .filter((transition) => transition.influence?.targetPlacementIds.includes(weaponQa.placement!.id)) .map((transition) => transition.label); if (reshapers.length > 0) { lines.push(`Reshaped by: ${reshapers.join(', ')}`); } } if (weaponQa.transition) { lines.push(`Transition: ${weaponQa.transition.label} (${weaponQa.transition.style})`); if (weaponQa.transition.influence?.targetPlacementIds.length) { lines.push(`Influence: ${weaponQa.transition.influence.targetPlacementIds.join(', ')}`); } } } if (!baseQuat.equals(new THREE.Quaternion())) { lines.push(`Auto base rotation XYZ deg: ${formatDiagQuatDegrees(baseQuat)}`); lines.push('Why: some limb primitives are authored upright, then auto-rotated to align with anchor forward.'); } if (!weaponHandForwardCorrectionQuat.equals(new THREE.Quaternion())) { lines.push(`Auto weapon hand forward correction XYZ deg: ${formatDiagQuatDegrees(weaponHandForwardCorrectionQuat)}`); lines.push('Why: hand-mounted standalone weapon anchors are corrected to face downrange by default.'); } if (frame) { lines.push(`Anchor origin: ${formatDiagVec3(fromThree(frame.origin))}`); } controls.attachmentInfo.textContent = lines.join('\n'); } async function loadPoseManifest(options?: { autoLoadClip?: boolean }): Promise { try { const candidates = [ POSE_MANIFEST_URL, './poses/poses.json', '/poses/poses.json' ]; let list: unknown = null; for (const candidate of candidates) { const response = await fetch(candidate, { cache: 'no-cache' }); if (!response.ok) { continue; } const raw = await response.text(); if (raw.trim().startsWith('<')) { continue; } list = JSON.parse(raw); if (Array.isArray(list)) { console.info(`[Pose] Loaded manifest from ${candidate}`); break; } list = null; } if (!Array.isArray(list)) { updatePoseInfo('Pose manifest not found. Check public/poses/poses.json.'); return; } controls.poseSelect.innerHTML = ''; for (const entry of list) { if (typeof entry !== 'string') continue; const option = document.createElement('option'); option.value = entry; option.textContent = entry.replace(/\.(bvh|fbx)$/i, ''); controls.poseSelect.appendChild(option); } if (controls.poseSelect.options.length > 0) { controls.poseSelect.selectedIndex = 0; const availableFiles = getAvailablePoseFiles(); const aaaLibrary = buildAaaRiflePoseLibrary(availableFiles); updatePoseInfo(`Select a pose and click Load. ${formatAaaRiflePoseCoverageSummary(aaaLibrary)}`); setStatus(`Pose manifest loaded · ${formatAaaRiflePoseCoverageSummary(aaaLibrary)}`); await autoSelectPoseForCurrentArchetype({ force: true, load: options?.autoLoadClip === true }); } else { updatePoseInfo('No poses found.'); } updatePoseControls(); } catch (error) { updatePoseInfo('Failed to load poses.'); updatePoseControls(); console.error(error); } } function getAvailablePoseFiles(): string[] { return Array.from(controls.poseSelect.options).map((option) => option.value); } function findPoseFileCaseInsensitive( candidates: readonly string[], availableFiles: readonly string[] ): string | null { const availableMap = new Map(); for (const file of availableFiles) { availableMap.set(file.toLowerCase(), file); } for (const candidate of candidates) { const found = availableMap.get(candidate.toLowerCase()); if (found) return found; } return null; } function resolvePreferredYBotPoseFile(availableFiles: readonly string[]): string | null { const preferred = findPoseFileCaseInsensitive(YBOT_POSE_ASSET_CANDIDATES, availableFiles); if (preferred) return preferred; return availableFiles.find((file) => file.toLowerCase().includes('y bot')) ?? null; } function normalizePoseFileNameForRoleMatching(file: string): string { return file .toLowerCase() .replace(/\.(fbx|bvh)$/i, '') .replace(/[^a-z0-9]+/g, ' ') .trim(); } function poseNameIncludesToken(normalizedName: string, token: string): boolean { const normalizedToken = token.toLowerCase().trim(); if (!normalizedToken) return false; if (normalizedName.includes(normalizedToken)) return true; const collapsedName = normalizedName.replace(/\s+/g, ''); const collapsedToken = normalizedToken.replace(/\s+/g, ''); return collapsedName.includes(collapsedToken); } function dedupePoseCandidates(candidates: readonly (string | null | undefined)[]): string[] { const output: string[] = []; const seen = new Set(); for (const candidate of candidates) { if (!candidate) continue; const key = candidate.toLowerCase(); if (seen.has(key)) continue; seen.add(key); output.push(candidate); } return output; } function resolveAaaRifleRoleCandidate( role: AaaRiflePoseRole, availableFiles: readonly string[] ): string | null { const spec = AAA_RIFLE_ROLE_SPECS[role]; const direct = findPoseFileCaseInsensitive(spec.exact, availableFiles); if (direct) return direct; let bestFile: string | null = null; let bestScore = -Infinity; for (const file of availableFiles) { const normalized = normalizePoseFileNameForRoleMatching(file); if (!normalized) continue; let score = -1; for (let i = 0; i < spec.tokenSets.length; i += 1) { const tokenSet = spec.tokenSets[i]; if (tokenSet.every((token) => poseNameIncludesToken(normalized, token))) { score = Math.max(score, 110 - i * 12); } } if (score < 0) continue; if (poseNameIncludesToken(normalized, 'rifle')) score += 8; if (poseNameIncludesToken(normalized, 'pistol')) score -= 14; if (poseNameIncludesToken(normalized, 'shotgun')) score -= 10; if (poseNameIncludesToken(normalized, 'rpg')) score -= 12; if ((role === 'walk' || role === 'run' || role === 'turnLeft' || role === 'turnRight') && poseNameIncludesToken(normalized, 'male1')) { score += 6; } if (score > bestScore) { bestScore = score; bestFile = file; } } return bestFile; } function buildAaaRiflePoseLibrary(availableFiles: readonly string[]): AaaRiflePoseLibrary { const roles: Partial> = {}; const missing: AaaRiflePoseRole[] = []; for (const role of AAA_RIFLE_ROLE_ORDER) { const resolved = resolveAaaRifleRoleCandidate(role, availableFiles); if (resolved) { roles[role] = resolved; } else { missing.push(role); } } return { roles, missing, coverage: AAA_RIFLE_ROLE_ORDER.length - missing.length }; } function formatAaaRiflePoseCoverageSummary(library: AaaRiflePoseLibrary): string { const missingLabels = library.missing.map((role) => AAA_RIFLE_ROLE_LABELS[role]); if (missingLabels.length <= 0) { return `AAA rifle coverage ${library.coverage}/${AAA_RIFLE_ROLE_ORDER.length} (complete).`; } const preview = missingLabels.slice(0, 4).join(', '); const suffix = missingLabels.length > 4 ? ', ...' : ''; return `AAA rifle coverage ${library.coverage}/${AAA_RIFLE_ROLE_ORDER.length} · missing ${preview}${suffix}.`; } function resolveAaaRifleRoleForFile(file: string, library: AaaRiflePoseLibrary): AaaRiflePoseRole | null { const match = AAA_RIFLE_ROLE_ORDER.find((role) => library.roles[role]?.toLowerCase() === file.toLowerCase()); return match ?? null; } async function loadYBotPoseAssetAndPlay(): Promise { const available = getAvailablePoseFiles(); if (available.length <= 0) { updatePoseInfo('No pose assets available.'); return; } const target = resolvePreferredYBotPoseFile(available); if (!target) { updatePoseInfo('Y Bot pose asset not found in poses manifest.'); return; } controls.poseSelect.value = target; await loadPoseFromSelection(); if (!poseState.clip) { return; } poseState.playing = true; poseClock.start(); updatePoseControls(); setStatus(`Loaded Y Bot asset clip: ${target}`); } function getCharacterSandboxPosePresetCandidates( preset: CharacterSandboxPosePreset, availableFiles: readonly string[] ): readonly string[] { const aaaLibrary = buildAaaRiflePoseLibrary(availableFiles); const pick = (...roles: AaaRiflePoseRole[]) => roles.map((role) => aaaLibrary.roles[role]); if (preset === 'idle') { return dedupePoseCandidates([ ...pick('idle', 'aimIdle', 'crouchAim'), 'Rifle Idle.fbx', 'Y Bot@Rifle Idle.fbx', 'Y Bot@Rifle Idle (1).fbx', 'idle.fbx', 'Rifle_Medium_mixamo.fbx' ]); } if (preset === 'fire') { return dedupePoseCandidates([ ...pick('fireShot', 'aimIdle', 'idle', 'proneAim'), 'Rifle Aiming Idle.fbx', 'idle aiming.fbx', 'Rifle Idle.fbx', 'Y Bot@Rifle Idle.fbx', 'Rifle_Medium_mixamo.fbx' ]); } return dedupePoseCandidates([ ...pick('aimIdle', 'crouchAim', 'idle', 'fireShot'), 'Rifle Aiming Idle.fbx', 'idle aiming.fbx', 'idle crouching aiming.fbx', 'Rifle Idle.fbx', 'Y Bot@Rifle Idle.fbx', 'Rifle_Medium_mixamo.fbx' ]); } function getCharacterSandboxIkProfile( preset: CharacterSandboxPosePreset = characterSandboxState.posePreset ): CharacterSandboxIkProfile { if (preset === 'fire') { return { handWeight: 1, shoulderWeight: 0.74, supportElbowBias: 0.82 }; } if (preset === 'aim') { return { handWeight: 0.96, shoulderWeight: 0.58, supportElbowBias: 0.68 }; } return { handWeight: 0.78, shoulderWeight: 0.28, supportElbowBias: 0.34 }; } function isLeaperSandboxActivePreset(): boolean { return currentBasePresetName === 'leaper-arc'; } function setLeaperSandboxMotionPreset( preset: LeaperSandboxMotionPreset, options?: { restartCycle?: boolean } ): void { characterSandboxState.leaperMotionPreset = preset; if (options?.restartCycle ?? true) { characterSandboxState.leaperMotionStartedAtMs = performance.now(); } updatePoseControls(); updateCharacterSandboxInfo(performance.now(), true); markRenderDirty(2); } function getLeaperSandboxMotionElapsedSec(nowMs = performance.now()): number { return Math.max(0, (nowMs - characterSandboxState.leaperMotionStartedAtMs) / 1000); } function sampleLeaperSandboxMotion( preset: LeaperSandboxMotionPreset, elapsedSec: number, legId: 'fl' | 'fr' | 'bl' | 'br', enabled = true ): LeaperSandboxMotionSample { return sampleLeaperArcPreviewMotion(preset, elapsedSec, legId, enabled); } function getLeaperSandboxMotionPhaseLabel(nowMs = performance.now()): string { return sampleLeaperSandboxMotion( characterSandboxState.leaperMotionPreset, getLeaperSandboxMotionElapsedSec(nowMs), 'fl', characterSandboxState.enabled ).phaseLabel; } function configureCharacterSandboxPoseProfile(preset: CharacterSandboxPosePreset): void { const ik = getCharacterSandboxIkProfile(preset); characterSandboxState.posePreset = preset; poseState.weaponHoldEnabled = true; poseState.rootMotion = false; poseState.loop = true; poseState.speed = 1; poseState.weaponHoldHandBlend = ik.handWeight; poseState.weaponHoldShoulderBlend = ik.shoulderWeight; poseState.weaponHoldSupportElbowBias = ik.supportElbowBias; if (preset === 'idle') { poseState.influence = 0.62; poseState.upperBodyInfluence = 0.72; characterSandboxState.adsBlend = 0.45; characterSandboxState.aimYawDeg = THREE.MathUtils.clamp(characterSandboxState.aimYawDeg, -50, 50); characterSandboxState.aimPitchDeg = THREE.MathUtils.clamp(characterSandboxState.aimPitchDeg, -35, 35); characterSandboxState.autoFire = false; return; } if (preset === 'aim') { poseState.influence = 0.46; poseState.upperBodyInfluence = 0.52; characterSandboxState.adsBlend = Math.max(characterSandboxState.adsBlend, 0.78); return; } // Fire stance keeps base pose subtle and lets IK/recoil drive the motion. poseState.influence = 0.34; poseState.upperBodyInfluence = 0.44; characterSandboxState.adsBlend = Math.max(characterSandboxState.adsBlend, 0.88); } async function setCharacterSandboxPosePreset( preset: CharacterSandboxPosePreset, options?: { autoplay?: boolean } ): Promise { const autoplay = options?.autoplay ?? true; configureCharacterSandboxPoseProfile(preset); const availableFiles = getAvailablePoseFiles(); if (availableFiles.length <= 0) { updateCharacterSandboxInfo(performance.now(), true); return; } const aaaLibrary = buildAaaRiflePoseLibrary(availableFiles); const selected = findPoseFileCaseInsensitive( getCharacterSandboxPosePresetCandidates(preset, availableFiles), availableFiles ) ?? resolvePreferredYBotPoseFile(availableFiles) ?? availableFiles[0] ?? null; if (!selected) return; controls.poseSelect.value = selected; await loadPoseFromSelection(); const selectedRole = resolveAaaRifleRoleForFile(selected, aaaLibrary); const selectedRoleLabel = selectedRole ? AAA_RIFLE_ROLE_LABELS[selectedRole] : 'Unclassified'; updatePoseInfo( `Sandbox ${preset} clip: ${selected} (${selectedRoleLabel}) · ${formatAaaRiflePoseCoverageSummary(aaaLibrary)}` ); if (autoplay && poseState.clip) { poseState.playing = true; poseClock.start(); applyPoseLoopSetting(); if (preset === 'fire') { setPoseTime(0); } updatePoseControls(); } if (poseState.clip) { applyPoseToNodes(); } updateCharacterSandboxInfo(performance.now(), true); } function findFirstAvailablePose( candidates: readonly string[], available: ReadonlySet ): string | null { for (const candidate of candidates) { if (available.has(candidate)) return candidate; } return null; } function resolveActiveWeaponPoseArchetype(): HumanoidWeaponArchetype | null { return getActiveWeaponDesignSpec()?.archetype ?? null; } function resolveAutoPoseForArchetype( archetype: HumanoidWeaponArchetype, available: ReadonlySet ): string | null { const archetypeCandidates = WEAPON_ARCHETYPE_POSE_PREFERENCES[archetype]; const preferred = findFirstAvailablePose(archetypeCandidates, available); if (preferred) return preferred; return findFirstAvailablePose(GLOBAL_POSE_FALLBACKS, available); } async function autoSelectPoseForCurrentArchetype(options?: { force?: boolean; load?: boolean }): Promise { if (controls.poseSelect.options.length <= 0) return; const archetype = resolveActiveWeaponPoseArchetype(); if (!archetype) return; const manualOverrideDetected = Boolean(lastAutoPoseFile) && Boolean(controls.poseSelect.value) && controls.poseSelect.value !== lastAutoPoseFile; if (!options?.force && archetype === lastAutoPoseArchetype && manualOverrideDetected) { return; } const shouldSwitch = Boolean(options?.force) || archetype !== lastAutoPoseArchetype || !controls.poseSelect.value; if (!shouldSwitch) return; const available = new Set(getAvailablePoseFiles()); const selection = resolveAutoPoseForArchetype(archetype, available); if (!selection) return; if (controls.poseSelect.value !== selection) { controls.poseSelect.value = selection; updatePoseInfo(`Auto-selected ${selection} for ${archetype} archetype.`); } lastAutoPoseArchetype = archetype; lastAutoPoseFile = selection; if (options?.load !== false) { await loadPoseFromSelection(); } } function snapshotNodesForPose(): void { poseState.snapshot.clear(); for (const node of state.nodes) { poseState.snapshot.set(node.id, { position: { ...node.position }, rotation: node.rotation ? { ...node.rotation } : undefined, mode: node.mode, parentId: node.parentId }); } } function restoreNodesFromPoseSnapshot(): void { if (poseState.snapshot.size === 0) return; for (const node of state.nodes) { const snap = poseState.snapshot.get(node.id); if (!snap) continue; node.position = { ...snap.position }; node.rotation = snap.rotation ? { ...snap.rotation } : undefined; node.mode = snap.mode; node.parentId = snap.parentId; } scheduleLiveRebuild(); } function resolveSnapshotNodeWorldTransform( nodeId: string, visited = new Set() ): { position: THREE.Vector3; quaternion: THREE.Quaternion } | null { const snap = poseState.snapshot.get(nodeId); if (!snap) return null; if (visited.has(nodeId)) { return { position: toThree(snap.position), quaternion: rotationToQuaternion(snap.rotation) }; } visited.add(nodeId); const localPos = toThree(snap.position); const localQuat = rotationToQuaternion(snap.rotation); if (snap.mode !== 'relative' || !snap.parentId) { return { position: localPos, quaternion: localQuat }; } const parent = resolveSnapshotNodeWorldTransform(snap.parentId, visited); if (!parent) { return { position: localPos, quaternion: localQuat }; } return { position: localPos.clone().applyQuaternion(parent.quaternion).add(parent.position), quaternion: parent.quaternion.clone().multiply(localQuat) }; } function inferBvhJointFromNodeId(nodeId: string): string | null { const lower = nodeId.toLowerCase(); const isLeft = /(^|[-_])l($|[-_])/.test(lower) || lower.includes('left'); const isRight = /(^|[-_])r($|[-_])/.test(lower) || lower.includes('right'); const side = isLeft ? 'Left' : isRight ? 'Right' : ''; const hasSide = side.length > 0; const pickExistingPoseJoint = (candidates: string[]): string => { if (!poseState.boneMap || poseState.boneMap.size === 0) { return candidates[0] ?? ''; } for (const candidate of candidates) { const exact = candidate.toLowerCase(); if (poseState.boneMap.has(exact)) return candidate; const bracketLower = `:${exact}`; for (const name of poseState.boneMap.keys()) { if (name === exact || name.endsWith(bracketLower)) return name; } const withWhitespace = candidate.replace(/([A-Z])/g, ' $1').trim().toLowerCase(); for (const name of poseState.boneMap.keys()) { if (name === withWhitespace || name.endsWith(withWhitespace)) return candidate; } } return candidates[0] ?? ''; }; if (!hasSide && (lower.includes('pelvis') || lower.endsWith('hips') || lower === 'hips')) return 'Hips'; if (!hasSide && (lower.includes('torso') || lower.includes('spine'))) { return pickExistingPoseJoint(['Spine2', 'Spine1', 'Spine', 'Spine_2', 'Chest', 'UpperBack']); } if (lower.includes('neck')) { return pickExistingPoseJoint(['Neck', 'Neck_1', 'Neck1', 'Neck_2']); } if (lower.includes('head')) return pickExistingPoseJoint(['Head', 'Skull']); if (lower.includes('shoulder') && hasSide) { return pickExistingPoseJoint([`${side}Shoulder`, `${side}Shoulder1`, `${side}Shoulder_01`, `${side}Clavicle`, `${side}Collar`, `${side}Clavicle1`]); } if ((lower.includes('elbow') || lower.includes('forearm')) && hasSide) { return pickExistingPoseJoint([`${side}ForeArm`, `${side}Arm`, `${side}LowerArm`, `${side}ForeArmTwist`, `${side}ForeArm1`]); } if (lower.includes('arm') && hasSide) { return pickExistingPoseJoint([`${side}Arm`, `${side}UpperArm`, `${side}Bicep`, `${side}Clavicle`]); } if (lower.includes('hand') && hasSide) { return pickExistingPoseJoint([`${side}Hand`, `${side}Wrist`, `${side}Palm`]); } if (lower.includes('hip') && hasSide) { return pickExistingPoseJoint([`${side}UpLeg`, `${side}Thigh`, `${side}UpperLeg`, `${side}Leg`, `${side}Hips`]); } if ((lower.includes('knee') || lower.includes('leg') || lower.includes('thigh')) && hasSide) { return pickExistingPoseJoint([`${side}Knee`, `${side}Leg`, `${side}Thigh`, `${side}UpperLeg`]); } if (lower.includes('foot') && hasSide) { return pickExistingPoseJoint([`${side}Foot`, `${side}Ankle`, `${side}Toe`]); } if (lower.includes('toe') && hasSide) return pickExistingPoseJoint([`${side}ToeBase`, `${side}Toe`]); return null; } function getPoseJointName(node: NodeEntry): string | null { const direct = node.poseJoint?.trim(); if (direct) return direct; const poseTag = node.tags.find((entry) => entry.toLowerCase().startsWith('pose:')); if (poseTag) { const trimmed = poseTag.slice(5).trim(); if (trimmed) return trimmed; } const bvhTag = node.tags.find((entry) => entry.toLowerCase().startsWith('bvh:')); if (bvhTag) { const trimmed = bvhTag.slice(4).trim(); if (trimmed) return trimmed; } return inferBvhJointFromNodeId(node.id); } function normalizePoseBoneLookupKey(value: string): string { return value .toLowerCase() .split(':') .pop()! .replace(/[^a-z0-9]+/g, '') .replace(/0+(?=\d)/g, ''); } type PoseBoneSide = 'left' | 'right' | 'center'; function inferPoseBoneSide(value: string): PoseBoneSide { const lower = value.toLowerCase(); const normalized = normalizePoseBoneLookupKey(value); const isLeft = lower.includes('left') || normalized.includes('left') || /(^|[^a-z])l($|[^a-z])/.test(lower) || /^(l)(arm|shoulder|clavicle|collar|forearm|hand|wrist|hip|leg|upleg|thigh|knee|ankle|foot|toe)/.test(normalized); const isRight = lower.includes('right') || normalized.includes('right') || /(^|[^a-z])r($|[^a-z])/.test(lower) || /^(r)(arm|shoulder|clavicle|collar|forearm|hand|wrist|hip|leg|upleg|thigh|knee|ankle|foot|toe)/.test(normalized); if (isLeft && !isRight) return 'left'; if (isRight && !isLeft) return 'right'; return 'center'; } function findBestPoseBoneMatch( targetName: string, boneMap: ReadonlyMap, options?: { allowSideMismatch?: boolean } ): { key: string; bone: THREE.Bone } | null { const trimmed = targetName.trim(); if (!trimmed) return null; const exact = boneMap.get(trimmed.toLowerCase()); if (exact) { return { key: exact.name, bone: exact }; } const targetNormalized = normalizePoseBoneLookupKey(trimmed); if (!targetNormalized) return null; const targetSide = inferPoseBoneSide(trimmed); const allowSideMismatch = Boolean(options?.allowSideMismatch); let best: { key: string; bone: THREE.Bone; score: number } | null = null; for (const [key, bone] of boneMap.entries()) { const candidateNormalized = normalizePoseBoneLookupKey(key); if (!candidateNormalized) continue; const candidateSide = inferPoseBoneSide(key); if ( !allowSideMismatch && targetSide !== 'center' && candidateSide !== 'center' && targetSide !== candidateSide ) { continue; } let score = -1; if (candidateNormalized === targetNormalized) { score = 400; } else if ( candidateNormalized.endsWith(targetNormalized) || targetNormalized.endsWith(candidateNormalized) ) { score = 280; } else if ( targetNormalized.length >= 4 && (candidateNormalized.includes(targetNormalized) || targetNormalized.includes(candidateNormalized)) ) { score = 180; } if (score < 0) continue; if (targetSide !== 'center') { if (candidateSide === targetSide) { score += 40; } else if (candidateSide === 'center') { score -= 30; } } score -= Math.min(30, Math.abs(candidateNormalized.length - targetNormalized.length)); if (!best || score > best.score) { best = { key, bone, score }; } } return best ? { key: best.key, bone: best.bone } : null; } function resolvePoseBoneByJointName(jointName: string): THREE.Bone | null { if (!poseState.boneMap || poseState.boneMap.size === 0) return null; return findBestPoseBoneMatch(jointName, poseState.boneMap)?.bone ?? null; } function getPoseMappedNodeCount(): number { return poseState.mappings.length + poseState.derivedMappings.length; } function resolvePoseRotationMode( node: NodeEntry, boneName?: string ): PoseMapping['rotationMode'] | undefined { const nodeLower = node.id.toLowerCase(); const jointLower = (getPoseJointName(node) ?? '').toLowerCase(); const boneLower = (boneName ?? '').toLowerCase(); if ( nodeLower === 'node-torso' && (boneLower.includes('spine') || boneLower.includes('chest') || boneLower.includes('upperback')) ) { return 'yawOnly'; } const combined = `${nodeLower} ${jointLower} ${boneLower}`; if (/(hand|wrist|foot|ankle)/.test(combined)) { return 'noRoll'; } return undefined; } function getDerivedPoseMapping(node: NodeEntry): DerivedPoseMapping | null { if (!poseState.boneMap || poseState.boneMap.size === 0) return null; const parent = node.parentId ? getNodeById(node.parentId) : undefined; if (!parent) return null; const child = state.nodes.find((candidate) => candidate.parentId === node.id); if (!child) return null; const parentJoint = getPoseJointName(parent); const childJoint = getPoseJointName(child); if (!parentJoint || !childJoint) return null; const parentBone = resolvePoseBoneByJointName(parentJoint); const childBone = resolvePoseBoneByJointName(childJoint); if (!parentBone || !childBone) return null; const parentPos = getNodeWorldPosition(parent); const nodePos = getNodeWorldPosition(node); const childPos = getNodeWorldPosition(child); const total = parentPos.distanceTo(childPos); if (total < 1e-5) return null; const ratio = THREE.MathUtils.clamp(parentPos.distanceTo(nodePos) / total, 0, 1); const parentQuat = parentBone.getWorldQuaternion(new THREE.Quaternion()); const childQuat = childBone.getWorldQuaternion(new THREE.Quaternion()); const bindQuat = parentQuat.clone().slerp(childQuat, ratio); const nodeBindQuat = resolveNodeWorldTransform(node).quaternion; const rotationOffset = nodeBindQuat.clone().multiply(bindQuat.clone().invert()); const rotationMode = resolvePoseRotationMode(node, `${parentBone.name} ${childBone.name}`); return { node, parentBone, childBone, ratio, rotationOffset, rotationMode }; } function rebuildPoseMappings(): void { poseState.mappings = []; poseState.derivedMappings = []; if (!poseState.boneMap || poseState.boneMap.size === 0) return; const mappedNodeIds = new Set(); for (const node of state.nodes) { const joint = getPoseJointName(node); if (!joint) continue; const bone = resolvePoseBoneByJointName(joint); if (!bone) continue; const boneBindQuat = bone.getWorldQuaternion(new THREE.Quaternion()); const nodeBindQuat = resolveNodeWorldTransform(node).quaternion; const rotationOffset = nodeBindQuat.clone().multiply(boneBindQuat.clone().invert()); const rotationMode = resolvePoseRotationMode(node, bone.name); poseState.mappings.push({ node, bone, rotationOffset, rotationMode }); mappedNodeIds.add(node.id); } for (const node of state.nodes) { if (mappedNodeIds.has(node.id)) continue; const derived = getDerivedPoseMapping(node); if (!derived) continue; poseState.derivedMappings.push(derived); } } function refreshPoseMappingsIfLoaded(): void { if (!poseState.clip) return; rebuildPoseMappings(); computePoseAlignment(); setPoseTime(poseState.time, false); updatePoseDebugInfo(); } function resolveMappedBoneWorldPositionForNode(nodeId: string): THREE.Vector3 | null { const direct = poseState.mappings.find((mapping) => mapping.node.id === nodeId); if (direct) { return direct.bone.getWorldPosition(new THREE.Vector3()); } const derived = poseState.derivedMappings.find((mapping) => mapping.node.id === nodeId); if (!derived) return null; const parentPos = derived.parentBone.getWorldPosition(new THREE.Vector3()); const childPos = derived.childBone.getWorldPosition(new THREE.Vector3()); return parentPos.lerp(childPos, derived.ratio); } function resolveReferenceNodeWorldPosition(nodeId: string): THREE.Vector3 | null { const fromSnapshot = resolveSnapshotNodeWorldTransform(nodeId); if (fromSnapshot) return fromSnapshot.position.clone(); const node = getNodeById(nodeId); if (!node) return null; return getNodeWorldPosition(node); } function deriveYawForwardFromSidePoints(left: THREE.Vector3, right: THREE.Vector3): THREE.Vector3 | null { const lateral = right.clone().sub(left); lateral.y = 0; const lateralLengthSq = lateral.lengthSq(); if (!Number.isFinite(lateralLengthSq) || lateralLengthSq < 1e-8) { return null; } lateral.multiplyScalar(1 / Math.sqrt(lateralLengthSq)); const forward = lateral.clone().cross(poseWorldUp); const forwardLengthSq = forward.lengthSq(); if (!Number.isFinite(forwardLengthSq) || forwardLengthSq < 1e-8) { return null; } forward.multiplyScalar(1 / Math.sqrt(forwardLengthSq)); return forward; } function computePoseForwardAlignment(): void { poseState.forwardAlignmentQuat.identity(); poseState.forwardAlignmentRad = 0; poseState.forwardAlignmentPivot.copy(poseState.baseRoot).multiplyScalar(poseState.scale).add(poseState.offset); const poseLeftShoulder = resolveMappedBoneWorldPositionForNode('node-shoulder-l'); const poseRightShoulder = resolveMappedBoneWorldPositionForNode('node-shoulder-r'); const nodeLeftShoulder = resolveReferenceNodeWorldPosition('node-shoulder-l'); const nodeRightShoulder = resolveReferenceNodeWorldPosition('node-shoulder-r'); const poseLeftHip = resolveMappedBoneWorldPositionForNode('node-hip-l'); const poseRightHip = resolveMappedBoneWorldPositionForNode('node-hip-r'); const nodeLeftHip = resolveReferenceNodeWorldPosition('node-hip-l'); const nodeRightHip = resolveReferenceNodeWorldPosition('node-hip-r'); let poseForward: THREE.Vector3 | null = null; let nodeForward: THREE.Vector3 | null = null; if (poseLeftShoulder && poseRightShoulder && nodeLeftShoulder && nodeRightShoulder) { poseForward = deriveYawForwardFromSidePoints(poseLeftShoulder, poseRightShoulder); nodeForward = deriveYawForwardFromSidePoints(nodeLeftShoulder, nodeRightShoulder); } else if (poseLeftHip && poseRightHip && nodeLeftHip && nodeRightHip) { poseForward = deriveYawForwardFromSidePoints(poseLeftHip, poseRightHip); nodeForward = deriveYawForwardFromSidePoints(nodeLeftHip, nodeRightHip); } if (!poseForward || !nodeForward) { return; } const dot = THREE.MathUtils.clamp(poseForward.dot(nodeForward), -1, 1); const crossY = poseForward.clone().cross(nodeForward).y; const yaw = Math.atan2(crossY, dot); if (!Number.isFinite(yaw)) return; poseState.forwardAlignmentRad = yaw; if (Math.abs(yaw) <= 1e-5) return; poseState.forwardAlignmentQuat.setFromAxisAngle(poseWorldUp, yaw); } function computePoseAlignment(): void { if (!poseState.root || !poseState.rootBone) return; poseState.root.updateMatrixWorld(true); const bonePositions = poseState.boneMap.size ? Array.from(poseState.boneMap.values()).map((bone) => bone.getWorldPosition(new THREE.Vector3())) : []; if (bonePositions.length === 0) { poseState.scale = 1; poseState.offset.set(0, 0, 0); poseState.baseRoot.copy(poseState.rootBone.getWorldPosition(new THREE.Vector3())); poseState.forwardAlignmentQuat.identity(); poseState.forwardAlignmentPivot.copy(poseState.baseRoot).multiplyScalar(poseState.scale).add(poseState.offset); poseState.forwardAlignmentRad = 0; return; } let minY = Infinity; let maxY = -Infinity; for (const pos of bonePositions) { minY = Math.min(minY, pos.y); maxY = Math.max(maxY, pos.y); } const boneHeight = Math.max(1e-5, maxY - minY); let nodeMinY = Infinity; let nodeMaxY = -Infinity; for (const node of state.nodes) { const pos = getNodeWorldPosition(node); nodeMinY = Math.min(nodeMinY, pos.y); nodeMaxY = Math.max(nodeMaxY, pos.y); } const nodeHeight = Math.max(1e-5, nodeMaxY - nodeMinY); poseState.scale = nodeHeight / boneHeight; const rootPos = poseState.rootBone.getWorldPosition(new THREE.Vector3()); poseState.baseRoot.copy(rootPos); const hipsNode = poseState.mappings.find((mapping) => mapping.bone.name.toLowerCase() === 'hips')?.node; const pelvisNode = getNodeById('node-pelvis'); const targetNode = pelvisNode ?? hipsNode ?? state.nodes[0]; const targetPos = targetNode ? getNodeWorldPosition(targetNode) : new THREE.Vector3(); const scaledRoot = rootPos.clone().multiplyScalar(poseState.scale); poseState.offset.copy(targetPos).sub(scaledRoot); // Anchor pose alignment from the feet when possible so the rig doesn't float around hips center. const footHintTokens = ['foot', 'ankle', 'toe', 'ball']; const poseFootPositions = Array.from(poseState.boneMap.entries()) .filter(([name]) => footHintTokens.some((token) => name.includes(token))) .map(([, bone]) => bone.getWorldPosition(new THREE.Vector3())); const nodeFootIds = ['node-foot-l', 'node-foot-r', 'node-ankle-l', 'node-ankle-r']; const nodeFootPositions = nodeFootIds .map((id) => getNodeById(id)) .filter((node): node is NodeEntry => Boolean(node)) .map((node) => getNodeWorldPosition(node)); if (poseFootPositions.length > 0 && nodeFootPositions.length > 0) { const poseFootMinY = poseFootPositions.reduce((min, pos) => Math.min(min, pos.y), Number.POSITIVE_INFINITY); const nodeFootMinY = nodeFootPositions.reduce((min, pos) => Math.min(min, pos.y), Number.POSITIVE_INFINITY); if (Number.isFinite(poseFootMinY) && Number.isFinite(nodeFootMinY)) { poseState.offset.y = nodeFootMinY - poseFootMinY * poseState.scale; } } computePoseForwardAlignment(); } const POSE_UPPER_BODY_HINTS = [ 'torso', 'spine', 'chest', 'neck', 'head', 'clavicle', 'shoulder', 'arm', 'elbow', 'wrist', 'hand', 'finger' ]; function isUpperBodyPoseNode(node: NodeEntry): boolean { const key = `${node.id} ${getPoseJointName(node) ?? ''}`.toLowerCase(); return POSE_UPPER_BODY_HINTS.some((hint) => key.includes(hint)); } function applyPoseNodeInfluence(node: NodeEntry, worldPosition: THREE.Vector3, worldQuaternion: THREE.Quaternion): void { const baseTransform = resolveSnapshotNodeWorldTransform(node.id); if (!baseTransform) { updateNodeFromWorld(node, worldPosition, worldQuaternion); return; } const upperBodyScale = isUpperBodyPoseNode(node) ? poseState.upperBodyInfluence : 1; const influence = THREE.MathUtils.clamp(poseState.influence * upperBodyScale, 0, 1); if (influence <= 0.001) { updateNodeFromWorld(node, baseTransform.position, baseTransform.quaternion); return; } if (influence < 0.999) { const baseWeight = 1 - influence; worldPosition.lerp(baseTransform.position, baseWeight); worldQuaternion.slerp(baseTransform.quaternion, baseWeight); } updateNodeFromWorld(node, worldPosition, worldQuaternion); } function applyPoseToNodes(): void { if (!poseState.root || !poseState.rootBone) return; if (poseState.mappings.length === 0 && poseState.derivedMappings.length === 0) return; poseState.root.updateMatrixWorld(true); const currentRoot = poseState.rootBone.getWorldPosition(new THREE.Vector3()); const rootDelta = currentRoot.clone().sub(poseState.baseRoot); const getMappedBonePosition = (bone: THREE.Bone): THREE.Vector3 => { const worldPos = bone.getWorldPosition(new THREE.Vector3()); if (!poseState.rootMotion) { worldPos.sub(rootDelta); } worldPos.multiplyScalar(poseState.scale).add(poseState.offset); if (Math.abs(poseState.forwardAlignmentRad) > 1e-5) { worldPos .sub(poseState.forwardAlignmentPivot) .applyQuaternion(poseState.forwardAlignmentQuat) .add(poseState.forwardAlignmentPivot); } return worldPos; }; const applyPoseRotationMode = ( mode: PoseMapping['rotationMode'], worldQuat: THREE.Quaternion, node: NodeEntry ): THREE.Quaternion => { if (mode === 'yawOnly') { torsoPoseYawEuler.setFromQuaternion(worldQuat, 'YXZ'); torsoPoseYawEuler.x = 0; torsoPoseYawEuler.z = 0; torsoPoseYawQuat.setFromEuler(torsoPoseYawEuler); return torsoPoseYawQuat.clone(); } if (mode === 'noRoll') { poseNoRollEuler.setFromQuaternion(worldQuat, 'YXZ'); poseNoRollEuler.z = 0; const nodeLower = node.id.toLowerCase(); if (nodeLower.includes('foot') || nodeLower.includes('ankle')) { poseNoRollEuler.x = THREE.MathUtils.clamp(poseNoRollEuler.x, -0.9, 0.9); } poseNoRollQuat.setFromEuler(poseNoRollEuler); return poseNoRollQuat.clone(); } return worldQuat; }; for (const mapping of poseState.mappings) { const worldPos = getMappedBonePosition(mapping.bone); const worldQuat = mapping.rotationOffset .clone() .multiply(mapping.bone.getWorldQuaternion(new THREE.Quaternion())); if (Math.abs(poseState.forwardAlignmentRad) > 1e-5) { worldQuat.premultiply(poseState.forwardAlignmentQuat); } const mappedQuat = applyPoseRotationMode(mapping.rotationMode, worldQuat, mapping.node); applyPoseNodeInfluence(mapping.node, worldPos, mappedQuat); } for (const mapping of poseState.derivedMappings) { const parentPos = getMappedBonePosition(mapping.parentBone); const childPos = getMappedBonePosition(mapping.childBone); const worldPos = parentPos.clone().lerp(childPos, mapping.ratio); const parentQuat = mapping.parentBone.getWorldQuaternion(new THREE.Quaternion()); const childQuat = mapping.childBone.getWorldQuaternion(new THREE.Quaternion()); const worldQuat = mapping.rotationOffset .clone() .multiply(parentQuat.clone().slerp(childQuat, mapping.ratio)); if (Math.abs(poseState.forwardAlignmentRad) > 1e-5) { worldQuat.premultiply(poseState.forwardAlignmentQuat); } const mappedQuat = applyPoseRotationMode(mapping.rotationMode, worldQuat, mapping.node); applyPoseNodeInfluence(mapping.node, worldPos, mappedQuat); } const sandboxIkProfile = characterSandboxState.enabled ? getCharacterSandboxIkProfile(characterSandboxState.posePreset) : null; const upperBodyIkScale = THREE.MathUtils.clamp(poseState.upperBodyInfluence, 0, 1); const effectiveHandBlend = Math.max( poseState.weaponHoldEnabled ? poseState.weaponHoldHandBlend : 0, sandboxIkProfile?.handWeight ?? 0 ) * upperBodyIkScale; const effectiveShoulderBlend = Math.max( poseState.weaponHoldEnabled ? poseState.weaponHoldShoulderBlend : 0, sandboxIkProfile?.shoulderWeight ?? 0 ) * upperBodyIkScale; const effectiveSupportElbowBias = Math.max( poseState.weaponHoldEnabled ? poseState.weaponHoldSupportElbowBias : 0, sandboxIkProfile?.supportElbowBias ?? 0 ) * upperBodyIkScale; if ( effectiveHandBlend > 0.001 || effectiveShoulderBlend > 0.001 || effectiveSupportElbowBias > 0.001 ) { applyWeaponHoldPoseBlend( effectiveHandBlend, effectiveShoulderBlend, effectiveSupportElbowBias ); } else { resetWeaponHoldDebugState(); } applyCharacterSandboxAimOffsets(); rebuildWeaponHoldDebugGizmo(); updatePoseDebugInfo(); updateVisibility(); scheduleLiveRebuild({ skipVisualState: true }); } function updatePoseControls(): void { const hasClip = Boolean(poseState.clip); const hasPoseOptions = controls.poseSelect.options.length > 0; const isLeaperSandbox = isLeaperSandboxActivePreset(); controls.poseLoad.disabled = !hasPoseOptions; controls.poseLoadYBot.disabled = !hasPoseOptions; controls.posePlay.disabled = !hasClip; controls.poseStop.disabled = !hasClip; controls.poseTime.disabled = !hasClip; controls.poseSpeed.disabled = !hasClip; controls.poseLoop.disabled = !hasClip; controls.poseRoot.disabled = !hasClip; controls.poseInfluence.disabled = !hasClip; controls.poseUpperBodyInfluence.disabled = !hasClip; controls.poseWeaponHold.disabled = !hasClip; controls.poseWeaponHoldHandBlend.disabled = !hasClip || !poseState.weaponHoldEnabled; controls.poseWeaponHoldShoulderBlend.disabled = !hasClip || !poseState.weaponHoldEnabled; controls.poseWeaponHoldSupportElbowBias.disabled = !hasClip || !poseState.weaponHoldEnabled; controls.poseInfluence.value = poseState.influence.toFixed(2); controls.poseUpperBodyInfluence.value = poseState.upperBodyInfluence.toFixed(2); controls.poseWeaponHold.checked = poseState.weaponHoldEnabled; controls.poseWeaponHoldHandBlend.value = poseState.weaponHoldHandBlend.toFixed(2); controls.poseWeaponHoldShoulderBlend.value = poseState.weaponHoldShoulderBlend.toFixed(2); controls.poseWeaponHoldSupportElbowBias.value = poseState.weaponHoldSupportElbowBias.toFixed(2); controls.posePlay.textContent = poseState.playing ? 'Pause' : 'Play'; controls.charSandboxEnable.checked = characterSandboxState.enabled; controls.charAnimIdle.textContent = isLeaperSandbox ? 'Idle' : 'Idle Clip'; controls.charAnimAim.textContent = isLeaperSandbox ? 'Walk' : 'Aim Clip'; controls.charAnimFire.textContent = isLeaperSandbox ? 'Leap' : 'Fire Clip'; controls.charAimYaw.value = characterSandboxState.aimYawDeg.toFixed(0); controls.charAimPitch.value = characterSandboxState.aimPitchDeg.toFixed(0); controls.charAimAds.value = characterSandboxState.adsBlend.toFixed(2); controls.charFireLoop.checked = characterSandboxState.autoFire; controls.charFireRate.value = characterSandboxState.fireRateHz.toFixed(1); controls.charAimYaw.disabled = !characterSandboxState.enabled || isLeaperSandbox; controls.charAimPitch.disabled = !characterSandboxState.enabled || isLeaperSandbox; controls.charAimAds.disabled = !characterSandboxState.enabled || isLeaperSandbox; controls.charFireOnce.disabled = !characterSandboxState.enabled || isLeaperSandbox; controls.charFireLoop.disabled = !characterSandboxState.enabled || isLeaperSandbox; controls.charFireRate.disabled = !characterSandboxState.enabled || isLeaperSandbox; if (!poseState.clip || !poseState.weaponHoldEnabled) { resetWeaponHoldDebugState(); rebuildWeaponHoldDebugGizmo(); } updateCharacterSandboxInfo(performance.now(), true); updateVisibility(); } function getWeaponSnapAnchorFrame(anchorId: string): Frame | null { const anchor = getAnchorById(anchorId); return anchor ? resolveAnchorFrame(anchor) : null; } function getActiveWeaponHoldProfile(): WeaponHoldProfile { return getActiveWeaponDesignSpec()?.holdProfile ?? DEFAULT_WEAPON_HOLD_PROFILE; } function getArmChainIds(side: 'left' | 'right'): { shoulderId: string; elbowId: string; wristId: string; handId: string; } { const suffix = side === 'left' ? 'l' : 'r'; return { shoulderId: `node-shoulder-${suffix}`, elbowId: `node-elbow-${suffix}`, wristId: `node-wrist-${suffix}`, handId: `node-hand-${suffix}` }; } function getWeaponHandDirection(frame: Frame, side: 'left' | 'right'): THREE.Vector3 { const profile = getActiveWeaponHoldProfile(); const lateralStrength = side === 'left' ? profile.handDirection.leftLateral : profile.handDirection.rightLateral; const lateral = frame.right.clone().multiplyScalar(lateralStrength); const vertical = frame.up.clone().multiplyScalar(profile.handDirection.vertical); return frame.forward.clone().multiplyScalar(profile.handDirection.forward).add(vertical).add(lateral).normalize(); } function worldToFrameLocal(frame: Frame, point: THREE.Vector3): THREE.Vector3 { const delta = point.clone().sub(frame.origin); return new THREE.Vector3( delta.dot(frame.right), delta.dot(frame.up), delta.dot(frame.forward) ); } function frameLocalToWorld(frame: Frame, local: THREE.Vector3): THREE.Vector3 { return frame.origin.clone() .add(frame.right.clone().multiplyScalar(local.x)) .add(frame.up.clone().multiplyScalar(local.y)) .add(frame.forward.clone().multiplyScalar(local.z)); } function getTorsoClearanceVolume(profile = getActiveWeaponHoldProfile().torsoClearance): TorsoClearanceVolume | null { const torso = getNodeById('node-torso'); if (!torso) return null; const shoulderL = getNodeById('node-shoulder-l'); const shoulderR = getNodeById('node-shoulder-r'); const neck = getNodeById('node-neck'); const pelvis = getNodeById('node-pelvis'); const torsoFrame = resolveNodeFrame(torso); const shoulderSpread = shoulderL && shoulderR ? getNodeWorldPosition(shoulderL).distanceTo(getNodeWorldPosition(shoulderR)) : 0.44; const torsoHeight = neck && pelvis ? getNodeWorldPosition(neck).distanceTo(getNodeWorldPosition(pelvis)) : 0.54; const radii = new THREE.Vector3( Math.max(profile.minRadii.x, shoulderSpread * profile.radiiScale.x), Math.max(profile.minRadii.y, torsoHeight * profile.radiiScale.y), Math.max(profile.minRadii.z, shoulderSpread * profile.radiiScale.z) ); const origin = torsoFrame.origin.clone() .add(torsoFrame.right.clone().multiplyScalar(radii.x * profile.centerOffsetScale.x)) .add(torsoFrame.up.clone().multiplyScalar(radii.y * profile.centerOffsetScale.y)) .add(torsoFrame.forward.clone().multiplyScalar(radii.z * profile.centerOffsetScale.z)); return { frame: { ...torsoFrame, origin }, radii }; } function pushPointOutsideTorso( point: THREE.Vector3, volume: TorsoClearanceVolume, options: { lateralRatio: number; forwardRatio: number; padding: number } ): { point: THREE.Vector3; adjusted: boolean } { const local = worldToFrameLocal(volume.frame, point); const radii = volume.radii.clone().addScalar(options.padding); let adjusted = false; const minLateral = -radii.x * options.lateralRatio; const minForward = radii.z * options.forwardRatio; if (local.x > minLateral) { local.x = minLateral; adjusted = true; } if (local.z < minForward) { local.z = minForward; adjusted = true; } const normalized = new THREE.Vector3(local.x / radii.x, local.y / radii.y, local.z / radii.z); if (normalized.lengthSq() < 1) { const pushDir = new THREE.Vector3( Math.min(local.x, minLateral), local.y * 0.45, Math.max(local.z, minForward) ); if (pushDir.lengthSq() < 1e-5) { pushDir.set(-1, 0, 0.35); } pushDir.normalize(); const denom = Math.sqrt( (pushDir.x * pushDir.x) / (radii.x * radii.x) + (pushDir.y * pushDir.y) / (radii.y * radii.y) + (pushDir.z * pushDir.z) / (radii.z * radii.z) ); local.copy(pushDir.multiplyScalar(1 / Math.max(denom, 1e-5))); adjusted = true; } return { point: frameLocalToWorld(volume.frame, local), adjusted }; } function applySupportArmTorsoClearance(targets: WeaponArmTargets): { targets: WeaponArmTargets; adjusted: boolean; volume: TorsoClearanceVolume | null } { const profile = getActiveWeaponHoldProfile().torsoClearance; const volume = getTorsoClearanceVolume(profile); if (!volume) { return { targets, adjusted: false, volume: null }; } const elbow = pushPointOutsideTorso(targets.elbowTarget, volume, { lateralRatio: profile.elbowLateralRatio, forwardRatio: profile.elbowForwardRatio, padding: profile.padding }); const wrist = pushPointOutsideTorso(targets.wristTarget, volume, { lateralRatio: profile.wristLateralRatio, forwardRatio: profile.wristForwardRatio, padding: profile.padding }); return { targets: { elbowTarget: elbow.point, wristTarget: wrist.point, handTarget: targets.handTarget }, adjusted: elbow.adjusted || wrist.adjusted, volume }; } function getWeaponPoleHint(frame: Frame, side: 'left' | 'right', elbowBias = 0): THREE.Vector3 { const profile = getActiveWeaponHoldProfile(); const safeBias = THREE.MathUtils.clamp(elbowBias, 0, 1); const sideSign = side === 'left' ? -1 : 1; const baseHint = frame.right.clone() .multiplyScalar(sideSign * profile.supportPole.lateral) .add(frame.up.clone().multiplyScalar(profile.supportPole.vertical)) .add(frame.forward.clone().multiplyScalar(profile.supportPole.forward)); if (safeBias <= 0.001 || side !== 'left') { return baseHint; } return baseHint .add(frame.right.clone().multiplyScalar(-profile.supportPole.biasLateral * safeBias)) .add(frame.up.clone().multiplyScalar(profile.supportPole.biasVertical * safeBias)) .add(frame.forward.clone().multiplyScalar(profile.supportPole.biasForward * safeBias)); } function solveWeaponArmTargets(side: 'left' | 'right', anchorId: string, elbowBias = 0): WeaponArmTargets | null { const frame = getWeaponSnapAnchorFrame(anchorId); if (!frame) return null; const { shoulderId, elbowId, wristId, handId } = getArmChainIds(side); const shoulder = getNodeById(shoulderId); const elbow = getNodeById(elbowId); const wrist = getNodeById(wristId); const hand = getNodeById(handId); if (!shoulder || !elbow || !wrist || !hand) return null; const shoulderWorld = getNodeWorldPosition(shoulder); const elbowWorld = getNodeWorldPosition(elbow); const wristWorld = getNodeWorldPosition(wrist); const handWorld = getNodeWorldPosition(hand); const upperLen = Math.max(0.08, shoulderWorld.distanceTo(elbowWorld)); const lowerLen = Math.max(0.08, elbowWorld.distanceTo(wristWorld)); const handLen = Math.max(0.06, wristWorld.distanceTo(handWorld)); const handBoneRatio = 0.72; const handDir = getWeaponHandDirection(frame, side); const target = frame.origin.clone(); const wristTarget = target.clone().sub(handDir.clone().multiplyScalar(handLen * handBoneRatio)); const handTarget = wristTarget.clone().add(handDir.clone().multiplyScalar(handLen)); const shoulderToWrist = wristTarget.clone().sub(shoulderWorld); const straightDistance = Math.max(1e-4, shoulderToWrist.length()); const clampedDistance = THREE.MathUtils.clamp( straightDistance, Math.abs(upperLen - lowerLen) + 1e-3, upperLen + lowerLen - 1e-3 ); const dir = shoulderToWrist.normalize(); const poleHint = getWeaponPoleHint(frame, side, elbowBias); const perp = poleHint.projectOnPlane(dir); if (perp.lengthSq() < 1e-5) { perp.copy(frame.up).projectOnPlane(dir); } perp.normalize(); const x = (upperLen * upperLen - lowerLen * lowerLen + clampedDistance * clampedDistance) / (2 * clampedDistance); const h = Math.sqrt(Math.max(upperLen * upperLen - x * x, 0)); const elbowTarget = shoulderWorld.clone() .add(dir.clone().multiplyScalar(x)) .add(perp.multiplyScalar(h)); return { elbowTarget, wristTarget, handTarget }; } function blendNodeWorldPosition(nodeId: string, target: THREE.Vector3, weight: number): boolean { const node = getNodeById(nodeId); if (!node) return false; const current = getNodeWorldPosition(node); return setNodeWorldPosition(nodeId, current.lerp(target, weight)); } function resetWeaponHoldDebugState(): void { weaponHoldDebugState.gripFrame = null; weaponHoldDebugState.supportFrame = null; weaponHoldDebugState.shoulderFrame = null; weaponHoldDebugState.supportPoleHint = null; weaponHoldDebugState.supportTargets = null; weaponHoldDebugState.torsoClearance = null; weaponHoldDebugState.supportClearanceApplied = false; } function addPoseDebugLine(start: THREE.Vector3, end: THREE.Vector3, material: THREE.LineBasicMaterial): void { const geometry = new THREE.BufferGeometry().setFromPoints([start, end]); const line = new THREE.Line(geometry, material); poseDebugGroup.add(line); } function addPoseDebugMarker(position: THREE.Vector3, material: THREE.Material): void { const marker = new THREE.Mesh(poseDebugMarkerGeometry, material); marker.position.copy(position); poseDebugGroup.add(marker); } function rebuildWeaponHoldDebugGizmo(): void { clearGroup(poseDebugGroup, false, true); if (!poseState.clip || !poseState.weaponHoldEnabled) return; const addFrameMarker = (frame: Frame | null, material: THREE.LineBasicMaterial, markerMaterial: THREE.Material) => { if (!frame) return; addPoseDebugMarker(frame.origin, markerMaterial); addPoseDebugLine(frame.origin, frame.origin.clone().add(frame.forward.clone().multiplyScalar(0.09)), material); }; addFrameMarker(weaponHoldDebugState.gripFrame, poseDebugGripMaterial, poseDebugGripMarkerMaterial); addFrameMarker(weaponHoldDebugState.supportFrame, poseDebugSupportMaterial, poseDebugSupportMarkerMaterial); addFrameMarker(weaponHoldDebugState.shoulderFrame, poseDebugShoulderMaterial, poseDebugShoulderMarkerMaterial); if (weaponHoldDebugState.supportFrame && weaponHoldDebugState.supportPoleHint) { const poleStart = weaponHoldDebugState.supportFrame.origin; const poleEnd = poleStart.clone().add(weaponHoldDebugState.supportPoleHint.clone().normalize().multiplyScalar(0.18)); addPoseDebugLine(poleStart, poleEnd, poseDebugSupportMaterial); } if (weaponHoldDebugState.supportTargets) { addPoseDebugLine(weaponHoldDebugState.supportTargets.elbowTarget, weaponHoldDebugState.supportTargets.wristTarget, poseDebugSupportMaterial); } if (weaponHoldDebugState.torsoClearance) { const { frame, radii } = weaponHoldDebugState.torsoClearance; addPoseDebugLine( frame.origin.clone().add(frame.right.clone().multiplyScalar(-radii.x)), frame.origin.clone().add(frame.right.clone().multiplyScalar(radii.x)), poseDebugTorsoMaterial ); addPoseDebugLine( frame.origin.clone().add(frame.forward.clone().multiplyScalar(-radii.z)), frame.origin.clone().add(frame.forward.clone().multiplyScalar(radii.z)), poseDebugTorsoMaterial ); } } function applyWeaponHoldPoseBlend(handWeight: number, shoulderWeight: number, supportElbowBias: number): void { const holdProfile = getActiveWeaponHoldProfile(); const safeHandWeight = THREE.MathUtils.clamp(handWeight, 0, 1); const safeShoulderWeight = THREE.MathUtils.clamp(shoulderWeight, 0, 1); const safeSupportElbowBias = THREE.MathUtils.clamp(supportElbowBias, 0, 1); resetWeaponHoldDebugState(); weaponHoldDebugState.gripFrame = getWeaponSnapAnchorFrame('anchor-weapon-grip-r'); weaponHoldDebugState.supportFrame = getWeaponSnapAnchorFrame('anchor-weapon-support-l'); weaponHoldDebugState.shoulderFrame = getWeaponSnapAnchorFrame('anchor-weapon-shoulder'); weaponHoldDebugState.torsoClearance = getTorsoClearanceVolume(holdProfile.torsoClearance); weaponHoldDebugState.supportPoleHint = weaponHoldDebugState.supportFrame ? getWeaponPoleHint(weaponHoldDebugState.supportFrame, 'left', safeSupportElbowBias) : null; if (safeHandWeight <= 0.001 && safeShoulderWeight <= 0.001) return; const rightTargets = safeHandWeight > 0.001 ? solveWeaponArmTargets('right', 'anchor-weapon-grip-r') : null; const rawLeftTargets = safeHandWeight > 0.001 ? solveWeaponArmTargets('left', 'anchor-weapon-support-l', safeSupportElbowBias) : null; const leftCorrection = rawLeftTargets ? applySupportArmTorsoClearance(rawLeftTargets) : null; const leftTargets = leftCorrection?.targets ?? null; weaponHoldDebugState.supportTargets = leftTargets; weaponHoldDebugState.torsoClearance = leftCorrection?.volume ?? weaponHoldDebugState.torsoClearance; weaponHoldDebugState.supportClearanceApplied = leftCorrection?.adjusted ?? false; if (rightTargets) { blendNodeWorldPosition('node-elbow-r', rightTargets.elbowTarget, safeHandWeight * 0.82); blendNodeWorldPosition('node-wrist-r', rightTargets.wristTarget, safeHandWeight); blendNodeWorldPosition('node-hand-r', rightTargets.handTarget, safeHandWeight); } if (leftTargets) { const leftElbowWeight = Math.min(1, safeHandWeight * (0.82 + safeSupportElbowBias * 0.22)); blendNodeWorldPosition('node-elbow-l', leftTargets.elbowTarget, leftElbowWeight); blendNodeWorldPosition('node-wrist-l', leftTargets.wristTarget, safeHandWeight); blendNodeWorldPosition('node-hand-l', leftTargets.handTarget, safeHandWeight); } const shoulderFrame = getWeaponSnapAnchorFrame('anchor-weapon-shoulder'); if (shoulderFrame && safeShoulderWeight > 0.001) { const shoulderTarget = shoulderFrame.origin.clone() .add(shoulderFrame.right.clone().multiplyScalar(holdProfile.shoulderOffset.x)) .add(shoulderFrame.up.clone().multiplyScalar(holdProfile.shoulderOffset.y)) .add(shoulderFrame.forward.clone().multiplyScalar(holdProfile.shoulderOffset.z)); blendNodeWorldPosition('node-shoulder-r', shoulderTarget, safeShoulderWeight * 0.22); } } function applyPoseLoopSetting(): void { if (!poseState.action) return; if (poseState.loop) { poseState.action.setLoop(THREE.LoopRepeat, Infinity); poseState.action.clampWhenFinished = false; } else { poseState.action.setLoop(THREE.LoopOnce, 0); poseState.action.clampWhenFinished = true; } } function setPoseTime(seconds: number, updateSlider = true): void { if (!poseState.mixer || !poseState.action || !poseState.clip) return; poseState.time = Math.max(0, Math.min(seconds, poseState.duration)); poseState.action.time = poseState.time; poseState.mixer.update(0); poseState.root?.updateMatrixWorld(true); applyPoseToNodes(); if (updateSlider && poseState.duration > 0) { controls.poseTime.value = (poseState.time / poseState.duration).toFixed(3); } } function stopPosePlayback(restore = true): void { poseState.playing = false; if (poseState.action) { poseState.action.stop(); } if (restore) { restoreNodesFromPoseSnapshot(); } updatePoseControls(); } function collectPoseBonesFromRoot(root: THREE.Object3D): THREE.Bone[] { const bones: THREE.Bone[] = []; root.traverse((child) => { if (child instanceof THREE.Bone) { bones.push(child); } }); return bones; } function resolvePoseRootBone(bones: THREE.Bone[]): THREE.Bone | null { if (bones.length <= 0) return null; const topBone = bones.find((bone) => !(bone.parent instanceof THREE.Bone)); return topBone ?? bones[0] ?? null; } function buildPoseBoneMap(bones: readonly THREE.Bone[]): Map { return new Map(bones.map((bone) => [bone.name.toLowerCase(), bone])); } function resolveCanonicalBoneName( sourceBoneName: string, sourceBoneMap: ReadonlyMap, canonicalBoneMap: ReadonlyMap ): string | null { const trimmed = sourceBoneName.trim(); if (!trimmed) return null; const exact = canonicalBoneMap.get(trimmed.toLowerCase()); if (exact) return exact.name; const sourceFromMap = sourceBoneMap.get(trimmed.toLowerCase()); const match = findBestPoseBoneMatch(sourceFromMap?.name ?? trimmed, canonicalBoneMap); return match?.bone.name ?? null; } function resolveTrackSourceBone( sourceTrackBoneName: string, sourceBoneMap: ReadonlyMap ): THREE.Bone | null { return findBestPoseBoneMatch(sourceTrackBoneName, sourceBoneMap)?.bone ?? null; } function resolveCanonicalTrackBone( canonicalTrackBoneName: string, canonicalBoneMap: ReadonlyMap ): THREE.Bone | null { return findBestPoseBoneMatch(canonicalTrackBoneName, canonicalBoneMap)?.bone ?? null; } function remapQuaternionTrackToCanonicalBind( track: THREE.KeyframeTrack, sourceBindLocalQuat: THREE.Quaternion, canonicalBindLocalQuat: THREE.Quaternion ): void { if (!(track instanceof THREE.QuaternionKeyframeTrack)) return; const values = track.values; if (!values || values.length < 4 || (values.length % 4) !== 0) return; const sourceBindInv = sourceBindLocalQuat.clone().invert(); const sourceAnimQuat = new THREE.Quaternion(); const deltaQuat = new THREE.Quaternion(); const canonicalAnimQuat = new THREE.Quaternion(); for (let i = 0; i < values.length; i += 4) { sourceAnimQuat.set(values[i], values[i + 1], values[i + 2], values[i + 3]).normalize(); deltaQuat.copy(sourceBindInv).multiply(sourceAnimQuat); canonicalAnimQuat.copy(canonicalBindLocalQuat).multiply(deltaQuat).normalize(); values[i] = canonicalAnimQuat.x; values[i + 1] = canonicalAnimQuat.y; values[i + 2] = canonicalAnimQuat.z; values[i + 3] = canonicalAnimQuat.w; } } function retargetClipToCanonicalRig( clip: THREE.AnimationClip, sourceBones: readonly THREE.Bone[], canonicalRig: CanonicalPoseRig ): THREE.AnimationClip { const sourceBoneMap = buildPoseBoneMap(sourceBones); const retargetedTracks: THREE.KeyframeTrack[] = []; for (const track of clip.tracks) { const dotIndex = track.name.indexOf('.'); if (dotIndex <= 0) continue; const sourceBoneName = track.name.slice(0, dotIndex); const propertyPath = track.name.slice(dotIndex + 1); const canonicalBoneName = resolveCanonicalBoneName(sourceBoneName, sourceBoneMap, canonicalRig.boneMap); if (!canonicalBoneName) continue; const clonedTrack = track.clone(); clonedTrack.name = `${canonicalBoneName}.${propertyPath}`; if (propertyPath.trim().toLowerCase() === 'quaternion') { const sourceBone = resolveTrackSourceBone(sourceBoneName, sourceBoneMap); const canonicalBone = resolveCanonicalTrackBone(canonicalBoneName, canonicalRig.boneMap); if (sourceBone && canonicalBone) { remapQuaternionTrackToCanonicalBind(clonedTrack, sourceBone.quaternion, canonicalBone.quaternion); } } retargetedTracks.push(clonedTrack); } if (retargetedTracks.length <= 0) { return clip.clone(); } const retargeted = new THREE.AnimationClip( clip.name ? `${clip.name}@canonical` : 'canonical_pose', clip.duration, retargetedTracks ); retargeted.optimize(); return retargeted; } async function loadPoseRig(file: string): Promise { const loaders = await getPoseLoaderBundle(); const extension = file.split('.').pop()?.toLowerCase(); if (extension === 'fbx') { const fbxRoot = await loaders.fbxLoader.loadAsync(`./poses/${file}`); const bones = collectPoseBonesFromRoot(fbxRoot); const rootBone = resolvePoseRootBone(bones); if (!rootBone) { throw new Error(`[Pose] FBX file has no skeleton bones: ${file}`); } return { format: 'fbx', root: fbxRoot, rootBone, bones, clip: fbxRoot.animations[0] ?? null }; } const bvh = await loaders.bvhLoader.loadAsync(`./poses/${file}`); const root = new THREE.Group(); const rootBone = bvh.skeleton.bones[0]; if (!rootBone) { throw new Error(`[Pose] BVH file has no root bone: ${file}`); } root.add(rootBone); return { format: 'bvh', root, rootBone, bones: bvh.skeleton.bones, clip: bvh.clip }; } async function ensureCanonicalPoseRig(): Promise { if (canonicalPoseRig) { return canonicalPoseRig; } if (!canonicalPoseRigPromise) { canonicalPoseRigPromise = (async () => { let lastError: unknown = null; for (const file of POSE_BASE_RIG_CANDIDATES) { try { const rig = await loadPoseRig(file); const canonical: CanonicalPoseRig = { file, root: rig.root, rootBone: rig.rootBone, bones: rig.bones, boneMap: buildPoseBoneMap(rig.bones) }; canonicalPoseRig = canonical; return canonical; } catch (error) { lastError = error; } } throw new Error( `[Pose] Failed to load canonical base rig from ${POSE_BASE_RIG_CANDIDATES.join(', ')}. ${String(lastError)}` ); })(); } canonicalPoseRig = await canonicalPoseRigPromise; return canonicalPoseRig; } function attachPoseAvatarRoot(root: THREE.Object3D): void { if (activePoseAvatarRoot === root) return; if (activePoseAvatarRoot && activePoseAvatarRoot.parent) { activePoseAvatarRoot.parent.remove(activePoseAvatarRoot); } activePoseAvatarRoot = root; root.position.set(0, 0, 0); root.rotation.set(0, 0, 0); root.scale.set(1, 1, 1); root.updateMatrixWorld(true); const initialBounds = new THREE.Box3().setFromObject(root); const initialSize = initialBounds.getSize(new THREE.Vector3()); if (Number.isFinite(initialSize.y) && initialSize.y > 1e-4) { const fitScale = THREE.MathUtils.clamp(POSE_AVATAR_TARGET_HEIGHT / initialSize.y, 0.001, 100); root.scale.setScalar(fitScale); root.updateMatrixWorld(true); const fittedBounds = new THREE.Box3().setFromObject(root); const fittedCenter = fittedBounds.getCenter(new THREE.Vector3()); root.position.x -= fittedCenter.x; root.position.z -= fittedCenter.z; root.position.y -= fittedBounds.min.y; root.updateMatrixWorld(true); } root.traverse((child) => { if (child instanceof THREE.Mesh || child instanceof THREE.SkinnedMesh) { child.castShadow = true; child.receiveShadow = true; child.frustumCulled = false; } }); poseAvatarGroup.add(root); markRenderDirty(2); } async function loadPoseAsset(file: string): Promise { const rig = await loadPoseRig(file); if (!rig.clip) { throw new Error(`[Pose] File has no animation clip: ${file}`); } return { format: rig.format, root: rig.root, rootBone: rig.rootBone, bones: rig.bones, clip: rig.clip }; } async function loadPoseFromSelection(): Promise { const file = controls.poseSelect.value; if (!file) return; if (poseState.file === file && poseState.clip) { updatePoseControls(); return; } stopPosePlayback(true); snapshotNodesForPose(); try { const baseRig = await ensureCanonicalPoseRig(); attachPoseAvatarRoot(baseRig.root); const poseAsset = await loadPoseAsset(file); const retargetedClip = retargetClipToCanonicalRig(poseAsset.clip, poseAsset.bones, baseRig); const mixer = new THREE.AnimationMixer(baseRig.root); const action = mixer.clipAction(retargetedClip); action.play(); action.setLoop(THREE.LoopRepeat, Infinity); action.clampWhenFinished = false; poseState.file = file; poseState.clip = retargetedClip; poseState.mixer = mixer; poseState.action = action; poseState.root = baseRig.root; poseState.rootBone = baseRig.rootBone; poseState.duration = retargetedClip.duration; poseState.time = 0; poseState.loop = controls.poseLoop.checked; poseState.rootMotion = controls.poseRoot.checked; poseState.speed = parseNumber(controls.poseSpeed.value, 1); poseState.boneMap = new Map(baseRig.boneMap); applyPoseLoopSetting(); action.time = 0; mixer.update(0); baseRig.root.updateMatrixWorld(true); rebuildPoseMappings(); computePoseAlignment(); setPoseTime(0, true); const mapped = getPoseMappedNodeCount(); const total = state.nodes.length; const forwardYawDeg = THREE.MathUtils.radToDeg(poseState.forwardAlignmentRad); updatePoseInfo( `Loaded ${file} (${poseAsset.format.toUpperCase()}) on canonical rig ${baseRig.file} · ${mapped}/${total} nodes mapped · ${poseState.duration.toFixed(2)}s · yaw align ${forwardYawDeg.toFixed(1)}°` ); updatePoseDebugInfo(); updateVisibility(); updatePoseControls(); } catch (error) { poseState.file = null; poseState.clip = null; poseState.mixer = null; poseState.action = null; poseState.root = null; poseState.rootBone = null; poseState.boneMap = new Map(); poseState.mappings = []; poseState.derivedMappings = []; poseState.forwardAlignmentQuat.identity(); poseState.forwardAlignmentPivot.set(0, 0, 0); poseState.forwardAlignmentRad = 0; if (activePoseAvatarRoot && activePoseAvatarRoot.parent) { activePoseAvatarRoot.parent.remove(activePoseAvatarRoot); } activePoseAvatarRoot = null; updatePoseInfo(`Failed to load ${file}.`); updatePoseDebugInfo(); updateVisibility(); updatePoseControls(); console.error(error); } } function applyCharacterSandboxAimOffsetToNode( nodeId: string, yawOffsetRad: number, pitchOffsetRad: number, weight: number ): boolean { const node = getNodeById(nodeId); if (!node || weight <= 0.001) return false; const current = resolveNodeWorldTransform(node); const safeWeight = THREE.MathUtils.clamp(weight, 0, 1); const yawQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), yawOffsetRad); const pitchAxis = new THREE.Vector3(1, 0, 0).applyQuaternion(current.quaternion).normalize(); const pitchQuat = new THREE.Quaternion().setFromAxisAngle(pitchAxis, pitchOffsetRad); const targetQuat = yawQuat.multiply(pitchQuat).multiply(current.quaternion); const blended = current.quaternion.clone().slerp(targetQuat, safeWeight); updateNodeFromWorld(node, current.position, blended); return true; } function applyCharacterSandboxAimOffsets(): void { if (!characterSandboxState.enabled) return; const ads = THREE.MathUtils.clamp(characterSandboxState.adsBlend, 0, 1); const preset = characterSandboxState.posePreset; const aimYawScale = preset === 'idle' ? 0.58 : preset === 'aim' ? 0.82 : 0.9; const aimPitchScale = preset === 'idle' ? 0.52 : preset === 'aim' ? 0.78 : 0.88; const recoilPitchDeg = preset === 'fire' ? 4.2 : 2.6; const recoilYawDeg = preset === 'fire' ? 1.35 : 0.6; const yawRad = THREE.MathUtils.degToRad(characterSandboxState.aimYawDeg) * THREE.MathUtils.lerp(0.24, 0.72, ads) * aimYawScale; const pitchBaseRad = THREE.MathUtils.degToRad(characterSandboxState.aimPitchDeg) * THREE.MathUtils.lerp(0.2, 0.62, ads) * aimPitchScale; const recoilPitchRad = THREE.MathUtils.degToRad(characterSandboxState.recoilKick * recoilPitchDeg); const recoilYawRad = THREE.MathUtils.degToRad(characterSandboxState.recoilKick * recoilYawDeg); const pitchRad = pitchBaseRad + recoilPitchRad; const yawWithRecoil = yawRad + recoilYawRad; applyCharacterSandboxAimOffsetToNode('node-torso', yawWithRecoil * 0.42, pitchRad * 0.24, 0.7); applyCharacterSandboxAimOffsetToNode('node-chest', yawWithRecoil * 0.56, pitchRad * 0.38, 0.8); applyCharacterSandboxAimOffsetToNode('node-neck', yawWithRecoil * 0.72, pitchRad * 0.48, 0.86); applyCharacterSandboxAimOffsetToNode('node-head', yawWithRecoil * 0.88, pitchRad * 0.72, 0.92); applyCharacterSandboxAimOffsetToNode('node-shoulder-r', yawWithRecoil * 0.42, pitchRad * 0.58, 0.84); applyCharacterSandboxAimOffsetToNode('node-shoulder-l', yawWithRecoil * 0.3, pitchRad * 0.46, 0.76); applyCharacterSandboxAimOffsetToNode('node-elbow-r', yawWithRecoil * 0.26, pitchRad * 0.46, 0.66); applyCharacterSandboxAimOffsetToNode('node-wrist-r', yawWithRecoil * 0.16, pitchRad * 0.32, 0.54); applyCharacterSandboxAimOffsetToNode('node-hand-r', yawWithRecoil * 0.12, pitchRad * 0.22, 0.42); applyCharacterSandboxAimOffsetToNode('node-elbow-l', yawWithRecoil * 0.1, pitchRad * 0.2, 0.4); } function buildFrameQuaternion(frame: Frame): THREE.Quaternion { const basis = new THREE.Matrix4().makeBasis(frame.right, frame.up, frame.forward); return new THREE.Quaternion().setFromRotationMatrix(basis); } function triggerCharacterSandboxFireFx(nowMs = performance.now()): void { characterSandboxState.muzzleFlashTimer = Math.max(characterSandboxState.muzzleFlashTimer, 0.11); const kick = characterSandboxState.posePreset === 'fire' ? 1.18 : 0.84; characterSandboxState.recoilKick = THREE.MathUtils.clamp(characterSandboxState.recoilKick + kick, 0, 1.5); const intervalMs = 1000 / Math.max(1, characterSandboxState.fireRateHz); characterSandboxState.nextAutoFireAtMs = Math.max(nowMs, characterSandboxState.nextAutoFireAtMs) + intervalMs; } function updateCharacterSandboxFx(deltaSeconds: number, nowMs: number): boolean { if (!characterSandboxState.enabled) { characterSandboxMuzzleCore.visible = false; characterSandboxMuzzleHalo.visible = false; characterSandboxMuzzleLight.visible = false; return false; } if (characterSandboxState.autoFire && nowMs >= characterSandboxState.nextAutoFireAtMs) { triggerCharacterSandboxFireFx(nowMs); } characterSandboxState.recoilKick = Math.max(0, characterSandboxState.recoilKick - deltaSeconds * 6.4); characterSandboxState.muzzleFlashTimer = Math.max(0, characterSandboxState.muzzleFlashTimer - deltaSeconds); if (poseState.clip && !poseState.playing && characterSandboxState.recoilKick > 0.0001) { applyPoseToNodes(); } const muzzleFrame = getWeaponSnapAnchorFrame('anchor-weapon-muzzle-flash') ?? getWeaponSnapAnchorFrame('anchor-weapon-muzzle'); const flashActive = characterSandboxState.muzzleFlashTimer > 0 && Boolean(muzzleFrame); if (!flashActive || !muzzleFrame) { characterSandboxMuzzleCore.visible = false; characterSandboxMuzzleHalo.visible = false; characterSandboxMuzzleLight.visible = false; return false; } const alpha = THREE.MathUtils.clamp(characterSandboxState.muzzleFlashTimer / 0.11, 0, 1); const flashPos = muzzleFrame.origin.clone().add(muzzleFrame.forward.clone().multiplyScalar(0.05)); const orientation = buildFrameQuaternion(muzzleFrame); characterSandboxMuzzleCore.visible = true; characterSandboxMuzzleHalo.visible = true; characterSandboxMuzzleLight.visible = true; characterSandboxMuzzleCore.position.copy(flashPos); characterSandboxMuzzleCore.quaternion.copy(orientation); characterSandboxMuzzleCore.scale.setScalar(0.7 + (1 - alpha) * 0.5); characterSandboxMuzzleHalo.position.copy(flashPos); characterSandboxMuzzleHalo.quaternion.copy(orientation); characterSandboxMuzzleHalo.scale.setScalar(0.72 + (1 - alpha) * 0.6); characterSandboxMuzzleLight.position.copy(flashPos); characterSandboxMuzzleLight.intensity = THREE.MathUtils.lerp(0.3, 2.4, alpha); characterSandboxMuzzleCoreMaterial.opacity = THREE.MathUtils.lerp(0.05, 0.95, alpha); characterSandboxMuzzleHaloMaterial.opacity = THREE.MathUtils.lerp(0.04, 0.62, alpha); return true; } function updateCharacterSandboxInfo(nowMs: number, force = false): void { if (!force && nowMs < characterSandboxState.infoNextUpdateAtMs) return; characterSandboxState.infoNextUpdateAtMs = nowMs + 80; if (isLeaperSandboxActivePreset()) { const phaseLabel = getLeaperSandboxMotionPhaseLabel(nowMs); controls.charSandboxInfo.textContent = [ `Sandbox: ${characterSandboxState.enabled ? 'ON' : 'OFF'} · leaper=${characterSandboxState.leaperMotionPreset}`, `Preview motion: ${characterSandboxState.leaperMotionPreset} · phase=${phaseLabel} · terrain IK=${state.enableLeaperTerrainIk ? 'on' : 'off'} · probes=${state.showFootProbes ? 'on' : 'off'}`, `Tilt limits: fore/aft ${state.leaperIkForeAftLimitDeg.toFixed(0)} deg · left/right ${state.leaperIkLateralLimitDeg.toFixed(0)} deg`, `Controls: Idle holds a breathing stance · Walk loops a grounded gait · Leap loops crouch, launch, air, and landing phases.` ].join('\n'); return; } const clipLabel = poseState.file ?? 'none'; const grip = Boolean(getWeaponSnapAnchorFrame('anchor-weapon-grip-r')); const support = Boolean(getWeaponSnapAnchorFrame('anchor-weapon-support-l')); const shoulder = Boolean(getWeaponSnapAnchorFrame('anchor-weapon-shoulder')); const muzzle = Boolean(getWeaponSnapAnchorFrame('anchor-weapon-muzzle-flash') ?? getWeaponSnapAnchorFrame('anchor-weapon-muzzle')); const ik = getCharacterSandboxIkProfile(characterSandboxState.posePreset); controls.charSandboxInfo.textContent = [ `Sandbox: ${characterSandboxState.enabled ? 'ON' : 'OFF'} · preset=${characterSandboxState.posePreset}`, `Clip: ${clipLabel} · playing=${poseState.playing ? 'yes' : 'no'} · IK-dominant hold=${poseState.weaponHoldEnabled ? 'on' : 'forced'}`, `Aim: yaw ${characterSandboxState.aimYawDeg.toFixed(0)} deg · pitch ${characterSandboxState.aimPitchDeg.toFixed(0)} deg · ADS ${characterSandboxState.adsBlend.toFixed(2)}`, `IK weights: hand ${ik.handWeight.toFixed(2)} · shoulder ${ik.shoulderWeight.toFixed(2)} · elbowBias ${ik.supportElbowBias.toFixed(2)}`, `Auto fire: ${characterSandboxState.autoFire ? 'on' : 'off'} @ ${characterSandboxState.fireRateHz.toFixed(1)} Hz · flash=${characterSandboxState.muzzleFlashTimer > 0 ? 'active' : 'idle'}`, `Anchors: grip=${grip ? 'ok' : 'missing'} · support=${support ? 'ok' : 'missing'} · shoulder=${shoulder ? 'ok' : 'missing'} · muzzle=${muzzle ? 'ok' : 'missing'}` ].join('\n'); } function updatePosePlayback(delta: number): void { if (!poseState.clip || !poseState.mixer || !poseState.action) return; if (!poseState.playing) return; const step = delta * poseState.speed; poseState.mixer.update(step); poseState.time = poseState.action.time; if (!poseState.loop && poseState.time >= poseState.duration) { poseState.time = poseState.duration; poseState.playing = false; updatePoseControls(); } poseState.root?.updateMatrixWorld(true); applyPoseToNodes(); if (poseState.duration > 0) { controls.poseTime.value = (poseState.time / poseState.duration).toFixed(3); } markRenderDirty(); } function resolveIdSide(value?: string): 'left' | 'right' | null { if (!value) return null; const lower = value.toLowerCase(); if (/(^|[-_\s])(l|left)($|[-_\s])/.test(lower)) return 'left'; if (/(^|[-_\s])(r|right)($|[-_\s])/.test(lower)) return 'right'; return null; } function formatModelSideLabel(value?: string): string { const side = resolveIdSide(value); if (!side) return ''; return side === 'left' ? ' [model-left]' : ' [model-right]'; } function formatEditorItemLabel(value: string): string { return `${value}${formatModelSideLabel(value)}`; } function setSelection(kind: SelectionKind, id?: string, instanceIndex?: number | null): void { state.selection = { kind, id }; state.attachmentInstanceIndex = kind === 'attachment' ? (instanceIndex ?? null) : null; if (!kind) { controls.selection.textContent = 'None'; updateNodePoseInput(null); applyVisualState(); return; } const capability = getCapabilityForSelection(kind); if (capability && activeSidebarCapability === null) { setActiveSidebarCapability(capability, { forceOpen: true }); } const label = kind === 'attachment' ? 'MODULE' : kind === 'scene-prop' ? 'PROP' : kind.toUpperCase(); controls.selection.textContent = `${label}: ${id ? formatEditorItemLabel(id) : ''}`.trim(); if (kind !== 'node') { updateNodePoseInput(null); } applyVisualState(); } function refreshTransformTarget(): void { if (state.selection.kind === 'node' && state.selection.id) { const mesh = nodeMeshMap.get(state.selection.id); safeAttachTransform(mesh); if (transform.object) { transform.setSpace('world'); } return; } if (state.selection.kind === 'attachment' && state.selection.id) { const mesh = getAttachmentSelectionMesh(state.selection.id, state.attachmentInstanceIndex); safeAttachTransform(mesh); if (transform.object) { transform.setSpace('local'); } return; } if (state.selection.kind === 'scene-prop' && state.selection.id) { const object = scenePropMeshMap.get(state.selection.id); safeAttachTransform(object); if (transform.object) { transform.setSpace('world'); } return; } transform.detach(); } function selectAttachmentInstance(id: string, instanceIndex?: number | null): void { const attachment = getAttachmentById(id); if (!attachment) return; setSelection('attachment', id, instanceIndex ?? null); controls.attachmentList.value = id; updateAttachmentTransformInputs(attachment); updateAttachmentShapeInputs(attachment); updateSculptInputs(attachment); updateAttachmentGeneratorInputs(attachment); updateAttachmentDiagnosticInfo(attachment); refreshTransformTarget(); setStatus(`Selected module ${formatEditorItemLabel(id)}. Edit transforms in anchor-local right/up/forward space.`); } function selectNodeInstance(id: string): void { const node = getNodeById(id); if (!node) return; setSelection('node', id); controls.nodeList.value = id; updateNodeModeInputs(node); updatePoseDebugInfo(); refreshTransformTarget(); if (id === 'node-torso' && poseState.clip) { setStatus('Selected node-torso. Pose panel shows its BVH mapping and torso axis diagnosis.'); } } function setButtonActive(button: HTMLElement, active: boolean): void { button.classList.toggle('is-active', active); } function updateToolbarState(): void { setButtonActive(controls.modeSelect, state.mode === 'select'); setButtonActive(controls.modeAddNode, state.mode === 'add-node'); setButtonActive(controls.modeAddEdge, state.mode === 'add-edge'); setButtonActive(controls.modeSurface, state.mode === 'surface'); setButtonActive(controls.toggleGrid, state.showGrid); setButtonActive(controls.toggleEdges, state.showEdges); setButtonActive(controls.toggleAnchors, state.showAnchors); setButtonActive(controls.toggleGenerators, state.showGenerators); setButtonActive(controls.toggleModules, state.showModules); setButtonActive(controls.toggleProxy, state.showProxy); if (controls.toggleSkeleton) { setButtonActive(controls.toggleSkeleton, state.showSkeleton); } setButtonActive(controls.toggleGizmo, state.gizmoEnabled); setButtonActive(controls.gizmoTranslate, state.gizmoMode === 'translate'); setButtonActive(controls.gizmoRotate, state.gizmoMode === 'rotate'); setButtonActive(controls.gizmoScale, state.gizmoMode === 'scale'); } function setGizmoMode(mode: 'translate' | 'rotate' | 'scale'): void { state.gizmoMode = mode; transform.setMode(mode); updateToolbarState(); } function isObjectInScene(object: THREE.Object3D): boolean { let current: THREE.Object3D | null = object; while (current) { if (current === scene) return true; current = current.parent ?? null; } return false; } function safeDetachTransform(): void { if (transform.object && !isObjectInScene(transform.object)) { transform.detach(); return; } if (transform.object && !state.gizmoEnabled) { transform.detach(); } } function safeAttachTransform(object: THREE.Object3D | undefined): void { if (!object || !state.gizmoEnabled || state.mode !== 'select') { transform.detach(); return; } if (!isObjectInScene(object)) { transform.detach(); return; } try { transform.attach(object); } catch { transform.detach(); } } function scheduleLiveRebuild(options: LiveRebuildOptions = {}): void { const { skipVisualState = false } = options; pendingLiveRebuild = true; if (liveRebuildScheduled) return; liveRebuildScheduled = true; requestAnimationFrame(() => { liveRebuildScheduled = false; if (!pendingLiveRebuild) return; pendingLiveRebuild = false; syncNodeMeshesFromData(); rebuildSkeletonLinks(); invalidateEdgeCache(); syncDerivedTransforms(skipVisualState); if (!skipVisualState) { updateVisibility(); } markRenderDirty(); }); } function syncNodeMeshesFromData(): void { for (const node of state.nodes) { const mesh = nodeMeshMap.get(node.id); if (!mesh) continue; const world = resolveNodeWorldTransform(node); mesh.position.copy(world.position); mesh.quaternion.copy(world.quaternion); mesh.updateMatrixWorld(); } } function refreshRaycastTargets(): void { raycastNodeTargets = Array.from(nodeMeshMap.values()); raycastAnchorTargets = Array.from(anchorMeshMap.values()); raycastEdgeTargets = Array.from(edgeLineMap.values()); raycastAttachmentTargets = attachmentInstanceMeshes.slice(); raycastGeneratorTargets = generatorMarkerMeshes.slice(); raycastScenePropTargets = Array.from(scenePropMeshMap.values()); raycastSurfaceTargets = raycastAttachmentTargets.concat(raycastScenePropTargets); if (isLeaperRuntimeBodyPreviewActive()) { leaperRuntimePreviewGroup.traverse((child) => { if ((child as THREE.Mesh).isMesh) { raycastSurfaceTargets.push(child); } }); } if (state.proxyObject) { state.proxyObject.traverse((child) => { if ((child as THREE.Mesh).isMesh) { raycastSurfaceTargets.push(child); } }); } } function syncDerivedTransforms(skipVisualState = false): void { if ( edgeLineMap.size !== state.edges.length || anchorMeshMap.size !== state.anchors.length || (state.attachments.length > 0 && attachmentInstanceMeshes.length === 0) ) { rebuildDerivedGroups(); return; } if (state.showEdges) { for (const edge of state.edges) { const line = edgeLineMap.get(edge.id); if (!line) continue; const curve = buildCurve(edge); if (!curve) continue; const points = curve.getPoints(getEdgeRenderSamples()); const geometry = line.geometry as THREE.BufferGeometry; geometry.setFromPoints(points); geometry.computeBoundingSphere(); } } if (state.showAnchors) { for (const anchor of state.anchors) { const frame = resolveAnchorFrame(anchor); if (!frame) continue; const anchorMesh = anchorMeshMap.get(anchor.id); if (anchorMesh) { anchorMesh.position.copy(frame.origin); } const axes = anchorAxesMap.get(anchor.id); if (axes) { axes.position.copy(frame.origin); axes.setRotationFromMatrix(frameToMatrix(frame)); } } } if (state.showGenerators) { for (const generator of state.generators) { const frames = buildGeneratorFrames(generator); const markers = generatorMarkerMap.get(generator.id); const axes = generatorAxisMap.get(generator.id); const links = generatorLinkMap.get(generator.id); if (!markers || !axes) { rebuildDerivedGroups(); return; } const baseAnchor = getAnchorById(generator.baseAnchorId); const baseFrame = baseAnchor ? resolveAnchorFrame(baseAnchor) : null; frames.forEach((frame, index) => { const marker = markers[index]; if (marker) marker.position.copy(frame.origin); const axisLine = axes[index]; if (axisLine) { const tip = frame.origin.clone().add(frame.forward.clone().multiplyScalar(GENERATOR_AXIS_LENGTH)); const axisGeom = axisLine.geometry as THREE.BufferGeometry; axisGeom.setFromPoints([frame.origin, tip]); axisLine.computeLineDistances(); } const linkLine = links?.[index]; if (linkLine && baseFrame) { const linkGeom = linkLine.geometry as THREE.BufferGeometry; linkGeom.setFromPoints([baseFrame.origin, frame.origin]); linkLine.computeLineDistances(); } }); } } for (const mesh of attachmentInstanceMeshes) { const id = mesh.userData?.id as string | undefined; if (!id) continue; const attachment = getAttachmentById(id); if (!attachment) continue; const moduleEntry = getModuleById(attachment.moduleId); if (!moduleEntry) continue; const index = typeof mesh.userData?.generatorIndex === 'number' ? (mesh.userData.generatorIndex as number) : undefined; const frames = getAttachmentFrames(attachment, index); const frame = frames[0]; if (!frame) continue; const matrix = buildAttachmentMatrix(attachment, frame, moduleEntry); matrix.decompose(mesh.position, mesh.quaternion, mesh.scale); mesh.updateMatrix(); mesh.visible = !(currentBasePresetName === 'leaper-arc' && isLeaperRuntimeBodyAttachment(attachment)); } syncLeaperRuntimePreviewBody(); if (!skipVisualState) { applyVisualState(); } markRenderDirty(); } function rebuildSkeletonLinks(): void { skeletonGroup.clear(); if (!state.showSkeleton) return; for (const node of state.nodes) { if (node.mode !== 'relative') continue; const parent = resolveEffectiveParent(node); if (!parent) continue; const start = getNodeWorldPosition(node); const end = getNodeWorldPosition(parent); const geometry = new THREE.BufferGeometry().setFromPoints([start, end]); const line = new THREE.Line(geometry, skeletonLineMaterial); skeletonGroup.add(line); } } function updatePanelVisibility(): void { const availability = resolveSidebarCapabilityAvailability(); if (activeSidebarCapability && !availability[activeSidebarCapability]) { const fallback = (Object.keys(availability) as SidebarCapability[]).find((capability) => availability[capability]) ?? null; activeSidebarCapability = fallback; } const selected = activeSidebarCapability && availability[activeSidebarCapability] ? activeSidebarCapability : null; const expanded = Boolean(selected); const allPanels = Object.values(panels); for (const panel of allPanels) { panel.hidden = true; } if (selected) { for (const panel of capabilityPanels[selected]) { panel.hidden = false; } } for (const button of sidebarCapabilityButtons) { const capability = button.dataset.ueCapability as SidebarCapability | undefined; if (!capability || !(capability in availability)) continue; const enabled = availability[capability]; const active = selected === capability; button.disabled = !enabled; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', active ? 'true' : 'false'); } sidebarRoot?.classList.toggle('is-expanded', expanded); sidebarRoot?.classList.toggle('is-collapsed', !expanded); sidebarLayout?.classList.toggle('is-sidebar-expanded', expanded); sidebarLayout?.classList.toggle('is-sidebar-collapsed', !expanded); } function setMode(mode: EditorMode): void { state.mode = mode; state.surfacePickMode = mode === 'surface'; state.edgeDraftStart = null; state.dragState = null; state.anchorDragState = null; state.generatorDragState = null; state.pendingDeselect = false; orbit.enabled = true; if (mode === 'surface') { controls.anchorType.value = 'surface'; } updateAnchorTypeUi(); if (mode !== 'select') { transform.detach(); } transform.visible = state.gizmoEnabled && mode === 'select'; const modeCapability = getCapabilityForMode(mode); if (modeCapability && activeSidebarCapability === null) { setActiveSidebarCapability(modeCapability, { forceOpen: true }); } updateToolbarState(); updatePanelVisibility(); const message = mode === 'select' ? 'Select mode: click nodes. Alt-drag to move on grid, gizmo for 3D.' : mode === 'add-node' ? 'Add node mode: click on the grid to place.' : mode === 'add-edge' ? 'Add edge mode: click a start node, then an end node.' : 'Surface mode: click a mesh or proxy to add a surface anchor.'; setStatus(message); } function updateVisibility(): void { const poseAvatarActive = Boolean(activePoseAvatarRoot && poseState.clip && !characterSandboxState.enabled); const authorMeshVisible = state.showModules && (characterSandboxState.enabled || !poseAvatarActive); const leaperRuntimeBodyVisible = authorMeshVisible && currentBasePresetName === 'leaper-arc'; grid.visible = state.showGrid; edgeGroup.visible = state.showEdges; anchorGroup.visible = state.showAnchors; generatorGroup.visible = state.showGenerators; generatorGizmoGroup.visible = state.showGenerators; moduleGroup.visible = authorMeshVisible; leaperRuntimePreviewGroup.visible = leaperRuntimeBodyVisible; attachmentAxisGroup.visible = authorMeshVisible && state.showAttachmentAxes; scenePropGroup.visible = state.showSceneProps; footProbeGroup.visible = state.showFootProbes; proxyGroup.visible = state.showProxy; skeletonGroup.visible = state.showSkeleton; previewGroup.visible = state.mode === 'add-edge'; poseDebugGroup.visible = Boolean(poseState.clip) && poseState.weaponHoldEnabled; poseAvatarGroup.visible = poseAvatarActive; characterSandboxFxGroup.visible = Boolean(poseState.clip) && characterSandboxState.enabled && state.showModules; controls.showGenerators.checked = state.showGenerators; controls.showProxy.checked = state.showProxy; controls.showSceneProps.checked = state.showSceneProps; controls.enableLeaperTerrainIk.checked = state.enableLeaperTerrainIk; controls.leaperIkForeAftLimit.value = state.leaperIkForeAftLimitDeg.toFixed(0); controls.leaperIkLateralLimit.value = state.leaperIkLateralLimitDeg.toFixed(0); updateLeaperIkTiltLabels(); controls.showFootProbes.checked = state.showFootProbes; controls.showFootProbeLabels.checked = state.showFootProbeLabels; controls.showAttachmentAxes.checked = state.showAttachmentAxes; controls.profileGeneration.checked = state.profileGeneration; markRenderDirty(); } function makeId(prefix: keyof EditorState['nextIds']): string { const current = state.nextIds[prefix]; state.nextIds[prefix] += 1; return `${prefix}-${current}`; } function buildModuleCatalog(): ModuleEntry[] { const catalog = getHumanoidPartCatalog(); return Object.values(catalog) .map((def) => { const purpose = def.purpose ?? 'generic'; return { id: def.id, label: def.id.replace(/_/g, ' '), def, portType: purpose, portProfile: inferModulePortProfile(def), group: def.group, purpose }; }) .sort((a, b) => a.label.localeCompare(b.label)); } function inferModulePortProfile(def: HumanoidPartDefinition): PortProfile { const id = def.id.toLowerCase(); if (id.includes('rail') || id.includes('hardpoint')) return 'rail'; if (def.purpose === 'weapon' || id.includes('weapon')) return 'male'; if (def.purpose === 'connector' || def.purpose === 'joint' || def.purpose === 'limb_segment') return 'male'; if (def.purpose === 'backpack') return 'male'; if (def.purpose === 'helmet' || def.purpose === 'housing' || def.purpose === 'armor_plate') return 'female'; return 'any'; } function getModulePortProfile(entry: ModuleEntry): PortProfile { return entry.portProfile ?? 'any'; } function createMaterialForDefinition(def: HumanoidPartDefinition): THREE.MeshStandardMaterial { const slotProps = getDefaultMaterialForSlot(def.materialSlot ?? MaterialSlot.BASE); const typeProps = def.materialType ? MATERIAL_TYPE_PRESETS[def.materialType] : undefined; const color = typeProps?.baseColorTint ? new THREE.Color(typeProps.baseColorTint.r, typeProps.baseColorTint.g, typeProps.baseColorTint.b) : new THREE.Color(SLOT_COLORS[def.materialSlot ?? MaterialSlot.BASE] ?? 0x9aa7b4); const metalness = typeProps?.metalness ?? slotProps.metalness ?? 0.4; const roughness = typeProps?.roughness ?? slotProps.roughness ?? 0.6; const emissiveStrength = typeProps?.emissive ?? slotProps.emissive ?? 0; const emissive = emissiveStrength > 0 ? color.clone().multiplyScalar(emissiveStrength) : new THREE.Color(0x000000); return new THREE.MeshStandardMaterial({ color, metalness, roughness, emissive }); } const HUMAN_PRESET_SKIN_MODULES: ReadonlySet = new Set([ 'core_pelvis', 'core_abdomen_block', 'core_torso_command', 'core_torso', 'core_neck_column', 'core_head_cowl', 'core_shoulder_joint', 'core_upper_arm', 'core_forearm', 'core_hand_guard', 'core_hip_joint', 'core_upper_leg', 'core_lower_leg', 'core_foot_block', 'core_pelvis_buttress', 'core_torso_flank' ]); const HUMAN_PRESET_ANATOMY_SHAPES: ReadonlyMap = new Map([ ['core_pelvis', { kind: 'torso', intensity: 0.95, scale: { x: 1.08, y: 0.88, z: 1.0 } }], ['core_abdomen_block', { kind: 'torso', intensity: 0.9, scale: { x: 1.06, y: 0.82, z: 1.04 } }], ['core_torso', { kind: 'torso', intensity: 0.88, scale: { x: 1.1, y: 1.06, z: 1.01 } }], ['core_torso_command', { kind: 'torso', intensity: 0.98, scale: { x: 1.12, y: 1.08, z: 1.02 } }], ['core_neck_column', { kind: 'limb', intensity: 0.52, scale: { x: 0.74, y: 1.24, z: 0.79 } }], ['core_head_cowl', { kind: 'torso', intensity: 0.74, scale: { x: 1.02, y: 1.1, z: 1.05 } }], ['core_shoulder_joint', { kind: 'limb', intensity: 0.72, scale: { x: 0.8, y: 0.8, z: 0.8 } }], ['core_upper_arm', { kind: 'limb', intensity: 0.86, scale: { x: 1.02, y: 1.0, z: 0.98 } }], ['core_forearm', { kind: 'limb', intensity: 0.84, scale: { x: 0.96, y: 1.0, z: 0.96 } }], ['core_hand_guard', { kind: 'limb', intensity: 0.6, scale: { x: 1.14, y: 0.96, z: 1.12 } }], ['core_hip_joint', { kind: 'limb', intensity: 0.64, scale: { x: 0.84, y: 0.84, z: 0.84 } }], ['core_upper_leg', { kind: 'limb', intensity: 0.9, scale: { x: 0.96, y: 1.0, z: 0.96 } }], ['core_lower_leg', { kind: 'limb', intensity: 0.92, scale: { x: 0.96, y: 1.04, z: 0.95 } }], ['core_foot_block', { kind: 'plate', intensity: 0.8, scale: { x: 1.0, y: 0.9, z: 1.24 } }], ['core_ankle_joint', { kind: 'limb', intensity: 0.58, scale: { x: 0.82, y: 0.86, z: 0.82 } }], ['core_pelvis_buttress', { kind: 'plate', intensity: 0.62, scale: { x: 1.16, y: 0.7, z: 1.08 } }], ['core_torso_flank', { kind: 'plate', intensity: 0.62, scale: { x: 1.12, y: 0.88, z: 1.06 } }] ]); const HUMAN_PRESET_NON_SKIN_PURPOSES = new Set(['weapon', 'backpack', 'helmet', 'housing', 'armor_plate']); const HUMANOID_WEAPON_PRESET_ARCHETYPES = new Map([ ['humanoid-rifle', 'rifle'], ['humanoid-marksman', 'marksman'], ['humanoid-beam', 'beam'], ['humanoid-heavy', 'heavy'], ['humanoid-launcher', 'launcher'], ['humanoid-forearm-pod', 'forearm-pod'] ]); const STANDALONE_WEAPON_PRESET_ARCHETYPES = new Map([ ['weapon-marksman-standalone', 'marksman'] ]); const HUMANOID_LINE_PRESET_NAMES = new Set([ 'human', 'humanoid-full', ...HUMANOID_WEAPON_PRESET_ARCHETYPES.keys(), ...STANDALONE_WEAPON_PRESET_ARCHETYPES.keys() ]); const DEFAULT_WEAPON_ROTATION = vec3(90, 0, 0); const getWeaponPresetArchetype = (name: string): HumanoidWeaponArchetype | null => STANDALONE_WEAPON_PRESET_ARCHETYPES.get(name) ?? HUMANOID_WEAPON_PRESET_ARCHETYPES.get(name) ?? null; const isStandaloneWeaponPreset = (name: string): boolean => STANDALONE_WEAPON_PRESET_ARCHETYPES.has(name); const isMarksmanVisualPreset = (name: string): boolean => name === 'humanoid-marksman' || name === 'humanoid-full' || name === 'weapon-marksman-standalone'; function isHumanSkinTarget(moduleId: string, def: HumanoidPartDefinition): boolean { if (currentBasePresetName !== 'human' && currentBasePresetName !== 'humanoid-full') return false; if (HUMAN_PRESET_SKIN_MODULES.has(moduleId)) return true; if (!moduleId.startsWith('core_')) return false; if (HUMAN_PRESET_NON_SKIN_PURPOSES.has(def.purpose ?? '')) return false; if (moduleId.includes('_plate') || moduleId.includes('_guard') || moduleId.includes('_receiver')) return false; if (moduleId.includes('_barrel') || moduleId.includes('_clip') || moduleId.includes('_vent')) return false; return true; } function applyHumanSkinToneToMaterial(material: THREE.Material): THREE.Material { if (!(material instanceof THREE.MeshStandardMaterial)) return material; const skin = material.clone(); const physicalSkin = skin as THREE.MeshStandardMaterial & { clearcoat?: number; clearcoatRoughness?: number; }; skin.color.lerp(new THREE.Color(0xc18a6f), 0.75); skin.roughness = THREE.MathUtils.lerp(skin.roughness, 0.72, 0.55); skin.metalness = Math.min(skin.metalness, 0.08); skin.emissive.set(0x18100b); skin.emissiveIntensity = 0.08; physicalSkin.clearcoat = Math.max(0.05, physicalSkin.clearcoat ?? 0); physicalSkin.clearcoatRoughness = 0.65; return skin; } function applyHumanAnatomyShapeProfile(object: THREE.Object3D, moduleId: string, def: HumanoidPartDefinition): void { if (currentBasePresetName !== 'human') return; const profile = HUMAN_PRESET_ANATOMY_SHAPES.get(moduleId); if (!profile) return; if (!def.id || !def.primitive) return; if (profile.scale) { const { x = 1, y = 1, z = 1 } = profile.scale; object.scale.multiply(new THREE.Vector3(x, y, z)); } const syntheticShape = { anchorId: 'center', id: `${moduleId}-shape-tune`, shape: { profile: { kind: profile.kind, intensity: profile.intensity } } } as unknown as AttachmentEntry; applyAttachmentShapeModifiersToObject(object, syntheticShape); } function applyHumanSkinMaterials(object: THREE.Object3D, moduleId: string, def: HumanoidPartDefinition): void { applyHumanAnatomyShapeProfile(object, moduleId, def); if (!isHumanSkinTarget(moduleId, def)) return; object.traverse((node) => { const mesh = node as THREE.Mesh; if (!mesh.isMesh || !mesh.material) return; if (Array.isArray(mesh.material)) { mesh.material = mesh.material.map((material) => applyHumanSkinToneToMaterial(material)); return; } mesh.material = applyHumanSkinToneToMaterial(mesh.material); }); } function applyMarksmanAttachmentFinish( object: THREE.Object3D, attachment: AttachmentEntry, def: HumanoidPartDefinition ): void { if (!isMarksmanVisualPreset(currentBasePresetName)) return; const attachmentId = attachment.id; if (!attachmentId.includes('marksman')) return; const isLens = attachmentId.includes('optic-front-lens') || attachmentId.includes('optic-rear-lens'); const isOpticTube = attachmentId.includes('optic-tube'); const isOpticRing = attachmentId.includes('optic-ring'); const isBarrel = attachmentId.endsWith('marksman-barrel') || attachmentId.includes('marksman-barrel'); const isShroud = attachmentId.includes('marksman-shroud'); const isCoolingJacket = attachmentId.includes('marksman-cooling-jacket'); const isGasBlock = attachmentId.includes('gas-block'); const isFrontCollar = attachmentId.includes('throat-collar') || attachmentId.includes('muzzle-collar'); const isRailBody = attachmentId.endsWith('marksman-rail'); const isStockBody = attachmentId.endsWith('marksman-stock'); const isCradleBody = attachmentId.endsWith('marksman-cradle'); const isSight = attachmentId.includes('front-sight') || attachmentId.includes('rear-sight'); const isShoulderPad = attachmentId.includes('shoulder-piece'); const isSpring = attachmentId.includes('recoil-spring'); const isBrace = attachmentId.includes('brace') || attachmentId.includes('latch') || attachmentId.includes('clamp'); const isCollar = attachmentId.includes('collar') || attachmentId.includes('gas-block'); const isMuzzle = attachmentId.includes('muzzle'); const isMountHardware = attachmentId.includes('hardpoint') || attachmentId.includes('bridge') || attachmentId.includes('saddle') || attachmentId.includes('yoke') || attachmentId.includes('stanchion'); const isMag = attachmentId.includes('magazine') || attachmentId.includes('magwell'); const isReceiverMetal = attachmentId.includes('receiver') || attachmentId.includes('breach') || attachmentId.includes('jacket') || attachmentId.includes('bolt-housing') || attachmentId.includes('action-cover') || attachmentId.includes('action-spine'); object.traverse((node) => { const mesh = node as THREE.Mesh; if (!mesh.isMesh || !mesh.material) return; const applyToMaterial = (material: THREE.Material): THREE.Material => { if (!(material instanceof THREE.MeshStandardMaterial)) return material; const tuned = material.clone(); if (isLens || def.id === 'detail_scope_lens') { tuned.color.set(isLens && attachmentId.includes('front') ? 0x4e6878 : 0x56616d); tuned.metalness = 0.08; tuned.roughness = 0.16; tuned.emissive.set(0x132637); tuned.emissiveIntensity = attachmentId.includes('front') ? 0.22 : 0.14; tuned.transparent = true; tuned.opacity = attachmentId.includes('front') ? 0.9 : 0.84; return tuned; } if (isShoulderPad) { tuned.color.set(0x2a2d31); tuned.metalness = 0.06; tuned.roughness = 0.82; tuned.emissive.set(0x000000); tuned.flatShading = false; tuned.needsUpdate = true; return tuned; } if (isSpring) { tuned.color.set(0x525a61); tuned.metalness = 0.72; tuned.roughness = 0.34; tuned.emissive.set(0x000000); return tuned; } if (isSight || isBrace || isCollar || isMountHardware || isOpticRing) { tuned.color.set(isSight ? 0x50565d : 0x4a5057); tuned.metalness = 0.68; tuned.roughness = isSight ? 0.32 : 0.4; tuned.emissive.set(0x000000); return tuned; } if (isOpticTube) { tuned.color.set(0x3d454d); tuned.metalness = 0.54; tuned.roughness = 0.28; tuned.emissive.set(0x000000); return tuned; } if (isRailBody) { tuned.color.set(0x3e444b); tuned.metalness = 0.64; tuned.roughness = 0.26; tuned.emissive.set(0x000000); tuned.flatShading = false; tuned.needsUpdate = true; return tuned; } if (isCradleBody) { tuned.color.set(0x434a52); tuned.metalness = 0.62; tuned.roughness = 0.34; tuned.emissive.set(0x000000); return tuned; } if (isStockBody) { tuned.color.set(0x383e45); tuned.metalness = 0.3; tuned.roughness = 0.48; tuned.emissive.set(0x000000); tuned.flatShading = false; tuned.needsUpdate = true; return tuned; } if (isBarrel) { tuned.color.set(0x343b43); tuned.metalness = 0.78; tuned.roughness = 0.24; tuned.emissive.set(0x000000); return tuned; } if (isShroud || isCoolingJacket) { tuned.color.set(isCoolingJacket ? 0x3b434b : 0x444b53); tuned.metalness = isCoolingJacket ? 0.72 : 0.66; tuned.roughness = isCoolingJacket ? 0.28 : 0.34; tuned.emissive.set(0x000000); return tuned; } if (isGasBlock || isFrontCollar) { tuned.color.set(isGasBlock ? 0x4a5057 : 0x515860); tuned.metalness = isGasBlock ? 0.74 : 0.7; tuned.roughness = isGasBlock ? 0.24 : 0.28; tuned.emissive.set(0x000000); return tuned; } if (isMag) { tuned.color.set(attachmentId.includes('throat') ? 0x44484e : 0x3d4349); tuned.metalness = attachmentId.includes('latch') ? 0.56 : 0.44; tuned.roughness = attachmentId.includes('throat') ? 0.38 : 0.48; tuned.emissive.set(0x000000); tuned.flatShading = false; tuned.needsUpdate = true; return tuned; } if (isMuzzle) { tuned.color.set( attachmentId.includes('vent') ? 0x3c4248 : attachmentId.includes('brake') ? 0x4a4f56 : 0x313840 ); tuned.metalness = attachmentId.includes('vent') ? 0.78 : attachmentId.includes('brake') ? 0.76 : 0.7; tuned.roughness = attachmentId.includes('brake') ? 0.2 : attachmentId.includes('vent') ? 0.22 : 0.34; tuned.emissive.set(0x000000); return tuned; } if (isReceiverMetal) { tuned.color.set(attachmentId.includes('port-cover') ? 0x68717b : 0x5f6670); tuned.metalness = attachmentId.includes('bolt-housing') ? 0.68 : 0.58; tuned.roughness = attachmentId.includes('bolt-housing') ? 0.3 : 0.38; tuned.emissive.set(0x000000); tuned.flatShading = false; tuned.needsUpdate = true; return tuned; } return tuned; }; if (Array.isArray(mesh.material)) { mesh.material = mesh.material.map((material) => applyToMaterial(material)); return; } mesh.material = applyToMaterial(mesh.material); }); } function resolveGeneratedModuleFamily(): 'humanoid_command' | 'humanoid_line' | null { if (currentBasePresetName === 'commander') return 'humanoid_command'; if (HUMANOID_LINE_PRESET_NAMES.has(currentBasePresetName)) return 'humanoid_line'; return null; } function resolveAttachmentSide(attachment?: AttachmentEntry): 'left' | 'right' | 'center' { const value = `${attachment?.anchorId ?? ''} ${attachment?.id ?? ''}`.toLowerCase(); if (/(^|[-_\\s])(l|left)($|[-_\\s])/.test(value)) return 'left'; if (/(^|[-_\\s])(r|right)($|[-_\\s])/.test(value)) return 'right'; return 'center'; } function buildGeneratedModuleMesh(entry: ModuleEntry, lowLod = false, side: 'left' | 'right' | 'center' = 'center'): THREE.Object3D | null { const family = resolveGeneratedModuleFamily(); if (!family) return null; const key = `${entry.id}:${family}:${side}:${lowLod ? 'generated-low' : 'generated-high'}`; const cached = moduleMeshCache.get(key); if (cached) { if (activeGenerationProfile) { bumpGenerationCounter(activeGenerationProfile, 'generatedCacheHit'); } const clone = cached.clone(true); applyHumanSkinMaterials(clone, entry.id, entry.def); return clone; } const profile = activeGenerationProfile; const profileStart = profile ? performance.now() : 0; const mesh = HumanoidUnitAssembler.generatePartMesh(entry.id, { family, axis: 'y', side, materialSlot: entry.def.materialSlot, materialType: entry.def.materialType, assetId: family === 'humanoid_command' ? 'UNIT_COMMANDER_EDITOR' : 'UNIT_HUMANOID_EDITOR' }); if (!mesh) { return null; } const geometry = proceduralMeshToThreeGeometry(mesh); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingSphere(); const material = createMaterialForDefinition(entry.def); const threeMesh = new THREE.Mesh(geometry, material); threeMesh.castShadow = false; threeMesh.receiveShadow = false; moduleMeshCache.set(key, threeMesh); if (profile) { bumpGenerationCounter(profile, 'generatedCacheMiss'); recordGenerationMesh(profile, `generated:${entry.id}`, performance.now() - profileStart); } const clone = threeMesh.clone(true); applyHumanSkinMaterials(clone, entry.id, entry.def); return clone; } function buildPrimitiveMesh(def: HumanoidPartDefinition): ProceduralMesh { const size = def.size ?? { x: 0.12, y: 0.12, z: 0.12 }; const depth = size.z ?? size.x; const materialSlot = def.materialSlot ?? MaterialSlot.BASE; const materialType = def.materialType; switch (def.primitive) { case 'box': return BoxPrimitive.generate({ width: size.x, height: size.y, depth, materialSlot, materialType }); case 'wedge': return WedgePrimitive.generate({ width: size.x, height: size.y, depth, materialSlot, materialType }); case 'cylinder': return CylinderPrimitive.generate({ radius: size.x, height: size.y, segments: size.segments, materialSlot, materialType }); case 'cone': return ConePrimitive.generate({ radius: size.x, height: size.y, segments: size.segments, materialSlot, materialType }); case 'capsule': return CapsulePrimitive.generate({ radius: size.x, height: size.y, segments: size.segments, rings: size.rings, materialSlot, materialType }); case 'sphere': return SpherePrimitive.generate({ radius: size.x, segments: size.segments, rings: size.rings, materialSlot, materialType }); case 'hemisphere': return SpherePrimitive.generate({ radius: size.x, segments: size.segments, rings: size.rings, materialSlot, materialType }); case 'torus': return TorusPrimitive.generate({ majorRadius: size.x, minorRadius: size.minor ?? size.x * 0.25, majorSegments: size.segments, minorSegments: size.rings, materialSlot, materialType }); default: return BoxPrimitive.generate({ width: size.x, height: size.y, depth, materialSlot, materialType }); } } function buildExtrudedWeaponProfileGeometry( size: PartSize, points: Array<[number, number]>, lowLod: boolean, options: { bevelScale?: number; bevelThicknessScale?: number; curveSegments?: number; bevelSegments?: number; } = {} ): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const shapePoints = points.map( ([u, v]) => new THREE.Vector2(u * length, v * height) ); const shape = new THREE.Shape(shapePoints); const minDim = Math.min(width, height, length); const bevelSize = minDim * (options.bevelScale ?? 0.08); const bevelThickness = minDim * (options.bevelThicknessScale ?? 0.08); const geometry = new THREE.ExtrudeGeometry(shape, { depth: width, steps: 1, curveSegments: options.curveSegments ?? (lowLod ? 6 : 32), bevelEnabled: true, bevelSegments: options.bevelSegments ?? (lowLod ? 1 : 6), bevelSize, bevelThickness }); geometry.center(); geometry.rotateY(Math.PI * 0.5); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); return geometry; } function buildRevolvedWeaponProfileGeometry( size: PartSize, options: { radialSegments: number; rearRadius?: number; frontRadius?: number; openEnded?: boolean; } ): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const rearRadius = Math.max(0.05, options.rearRadius ?? 1); const frontRadius = Math.max(0.05, options.frontRadius ?? rearRadius); const geometry = new THREE.CylinderGeometry( frontRadius * 0.5, rearRadius * 0.5, 1, options.radialSegments, 1, options.openEnded ?? false ); geometry.rotateX(Math.PI * 0.5); geometry.scale(width, height, length); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); return geometry; } function computeGeometrySignedVolume(geometry: THREE.BufferGeometry): number { const position = geometry.getAttribute('position'); if (!position || position.itemSize < 3) { return 0; } const index = geometry.getIndex(); const a = new THREE.Vector3(); const b = new THREE.Vector3(); const c = new THREE.Vector3(); let signedVolume = 0; const triangleCount = index ? Math.floor(index.count / 3) : Math.floor(position.count / 3); for (let tri = 0; tri < triangleCount; tri += 1) { const ia = index ? index.getX(tri * 3) : tri * 3; const ib = index ? index.getX(tri * 3 + 1) : tri * 3 + 1; const ic = index ? index.getX(tri * 3 + 2) : tri * 3 + 2; a.fromBufferAttribute(position, ia); b.fromBufferAttribute(position, ib); c.fromBufferAttribute(position, ic); signedVolume += a.dot(b.clone().cross(c)); } return signedVolume / 6; } function ensureOutwardGeometryWinding(geometry: THREE.BufferGeometry): THREE.BufferGeometry { const signedVolume = computeGeometrySignedVolume(geometry); if (signedVolume >= 0) { return geometry; } const index = geometry.getIndex(); if (!index) { return geometry; } for (let tri = 0; tri < index.count; tri += 3) { const b = index.getX(tri + 1); const c = index.getX(tri + 2); index.setX(tri + 1, c); index.setX(tri + 2, b); } index.needsUpdate = true; geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); return geometry; } function makeGeometryFlatShaded(geometry: THREE.BufferGeometry): THREE.BufferGeometry { const smooth = geometry.clone(); smooth.computeVertexNormals(); smooth.normalizeNormals(); smooth.computeBoundingBox(); smooth.computeBoundingSphere(); return smooth; } function centerGeometryOnBounds(geometry: THREE.BufferGeometry): THREE.BufferGeometry { geometry.computeBoundingBox(); const bounds = geometry.boundingBox; if (!bounds) { return geometry; } const center = new THREE.Vector3(); bounds.getCenter(center); if (center.lengthSq() < 1e-8) { return geometry; } geometry.translate(-center.x, -center.y, -center.z); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); return geometry; } function buildScopeTubeGeometry( size: PartSize, lowLod: boolean, options?: { rearBellScale?: number; frontBellScale?: number; waistScale?: number; bodyScale?: number; } ): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const rearBellScale = options?.rearBellScale ?? 1.08; const frontBellScale = options?.frontBellScale ?? 1.16; const waistScale = options?.waistScale ?? 0.82; const bodyScale = options?.bodyScale ?? 0.9; const radialSegments = lowLod ? 16 : 32; const lengthSegments = lowLod ? 14 : 32; const geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, radialSegments, lengthSegments, false); geometry.rotateX(Math.PI * 0.5); const position = geometry.getAttribute('position'); for (let index = 0; index < position.count; index += 1) { const x = position.getX(index); const y = position.getY(index); const z = position.getZ(index); const t = THREE.MathUtils.clamp(z + 0.5, 0, 1); const rearBell = 1 - THREE.MathUtils.smoothstep(t, 0.12, 0.34); const frontBell = THREE.MathUtils.smoothstep(t, 0.66, 0.9); const bodyBlend = 1 - Math.min(1, Math.abs(t - 0.5) / 0.34); const eyepieceLip = 1 - THREE.MathUtils.smoothstep(t, 0.02, 0.08); const objectiveLip = THREE.MathUtils.smoothstep(t, 0.92, 0.98); const radiusScale = bodyScale + rearBell * (rearBellScale - bodyScale) + frontBell * (frontBellScale - bodyScale) - bodyBlend * (bodyScale - waistScale) + eyepieceLip * 0.04 + objectiveLip * 0.06; position.setXYZ(index, x * radiusScale, y * radiusScale, z); } geometry.scale(width, height, length); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); return ensureOutwardGeometryWinding(geometry); } function buildMarksmanStockGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const roundness = Math.max(0.003, Math.min(width, height, length) * 0.16); const smoothness = Math.max(0.004, roundness * 2.2); let sdf: SdfFn | null = null; let min = vec3(Infinity, Infinity, Infinity); let max = vec3(-Infinity, -Infinity, -Infinity); const expandBounds = (half: Vec3Like, offset: Vec3Like, extra = roundness * 1.25): void => { min = vec3( Math.min(min.x, offset.x - half.x - extra), Math.min(min.y, offset.y - half.y - extra), Math.min(min.z, offset.z - half.z - extra) ); max = vec3( Math.max(max.x, offset.x + half.x + extra), Math.max(max.y, offset.y + half.y + extra), Math.max(max.z, offset.z + half.z + extra) ); }; const addRoundedMass = ( half: Vec3Like, offset: Vec3Like, localRoundness = roundness, localSmoothness = smoothness ): void => { const mass = translate(sdfRoundedBox(half, localRoundness), offset); expandBounds(half, offset, localRoundness); sdf = sdf ? smoothUnion(sdf, mass, localSmoothness) : mass; }; addRoundedMass( { x: width * 0.4, y: height * 0.28, z: length * 0.12 }, vec3(0, height * 0.01, -length * 0.38), roundness * 1.1, smoothness * 1.05 ); addRoundedMass( { x: width * 0.26, y: height * 0.12, z: length * 0.12 }, vec3(0, -height * 0.12, -length * 0.26), roundness, smoothness * 0.9 ); addRoundedMass( { x: width * 0.34, y: height * 0.2, z: length * 0.36 }, vec3(0, -height * 0.01, -length * 0.03), roundness, smoothness ); addRoundedMass( { x: width * 0.22, y: height * 0.14, z: length * 0.22 }, vec3(0, height * 0.17, -length * 0.18), roundness * 0.95, smoothness ); addRoundedMass( { x: width * 0.16, y: height * 0.1, z: length * 0.26 }, vec3(0, height * 0.08, length * 0.01), roundness * 0.8, smoothness * 0.8 ); addRoundedMass( { x: width * 0.14, y: height * 0.14, z: length * 0.2 }, vec3(0, -height * 0.05, length * 0.29), roundness * 0.75, smoothness * 0.8 ); if (!sdf) { const geometry = buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.12], [0.02, 0.42], [0.08, 0.78], [0.18, 0.94], [0.34, 0.9], [0.48, 0.78], [0.66, 0.62], [0.84, 0.4], [1.0, 0.24], [1.0, 0.12], [0.84, 0.06], [0.48, 0.04], [0.18, 0.06], [0.04, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.08, bevelThicknessScale: 0.07 } ); centerGeometryOnBounds(geometry); return geometry; } const cheekRelief = translate( sdfRoundedBox( { x: width * 0.14, y: height * 0.08, z: length * 0.16 }, roundness * 0.7 ), vec3(0, height * 0.12, -length * 0.12) ); const wristRelief = translate( sdfRoundedBox( { x: width * 0.14, y: height * 0.06, z: length * 0.12 }, roundness * 0.6 ), vec3(0, -height * 0.11, length * 0.14) ); const toeRelief = translate( sdfRoundedBox( { x: width * 0.14, y: height * 0.06, z: length * 0.12 }, roundness * 0.55 ), vec3(0, -height * 0.12, -length * 0.18) ); sdf = subtract(sdf, cheekRelief); sdf = subtract(sdf, wristRelief); sdf = subtract(sdf, toeRelief); const resolution = lowLod ? 30 : 72; const mesh = tryMeshSdf(sdf, { min, max }, { resolution, minAxisResolution: lowLod ? 12 : 28, surfaceThreshold: 5e-6, materialSlot: MaterialSlot.TEAM_PRIMARY }); if (!mesh) { return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.12], [0.02, 0.42], [0.08, 0.78], [0.18, 0.94], [0.34, 0.9], [0.48, 0.78], [0.66, 0.62], [0.84, 0.4], [1.0, 0.24], [1.0, 0.12], [0.84, 0.06], [0.48, 0.04], [0.18, 0.06], [0.04, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.08, bevelThicknessScale: 0.07 } ); } const geometry = ensureOutwardGeometryWinding(proceduralMeshToThreeGeometry(mesh)); centerGeometryOnBounds(geometry); return makeGeometryFlatShaded(geometry); } function buildReceiverUpperGeometry( size: PartSize, lowLod: boolean, options?: { deckScale?: number; noseScale?: number; tailScale?: number; } ): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const roundness = Math.max(0.003, Math.min(width, height, length) * 0.12); const smoothness = Math.max(0.004, roundness * 2.2); const deckScale = options?.deckScale ?? 1; const noseScale = options?.noseScale ?? 1; const tailScale = options?.tailScale ?? 1; let sdf: SdfFn | null = null; let min = vec3(Infinity, Infinity, Infinity); let max = vec3(-Infinity, -Infinity, -Infinity); const expandBounds = (half: Vec3Like, offset: Vec3Like, extra = roundness * 1.25): void => { min = vec3( Math.min(min.x, offset.x - half.x - extra), Math.min(min.y, offset.y - half.y - extra), Math.min(min.z, offset.z - half.z - extra) ); max = vec3( Math.max(max.x, offset.x + half.x + extra), Math.max(max.y, offset.y + half.y + extra), Math.max(max.z, offset.z + half.z + extra) ); }; const addRoundedMass = ( half: Vec3Like, offset: Vec3Like, localRoundness = roundness, localSmoothness = smoothness ): void => { const mass = translate(sdfRoundedBox(half, localRoundness), offset); expandBounds(half, offset, localRoundness); sdf = sdf ? smoothUnion(sdf, mass, localSmoothness) : mass; }; addRoundedMass( { x: width * 0.46, y: height * 0.22, z: length * 0.34 }, vec3(0, height * 0.01, length * 0.02) ); addRoundedMass( { x: width * 0.4, y: height * 0.15 * deckScale, z: length * 0.26 }, vec3(0, height * 0.18 * deckScale, 0), roundness * 0.8, smoothness * 0.8 ); addRoundedMass( { x: width * 0.28, y: height * 0.16 * noseScale, z: length * 0.18 }, vec3(0, height * 0.02, length * 0.3 * noseScale), roundness * 0.85, smoothness * 0.8 ); addRoundedMass( { x: width * 0.26, y: height * 0.18 * tailScale, z: length * 0.16 }, vec3(0, height * 0.03, -length * 0.28 * tailScale), roundness * 0.8, smoothness * 0.8 ); addRoundedMass( { x: width * 0.22, y: height * 0.06, z: length * 0.34 }, vec3(0, height * 0.24, length * 0.02), roundness * 0.6, smoothness * 0.6 ); addRoundedMass( { x: width * 0.3, y: height * 0.14, z: length * 0.12 }, vec3(0, height * 0.08, -length * 0.32), roundness * 0.7, smoothness * 0.7 ); const waistRelief = translate( sdfRoundedBox( { x: width * 0.22, y: height * 0.11, z: length * 0.22 }, roundness * 0.7 ), vec3(0, -height * 0.03, length * 0.02) ); const undersideRelief = translate( sdfRoundedBox( { x: width * 0.22, y: height * 0.09, z: length * 0.18 }, roundness * 0.65 ), vec3(0, -height * 0.16, length * 0.18) ); const railChannel = translate( sdfRoundedBox( { x: width * 0.14, y: height * 0.05, z: length * 0.34 }, roundness * 0.5 ), vec3(0, height * 0.18, length * 0.02) ); const ejectionPort = translate( sdfRoundedBox( { x: width * 0.08, y: height * 0.08, z: length * 0.18 }, roundness * 0.4 ), vec3(width * 0.24, height * 0.02, length * 0.08) ); if (sdf) { sdf = subtract(sdf, waistRelief); sdf = subtract(sdf, undersideRelief); sdf = subtract(sdf, railChannel); sdf = subtract(sdf, ejectionPort); } if (!sdf) { const geometry = buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.18], [0.02, 0.3], [0.05, 0.5], [0.1, 0.66], [0.18, 0.76], [0.3, 0.82], [0.52, 0.84], [0.72, 0.82], [0.84, 0.74], [0.92, 0.62], [1.0, 0.42], [1.0, 0.2], [0.9, 0.14], [0.7, 0.12], [0.46, 0.11], [0.22, 0.1], [0.06, 0.11], [0.0, 0.14] ], lowLod, { bevelScale: 0.06, bevelThicknessScale: 0.05 } ); centerGeometryOnBounds(geometry); return geometry; } const resolution = lowLod ? 30 : 72; const mesh = tryMeshSdf(sdf, { min, max }, { resolution, minAxisResolution: lowLod ? 12 : 28, surfaceThreshold: 5e-6, materialSlot: MaterialSlot.TEAM_PRIMARY }); if (!mesh) { const fallback = new THREE.BoxGeometry(width, height, length); return makeGeometryFlatShaded(fallback); } const geometry = ensureOutwardGeometryWinding(proceduralMeshToThreeGeometry(mesh)); centerGeometryOnBounds(geometry); return makeGeometryFlatShaded(geometry); } function buildBoxMagazineGeometry( size: PartSize, lowLod: boolean, options?: { feedScale?: number; toeScale?: number; } ): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const roundness = Math.max(0.0025, Math.min(width, height, length) * 0.11); const smoothness = Math.max(0.004, roundness * 2); const feedScale = options?.feedScale ?? 1; const toeScale = options?.toeScale ?? 1; let sdf: SdfFn | null = null; let min = vec3(Infinity, Infinity, Infinity); let max = vec3(-Infinity, -Infinity, -Infinity); const expandBounds = (half: Vec3Like, offset: Vec3Like, extra = roundness * 1.25): void => { min = vec3( Math.min(min.x, offset.x - half.x - extra), Math.min(min.y, offset.y - half.y - extra), Math.min(min.z, offset.z - half.z - extra) ); max = vec3( Math.max(max.x, offset.x + half.x + extra), Math.max(max.y, offset.y + half.y + extra), Math.max(max.z, offset.z + half.z + extra) ); }; const addRoundedMass = ( half: Vec3Like, offset: Vec3Like, localRoundness = roundness, localSmoothness = smoothness ): void => { const mass = translate(sdfRoundedBox(half, localRoundness), offset); expandBounds(half, offset, localRoundness); sdf = sdf ? smoothUnion(sdf, mass, localSmoothness) : mass; }; addRoundedMass( { x: width * 0.36, y: height * 0.42, z: length * 0.28 }, vec3(0, -height * 0.02, 0) ); addRoundedMass( { x: width * 0.28, y: height * 0.12 * feedScale, z: length * 0.16 }, vec3(0, height * 0.31 * feedScale, length * 0.12), roundness * 0.8, smoothness * 0.7 ); addRoundedMass( { x: width * 0.38, y: height * 0.08 * toeScale, z: length * 0.22 }, vec3(0, -height * 0.36 * toeScale, -length * 0.04), roundness * 0.7, smoothness * 0.7 ); const frontRelief = translate( sdfRoundedBox( { x: width * 0.18, y: height * 0.34, z: length * 0.12 }, roundness * 0.7 ), vec3(0, -height * 0.02, length * 0.22) ); const rearRelief = translate( sdfRoundedBox( { x: width * 0.16, y: height * 0.22, z: length * 0.1 }, roundness * 0.65 ), vec3(0, height * 0.02, -length * 0.24) ); if (sdf) { sdf = subtract(sdf, frontRelief); sdf = subtract(sdf, rearRelief); } if (!sdf) { const geometry = buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.08], [0.02, 0.28], [0.06, 0.72], [0.12, 0.94], [0.66, 0.92], [0.84, 0.74], [0.96, 0.34], [1.0, 0.14], [1.0, 0.04], [0.84, 0.02], [0.24, 0.02], [0.06, 0.04], [0.0, 0.06] ], lowLod, { bevelScale: 0.06, bevelThicknessScale: 0.055 } ); centerGeometryOnBounds(geometry); return geometry; } const resolution = lowLod ? 24 : 52; const mesh = tryMeshSdf(sdf, { min, max }, { resolution, materialSlot: MaterialSlot.TEAM_PRIMARY }); if (!mesh) { const fallback = new THREE.BoxGeometry(width, height, length); centerGeometryOnBounds(fallback); return fallback; } const geometry = ensureOutwardGeometryWinding(proceduralMeshToThreeGeometry(mesh)); centerGeometryOnBounds(geometry); return geometry; } function buildBreachGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const roundness = Math.max(0.003, Math.min(width, height, length) * 0.12); const smoothness = Math.max(0.004, roundness * 2.1); let sdf: SdfFn | null = null; let min = vec3(Infinity, Infinity, Infinity); let max = vec3(-Infinity, -Infinity, -Infinity); const expandBounds = (half: Vec3Like, offset: Vec3Like, extra = roundness * 1.25): void => { min = vec3( Math.min(min.x, offset.x - half.x - extra), Math.min(min.y, offset.y - half.y - extra), Math.min(min.z, offset.z - half.z - extra) ); max = vec3( Math.max(max.x, offset.x + half.x + extra), Math.max(max.y, offset.y + half.y + extra), Math.max(max.z, offset.z + half.z + extra) ); }; const addRoundedMass = ( half: Vec3Like, offset: Vec3Like, localRoundness = roundness, localSmoothness = smoothness ): void => { const mass = translate(sdfRoundedBox(half, localRoundness), offset); expandBounds(half, offset, localRoundness); sdf = sdf ? smoothUnion(sdf, mass, localSmoothness) : mass; }; addRoundedMass( { x: width * 0.4, y: height * 0.22, z: length * 0.28 }, vec3(0, height * 0.01, -length * 0.06) ); addRoundedMass( { x: width * 0.24, y: height * 0.15, z: length * 0.14 }, vec3(0, height * 0.16, -length * 0.04), roundness * 0.8, smoothness * 0.8 ); addRoundedMass( { x: width * 0.28, y: height * 0.16, z: length * 0.16 }, vec3(0, height * 0.03, length * 0.24), roundness * 0.85, smoothness * 0.8 ); addRoundedMass( { x: width * 0.3, y: height * 0.18, z: length * 0.14 }, vec3(0, height * 0.02, -length * 0.22), roundness * 0.7, smoothness * 0.7 ); addRoundedMass( { x: width * 0.2, y: height * 0.08, z: length * 0.22 }, vec3(0, height * 0.18, -length * 0.02), roundness * 0.6, smoothness * 0.6 ); addRoundedMass( { x: width * 0.36, y: height * 0.12, z: length * 0.2 }, vec3(0, height * 0.06, length * 0.1), roundness * 0.7, smoothness * 0.7 ); addRoundedMass( { x: width * 0.22, y: height * 0.1, z: length * 0.18 }, vec3(0, height * 0.14, -length * 0.18), roundness * 0.6, smoothness * 0.6 ); addRoundedMass( { x: width * 0.24, y: height * 0.08, z: length * 0.2 }, vec3(0, height * 0.22, 0), roundness * 0.55, smoothness * 0.6 ); addRoundedMass( { x: width * 0.18, y: height * 0.12, z: length * 0.14 }, vec3(0, height * 0.06, -length * 0.3), roundness * 0.6, smoothness * 0.6 ); const throatRelief = translate( sdfRoundedBox( { x: width * 0.18, y: height * 0.1, z: length * 0.1 }, roundness * 0.65 ), vec3(0, -height * 0.05, length * 0.14) ); const sideRelief = translate( sdfRoundedBox( { x: width * 0.12, y: height * 0.12, z: length * 0.18 }, roundness * 0.55 ), vec3(width * 0.18, height * 0.01, -length * 0.02) ); const latchRelief = translate( sdfRoundedBox( { x: width * 0.12, y: height * 0.08, z: length * 0.12 }, roundness * 0.5 ), vec3(0, -height * 0.08, -length * 0.18) ); const topSlot = translate( sdfRoundedBox( { x: width * 0.12, y: height * 0.06, z: length * 0.22 }, roundness * 0.45 ), vec3(0, height * 0.14, length * 0.02) ); const undersideRelief = translate( sdfRoundedBox( { x: width * 0.2, y: height * 0.08, z: length * 0.2 }, roundness * 0.55 ), vec3(0, -height * 0.16, length * 0.04) ); const rearSlot = translate( sdfRoundedBox( { x: width * 0.14, y: height * 0.08, z: length * 0.16 }, roundness * 0.5 ), vec3(0, height * 0.02, -length * 0.22) ); if (sdf) { sdf = subtract(sdf, throatRelief); sdf = subtract(sdf, sideRelief); sdf = subtract(sdf, latchRelief); sdf = subtract(sdf, topSlot); sdf = subtract(sdf, undersideRelief); sdf = subtract(sdf, rearSlot); } if (!sdf) { return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.18], [0.05, 0.46], [0.14, 0.7], [0.3, 0.82], [0.72, 0.82], [0.9, 0.62], [1.0, 0.28], [1.0, 0.14], [0.86, 0.08], [0.36, 0.06], [0.08, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.062, bevelThicknessScale: 0.056 } ); } const resolution = lowLod ? 26 : 64; const mesh = tryMeshSdf(sdf, { min, max }, { resolution, minAxisResolution: lowLod ? 12 : 26, surfaceThreshold: 5e-6, materialSlot: MaterialSlot.TEAM_PRIMARY }); if (!mesh) { const fallback = new THREE.BoxGeometry(width, height, length); centerGeometryOnBounds(fallback); return fallback; } const geometry = ensureOutwardGeometryWinding(proceduralMeshToThreeGeometry(mesh)); centerGeometryOnBounds(geometry); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); return geometry; } function buildRecoilSpringGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry { const radius = Math.max(0.01, size.x ?? 0.05); const length = Math.max(0.01, size.z ?? size.y ?? radius); const radialSegments = lowLod ? 10 : 20; const lengthSegments = lowLod ? 10 : 30; const geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, radialSegments, lengthSegments, false); geometry.rotateX(Math.PI * 0.5); const position = geometry.getAttribute('position'); for (let index = 0; index < position.count; index += 1) { const x = position.getX(index); const y = position.getY(index); const z = position.getZ(index); const t = THREE.MathUtils.clamp(z + 0.5, 0, 1); const endFade = Math.min( THREE.MathUtils.smoothstep(t, 0.06, 0.14), 1 - THREE.MathUtils.smoothstep(t, 0.86, 0.94) ); const coil = Math.sin(t * Math.PI * 2 * 5.5) * 0.12 * endFade; const radiusScale = 0.56 + coil; position.setXYZ(index, x * radiusScale, y * radiusScale, z); } geometry.scale(radius, radius, length); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); return geometry; } function buildReceiverLowerGeometry( size: PartSize, lowLod: boolean, options?: { shelfScale?: number; tailScale?: number; } ): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const roundness = Math.max(0.003, Math.min(width, height, length) * 0.12); const smoothness = Math.max(0.004, roundness * 2.1); const shelfScale = options?.shelfScale ?? 1; const tailScale = options?.tailScale ?? 1; let sdf: SdfFn | null = null; let min = vec3(Infinity, Infinity, Infinity); let max = vec3(-Infinity, -Infinity, -Infinity); const expandBounds = (half: Vec3Like, offset: Vec3Like, extra = roundness * 1.25): void => { min = vec3( Math.min(min.x, offset.x - half.x - extra), Math.min(min.y, offset.y - half.y - extra), Math.min(min.z, offset.z - half.z - extra) ); max = vec3( Math.max(max.x, offset.x + half.x + extra), Math.max(max.y, offset.y + half.y + extra), Math.max(max.z, offset.z + half.z + extra) ); }; const addRoundedMass = ( half: Vec3Like, offset: Vec3Like, localRoundness = roundness, localSmoothness = smoothness ): void => { const mass = translate(sdfRoundedBox(half, localRoundness), offset); expandBounds(half, offset, localRoundness); sdf = sdf ? smoothUnion(sdf, mass, localSmoothness) : mass; }; addRoundedMass( { x: width * 0.42, y: height * 0.18, z: length * 0.3 }, vec3(0, -height * 0.02, length * 0.02) ); addRoundedMass( { x: width * 0.34, y: height * 0.12 * shelfScale, z: length * 0.24 }, vec3(0, -height * 0.16 * shelfScale, length * 0.05), roundness * 0.8, smoothness * 0.75 ); addRoundedMass( { x: width * 0.22, y: height * 0.16 * tailScale, z: length * 0.16 }, vec3(0, -height * 0.01, -length * 0.26 * tailScale), roundness * 0.8, smoothness * 0.75 ); addRoundedMass( { x: width * 0.28, y: height * 0.12, z: length * 0.18 }, vec3(0, -height * 0.18, length * 0.18), roundness * 0.7, smoothness * 0.7 ); addRoundedMass( { x: width * 0.2, y: height * 0.1, z: length * 0.12 }, vec3(0, height * 0.02, -length * 0.3), roundness * 0.7, smoothness * 0.7 ); addRoundedMass( { x: width * 0.36, y: height * 0.08, z: length * 0.28 }, vec3(0, height * 0.08, length * 0.02), roundness * 0.6, smoothness * 0.6 ); addRoundedMass( { x: width * 0.18, y: height * 0.1, z: length * 0.1 }, vec3(0, height * 0.02, -length * 0.36), roundness * 0.6, smoothness * 0.6 ); const throatRelief = translate( sdfRoundedBox( { x: width * 0.18, y: height * 0.12, z: length * 0.14 }, roundness * 0.65 ), vec3(0, -height * 0.09, length * 0.18) ); const bellyRelief = translate( sdfRoundedBox( { x: width * 0.18, y: height * 0.1, z: length * 0.18 }, roundness * 0.6 ), vec3(0, -height * 0.2, -length * 0.02) ); const triggerRelief = translate( sdfRoundedBox( { x: width * 0.16, y: height * 0.08, z: length * 0.12 }, roundness * 0.5 ), vec3(0, -height * 0.24, length * 0.06) ); const magwellRelief = translate( sdfRoundedBox( { x: width * 0.14, y: height * 0.12, z: length * 0.12 }, roundness * 0.45 ), vec3(0, -height * 0.22, length * 0.18) ); const sideCut = translate( sdfRoundedBox( { x: width * 0.08, y: height * 0.1, z: length * 0.18 }, roundness * 0.45 ), vec3(width * 0.2, -height * 0.02, length * 0.02) ); if (sdf) { sdf = subtract(sdf, throatRelief); sdf = subtract(sdf, bellyRelief); sdf = subtract(sdf, triggerRelief); sdf = subtract(sdf, magwellRelief); sdf = subtract(sdf, sideCut); } if (!sdf) { const geometry = buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.26], [0.06, 0.56], [0.22, 0.68], [0.54, 0.68], [0.8, 0.58], [1.0, 0.42], [1.0, 0.18], [0.82, 0.12], [0.3, 0.1], [0.06, 0.14], [0.0, 0.18] ], lowLod, { bevelScale: 0.07, bevelThicknessScale: 0.06 } ); centerGeometryOnBounds(geometry); return geometry; } const resolution = lowLod ? 30 : 68; const mesh = tryMeshSdf(sdf, { min, max }, { resolution, minAxisResolution: lowLod ? 12 : 28, surfaceThreshold: 5e-6, materialSlot: MaterialSlot.TEAM_PRIMARY }); if (!mesh) { const fallback = new THREE.BoxGeometry(width, height, length); centerGeometryOnBounds(fallback); return makeGeometryFlatShaded(fallback); } const geometry = ensureOutwardGeometryWinding(proceduralMeshToThreeGeometry(mesh)); centerGeometryOnBounds(geometry); return makeGeometryFlatShaded(geometry); } function buildRailGeometry( size: PartSize, lowLod: boolean, options?: { toothScale?: number; seatScale?: number; } ): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const roundness = Math.max(0.0025, Math.min(width, height, length) * 0.09); const smoothness = Math.max(0.0035, roundness * 1.9); const toothScale = options?.toothScale ?? 1; const seatScale = options?.seatScale ?? 1; let sdf: SdfFn | null = null; let min = vec3(Infinity, Infinity, Infinity); let max = vec3(-Infinity, -Infinity, -Infinity); const expandBounds = (half: Vec3Like, offset: Vec3Like, extra = roundness * 1.25): void => { min = vec3( Math.min(min.x, offset.x - half.x - extra), Math.min(min.y, offset.y - half.y - extra), Math.min(min.z, offset.z - half.z - extra) ); max = vec3( Math.max(max.x, offset.x + half.x + extra), Math.max(max.y, offset.y + half.y + extra), Math.max(max.z, offset.z + half.z + extra) ); }; const addRoundedMass = ( half: Vec3Like, offset: Vec3Like, localRoundness = roundness, localSmoothness = smoothness ): void => { const mass = translate(sdfRoundedBox(half, localRoundness), offset); expandBounds(half, offset, localRoundness); sdf = sdf ? smoothUnion(sdf, mass, localSmoothness) : mass; }; addRoundedMass( { x: width * 0.42, y: height * 0.12, z: length * 0.42 }, vec3(0, 0, 0) ); addRoundedMass( { x: width * 0.26, y: height * 0.08 * toothScale, z: length * 0.34 }, vec3(0, height * 0.11 * toothScale, 0), roundness * 0.75, smoothness * 0.75 ); addRoundedMass( { x: width * 0.18, y: height * 0.08 * seatScale, z: length * 0.26 }, vec3(0, -height * 0.08 * seatScale, -length * 0.02), roundness * 0.7, smoothness * 0.7 ); const undersideChannel = translate( sdfRoundedBox( { x: width * 0.14, y: height * 0.07, z: length * 0.28 }, roundness * 0.55 ), vec3(0, -height * 0.08, 0) ); if (sdf) { sdf = subtract(sdf, undersideChannel); } if (!sdf) { return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.2], [0.04, 0.56], [0.12, 0.68], [0.88, 0.68], [0.96, 0.56], [1.0, 0.2], [1.0, 0.1], [0.86, 0.06], [0.14, 0.06], [0.0, 0.1] ], lowLod, { bevelScale: 0.05, bevelThicknessScale: 0.045 } ); } const resolution = lowLod ? 24 : 44; const mesh = tryMeshSdf(sdf, { min, max }, { resolution, materialSlot: MaterialSlot.TEAM_PRIMARY }); if (!mesh) { return makeGeometryFlatShaded(new THREE.BoxGeometry(width, height, length)); } return makeGeometryFlatShaded( ensureOutwardGeometryWinding(proceduralMeshToThreeGeometry(mesh)) ); } function buildJacketGeometry( size: PartSize, lowLod: boolean, options?: { bridgeScale?: number; tailScale?: number; } ): THREE.BufferGeometry { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); const roundness = Math.max(0.003, Math.min(width, height, length) * 0.1); const smoothness = Math.max(0.004, roundness * 2.0); const bridgeScale = options?.bridgeScale ?? 1; const tailScale = options?.tailScale ?? 1; let sdf: SdfFn | null = null; let min = vec3(Infinity, Infinity, Infinity); let max = vec3(-Infinity, -Infinity, -Infinity); const expandBounds = (half: Vec3Like, offset: Vec3Like, extra = roundness * 1.25): void => { min = vec3( Math.min(min.x, offset.x - half.x - extra), Math.min(min.y, offset.y - half.y - extra), Math.min(min.z, offset.z - half.z - extra) ); max = vec3( Math.max(max.x, offset.x + half.x + extra), Math.max(max.y, offset.y + half.y + extra), Math.max(max.z, offset.z + half.z + extra) ); }; const addRoundedMass = ( half: Vec3Like, offset: Vec3Like, localRoundness = roundness, localSmoothness = smoothness ): void => { const mass = translate(sdfRoundedBox(half, localRoundness), offset); expandBounds(half, offset, localRoundness); sdf = sdf ? smoothUnion(sdf, mass, localSmoothness) : mass; }; addRoundedMass( { x: width * 0.4, y: height * 0.18, z: length * 0.3 }, vec3(0, 0, 0) ); addRoundedMass( { x: width * 0.24, y: height * 0.12 * bridgeScale, z: length * 0.22 }, vec3(0, height * 0.12 * bridgeScale, 0), roundness * 0.75, smoothness * 0.75 ); addRoundedMass( { x: width * 0.2, y: height * 0.14 * tailScale, z: length * 0.16 }, vec3(0, height * 0.01, -length * 0.24 * tailScale), roundness * 0.8, smoothness * 0.75 ); addRoundedMass( { x: width * 0.18, y: height * 0.08, z: length * 0.2 }, vec3(0, height * 0.18, length * 0.1), roundness * 0.7, smoothness * 0.7 ); addRoundedMass( { x: width * 0.16, y: height * 0.12, z: length * 0.18 }, vec3(0, height * 0.04, length * 0.24), roundness * 0.7, smoothness * 0.7 ); const undersideRelief = translate( sdfRoundedBox( { x: width * 0.16, y: height * 0.08, z: length * 0.22 }, roundness * 0.55 ), vec3(0, -height * 0.11, length * 0.04) ); const topChannel = translate( sdfRoundedBox( { x: width * 0.12, y: height * 0.05, z: length * 0.22 }, roundness * 0.5 ), vec3(0, height * 0.14, 0) ); const frontRelief = translate( sdfRoundedBox( { x: width * 0.16, y: height * 0.08, z: length * 0.14 }, roundness * 0.5 ), vec3(0, height * 0.02, length * 0.26) ); if (sdf) { sdf = subtract(sdf, undersideRelief); sdf = subtract(sdf, topChannel); sdf = subtract(sdf, frontRelief); } if (!sdf) { return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.16], [0.04, 0.42], [0.12, 0.62], [0.26, 0.74], [0.8, 0.74], [0.94, 0.54], [1.0, 0.22], [1.0, 0.1], [0.84, 0.06], [0.24, 0.06], [0.06, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.056, bevelThicknessScale: 0.05 } ); } const resolution = lowLod ? 24 : 48; const mesh = tryMeshSdf(sdf, { min, max }, { resolution, materialSlot: MaterialSlot.TEAM_PRIMARY }); if (!mesh) { return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.16], [0.04, 0.42], [0.12, 0.62], [0.26, 0.74], [0.8, 0.74], [0.94, 0.54], [1.0, 0.22], [1.0, 0.1], [0.84, 0.06], [0.24, 0.06], [0.06, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.056, bevelThicknessScale: 0.05 } ); } const geometry = makeGeometryFlatShaded( ensureOutwardGeometryWinding(proceduralMeshToThreeGeometry(mesh)) ); centerGeometryOnBounds(geometry); return geometry; } type WeaponCompositeMass = { half: Vec3Like; offset: Vec3Like; roundnessScale?: number; smoothnessScale?: number; }; type WeaponCompositeCut = { half: Vec3Like; offset: Vec3Like; roundnessScale?: number; }; function getWeaponPartDimensions(size: PartSize): { width: number; height: number; length: number } { const width = Math.max(0.01, size.x ?? 0.05); const height = Math.max(0.01, size.y ?? width); const length = Math.max(0.01, size.z ?? width); return { width, height, length }; } function buildRoundedWeaponCompositeGeometry( size: PartSize, lowLod: boolean, config: { roundnessScale: number; smoothnessScale: number; resolutionLow: number; resolutionHigh: number; masses: WeaponCompositeMass[]; cuts?: WeaponCompositeCut[]; } ): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const roundness = Math.max(0.0025, Math.min(width, height, length) * config.roundnessScale); const smoothness = Math.max(0.0035, roundness * config.smoothnessScale); let sdf: SdfFn | null = null; let min = vec3(Infinity, Infinity, Infinity); let max = vec3(-Infinity, -Infinity, -Infinity); const expandBounds = (half: Vec3Like, offset: Vec3Like, extra = roundness * 1.25): void => { min = vec3( Math.min(min.x, offset.x - half.x - extra), Math.min(min.y, offset.y - half.y - extra), Math.min(min.z, offset.z - half.z - extra) ); max = vec3( Math.max(max.x, offset.x + half.x + extra), Math.max(max.y, offset.y + half.y + extra), Math.max(max.z, offset.z + half.z + extra) ); }; for (const mass of config.masses) { const localRoundness = Math.max(0.0015, roundness * (mass.roundnessScale ?? 1)); const localSmoothness = Math.max(0.0025, smoothness * (mass.smoothnessScale ?? 1)); const shape = translate(sdfRoundedBox(mass.half, localRoundness), mass.offset); expandBounds(mass.half, mass.offset, localRoundness); sdf = sdf ? smoothUnion(sdf, shape, localSmoothness) : shape; } if (!sdf) { return null; } for (const cut of config.cuts ?? []) { const localRoundness = Math.max(0.0015, roundness * (cut.roundnessScale ?? 0.7)); const shape = translate(sdfRoundedBox(cut.half, localRoundness), cut.offset); sdf = subtract(sdf, shape); } const mesh = tryMeshSdf( sdf, { min, max }, { resolution: lowLod ? config.resolutionLow : config.resolutionHigh, materialSlot: MaterialSlot.TEAM_PRIMARY } ); if (!mesh) { return null; } const geometry = ensureOutwardGeometryWinding(proceduralMeshToThreeGeometry(mesh)); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); return geometry; } function buildCradleGeometry( size: PartSize, lowLod: boolean, options?: { yokeScale?: number; noseScale?: number } ): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const yokeScale = options?.yokeScale ?? 1; const noseScale = options?.noseScale ?? 1; return buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.1, smoothnessScale: 1.9, resolutionLow: 24, resolutionHigh: 48, masses: [ { half: { x: width * 0.34, y: height * 0.12, z: length * 0.38 }, offset: vec3(0, -height * 0.04, 0) }, { half: { x: width * 0.22, y: height * 0.2 * yokeScale, z: length * 0.16 }, offset: vec3(0, height * 0.12 * yokeScale, -length * 0.18), roundnessScale: 0.8, smoothnessScale: 0.8 }, { half: { x: width * 0.16, y: height * 0.16 * noseScale, z: length * 0.14 }, offset: vec3(0, height * 0.08 * noseScale, length * 0.24), roundnessScale: 0.8, smoothnessScale: 0.8 } ], cuts: [ { half: { x: width * 0.14, y: height * 0.08, z: length * 0.24 }, offset: vec3(0, -height * 0.08, 0) }, { half: { x: width * 0.12, y: height * 0.08, z: length * 0.12 }, offset: vec3(0, height * 0.04, length * 0.08) } ] }); } function buildMagwellGeometry( size: PartSize, lowLod: boolean, options?: { funnelScale?: number; heelScale?: number } ): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const funnelScale = options?.funnelScale ?? 1; const heelScale = options?.heelScale ?? 1; return buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.095, smoothnessScale: 2.0, resolutionLow: 24, resolutionHigh: 48, masses: [ { half: { x: width * 0.34, y: height * 0.42, z: length * 0.26 }, offset: vec3(0, -height * 0.02, 0) }, { half: { x: width * 0.24, y: height * 0.14 * funnelScale, z: length * 0.16 }, offset: vec3(0, height * 0.22 * funnelScale, length * 0.1), roundnessScale: 0.78, smoothnessScale: 0.75 }, { half: { x: width * 0.2, y: height * 0.14 * heelScale, z: length * 0.12 }, offset: vec3(0, -height * 0.16 * heelScale, -length * 0.18), roundnessScale: 0.75, smoothnessScale: 0.72 } ], cuts: [ { half: { x: width * 0.16, y: height * 0.34, z: length * 0.14 }, offset: vec3(0, -height * 0.02, length * 0.06) }, { half: { x: width * 0.12, y: height * 0.16, z: length * 0.12 }, offset: vec3(0, -height * 0.18, -length * 0.04) } ] }); } function buildMagwellThroatGeometry( size: PartSize, lowLod: boolean, options?: { throatScale?: number } ): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const throatScale = options?.throatScale ?? 1; return buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.09, smoothnessScale: 1.9, resolutionLow: 22, resolutionHigh: 44, masses: [ { half: { x: width * 0.3, y: height * 0.32, z: length * 0.24 }, offset: vec3(0, -height * 0.02, 0) }, { half: { x: width * 0.2, y: height * 0.11 * throatScale, z: length * 0.14 }, offset: vec3(0, height * 0.16 * throatScale, length * 0.08), roundnessScale: 0.76, smoothnessScale: 0.74 } ], cuts: [ { half: { x: width * 0.14, y: height * 0.24, z: length * 0.12 }, offset: vec3(0, -height * 0.01, length * 0.05) } ] }); } function buildOpticClampGeometry( size: PartSize, lowLod: boolean, options?: { jawScale?: number } ): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const jawScale = options?.jawScale ?? 1; return buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.085, smoothnessScale: 1.9, resolutionLow: 22, resolutionHigh: 42, masses: [ { half: { x: width * 0.34, y: height * 0.14, z: length * 0.28 }, offset: vec3(0, -height * 0.08, 0) }, { half: { x: width * 0.28, y: height * 0.16 * jawScale, z: length * 0.24 }, offset: vec3(0, height * 0.08 * jawScale, 0), roundnessScale: 0.75, smoothnessScale: 0.76 }, { half: { x: width * 0.18, y: height * 0.14 * jawScale, z: length * 0.16 }, offset: vec3(0, height * 0.16 * jawScale, 0), roundnessScale: 0.72, smoothnessScale: 0.72 } ], cuts: [ { half: { x: width * 0.14, y: height * 0.18, z: length * 0.18 }, offset: vec3(0, height * 0.06, 0) }, { half: { x: width * 0.12, y: height * 0.08, z: length * 0.16 }, offset: vec3(0, -height * 0.12, 0) } ] }); } function buildOpticStanchionGeometry( size: PartSize, lowLod: boolean, options?: { webScale?: number; capScale?: number } ): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const webScale = options?.webScale ?? 1; const capScale = options?.capScale ?? 1; return buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.08, smoothnessScale: 1.8, resolutionLow: 20, resolutionHigh: 40, masses: [ { half: { x: width * 0.28, y: height * 0.12, z: length * 0.24 }, offset: vec3(0, -height * 0.16, 0) }, { half: { x: width * 0.12, y: height * 0.28 * webScale, z: length * 0.18 }, offset: vec3(0, height * 0.02, 0), roundnessScale: 0.72, smoothnessScale: 0.7 }, { half: { x: width * 0.2, y: height * 0.1 * capScale, z: length * 0.2 }, offset: vec3(0, height * 0.22 * capScale, 0), roundnessScale: 0.7, smoothnessScale: 0.7 } ], cuts: [ { half: { x: width * 0.08, y: height * 0.18, z: length * 0.12 }, offset: vec3(0, height * 0.02, 0) } ] }); } function buildShoulderPadGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const geometry = buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.11, smoothnessScale: 1.95, resolutionLow: 24, resolutionHigh: 52, masses: [ { half: { x: width * 0.36, y: height * 0.3, z: length * 0.22 }, offset: vec3(0, 0, -length * 0.04) }, { half: { x: width * 0.26, y: height * 0.18, z: length * 0.16 }, offset: vec3(0, height * 0.11, length * 0.12), roundnessScale: 0.8, smoothnessScale: 0.78 }, { half: { x: width * 0.2, y: height * 0.12, z: length * 0.12 }, offset: vec3(0, -height * 0.12, length * 0.08), roundnessScale: 0.74, smoothnessScale: 0.72 }, { half: { x: width * 0.2, y: height * 0.1, z: length * 0.1 }, offset: vec3(0, height * 0.18, -length * 0.12), roundnessScale: 0.7, smoothnessScale: 0.7 } ], cuts: [ { half: { x: width * 0.14, y: height * 0.12, z: length * 0.12 }, offset: vec3(0, -height * 0.02, length * 0.18) }, { half: { x: width * 0.16, y: height * 0.08, z: length * 0.18 }, offset: vec3(0, height * 0.08, -length * 0.02), roundnessScale: 0.65 } ] }); if (!geometry) return null; centerGeometryOnBounds(geometry); return geometry; } function buildStockBridgeGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const geometry = buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.085, smoothnessScale: 1.85, resolutionLow: 22, resolutionHigh: 46, masses: [ { half: { x: width * 0.28, y: height * 0.14, z: length * 0.34 }, offset: vec3(0, 0, 0) }, { half: { x: width * 0.18, y: height * 0.1, z: length * 0.24 }, offset: vec3(0, height * 0.12, -length * 0.06), roundnessScale: 0.74, smoothnessScale: 0.74 }, { half: { x: width * 0.14, y: height * 0.1, z: length * 0.18 }, offset: vec3(0, -height * 0.1, length * 0.12), roundnessScale: 0.72, smoothnessScale: 0.7 }, { half: { x: width * 0.16, y: height * 0.08, z: length * 0.2 }, offset: vec3(0, height * 0.16, length * 0.04), roundnessScale: 0.7, smoothnessScale: 0.7 } ], cuts: [ { half: { x: width * 0.12, y: height * 0.08, z: length * 0.18 }, offset: vec3(0, -height * 0.12, -length * 0.04) }, { half: { x: width * 0.1, y: height * 0.06, z: length * 0.14 }, offset: vec3(0, height * 0.02, length * 0.18), roundnessScale: 0.65 } ] }); if (!geometry) return null; centerGeometryOnBounds(geometry); return geometry; } function buildActionCoverGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const geometry = buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.08, smoothnessScale: 1.85, resolutionLow: 22, resolutionHigh: 48, masses: [ { half: { x: width * 0.3, y: height * 0.22, z: length * 0.3 }, offset: vec3(0, height * 0.02, 0) }, { half: { x: width * 0.2, y: height * 0.12, z: length * 0.22 }, offset: vec3(0, height * 0.18, -length * 0.02), roundnessScale: 0.72, smoothnessScale: 0.74 }, { half: { x: width * 0.16, y: height * 0.14, z: length * 0.12 }, offset: vec3(width * 0.06, 0, length * 0.16), roundnessScale: 0.72, smoothnessScale: 0.72 }, { half: { x: width * 0.14, y: height * 0.08, z: length * 0.18 }, offset: vec3(0, height * 0.24, length * 0.04), roundnessScale: 0.68, smoothnessScale: 0.7 }, { half: { x: width * 0.18, y: height * 0.08, z: length * 0.14 }, offset: vec3(0, height * 0.08, -length * 0.22), roundnessScale: 0.7, smoothnessScale: 0.7 }, { half: { x: width * 0.12, y: height * 0.1, z: length * 0.16 }, offset: vec3(width * 0.12, height * 0.06, length * 0.02), roundnessScale: 0.66, smoothnessScale: 0.68 }, { half: { x: width * 0.12, y: height * 0.06, z: length * 0.14 }, offset: vec3(0, height * 0.26, -length * 0.16), roundnessScale: 0.64, smoothnessScale: 0.66 } ], cuts: [ { half: { x: width * 0.14, y: height * 0.12, z: length * 0.18 }, offset: vec3(0, -height * 0.12, length * 0.08) }, { half: { x: width * 0.12, y: height * 0.08, z: length * 0.1 }, offset: vec3(-width * 0.04, height * 0.04, -length * 0.18) }, { half: { x: width * 0.1, y: height * 0.06, z: length * 0.14 }, offset: vec3(width * 0.06, height * 0.02, -length * 0.02), roundnessScale: 0.65 }, { half: { x: width * 0.08, y: height * 0.06, z: length * 0.12 }, offset: vec3(width * 0.16, height * 0.02, length * 0.08), roundnessScale: 0.6 }, { half: { x: width * 0.1, y: height * 0.05, z: length * 0.16 }, offset: vec3(0, height * 0.2, length * 0.18), roundnessScale: 0.6 } ] }); if (!geometry) return null; centerGeometryOnBounds(geometry); return geometry; } function buildActionSpineGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const geometry = buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.075, smoothnessScale: 1.8, resolutionLow: 20, resolutionHigh: 46, masses: [ { half: { x: width * 0.22, y: height * 0.12, z: length * 0.34 }, offset: vec3(0, height * 0.08, 0) }, { half: { x: width * 0.12, y: height * 0.08, z: length * 0.18 }, offset: vec3(0, height * 0.18, -length * 0.18), roundnessScale: 0.7, smoothnessScale: 0.72 }, { half: { x: width * 0.1, y: height * 0.08, z: length * 0.16 }, offset: vec3(0, height * 0.12, length * 0.18), roundnessScale: 0.68, smoothnessScale: 0.7 }, { half: { x: width * 0.12, y: height * 0.06, z: length * 0.24 }, offset: vec3(0, height * 0.2, 0), roundnessScale: 0.65, smoothnessScale: 0.68 } ], cuts: [ { half: { x: width * 0.08, y: height * 0.06, z: length * 0.2 }, offset: vec3(0, 0, 0) }, { half: { x: width * 0.1, y: height * 0.05, z: length * 0.16 }, offset: vec3(0, height * 0.06, -length * 0.22), roundnessScale: 0.65 } ] }); if (!geometry) return null; centerGeometryOnBounds(geometry); return geometry; } function buildGasBlockGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); return buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.08, smoothnessScale: 1.85, resolutionLow: 20, resolutionHigh: 40, masses: [ { half: { x: width * 0.22, y: height * 0.22, z: length * 0.22 }, offset: vec3(0, 0, 0) }, { half: { x: width * 0.12, y: height * 0.22, z: length * 0.12 }, offset: vec3(0, height * 0.22, -length * 0.02), roundnessScale: 0.72, smoothnessScale: 0.72 }, { half: { x: width * 0.16, y: height * 0.08, z: length * 0.12 }, offset: vec3(0, -height * 0.16, length * 0.04), roundnessScale: 0.7, smoothnessScale: 0.7 } ], cuts: [ { half: { x: width * 0.08, y: height * 0.1, z: length * 0.16 }, offset: vec3(0, -height * 0.04, 0) } ] }); } function buildGasBlockSaddleGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); return buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.075, smoothnessScale: 1.8, resolutionLow: 18, resolutionHigh: 36, masses: [ { half: { x: width * 0.24, y: height * 0.12, z: length * 0.24 }, offset: vec3(0, -height * 0.04, 0) }, { half: { x: width * 0.16, y: height * 0.08, z: length * 0.16 }, offset: vec3(0, height * 0.04, 0), roundnessScale: 0.72, smoothnessScale: 0.72 } ], cuts: [ { half: { x: width * 0.1, y: height * 0.08, z: length * 0.16 }, offset: vec3(0, 0, 0) } ] }); } function buildBoltHousingGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const geometry = buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.085, smoothnessScale: 1.9, resolutionLow: 22, resolutionHigh: 48, masses: [ { half: { x: width * 0.28, y: height * 0.24, z: length * 0.28 }, offset: vec3(0, height * 0.01, 0) }, { half: { x: width * 0.16, y: height * 0.14, z: length * 0.18 }, offset: vec3(width * 0.04, height * 0.12, -length * 0.12), roundnessScale: 0.74, smoothnessScale: 0.74 }, { half: { x: width * 0.14, y: height * 0.1, z: length * 0.14 }, offset: vec3(0, -height * 0.12, length * 0.02), roundnessScale: 0.7, smoothnessScale: 0.7 }, { half: { x: width * 0.12, y: height * 0.08, z: length * 0.18 }, offset: vec3(0, height * 0.2, length * 0.02), roundnessScale: 0.68, smoothnessScale: 0.68 } ], cuts: [ { half: { x: width * 0.12, y: height * 0.1, z: length * 0.14 }, offset: vec3(-width * 0.04, -height * 0.02, length * 0.08) }, { half: { x: width * 0.1, y: height * 0.06, z: length * 0.2 }, offset: vec3(0, height * 0.04, -length * 0.16), roundnessScale: 0.65 } ] }); if (!geometry) return null; centerGeometryOnBounds(geometry); return geometry; } function buildMagBraceGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const geometry = buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.075, smoothnessScale: 1.75, resolutionLow: 18, resolutionHigh: 40, masses: [ { half: { x: width * 0.22, y: height * 0.12, z: length * 0.28 }, offset: vec3(0, -height * 0.04, 0) }, { half: { x: width * 0.14, y: height * 0.12, z: length * 0.12 }, offset: vec3(0, height * 0.08, length * 0.1), roundnessScale: 0.72, smoothnessScale: 0.72 }, { half: { x: width * 0.12, y: height * 0.08, z: length * 0.16 }, offset: vec3(0, height * 0.14, -length * 0.1), roundnessScale: 0.7, smoothnessScale: 0.7 } ], cuts: [ { half: { x: width * 0.1, y: height * 0.06, z: length * 0.14 }, offset: vec3(0, -height * 0.08, -length * 0.04) }, { half: { x: width * 0.08, y: height * 0.04, z: length * 0.1 }, offset: vec3(0, height * 0.04, length * 0.18), roundnessScale: 0.65 } ] }); if (!geometry) return null; centerGeometryOnBounds(geometry); return geometry; } function buildMagLatchGeometry(size: PartSize, lowLod: boolean): THREE.BufferGeometry | null { const { width, height, length } = getWeaponPartDimensions(size); const geometry = buildRoundedWeaponCompositeGeometry(size, lowLod, { roundnessScale: 0.072, smoothnessScale: 1.7, resolutionLow: 18, resolutionHigh: 38, masses: [ { half: { x: width * 0.2, y: height * 0.12, z: length * 0.22 }, offset: vec3(0, 0, 0) }, { half: { x: width * 0.12, y: height * 0.08, z: length * 0.12 }, offset: vec3(0, -height * 0.06, length * 0.12), roundnessScale: 0.7, smoothnessScale: 0.7 }, { half: { x: width * 0.1, y: height * 0.06, z: length * 0.14 }, offset: vec3(0, height * 0.08, -length * 0.08), roundnessScale: 0.68, smoothnessScale: 0.68 } ], cuts: [ { half: { x: width * 0.08, y: height * 0.04, z: length * 0.1 }, offset: vec3(0, height * 0.04, -length * 0.04) }, { half: { x: width * 0.06, y: height * 0.04, z: length * 0.08 }, offset: vec3(0, -height * 0.02, length * 0.18), roundnessScale: 0.65 } ] }); if (!geometry) return null; centerGeometryOnBounds(geometry); return geometry; } function buildPlannedWeaponProfileGeometry( profile: WeaponPlannedProfile, size: PartSize, lowLod: boolean, extrusionOverride?: NonNullable['plannedWeaponExtrusion'] ): THREE.BufferGeometry | null { if (extrusionOverride) { return buildExtrudedWeaponProfileGeometry( size, extrusionOverride.points.map((point: { u: number; v: number }) => [point.u, point.v]), lowLod, { bevelScale: extrusionOverride.bevelScale, bevelThicknessScale: extrusionOverride.bevelThicknessScale } ); } switch (profile) { case 'precision_cradle': return buildCradleGeometry(size, lowLod); case 'precision_receiver_upper': return buildReceiverUpperGeometry(size, lowLod); case 'precision_receiver_lower': return buildReceiverLowerGeometry(size, lowLod); case 'precision_stock': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.18], [0.02, 0.58], [0.08, 0.82], [0.18, 0.96], [0.34, 0.9], [0.5, 0.8], [0.66, 0.64], [0.82, 0.46], [1.0, 0.3], [1.0, 0.18], [0.82, 0.12], [0.56, 0.08], [0.24, 0.08], [0.06, 0.12], [0.0, 0.14] ], lowLod, { bevelScale: 0.08, bevelThicknessScale: 0.07 } ); case 'precision_rail': return buildRailGeometry(size, lowLod); case 'precision_magwell': return buildMagwellGeometry(size, lowLod); case 'precision_magwell_throat': return buildMagwellThroatGeometry(size, lowLod); case 'box_magazine': return buildBoxMagazineGeometry(size, lowLod); case 'precision_shroud': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.1], [0.04, 0.28], [0.12, 0.58], [0.22, 0.82], [0.42, 0.9], [0.72, 0.86], [0.9, 0.62], [1.0, 0.24], [1.0, 0.1], [0.86, 0.04], [0.44, 0.03], [0.1, 0.05], [0.0, 0.08] ], lowLod, { bevelScale: 0.06, bevelThicknessScale: 0.052 } ); case 'precision_cooling_jacket': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.08], [0.04, 0.22], [0.1, 0.46], [0.18, 0.7], [0.36, 0.82], [0.68, 0.8], [0.86, 0.58], [0.98, 0.22], [1.0, 0.1], [0.84, 0.04], [0.34, 0.03], [0.08, 0.05], [0.0, 0.06] ], lowLod, { bevelScale: 0.055, bevelThicknessScale: 0.048 } ); case 'precision_optic_tube': return buildScopeTubeGeometry(size, lowLod, { rearBellScale: 1.08, frontBellScale: 1.15, waistScale: 0.82, bodyScale: 0.9 }); case 'precision_optic_clamp': return buildOpticClampGeometry(size, lowLod); case 'skeletal_optic_stanchion': return buildOpticStanchionGeometry(size, lowLod); case 'marksman_cradle': return buildCradleGeometry(size, lowLod, { yokeScale: 1.08, noseScale: 1.04 }); case 'marksman_stock': return buildMarksmanStockGeometry(size, lowLod); case 'marksman_rail': return buildRailGeometry(size, lowLod, { toothScale: 1.08, seatScale: 1.06 }); case 'marksman_magwell': return buildMagwellGeometry(size, lowLod, { funnelScale: 1.08, heelScale: 1.04 }); case 'marksman_magwell_throat': return buildMagwellThroatGeometry(size, lowLod, { throatScale: 1.05 }); case 'marksman_magazine': return buildBoxMagazineGeometry(size, lowLod, { feedScale: 1.05, toeScale: 1.04 }); case 'marksman_shroud': return buildRevolvedWeaponProfileGeometry( size, { radialSegments: lowLod ? 8 : 14, rearRadius: 1.08, frontRadius: 0.9 } ); case 'marksman_cooling_jacket': return buildRevolvedWeaponProfileGeometry( size, { radialSegments: lowLod ? 8 : 12, rearRadius: 1.0, frontRadius: 0.86 } ); case 'marksman_optic_clamp': return buildOpticClampGeometry(size, lowLod, { jawScale: 1.08 }); case 'marksman_optic_stanchion': return buildOpticStanchionGeometry(size, lowLod, { webScale: 1.08, capScale: 1.05 }); case 'marksman_shoulder_pad': return buildShoulderPadGeometry(size, lowLod); case 'marksman_stock_bridge': return buildStockBridgeGeometry(size, lowLod); case 'marksman_action_cover': return buildActionCoverGeometry(size, lowLod); case 'marksman_action_spine': return buildActionSpineGeometry(size, lowLod); case 'marksman_breach': return buildBreachGeometry(size, lowLod); case 'marksman_receiver_upper': return buildReceiverUpperGeometry(size, lowLod, { deckScale: 1.08, noseScale: 1.04, tailScale: 1.04 }); case 'marksman_receiver_lower': return buildReceiverLowerGeometry(size, lowLod, { shelfScale: 1.08, tailScale: 1.04 }); case 'marksman_jacket': return buildJacketGeometry(size, lowLod, { bridgeScale: 1.06, tailScale: 1.04 }); case 'marksman_barrel': return buildRevolvedWeaponProfileGeometry( size, { radialSegments: lowLod ? 10 : 18, rearRadius: 1, frontRadius: 0.96 } ); case 'marksman_mount_block': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.14], [0.06, 0.5], [0.2, 0.74], [0.8, 0.74], [0.94, 0.46], [1.0, 0.16], [1.0, 0.08], [0.8, 0.04], [0.18, 0.04], [0.0, 0.06] ], lowLod, { bevelScale: 0.055, bevelThicknessScale: 0.05 } ); case 'marksman_bridge': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.1], [0.08, 0.42], [0.24, 0.68], [0.76, 0.68], [0.92, 0.42], [1.0, 0.12], [1.0, 0.06], [0.82, 0.04], [0.18, 0.04], [0.0, 0.06] ], lowLod, { bevelScale: 0.05, bevelThicknessScale: 0.045 } ); case 'marksman_saddle': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.16], [0.06, 0.46], [0.18, 0.68], [0.78, 0.7], [0.94, 0.44], [1.0, 0.18], [1.0, 0.08], [0.8, 0.04], [0.18, 0.04], [0.0, 0.08] ], lowLod, { bevelScale: 0.054, bevelThicknessScale: 0.048 } ); case 'marksman_collar': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.12], [0.04, 0.28], [0.1, 0.54], [0.18, 0.76], [0.34, 0.86], [0.68, 0.84], [0.86, 0.6], [0.98, 0.24], [1.0, 0.12], [0.84, 0.04], [0.3, 0.03], [0.06, 0.04], [0.0, 0.08] ], lowLod, { bevelScale: 0.054, bevelThicknessScale: 0.048 } ); case 'marksman_throat_collar': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.04], [0.04, 0.08], [0.1, 0.18], [0.18, 0.36], [0.3, 0.52], [0.48, 0.58], [0.7, 0.52], [0.88, 0.28], [0.98, 0.1], [1.0, 0.08], [0.84, 0.03], [0.36, 0.02], [0.08, 0.02], [0.0, 0.03] ], lowLod, { bevelScale: 0.044, bevelThicknessScale: 0.038 } ); case 'marksman_muzzle_collar': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.04], [0.06, 0.1], [0.14, 0.24], [0.26, 0.42], [0.5, 0.48], [0.74, 0.42], [0.88, 0.24], [0.98, 0.1], [1.0, 0.08], [0.84, 0.03], [0.3, 0.02], [0.06, 0.02], [0.0, 0.03] ], lowLod, { bevelScale: 0.04, bevelThicknessScale: 0.034 } ); case 'marksman_fairing': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.14], [0.04, 0.4], [0.14, 0.62], [0.32, 0.74], [0.82, 0.72], [0.94, 0.48], [1.0, 0.22], [1.0, 0.1], [0.86, 0.06], [0.24, 0.05], [0.08, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.055, bevelThicknessScale: 0.05 } ); case 'marksman_brace': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.1], [0.06, 0.42], [0.18, 0.66], [0.76, 0.64], [0.92, 0.36], [1.0, 0.12], [1.0, 0.06], [0.78, 0.04], [0.18, 0.04], [0.0, 0.06] ], lowLod, { bevelScale: 0.05, bevelThicknessScale: 0.045 } ); case 'marksman_shroud_clamp': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.08], [0.04, 0.26], [0.12, 0.5], [0.24, 0.66], [0.72, 0.64], [0.9, 0.42], [1.0, 0.16], [1.0, 0.06], [0.84, 0.03], [0.24, 0.02], [0.06, 0.04], [0.0, 0.06] ], lowLod, { bevelScale: 0.04, bevelThicknessScale: 0.035 } ); case 'marksman_gas_block': return buildGasBlockGeometry(size, lowLod); case 'marksman_gas_block_saddle': return buildGasBlockSaddleGeometry(size, lowLod); case 'marksman_optic_tube': return buildScopeTubeGeometry(size, lowLod, { rearBellScale: 1.12, frontBellScale: 1.22, waistScale: 0.8, bodyScale: 0.9 }); case 'marksman_optic_ring': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.16], [0.06, 0.38], [0.18, 0.62], [0.34, 0.76], [0.68, 0.76], [0.86, 0.58], [1.0, 0.28], [1.0, 0.14], [0.84, 0.08], [0.32, 0.06], [0.08, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.04, bevelThicknessScale: 0.034 } ); case 'marksman_front_sight': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.08], [0.08, 0.42], [0.22, 0.82], [0.42, 0.96], [0.72, 0.88], [0.9, 0.46], [1.0, 0.12], [1.0, 0.04], [0.76, 0.02], [0.22, 0.02], [0.0, 0.04] ], lowLod, { bevelScale: 0.045, bevelThicknessScale: 0.04 } ); case 'marksman_front_sight_base': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.06], [0.06, 0.18], [0.16, 0.34], [0.34, 0.46], [0.76, 0.46], [0.92, 0.28], [1.0, 0.12], [1.0, 0.04], [0.82, 0.02], [0.22, 0.02], [0.06, 0.03], [0.0, 0.04] ], lowLod, { bevelScale: 0.042, bevelThicknessScale: 0.036 } ); case 'marksman_front_sight_post': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.04], [0.08, 0.18], [0.2, 0.7], [0.36, 0.98], [0.56, 0.96], [0.76, 0.64], [0.92, 0.2], [1.0, 0.06], [1.0, 0.02], [0.76, 0.01], [0.22, 0.01], [0.04, 0.02], [0.0, 0.03] ], lowLod, { bevelScale: 0.03, bevelThicknessScale: 0.026 } ); case 'marksman_rear_sight': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.08], [0.08, 0.38], [0.2, 0.76], [0.46, 0.9], [0.76, 0.78], [0.92, 0.4], [1.0, 0.1], [1.0, 0.04], [0.76, 0.02], [0.18, 0.02], [0.0, 0.04] ], lowLod, { bevelScale: 0.045, bevelThicknessScale: 0.04 } ); case 'marksman_optic_lens_front': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.18], [0.06, 0.42], [0.16, 0.62], [0.32, 0.78], [0.66, 0.8], [0.86, 0.6], [1.0, 0.28], [1.0, 0.14], [0.82, 0.08], [0.32, 0.06], [0.08, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.04, bevelThicknessScale: 0.035 } ); case 'marksman_optic_lens_rear': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.16], [0.06, 0.38], [0.14, 0.56], [0.28, 0.7], [0.62, 0.72], [0.84, 0.54], [1.0, 0.24], [1.0, 0.12], [0.8, 0.06], [0.3, 0.05], [0.08, 0.08], [0.0, 0.1] ], lowLod, { bevelScale: 0.038, bevelThicknessScale: 0.032 } ); case 'marksman_action_port_cover': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.08], [0.04, 0.24], [0.08, 0.52], [0.18, 0.68], [0.52, 0.72], [0.78, 0.68], [0.92, 0.48], [1.0, 0.22], [1.0, 0.1], [0.88, 0.06], [0.58, 0.06], [0.2, 0.03], [0.06, 0.04], [0.0, 0.08] ], lowLod, { bevelScale: 0.04, bevelThicknessScale: 0.035 } ); case 'marksman_recoil_spring': return buildRecoilSpringGeometry(size, lowLod); case 'marksman_muzzle': return buildRevolvedWeaponProfileGeometry( size, { radialSegments: lowLod ? 8 : 14, rearRadius: 1.06, frontRadius: 0.92 } ); case 'marksman_muzzle_brake': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.08], [0.04, 0.16], [0.12, 0.44], [0.22, 0.82], [0.38, 0.98], [0.64, 0.96], [0.82, 0.78], [0.94, 0.4], [1.0, 0.12], [1.0, 0.04], [0.84, 0.02], [0.52, 0.02], [0.18, 0.03], [0.04, 0.05], [0.0, 0.06] ], lowLod, { bevelScale: 0.06, bevelThicknessScale: 0.052 } ); case 'marksman_muzzle_baffle': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.04], [0.08, 0.16], [0.18, 0.42], [0.34, 0.62], [0.72, 0.6], [0.9, 0.34], [1.0, 0.1], [1.0, 0.04], [0.82, 0.02], [0.2, 0.02], [0.04, 0.03], [0.0, 0.03] ], lowLod, { bevelScale: 0.032, bevelThicknessScale: 0.028 } ); case 'marksman_muzzle_vent_insert': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.06], [0.06, 0.22], [0.14, 0.44], [0.28, 0.6], [0.72, 0.62], [0.9, 0.44], [1.0, 0.2], [1.0, 0.08], [0.82, 0.05], [0.2, 0.03], [0.0, 0.05] ], lowLod, { bevelScale: 0.035, bevelThicknessScale: 0.03 } ); case 'marksman_counterweight': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.14], [0.06, 0.42], [0.18, 0.66], [0.34, 0.76], [0.8, 0.68], [0.94, 0.4], [1.0, 0.16], [1.0, 0.08], [0.8, 0.04], [0.2, 0.04], [0.0, 0.08] ], lowLod, { bevelScale: 0.05, bevelThicknessScale: 0.045 } ); case 'marksman_rail_clamp': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.14], [0.06, 0.5], [0.2, 0.7], [0.8, 0.7], [0.94, 0.48], [1.0, 0.18], [1.0, 0.1], [0.8, 0.06], [0.2, 0.06], [0.0, 0.08] ], lowLod, { bevelScale: 0.055, bevelThicknessScale: 0.05 } ); case 'marksman_rear_sight_base': return buildExtrudedWeaponProfileGeometry( size, [ [0.0, 0.14], [0.08, 0.48], [0.22, 0.66], [0.74, 0.66], [0.92, 0.42], [1.0, 0.16], [1.0, 0.08], [0.8, 0.04], [0.18, 0.04], [0.0, 0.06] ], lowLod, { bevelScale: 0.05, bevelThicknessScale: 0.045 } ); case 'marksman_bolt_housing': return buildBoltHousingGeometry(size, lowLod); case 'marksman_mag_brace': return buildMagBraceGeometry(size, lowLod); case 'marksman_mag_latch': return buildMagLatchGeometry(size, lowLod); default: return null; } } function resolveAttachmentDefinition(attachment: AttachmentEntry, entry: ModuleEntry): HumanoidPartDefinition { if (!attachment.shape) return entry.def; const size = attachment.shape.size ? { ...entry.def.size, ...attachment.shape.size } : entry.def.size; return { ...entry.def, primitive: attachment.shape.primitive ?? entry.def.primitive, size }; } function cloneAttachmentShape(shape?: AttachmentEntry['shape']): AttachmentEntry['shape'] | undefined { if (!shape) return undefined; return { primitive: shape.primitive, plannedWeaponProfile: shape.plannedWeaponProfile, plannedWeaponExtrusion: shape.plannedWeaponExtrusion ? { points: shape.plannedWeaponExtrusion.points.map((point) => ({ ...point })), bevelScale: shape.plannedWeaponExtrusion.bevelScale, bevelThicknessScale: shape.plannedWeaponExtrusion.bevelThicknessScale } : undefined, size: shape.size ? { ...shape.size } : undefined, taper: shape.taper ? { xTop: shape.taper.xTop, xBottom: shape.taper.xBottom, zTop: shape.taper.zTop, zBottom: shape.taper.zBottom } : undefined, chamfer: shape.chamfer ? { edge: shape.chamfer.edge, corner: shape.chamfer.corner } : undefined, profile: shape.profile ? { kind: shape.profile.kind, intensity: shape.profile.intensity } : undefined }; } function hasAttachmentShapeModifiers(shape?: AttachmentEntry['shape']): boolean { if (!shape) return false; return Boolean( Math.abs(shape.taper?.xTop ?? 0) > 1e-4 || Math.abs(shape.taper?.xBottom ?? 0) > 1e-4 || Math.abs(shape.taper?.zTop ?? 0) > 1e-4 || Math.abs(shape.taper?.zBottom ?? 0) > 1e-4 || Math.abs(shape.chamfer?.edge ?? 0) > 1e-4 || Math.abs(shape.chamfer?.corner ?? 0) > 1e-4 || (shape.profile?.kind && shape.profile.kind !== 'none' && Math.abs(shape.profile.intensity ?? 0) > 1e-4) ); } function getAttachmentShapeSideSign(attachment: AttachmentEntry): number { const markers = [attachment.id.toLowerCase(), attachment.anchorId.toLowerCase()]; const hasRightMarker = markers.some((value) => value.endsWith('-r') || value.endsWith('-fr') || value.endsWith('-br') || value.includes('-right') ); if (hasRightMarker) return -1; const hasLeftMarker = markers.some((value) => value.endsWith('-l') || value.endsWith('-fl') || value.endsWith('-bl') || value.includes('-left') ); if (hasLeftMarker) return 1; return attachment.mirrored ? -1 : 1; } function applyAttachmentShapeModifiersToObject(object: THREE.Object3D, attachment: AttachmentEntry): THREE.Object3D { const shape = attachment.shape; if (!hasAttachmentShapeModifiers(shape)) { return object; } const sideSign = getAttachmentShapeSideSign(attachment); const outerDir = -sideSign; object.traverse((child) => { const mesh = child as THREE.Mesh; if (!mesh.isMesh || !(mesh.geometry instanceof THREE.BufferGeometry)) return; const geometry = mesh.geometry.clone(); const position = geometry.getAttribute('position'); if (!position || position.itemSize < 3) { mesh.geometry = geometry; return; } geometry.computeBoundingBox(); const bounds = geometry.boundingBox; if (!bounds) { mesh.geometry = geometry; return; } const center = new THREE.Vector3(); const size = new THREE.Vector3(); bounds.getCenter(center); bounds.getSize(size); const half = size.multiplyScalar(0.5); const taper = shape?.taper; const chamfer = shape?.chamfer; const profile = shape?.profile; for (let index = 0; index < position.count; index += 1) { const x = position.getX(index); const y = position.getY(index); const z = position.getZ(index); const localX = x - center.x; const localY = y - center.y; const localZ = z - center.z; const nx = half.x > 1e-5 ? localX / half.x : 0; const ny = half.y > 1e-5 ? localY / half.y : 0; const nz = half.z > 1e-5 ? localZ / half.z : 0; const top = clamp01((ny + 1) * 0.5); const bottom = 1 - top; let scaleX = 1 - top * (taper?.xTop ?? 0) - bottom * (taper?.xBottom ?? 0); let scaleZ = 1 - top * (taper?.zTop ?? 0) - bottom * (taper?.zBottom ?? 0); let offsetX = 0; let offsetZ = 0; let offsetY = 0; const mirroredX = nx * sideSign; const outerFace = smoothstep(0.08, 1, -mirroredX); const innerFace = smoothstep(0.08, 1, mirroredX); if (profile?.kind && profile.kind !== 'none') { const intensity = profile.intensity ?? 0; const shoulderBand = gaussianBand(ny, 0.45, 0.28); const chestBand = gaussianBand(ny, 0.18, 0.34); const waistBand = gaussianBand(ny, -0.05, 0.26); const hipBand = gaussianBand(ny, -0.55, 0.24); const midBand = gaussianBand(ny, 0, 0.34); const endBand = Math.max(gaussianBand(ny, 0.72, 0.22), gaussianBand(ny, -0.72, 0.22)); const frontBand = smoothstep(0.05, 1, nz); const edgeBand = smoothstep(0.35, 1, Math.max(Math.abs(nx), Math.abs(nz))); switch (profile.kind) { case 'torso': scaleX *= 1 + intensity * (0.18 * shoulderBand - 0.16 * waistBand + 0.08 * hipBand); scaleZ *= 1 + intensity * (0.14 * chestBand - 0.06 * waistBand); offsetZ += Math.sign(localZ || 1) * half.z * intensity * (0.12 * chestBand * frontBand - 0.04 * hipBand * (1 - frontBand)); offsetY += half.y * intensity * (0.03 * shoulderBand - 0.02 * waistBand); break; case 'limb': scaleX *= 1 + intensity * (0.1 * endBand - 0.12 * midBand); scaleZ *= 1 + intensity * (0.06 * endBand - 0.08 * midBand); offsetZ += Math.sign(localZ || 1) * half.z * intensity * 0.03 * frontBand; break; case 'mechLimb': { const frontPlate = smoothstep(0.02, 1, nz); const backPlate = smoothstep(0.02, 1, -nz); const sideFlat = smoothstep(0.18, 1, Math.abs(nx)); const centerTrack = gaussianBand(nx, 0, 0.22) * (0.65 + 0.35 * frontPlate); const endCap = Math.max(gaussianBand(ny, 0.72, 0.2), gaussianBand(ny, -0.72, 0.2)); const outerBand = outerFace * (0.45 + 0.55 * frontPlate); const innerBand = innerFace * (0.4 + 0.6 * backPlate); scaleX *= 1 + intensity * (0.16 * endCap - 0.22 * centerTrack + 0.06 * backPlate + 0.08 * outerBand - 0.04 * innerBand); scaleZ *= 1 + intensity * (0.18 * frontPlate - 0.1 * backPlate - 0.14 * sideFlat + 0.08 * outerBand - 0.03 * innerBand); offsetX += half.x * intensity * outerDir * (0.08 * outerBand - 0.03 * innerBand); offsetZ += half.z * intensity * (0.14 * frontPlate - 0.05 * backPlate); offsetY += half.y * intensity * (0.03 * gaussianBand(ny, -0.05, 0.42)); break; } case 'plate': scaleX *= 1 + intensity * (0.05 - 0.08 * edgeBand + 0.04 * outerFace - 0.02 * innerFace); scaleZ *= 1 + intensity * (0.16 * frontBand - 0.08 * (1 - frontBand)); offsetX += half.x * intensity * outerDir * (0.03 * outerFace - 0.015 * innerFace); offsetZ += half.z * intensity * (0.08 * frontBand - 0.03 * (1 - frontBand)); break; case 'block': scaleX *= 1 + intensity * (0.08 * shoulderBand - 0.04 * edgeBand); scaleZ *= 1 + intensity * (0.04 * chestBand); break; case 'hardSurface': { const sideFlat = smoothstep(0.14, 1, Math.abs(nx)); const frontFacet = smoothstep(0.08, 1, nz); const backFacet = smoothstep(0.08, 1, -nz); const midCore = gaussianBand(ny, 0, 0.4); const capBand = Math.max(gaussianBand(ny, 0.78, 0.18), gaussianBand(ny, -0.78, 0.18)); const outerFacet = outerFace * (0.55 + 0.45 * frontFacet); const innerFacet = innerFace * (0.5 + 0.5 * backFacet); scaleX *= 1 + intensity * (0.14 * capBand - 0.18 * midCore + 0.05 * frontFacet + 0.07 * outerFacet - 0.04 * innerFacet); scaleZ *= 1 + intensity * (0.16 * frontFacet - 0.12 * backFacet - 0.18 * sideFlat + 0.07 * outerFacet - 0.03 * innerFacet); offsetX += half.x * intensity * outerDir * (0.05 * outerFacet - 0.02 * innerFacet); offsetZ += half.z * intensity * (0.08 * frontFacet - 0.04 * backFacet); offsetY += half.y * intensity * 0.015 * gaussianBand(ny, 0.12, 0.34); break; } } } if (chamfer) { const edgeX = smoothstep(0.55, 1, Math.abs(nx)); const edgeY = smoothstep(0.55, 1, Math.abs(ny)); const edgeZ = smoothstep(0.55, 1, Math.abs(nz)); const edgePair = Math.max( Math.min(edgeX, edgeY), Math.min(edgeY, edgeZ), Math.min(edgeX, edgeZ) ); const cornerBand = edgeX * edgeY * edgeZ; const radialShrink = 1 - (chamfer.edge ?? 0) * edgePair * 0.18 - (chamfer.corner ?? 0) * cornerBand * 0.22; scaleX *= Math.max(0.35, radialShrink); scaleZ *= Math.max(0.35, radialShrink); offsetY -= Math.sign(localY || 1) * half.y * (chamfer.corner ?? 0) * cornerBand * 0.04; } position.setXYZ( index, center.x + localX * Math.max(0.2, scaleX) + offsetX, center.y + localY + offsetY, center.z + localZ * Math.max(0.2, scaleZ) + offsetZ ); } position.needsUpdate = true; geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingBox(); geometry.computeBoundingSphere(); mesh.geometry = geometry; }); return object; } function buildSculptSdf( sculpt: AttachmentEntry['sculpt'], fallbackSize: PartSize ): { sdf: SdfFn; bounds: { min: Vec3Like; max: Vec3Like } } | null { if (!sculpt || !sculpt.enabled) return null; const size = sculpt.size ?? fallbackSize; const sx = Math.max(0.01, size.x ?? 0.14); const sy = Math.max(0.01, size.y ?? 0.14); const sz = Math.max(0.01, size.z ?? sx); let sdf: SdfFn; let min = vec3(-sx * 0.5, -sy * 0.5, -sz * 0.5); let max = vec3(sx * 0.5, sy * 0.5, sz * 0.5); if (sculpt.primitive === 'sphere') { const radius = sx; sdf = sdfSphere(radius); min = vec3(-radius, -radius, -radius); max = vec3(radius, radius, radius); } else if (sculpt.primitive === 'capsule') { const radius = sx; const length = sz; const a = { x: 0, y: 0, z: -length * 0.5 }; const b = { x: 0, y: 0, z: length * 0.5 }; sdf = sdfCapsule(a, b, radius); min = vec3(-radius, -radius, -length * 0.5 - radius); max = vec3(radius, radius, length * 0.5 + radius); } else if (sculpt.primitive === 'roundedBox' || sculpt.primitive === 'armorPad') { const half = { x: sx * 0.5, y: sy * 0.5, z: sz * 0.5 }; const roundness = Math.max(0, sculpt.roundness ?? 0.02); const base = sdfRoundedBox(half, roundness); min = vec3(-half.x - roundness, -half.y - roundness, -half.z - roundness); max = vec3(half.x + roundness, half.y + roundness, half.z + roundness); if (sculpt.primitive === 'armorPad') { const lipThickness = Math.max(0.006, sz * 0.09); const lipHalf = { x: half.x * 1.02, y: half.y * 1.05, z: lipThickness }; const lipOffset = vec3(0, 0, half.z - lipThickness * 0.55); const lip = translate(sdfRoundedBox(lipHalf, roundness * 0.5), lipOffset); const ridgeHalf = { x: half.x * 0.38, y: half.y * 0.58, z: Math.max(0.004, sz * 0.08) }; const ridgeOffset = vec3(0, 0, half.z * 0.16); const ridge = translate(sdfRoundedBox(ridgeHalf, roundness * 0.45), ridgeOffset); const recessHalf = { x: half.x * 0.6, y: half.y * 0.82, z: half.z * 0.33 }; const recessOffset = vec3(0, 0, half.z * 0.04); const recess = translate(sdfRoundedBox(recessHalf, roundness * 0.5), recessOffset); sdf = smoothUnion(base, lip, Math.max(0.002, roundness * 0.6)); sdf = smoothUnion(sdf, ridge, Math.max(0.002, roundness * 0.4)); sdf = subtract(sdf, recess); const expand = (halfSize: Vec3Like, offset: Vec3Like, extra = 0): void => { min = vec3( Math.min(min.x, offset.x - halfSize.x - extra), Math.min(min.y, offset.y - halfSize.y - extra), Math.min(min.z, offset.z - halfSize.z - extra) ); max = vec3( Math.max(max.x, offset.x + halfSize.x + extra), Math.max(max.y, offset.y + halfSize.y + extra), Math.max(max.z, offset.z + halfSize.z + extra) ); }; expand(lipHalf, lipOffset, roundness * 0.6); expand(ridgeHalf, ridgeOffset, roundness * 0.5); expand(recessHalf, recessOffset, roundness * 0.5); } else { sdf = base; } } else { const half = { x: sx * 0.5, y: sy * 0.5, z: sz * 0.5 }; sdf = sdfBox(half); } if (sculpt.bulge?.enabled) { const radius = Math.max(0.01, sculpt.bulge.radius); const offset = sculpt.bulge.offset ?? vec3(0, 0, 0); const bulge = translate(sdfSphere(radius), offset); const k = Math.max(0.001, sculpt.bulge.smooth ?? 0.04); sdf = smoothUnion(sdf, bulge, k); min = vec3( Math.min(min.x, offset.x - radius), Math.min(min.y, offset.y - radius), Math.min(min.z, offset.z - radius) ); max = vec3( Math.max(max.x, offset.x + radius), Math.max(max.y, offset.y + radius), Math.max(max.z, offset.z + radius) ); } if (sculpt.cut?.enabled) { const radius = Math.max(0.01, sculpt.cut.radius); const offset = sculpt.cut.offset ?? vec3(0, 0, 0); const cutter = translate(sdfSphere(radius), offset); sdf = subtract(sdf, cutter); } return { sdf, bounds: { min, max } }; } function sanitizeSculpt( sculpt: AttachmentEntry['sculpt'] | undefined, fallbackSize: PartSize ): AttachmentEntry['sculpt'] | undefined { if (!sculpt || !sculpt.enabled) return sculpt; const size = sculpt.size ?? fallbackSize; const sx = Math.max(0.01, size.x ?? 0.14); const sy = Math.max(0.01, size.y ?? 0.14); const sz = Math.max(0.01, size.z ?? sx); const half = { x: sx * 0.5, y: sy * 0.5, z: sz * 0.5 }; const clone = cloneSculpt(sculpt) ?? sculpt; if (clone.cut.enabled) { const maxRadius = Math.max(0.002, Math.min(half.x, half.y, half.z) * 0.9); const radius = Math.min(Math.max(0.001, clone.cut.radius), maxRadius); const margin = radius * 0.2; const clampAxis = (value: number, halfDim: number): number => { const limit = Math.max(0, halfDim - margin); return Math.max(-limit, Math.min(limit, value)); }; const offset = clone.cut.offset ?? vec3(0, 0, 0); clone.cut.radius = radius; clone.cut.offset = vec3( clampAxis(offset.x ?? 0, half.x), clampAxis(offset.y ?? 0, half.y), clampAxis(offset.z ?? 0, half.z) ); clone.cut.enabled = radius > 0.002; } return clone; } function buildSculptMesh(attachment: AttachmentEntry, entry: ModuleEntry): THREE.Object3D { const sculpt = attachment.sculpt; const fallbackSize = entry.def.size ?? { x: 0.14, y: 0.14, z: 0.18 }; const built = buildSculptSdf(sculpt, fallbackSize); if (!built) { return getModuleMesh(entry, useAttachmentPreviewLod, resolveAttachmentSide(attachment)); } const size = sculpt?.size ?? fallbackSize; const maxDim = Math.max(size.x ?? 0.1, size.y ?? 0.1, size.z ?? size.x ?? 0.1); const resolution = useAttachmentPreviewLod ? 28 : (maxDim > 0.22 ? 52 : 40); const sculptKey = [ entry.id, resolution, sculpt?.primitive ?? 'box', size.x ?? 0, size.y ?? 0, size.z ?? 0, sculpt?.roundness ?? 0, sculpt?.bulge?.enabled ? 1 : 0, sculpt?.bulge?.radius ?? 0, sculpt?.bulge?.smooth ?? 0, sculpt?.bulge?.offset?.x ?? 0, sculpt?.bulge?.offset?.y ?? 0, sculpt?.bulge?.offset?.z ?? 0, sculpt?.cut?.enabled ? 1 : 0, sculpt?.cut?.radius ?? 0, sculpt?.cut?.offset?.x ?? 0, sculpt?.cut?.offset?.y ?? 0, sculpt?.cut?.offset?.z ?? 0 ].join('|'); const cached = sculptMeshCache.get(sculptKey); if (cached) { const clone = cached.clone(true); clone.userData = { ...(clone.userData ?? {}), disposable: false }; return clone; } const mesh = tryMeshSdf(built.sdf, built.bounds, { resolution, materialSlot: entry.def.materialSlot }); if (!mesh) { return getModuleMesh(entry, useAttachmentPreviewLod, resolveAttachmentSide(attachment)); } const geometry = proceduralMeshToThreeGeometry(mesh); geometry.computeVertexNormals(); geometry.normalizeNormals(); geometry.computeBoundingSphere(); const material = createMaterialForDefinition(entry.def); material.side = THREE.DoubleSide; if (entry.def.purpose === 'armor_plate') { material.polygonOffset = true; material.polygonOffsetFactor = 1; material.polygonOffsetUnits = 1; } const threeMesh = new THREE.Mesh(geometry, material); threeMesh.castShadow = false; threeMesh.receiveShadow = false; threeMesh.userData = { ...(threeMesh.userData ?? {}), disposable: false }; sculptMeshCache.set(sculptKey, threeMesh); return threeMesh.clone(true); } function getModuleMesh(entry: ModuleEntry, lowLod = false, side: 'left' | 'right' | 'center' = 'center'): THREE.Object3D { const generated = buildGeneratedModuleMesh(entry, lowLod, side); if (generated) { return generated; } const key = `${entry.id}:${side}:${lowLod ? 'low' : 'high'}`; const cached = moduleMeshCache.get(key); if (cached) { if (activeGenerationProfile) { bumpGenerationCounter(activeGenerationProfile, 'moduleCacheHit'); } const clone = cached.clone(true); applyHumanSkinMaterials(clone, entry.id, entry.def); return clone; } const profile = activeGenerationProfile; const profileStart = profile ? performance.now() : 0; const def = applyPreviewLodToDef(entry.def, lowLod); const mesh = buildPrimitiveMesh(def); const geometry = proceduralMeshToThreeGeometry(mesh); geometry.computeBoundingSphere(); const material = createMaterialForDefinition(entry.def); const threeMesh = new THREE.Mesh(geometry, material); threeMesh.castShadow = false; threeMesh.receiveShadow = false; moduleMeshCache.set(key, threeMesh); if (profile) { bumpGenerationCounter(profile, 'moduleCacheMiss'); recordGenerationMesh(profile, `module:${def.primitive ?? 'box'}`, performance.now() - profileStart); } const clone = threeMesh.clone(true); applyHumanSkinMaterials(clone, entry.id, entry.def); return clone; } function getModuleMeshForAttachment(attachment: AttachmentEntry, entry: ModuleEntry): THREE.Object3D { const weaponQa = getWeaponQaContext(attachment); const profile = activeGenerationProfile; const finalize = (object: THREE.Object3D): THREE.Object3D => { if (state.showWeaponQa && weaponQa) { applyWeaponQaOverlay(object, weaponQa); } return object; }; if (attachment.sculpt?.enabled) { const profileStart = profile ? performance.now() : 0; const mesh = buildSculptMesh(attachment, entry); applyHumanSkinMaterials(mesh, entry.id, entry.def); applyMarksmanAttachmentFinish(mesh, attachment, entry.def); if (profile) { bumpGenerationCounter(profile, 'sculptBuilds'); recordGenerationMesh(profile, `sculpt:${attachment.id}`, performance.now() - profileStart); } return finalize(mesh); } const hasPrimitiveOverride = Boolean(attachment.shape?.primitive); if (!attachment.shape || !hasPrimitiveOverride) { const base = getModuleMesh(entry, useAttachmentPreviewLod, resolveAttachmentSide(attachment)); const shaped = applyAttachmentShapeModifiersToObject(base, attachment); applyHumanSkinMaterials(shaped, entry.id, entry.def); applyMarksmanAttachmentFinish(shaped, attachment, entry.def); return finalize(shaped); } const def = applyPreviewLodToDef(resolveAttachmentDefinition(attachment, entry), useAttachmentPreviewLod); const size = def.size ?? { x: 0.1, y: 0.1, z: 0.1 }; const plannedWeaponProfile = attachment.shape?.plannedWeaponProfile; if (plannedWeaponProfile) { const extrusionCacheToken = getPlannedWeaponExtrusionCacheToken(attachment.shape?.plannedWeaponExtrusion); const plannedMeshKey = [ plannedWeaponGeometryVersion, entry.id, 'planned', plannedWeaponProfile, extrusionCacheToken, size.x ?? 0, size.y ?? 0, size.z ?? 0, useAttachmentPreviewLod ? 1 : 0 ].join('|'); const cachedPlanned = attachmentMeshCache.get(plannedMeshKey); const basePlannedObject = (() => { if (cachedPlanned) { if (profile) { bumpGenerationCounter(profile, 'plannedCacheHit'); } const clone = cachedPlanned.clone(true); clone.userData = { ...(clone.userData ?? {}), disposable: false }; return clone; } const profileStart = profile ? performance.now() : 0; const geometry = buildPlannedWeaponProfileGeometry( plannedWeaponProfile, size, useAttachmentPreviewLod, attachment.shape?.plannedWeaponExtrusion ); if (!geometry) return null; const material = createMaterialForDefinition(def); const threeMesh = new THREE.Mesh(geometry, material); threeMesh.castShadow = false; threeMesh.receiveShadow = false; threeMesh.userData = { ...(threeMesh.userData ?? {}), disposable: false }; attachmentMeshCache.set(plannedMeshKey, threeMesh); if (profile) { bumpGenerationCounter(profile, 'plannedCacheMiss'); recordGenerationMesh(profile, `planned:${plannedWeaponProfile}`, performance.now() - profileStart); } return threeMesh.clone(true); })(); if (basePlannedObject) { const shaped = applyAttachmentShapeModifiersToObject(basePlannedObject, attachment); applyHumanSkinMaterials(shaped, entry.id, entry.def); applyMarksmanAttachmentFinish(shaped, attachment, entry.def); return finalize(shaped); } } const meshKey = [ entry.id, def.primitive ?? 'box', size.x ?? 0, size.y ?? 0, size.z ?? 0, size.segments ?? 0, size.rings ?? 0, size.minor ?? 0, useAttachmentPreviewLod ? 1 : 0 ].join('|'); const cached = attachmentMeshCache.get(meshKey); const baseObject = (() => { if (cached) { if (profile) { bumpGenerationCounter(profile, 'attachmentPrimitiveCacheHit'); } const clone = cached.clone(true); clone.userData = { ...(clone.userData ?? {}), disposable: false }; return clone; } const profileStart = profile ? performance.now() : 0; const mesh = buildPrimitiveMesh(def); const geometry = proceduralMeshToThreeGeometry(mesh); geometry.computeBoundingSphere(); const material = createMaterialForDefinition(def); const threeMesh = new THREE.Mesh(geometry, material); threeMesh.castShadow = false; threeMesh.receiveShadow = false; threeMesh.userData = { ...(threeMesh.userData ?? {}), disposable: false }; attachmentMeshCache.set(meshKey, threeMesh); if (profile) { bumpGenerationCounter(profile, 'attachmentPrimitiveCacheMiss'); recordGenerationMesh(profile, `primitive:${def.primitive ?? 'box'}`, performance.now() - profileStart); } return threeMesh.clone(true); })(); const shaped = applyAttachmentShapeModifiersToObject(baseObject, attachment); applyHumanSkinMaterials(shaped, entry.id, entry.def); applyMarksmanAttachmentFinish(shaped, attachment, entry.def); return finalize(shaped); } function proceduralMeshToObjText(mesh: ProceduralMesh): string { const lines: string[] = []; for (const v of mesh.vertices) { lines.push(`v ${v.position.x} ${v.position.y} ${v.position.z}`); } for (let i = 0; i < mesh.indices.length; i += 3) { const a = mesh.indices[i] + 1; const b = mesh.indices[i + 1] + 1; const c = mesh.indices[i + 2] + 1; lines.push(`f ${a} ${b} ${c}`); } return lines.join('\n'); } function disposeModuleOverrides(): void { clearGroup(moduleGroup, true, true); clearGroup(attachmentAxisGroup, false, true); } function clearGroup(group: THREE.Group, disposeMaterials = true, disposeGeometry = true): void { group.traverse((child) => { const mesh = child as THREE.Mesh; if (disposeGeometry && mesh.geometry) { if ( mesh.geometry !== nodeGeometry && mesh.geometry !== anchorGeometry && mesh.geometry !== generatorGeometry && mesh.geometry !== footProbeMarkerGeometry && mesh.geometry !== poseDebugMarkerGeometry ) { mesh.geometry.dispose(); } } if (disposeMaterials) { if (Array.isArray(mesh.material)) { mesh.material.forEach((mat) => mat.dispose()); } else if (mesh.material) { mesh.material.dispose(); } } }); group.clear(); } function invalidateEdgeCache(): void { state.edgeCache.clear(); } function getNodeById(id: string): NodeEntry | undefined { return state.nodes.find((node) => node.id === id); } function getEdgeById(id: string): EdgeEntry | undefined { return state.edges.find((edge) => edge.id === id); } function getAnchorById(id: string): AnchorEntry | undefined { return state.anchors.find((anchor) => anchor.id === id); } function getGeneratorById(id: string): GeneratorEntry | undefined { return state.generators.find((gen) => gen.id === id); } function getAttachmentById(id: string): AttachmentEntry | undefined { return state.attachments.find((attachment) => attachment.id === id); } function getAttachmentSelectionMesh(id: string, instanceIndex: number | null): THREE.Object3D | undefined { if (instanceIndex !== null && Number.isFinite(instanceIndex)) { return attachmentInstanceMeshes.find( (mesh) => mesh.userData?.id === id && mesh.userData?.generatorIndex === instanceIndex ); } return attachmentMeshMap.get(id); } function getModuleById(id: string): ModuleEntry | undefined { return moduleCatalog.find((entry) => entry.id === id); } function buildCurve(edge: EdgeEntry): THREE.Curve | null { const a = getNodeById(edge.a); const b = getNodeById(edge.b); if (!a || !b) return null; const start = getNodeWorldPosition(a); const end = getNodeWorldPosition(b); if (edge.curve === 'line') { return new THREE.LineCurve3(start, end); } const mid = start.clone().add(end).multiplyScalar(0.5); const control = mid.clone().add(toThree(edge.controlOffset)); const curve = new THREE.CatmullRomCurve3([start, control, end], false, 'catmullrom', 0.5); return curve; } function getEdgeCache(edge: EdgeEntry): EdgeCache | null { const cached = state.edgeCache.get(edge.id); if (cached) return cached; const curve = buildCurve(edge); if (!curve) return null; const lengths = curve.getLengths(EDGE_LUT_SAMPLES); const totalLength = lengths[lengths.length - 1] ?? 0; const frames = curve.computeFrenetFrames(EDGE_FRAME_SAMPLES, false); const cache: EdgeCache = { curve, lengths, totalLength, frames }; state.edgeCache.set(edge.id, cache); return cache; } function getSFromLength(lengths: number[], targetLen: number): number { if (lengths.length === 0) return 0; const total = lengths[lengths.length - 1]; if (total <= 0) return 0; const len = Math.max(0, Math.min(targetLen, total)); let low = 0; let high = lengths.length - 1; while (low <= high) { const mid = Math.floor((low + high) / 2); if (lengths[mid] < len) { low = mid + 1; } else { high = mid - 1; } } const idx = Math.max(1, low); const l0 = lengths[idx - 1]; const l1 = lengths[idx]; const t = l1 > l0 ? (len - l0) / (l1 - l0) : 0; return (idx - 1 + t) / (lengths.length - 1); } function getLengthAtS(lengths: number[], s: number): number { if (lengths.length === 0) return 0; const clamped = Math.max(0, Math.min(1, s)); const idx = clamped * (lengths.length - 1); const i0 = Math.floor(idx); const i1 = Math.min(lengths.length - 1, i0 + 1); const t = idx - i0; const l0 = lengths[i0] ?? 0; const l1 = lengths[i1] ?? l0; return l0 + (l1 - l0) * t; } function rebuildGeneratorGizmo(): void { generatorGizmoGroup.clear(); generatorHandleMeshes = []; if (!state.showGenerators) return; if (state.selection.kind !== 'generator' || !state.selection.id) return; const generator = getGeneratorById(state.selection.id); if (!generator) return; const baseAnchor = getAnchorById(generator.baseAnchorId); if (!baseAnchor) return; const baseFrame = resolveAnchorFrame(baseAnchor); if (!baseFrame) return; const addHandle = (position: THREE.Vector3, handle: GeneratorDragState['handle'], axis: THREE.Vector3, basePoint: THREE.Vector3, baseValue: number) => { const active = state.generatorDragState && state.generatorDragState.generatorId === generator.id && state.generatorDragState.handle === handle; const mesh = new THREE.Mesh(generatorHandleGeometry, active ? generatorHandleActiveMaterial : generatorHandleMaterial); mesh.position.copy(position); mesh.userData = { kind: 'generator-handle', handle, generatorId: generator.id, axis, basePoint, baseValue }; generatorGizmoGroup.add(mesh); generatorHandleMeshes.push(mesh); }; if (generator.type === 'alongEdge') { if (baseAnchor.type !== 'edge' || !baseAnchor.edgeId) return; const edge = getEdgeById(baseAnchor.edgeId); if (!edge) return; const cache = getEdgeCache(edge); if (!cache) return; const params = generator.params as GeneratorParamsAlong; const startLen = Math.max(0, params.startLen ?? 0); const endLen = params.endLen > 0 ? Math.min(params.endLen, cache.totalLength) : cache.totalLength; const startS = getSFromLength(cache.lengths, startLen); const endS = getSFromLength(cache.lengths, endLen); const startPoint = cache.curve.getPointAt(startS); const endPoint = cache.curve.getPointAt(endS); addHandle(startPoint, 'start', new THREE.Vector3(), startPoint, startLen); addHandle(endPoint, 'end', new THREE.Vector3(), endPoint, endLen); if (params.mode === 'spacing' && params.spacing > 0) { const spacingLen = Math.min(startLen + params.spacing, endLen); const spacingS = getSFromLength(cache.lengths, spacingLen); const spacingPoint = cache.curve.getPointAt(spacingS); addHandle(spacingPoint, 'spacing', new THREE.Vector3(), spacingPoint, params.spacing); } } if (generator.type === 'radial') { const params = generator.params as GeneratorParamsRadial; const axis = params.axis === 'x' ? new THREE.Vector3(1, 0, 0) : params.axis === 'z' ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0); const radius = Math.max(0.01, params.radius ?? 0.2); const handlePos = baseFrame.origin.clone().add(baseFrame.right.clone().multiplyScalar(radius)); addHandle(handlePos, 'radius', axis, baseFrame.origin, radius); const circlePoints: THREE.Vector3[] = []; const segments = 32; for (let i = 0; i <= segments; i += 1) { const angle = (Math.PI * 2 * i) / segments; const radial = baseFrame.right.clone().applyAxisAngle(axis, angle).normalize(); circlePoints.push(baseFrame.origin.clone().add(radial.multiplyScalar(radius))); } const circleGeom = new THREE.BufferGeometry().setFromPoints(circlePoints); const circleLine = new THREE.Line(circleGeom, generatorLineMaterial); generatorGizmoGroup.add(circleLine); } if (generator.type === 'mirror') { const params = generator.params as GeneratorParamsMirror; const normal = params.axis === 'x' ? new THREE.Vector3(1, 0, 0) : params.axis === 'z' ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0); const offset = params.offset ?? 0; const basePoint = normal.clone().multiplyScalar(offset); const handlePos = basePoint.clone().add(normal.clone().multiplyScalar(0.12)); addHandle(handlePos, 'mirror', normal, basePoint, offset); } } function makeFrame(origin: THREE.Vector3, forward: THREE.Vector3, up: THREE.Vector3): Frame { const f = forward.clone().normalize(); let u = up.clone().normalize(); if (Math.abs(f.dot(u)) > 0.95) { u = Math.abs(f.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0); } const r = new THREE.Vector3().crossVectors(u, f).normalize(); const finalUp = new THREE.Vector3().crossVectors(f, r).normalize(); return { origin, right: r, up: finalUp, forward: f }; } function frameToMatrix(frame: Frame): THREE.Matrix4 { const matrix = new THREE.Matrix4(); matrix.makeBasis(frame.right, frame.up, frame.forward); matrix.setPosition(frame.origin); return matrix; } function frameToQuaternion(frame: Frame): THREE.Quaternion { const matrix = frameToMatrix(frame); const quaternion = new THREE.Quaternion(); quaternion.setFromRotationMatrix(matrix); return quaternion; } function applyRotationToFrame(frame: Frame, rotation?: Vec3Like): Frame { if (!rotation) return frame; const euler = new THREE.Euler( THREE.MathUtils.degToRad(rotation.x ?? 0), THREE.MathUtils.degToRad(rotation.y ?? 0), THREE.MathUtils.degToRad(rotation.z ?? 0), 'XYZ' ); const quat = new THREE.Quaternion().setFromEuler(euler); return { origin: frame.origin, right: frame.right.clone().applyQuaternion(quat), up: frame.up.clone().applyQuaternion(quat), forward: frame.forward.clone().applyQuaternion(quat) }; } function rotationToQuaternion(rotation?: Vec3Like): THREE.Quaternion { if (!rotation) return new THREE.Quaternion(); const euler = new THREE.Euler( THREE.MathUtils.degToRad(rotation.x ?? 0), THREE.MathUtils.degToRad(rotation.y ?? 0), THREE.MathUtils.degToRad(rotation.z ?? 0), 'XYZ' ); return new THREE.Quaternion().setFromEuler(euler); } function resolveEffectiveParent(node: NodeEntry): NodeEntry | undefined { if (node.mode !== 'relative') return undefined; if (node.parentId) { const explicit = getNodeById(node.parentId); if (explicit) return explicit; } const candidates = state.edges .filter((edge) => edge.a === node.id || edge.b === node.id) .map((edge) => (edge.a === node.id ? getNodeById(edge.b) : getNodeById(edge.a))) .filter((entry): entry is NodeEntry => Boolean(entry)); return candidates.find((candidate) => (candidate.mode ?? 'static') === 'static' || Boolean(candidate.parentId)); } function resolveNodeWorldTransform( node: NodeEntry, visited = new Set(), usePreviewOverrides = true ): { position: THREE.Vector3; quaternion: THREE.Quaternion } { if (visited.has(node.id)) { return { position: toThree(node.position), quaternion: rotationToQuaternion(node.rotation) }; } visited.add(node.id); const localPos = toThree(node.position); const localQuat = rotationToQuaternion(node.rotation); const parent = resolveEffectiveParent(node); let worldPosition: THREE.Vector3; let worldQuaternion: THREE.Quaternion; if (node.mode !== 'relative' || !parent) { worldPosition = localPos; worldQuaternion = localQuat; } else { const parentWorld = resolveNodeWorldTransform(parent, visited, usePreviewOverrides); worldPosition = localPos.clone().applyQuaternion(parentWorld.quaternion).add(parentWorld.position); worldQuaternion = parentWorld.quaternion.clone().multiply(localQuat); } if (usePreviewOverrides) { const override = nodePreviewOverrideMap.get(node.id); if (override) { return { position: override.position?.clone() ?? worldPosition, quaternion: override.quaternion?.clone() ?? worldQuaternion }; } } return { position: worldPosition, quaternion: worldQuaternion }; } function getNodeWorldPosition(node: NodeEntry): THREE.Vector3 { return resolveNodeWorldTransform(node).position; } function setNodeWorldPosition(nodeId: string, worldPosition: THREE.Vector3): boolean { const node = getNodeById(nodeId); if (!node) return false; const parent = node.parentId ? getNodeById(node.parentId) : undefined; if (!parent || node.mode === 'static') { node.position = fromThree(worldPosition); return true; } const parentWorld = resolveNodeWorldTransform(parent); const local = worldPosition.clone().sub(parentWorld.position); local.applyQuaternion(parentWorld.quaternion.clone().invert()); node.position = fromThree(local); return true; } function updateNodeFromWorld(node: NodeEntry, worldPosition: THREE.Vector3, worldQuaternion: THREE.Quaternion): void { if (node.mode === 'relative') { const parent = resolveEffectiveParent(node); if (parent) { const parentWorld = resolveNodeWorldTransform(parent); const invParentQuat = parentWorld.quaternion.clone().invert(); const localPos = worldPosition.clone().sub(parentWorld.position).applyQuaternion(invParentQuat); const localQuat = invParentQuat.clone().multiply(worldQuaternion); node.position = fromThree(localPos); const euler = new THREE.Euler().setFromQuaternion(localQuat, 'XYZ'); node.rotation = vec3( THREE.MathUtils.radToDeg(euler.x), THREE.MathUtils.radToDeg(euler.y), THREE.MathUtils.radToDeg(euler.z) ); return; } } node.position = fromThree(worldPosition); const euler = new THREE.Euler().setFromQuaternion(worldQuaternion, 'XYZ'); node.rotation = vec3( THREE.MathUtils.radToDeg(euler.x), THREE.MathUtils.radToDeg(euler.y), THREE.MathUtils.radToDeg(euler.z) ); } function makeSurfaceFrame(origin: THREE.Vector3, normal: THREE.Vector3): Frame { const up = normal.clone().normalize(); let forward = new THREE.Vector3(0, 0, 1); if (Math.abs(up.dot(forward)) > 0.95) { forward = new THREE.Vector3(1, 0, 0); } forward = forward.sub(up.clone().multiplyScalar(forward.dot(up))).normalize(); return makeFrame(origin, forward, up); } function applyAnchorOffset(frame: Frame, anchor: AnchorEntry): Frame { if (!anchor.offset) return frame; const origin = frame.origin.clone() .add(frame.right.clone().multiplyScalar(anchor.offset.x ?? 0)) .add(frame.up.clone().multiplyScalar(anchor.offset.y ?? 0)) .add(frame.forward.clone().multiplyScalar(anchor.offset.z ?? 0)); return { ...frame, origin }; } function getAttachmentIdFromObject(object: THREE.Object3D | null): string | undefined { let current: THREE.Object3D | null = object; while (current) { if (current.userData?.kind === 'attachment' && typeof current.userData?.id === 'string') { return current.userData.id as string; } current = current.parent ?? null; } return undefined; } function getScenePropById(id: string): ScenePropEntry | undefined { return state.sceneProps.find((entry) => entry.id === id); } function getScenePropIdFromObject(object: THREE.Object3D | null): string | undefined { let current: THREE.Object3D | null = object; while (current) { if (current.userData?.kind === 'scene-prop' && typeof current.userData?.id === 'string') { return current.userData.id as string; } current = current.parent ?? null; } return undefined; } function getAttachmentWorldMatrix(attachment: AttachmentEntry): THREE.Matrix4 | null { const frames = getAttachmentFrames(attachment); if (frames.length === 0) return null; const moduleEntry = getModuleById(attachment.moduleId); return buildAttachmentMatrix(attachment, frames[0], moduleEntry); } function getAttachmentHalfExtents( attachment: AttachmentEntry, moduleEntry: ModuleEntry ): Vec3Like { const primitive = attachment.shape?.primitive ?? moduleEntry.def.primitive; const size = attachment.shape?.size ?? moduleEntry.def.size ?? { x: 0.1, y: 0.1, z: 0.1 }; const scale = attachment.scale ?? { x: 1, y: 1, z: 1 }; let half: Vec3Like = { x: 0.05, y: 0.05, z: 0.05 }; if (primitive === 'sphere') { const r = size.x ?? 0.1; half = { x: r * (scale.x ?? 1), y: r * (scale.y ?? 1), z: r * (scale.z ?? 1) }; } else if (primitive === 'capsule' || primitive === 'cylinder' || primitive === 'cone') { const r = (size.x ?? 0.05); const h = size.y ?? r * 2; half = { x: r * (scale.x ?? 1), y: (h * 0.5) * (scale.y ?? 1), z: r * (scale.z ?? 1) }; } else if (primitive === 'torus') { const r = size.x ?? 0.1; const tube = size.y ?? 0.03; const radius = r + tube; half = { x: radius * (scale.x ?? 1), y: tube * (scale.y ?? 1), z: radius * (scale.z ?? 1) }; } else { const w = size.x ?? 0.1; const h = size.y ?? w; const d = size.z ?? w; half = { x: (w * 0.5) * (scale.x ?? 1), y: (h * 0.5) * (scale.y ?? 1), z: (d * 0.5) * (scale.z ?? 1) }; } return half; } function getAttachmentFaceOffset( attachment: AttachmentEntry, moduleEntry: ModuleEntry, face: AnchorFace ): Vec3Like { const half = getAttachmentHalfExtents(attachment, moduleEntry); if (face === 'top') return { x: 0, y: half.y, z: 0 }; if (face === 'bottom') return { x: 0, y: -half.y, z: 0 }; if (face === 'left') return { x: -half.x, y: 0, z: 0 }; if (face === 'right') return { x: half.x, y: 0, z: 0 }; if (face === 'front') return { x: 0, y: 0, z: half.z }; if (face === 'back') return { x: 0, y: 0, z: -half.z }; return { x: 0, y: 0, z: 0 }; } function resolveAttachmentFaceFrame( origin: THREE.Vector3, parentFrame: Frame, face: AnchorFace ): Frame { if (face === 'center') { return parentFrame; } const normal = face === 'top' ? parentFrame.up.clone() : face === 'bottom' ? parentFrame.up.clone().negate() : face === 'left' ? parentFrame.right.clone().negate() : face === 'right' ? parentFrame.right.clone() : face === 'back' ? parentFrame.forward.clone().negate() : parentFrame.forward.clone(); const tangentHint = (face === 'top' || face === 'bottom') ? parentFrame.forward : parentFrame.up; return makeFrame(origin, normal, tangentHint); } function getAttachmentFrame(attachment: AttachmentEntry, face?: AnchorFace): Frame | null { const matrix = getAttachmentWorldMatrix(attachment); if (!matrix) return null; const origin = new THREE.Vector3().setFromMatrixPosition(matrix); const right = new THREE.Vector3(); const up = new THREE.Vector3(); const forward = new THREE.Vector3(); matrix.extractBasis(right, up, forward); right.normalize(); up.normalize(); forward.normalize(); const parentFrame = { origin: origin.clone(), right, up, forward }; if (face && face !== 'center') { const moduleEntry = getModuleById(attachment.moduleId); if (moduleEntry) { const offset = getAttachmentFaceOffset(attachment, moduleEntry, face); origin .add(right.clone().multiplyScalar(offset.x)) .add(up.clone().multiplyScalar(offset.y)) .add(forward.clone().multiplyScalar(offset.z)); } return resolveAttachmentFaceFrame(origin, parentFrame, face); } return { origin, right, up, forward }; } function findClosestEdgeSample(ray: THREE.Ray, edge: EdgeEntry): { s: number; len: number; point: THREE.Vector3 } | null { const cache = getEdgeCache(edge); if (!cache) return null; const samples = Math.max(40, EDGE_LUT_SAMPLES); let bestS = 0; let bestDist = Infinity; let bestPoint = new THREE.Vector3(); const closest = new THREE.Vector3(); for (let i = 0; i <= samples; i += 1) { const s = i / samples; const point = cache.curve.getPointAt(s); ray.closestPointToPoint(point, closest); const dist = closest.distanceToSquared(point); if (dist < bestDist) { bestDist = dist; bestS = s; bestPoint.copy(point); } } return { s: bestS, len: getLengthAtS(cache.lengths, bestS), point: bestPoint }; } function getAttachmentBaseQuaternion( attachment: AttachmentEntry, moduleEntry?: ModuleEntry ): THREE.Quaternion { if (!moduleEntry) return new THREE.Quaternion(); const explicitRotationDeg = MODULE_BASE_ROTATION_DEG_BY_ID[moduleEntry.id]; if (explicitRotationDeg) { return new THREE.Quaternion().setFromEuler( new THREE.Euler( THREE.MathUtils.degToRad(explicitRotationDeg[0]), THREE.MathUtils.degToRad(explicitRotationDeg[1]), THREE.MathUtils.degToRad(explicitRotationDeg[2]), 'XYZ' ) ); } const purpose = moduleEntry.purpose; const id = moduleEntry.id.toLowerCase(); const primitive = attachment.shape?.primitive ?? moduleEntry.def.primitive; const isLimb = purpose === 'limb_segment' || id.includes('upper_arm') || id.includes('forearm') || id.includes('upper_leg') || id.includes('lower_leg') || id.includes('neck_column'); if (!isLimb) return new THREE.Quaternion(); const alignable = primitive === 'cylinder' || primitive === 'capsule' || primitive === 'cone' || primitive === 'box' || primitive === 'wedge'; if (!alignable) return new THREE.Quaternion(); return new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 2, 0, 0, 'XYZ')); } function getWeaponHandForwardCorrectionQuaternion(attachment: AttachmentEntry): THREE.Quaternion { const isHandWeaponAttachment = attachment.id.startsWith('attach-weapon-') && attachment.anchorId === 'anchor-hand-r'; if (!isHandWeaponAttachment) { return new THREE.Quaternion(); } // Hand frame forward in editor space is inverted for standalone weapon mount. // Rotate around local-right to preserve lateral handedness (weapon stays on the // right side) while correcting forward direction. return new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI, 0, 0, 'XYZ')); } function buildAttachmentMatrix( attachment: AttachmentEntry, frame: Frame, moduleEntry?: ModuleEntry ): THREE.Matrix4 { const offset = attachment.offset ?? { x: 0, y: 0, z: 0 }; const rotation = attachment.rotation ?? { x: 0, y: 0, z: 0 }; const scale = attachment.scale ?? { x: 1, y: 1, z: 1 }; const euler = new THREE.Euler( THREE.MathUtils.degToRad(rotation.x ?? 0), THREE.MathUtils.degToRad(rotation.y ?? 0), THREE.MathUtils.degToRad(rotation.z ?? 0), 'XYZ' ); const userQuat = new THREE.Quaternion().setFromEuler(euler); const baseQuat = getAttachmentBaseQuaternion(attachment, moduleEntry); const weaponHandCorrectionQuat = getWeaponHandForwardCorrectionQuaternion(attachment); const quat = baseQuat.clone().multiply(weaponHandCorrectionQuat).multiply(userQuat); const mirrorX = attachment.mirrored ? -1 : 1; const local = new THREE.Matrix4().compose( new THREE.Vector3(offset.x ?? 0, offset.y ?? 0, offset.z ?? 0), quat, new THREE.Vector3((scale.x ?? 1) * mirrorX, scale.y ?? 1, scale.z ?? 1) ); return frameToMatrix(frame).multiply(local); } function resolveNodeFrame(node: NodeEntry): Frame { const world = resolveNodeWorldTransform(node); const worldQuat = world.quaternion; return { origin: world.position, right: new THREE.Vector3(1, 0, 0).applyQuaternion(worldQuat).normalize(), up: new THREE.Vector3(0, 1, 0).applyQuaternion(worldQuat).normalize(), forward: new THREE.Vector3(0, 0, 1).applyQuaternion(worldQuat).normalize() }; } function resolveDirectedNodeFrame(anchor: AnchorEntry, node: NodeEntry): Frame | null { const nodeWorld = resolveNodeWorldTransform(node); const proximalNode = anchor.proximalNodeId ? getNodeById(anchor.proximalNodeId) : undefined; const distalNode = anchor.distalNodeId ? getNodeById(anchor.distalNodeId) : undefined; const proximalWorld = proximalNode ? resolveNodeWorldTransform(proximalNode) : null; const distalWorld = distalNode ? resolveNodeWorldTransform(distalNode) : null; let forward: THREE.Vector3 | null = null; if (proximalWorld && distalWorld) { const axis = distalWorld.position.clone().sub(proximalWorld.position); if (axis.lengthSq() > 1e-6) { forward = axis.normalize(); } } if (!forward && distalWorld) { const axis = distalWorld.position.clone().sub(nodeWorld.position); if (axis.lengthSq() > 1e-6) { forward = axis.normalize(); } } if (!forward && proximalWorld) { const axis = nodeWorld.position.clone().sub(proximalWorld.position); if (axis.lengthSq() > 1e-6) { forward = axis.normalize(); } } if (!forward) return null; const projectHint = (hint: THREE.Vector3): THREE.Vector3 | null => { const projected = hint.clone().projectOnPlane(forward!); if (projected.lengthSq() <= 1e-6) return null; return projected.normalize(); }; if (anchor.upNodeId) { const upNode = getNodeById(anchor.upNodeId); if (upNode) { const upHint = resolveNodeWorldTransform(upNode).position.clone().sub(nodeWorld.position); const projectedUp = projectHint(upHint); if (projectedUp) { return makeFrame(nodeWorld.position, forward, projectedUp); } } } const nodeUp = new THREE.Vector3(0, 1, 0).applyQuaternion(nodeWorld.quaternion).normalize(); const projectedNodeUp = projectHint(nodeUp); if (projectedNodeUp) { return makeFrame(nodeWorld.position, forward, projectedNodeUp); } const nodeRight = new THREE.Vector3(1, 0, 0).applyQuaternion(nodeWorld.quaternion).normalize(); const projectedNodeRight = projectHint(nodeRight); if (projectedNodeRight) { const upFromRight = new THREE.Vector3().crossVectors(forward, projectedNodeRight).normalize(); if (upFromRight.lengthSq() > 1e-6) { return makeFrame(nodeWorld.position, forward, upFromRight); } } const projectedWorldUp = projectHint(new THREE.Vector3(0, 1, 0)); if (projectedWorldUp) { return makeFrame(nodeWorld.position, forward, projectedWorldUp); } return makeFrame( nodeWorld.position, forward, Math.abs(forward.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0) ); } function resolveEdgeFrame(edge: EdgeEntry, s: number): Frame | null { const cache = getEdgeCache(edge); if (!cache) return null; const clamped = Math.max(0, Math.min(1, s)); const origin = cache.curve.getPointAt(clamped); const tangent = cache.curve.getTangentAt(clamped).normalize(); const nodeA = getNodeById(edge.a); const nodeB = getNodeById(edge.b); if (nodeA && nodeB) { const projectHint = (hint: THREE.Vector3): THREE.Vector3 | null => { const projected = hint.clone().sub(tangent.clone().multiplyScalar(hint.dot(tangent))); if (projected.lengthSq() <= 1e-5) return null; return projected.normalize(); }; if (edge.curve === 'line' && poseState.snapshot.size > 0) { const snapA = resolveSnapshotNodeWorldTransform(edge.a); const snapB = resolveSnapshotNodeWorldTransform(edge.b); if (snapA && snapB) { const bindTangent = snapB.position.clone().sub(snapA.position).normalize(); if (bindTangent.lengthSq() > 1e-5) { let bindUp = new THREE.Vector3(0, 1, 0).applyQuaternion(snapA.quaternion).normalize(); let projectedBindUp = bindUp.clone().sub(bindTangent.clone().multiplyScalar(bindUp.dot(bindTangent))); if (projectedBindUp.lengthSq() <= 1e-5) { bindUp = new THREE.Vector3(1, 0, 0).applyQuaternion(snapA.quaternion).normalize(); projectedBindUp = bindUp.clone().sub(bindTangent.clone().multiplyScalar(bindUp.dot(bindTangent))); } if (projectedBindUp.lengthSq() > 1e-5) { const transport = new THREE.Quaternion().setFromUnitVectors(bindTangent, tangent); const transportedUp = projectedBindUp.normalize().applyQuaternion(transport); const projectedUp = projectHint(transportedUp); if (projectedUp) { return makeFrame(origin, tangent, projectedUp); } } } } } const quatA = resolveNodeWorldTransform(nodeA).quaternion; const quatB = resolveNodeWorldTransform(nodeB).quaternion; const upHintA = new THREE.Vector3(0, 1, 0).applyQuaternion(quatA).normalize(); const upHintB = new THREE.Vector3(0, 1, 0).applyQuaternion(quatB).normalize(); if (edge.curve === 'line') { const projectedUpA = projectHint(upHintA); if (projectedUpA) { return makeFrame(origin, tangent, projectedUpA); } const rightHintA = new THREE.Vector3(1, 0, 0).applyQuaternion(quatA).normalize(); const projectedRightA = projectHint(rightHintA); if (projectedRightA) { const upFromRight = new THREE.Vector3().crossVectors(tangent, projectedRightA).normalize(); if (upFromRight.lengthSq() > 1e-5) { return makeFrame(origin, tangent, upFromRight); } } const projectedUpB = projectHint(upHintB); if (projectedUpB) { return makeFrame(origin, tangent, projectedUpB); } return makeFrame(origin, tangent, new THREE.Vector3(0, 1, 0)); } const refQuat = quatA.clone().slerp(quatB, clamped); const upHint = new THREE.Vector3(0, 1, 0).applyQuaternion(refQuat).normalize(); const rightHint = new THREE.Vector3(1, 0, 0).applyQuaternion(refQuat).normalize(); const projectedUp = projectHint(upHint); if (projectedUp) { return makeFrame(origin, tangent, projectedUp); } const projectedRight = projectHint(rightHint); if (projectedRight) { const upFromRight = new THREE.Vector3().crossVectors(tangent, projectedRight).normalize(); if (upFromRight.lengthSq() > 1e-5) { return makeFrame(origin, tangent, upFromRight); } } } if (edge.curve === 'line') { return makeFrame(origin, tangent, new THREE.Vector3(0, 1, 0)); } const frameIndex = Math.min( cache.frames.normals.length - 1, Math.max(0, Math.round(clamped * (cache.frames.normals.length - 1))) ); const normal = cache.frames.normals[frameIndex].clone(); return makeFrame(origin, tangent, normal); } function applyOrientationRule(frame: Frame, anchor: AnchorEntry): Frame { let oriented = { ...frame }; if (anchor.orientation === 'perpendicular' || anchor.orientation === 'fixedAngle') { const angle = THREE.MathUtils.degToRad(anchor.radialAngle || 0); const q = new THREE.Quaternion().setFromAxisAngle(oriented.forward, angle); oriented.right = oriented.right.clone().applyQuaternion(q); oriented.up = oriented.up.clone().applyQuaternion(q); } if (anchor.orientation === 'lookAt') { const targetNode = anchor.lookAtNodeId ? getNodeById(anchor.lookAtNodeId) : undefined; const target = targetNode ? toThree(targetNode.position) : new THREE.Vector3(0, 0, 0); const forward = target.clone().sub(oriented.origin); if (forward.lengthSq() > 0.0001) { oriented = makeFrame(oriented.origin, forward, oriented.up); } } if (anchor.orientation === 'surfaceNormal' && anchor.surface) { const normal = toThree(anchor.surface.normal); oriented = makeFrame(oriented.origin, oriented.forward, normal); } if (anchor.orientation === 'hybrid' && anchor.surface) { const normal = toThree(anchor.surface.normal); oriented = makeFrame(oriented.origin, oriented.forward, normal); } return oriented; } function resolveAnchorFrame(anchor: AnchorEntry): Frame | null { if (anchor.type === 'node') { const node = anchor.nodeId ? getNodeById(anchor.nodeId) : undefined; if (!node) return null; const baseFrame = (anchor.proximalNodeId || anchor.distalNodeId) ? (resolveDirectedNodeFrame(anchor, node) ?? resolveNodeFrame(node)) : resolveNodeFrame(node); return applyAnchorOffset(applyOrientationRule(baseFrame, anchor), anchor); } if (anchor.type === 'edge') { const edge = anchor.edgeId ? getEdgeById(anchor.edgeId) : undefined; if (!edge) return null; const cache = getEdgeCache(edge); if (!cache) return null; const s = anchor.len !== undefined ? getSFromLength(cache.lengths, anchor.len) : (anchor.s ?? 0.5); const frame = resolveEdgeFrame(edge, s); return frame ? applyAnchorOffset(applyOrientationRule(frame, anchor), anchor) : null; } if (anchor.type === 'surface' && anchor.surface) { let origin = toThree(anchor.surface.point); let normal = toThree(anchor.surface.normal); if (anchor.surface.parentAttachmentId && anchor.surface.localPoint && anchor.surface.localNormal) { const attachment = getAttachmentById(anchor.surface.parentAttachmentId); if (attachment) { const attachmentMatrix = getAttachmentWorldMatrix(attachment); if (attachmentMatrix) { origin = toThree(anchor.surface.localPoint).applyMatrix4(attachmentMatrix); const normalMatrix = new THREE.Matrix3().getNormalMatrix(attachmentMatrix); normal = toThree(anchor.surface.localNormal).applyMatrix3(normalMatrix).normalize(); } } } const frame = makeSurfaceFrame(origin, normal); return applyAnchorOffset(applyOrientationRule(frame, anchor), anchor); } if (anchor.type === 'attachment') { const attachment = anchor.attachmentId ? getAttachmentById(anchor.attachmentId) : undefined; if (!attachment) return null; const frame = getAttachmentFrame(attachment, anchor.attachmentFace ?? 'center'); return frame ? applyAnchorOffset(applyOrientationRule(frame, anchor), anchor) : null; } return null; } function buildGeneratorFrames(generator: GeneratorEntry): Frame[] { const baseAnchor = getAnchorById(generator.baseAnchorId); if (!baseAnchor) return []; const baseFrame = resolveAnchorFrame(baseAnchor); if (!baseFrame) return []; if (generator.type === 'alongEdge') { if (baseAnchor.type !== 'edge' || !baseAnchor.edgeId) return []; const edge = getEdgeById(baseAnchor.edgeId); if (!edge) return []; const cache = getEdgeCache(edge); if (!cache) return []; const params = generator.params as GeneratorParamsAlong; const startLen = Math.max(0, params.startLen ?? 0); const endLen = params.endLen > 0 ? Math.min(params.endLen, cache.totalLength) : cache.totalLength; const frames: Frame[] = []; if (params.mode === 'spacing' && params.spacing > 0) { for (let len = startLen; len <= endLen + 1e-4; len += params.spacing) { const s = getSFromLength(cache.lengths, len); const frame = resolveEdgeFrame(edge, s); if (frame) frames.push(frame); } return frames; } const count = Math.max(1, params.count); for (let i = 0; i < count; i += 1) { const t = count === 1 ? 0 : i / (count - 1); const len = startLen + (endLen - startLen) * t; const s = getSFromLength(cache.lengths, len); const frame = resolveEdgeFrame(edge, s); if (frame) frames.push(frame); } return frames; } if (generator.type === 'radial') { const params = generator.params as GeneratorParamsRadial; const axis = params.axis === 'x' ? new THREE.Vector3(1, 0, 0) : params.axis === 'z' ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0); const frames: Frame[] = []; const count = Math.max(1, params.count); const startAngle = THREE.MathUtils.degToRad(params.startAngleDeg || 0); for (let i = 0; i < count; i += 1) { const angle = startAngle + (Math.PI * 2 * i) / count; const radial = baseFrame.right.clone().applyAxisAngle(axis, angle).normalize(); const origin = baseFrame.origin.clone().add(radial.clone().multiplyScalar(params.radius)); frames.push(makeFrame(origin, radial, axis)); } return frames; } if (generator.type === 'mirror') { const params = generator.params as GeneratorParamsMirror; const normal = params.axis === 'x' ? new THREE.Vector3(1, 0, 0) : params.axis === 'z' ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0); const planePoint = normal.clone().multiplyScalar(params.offset); const reflect = (v: THREE.Vector3): THREE.Vector3 => { const toPoint = v.clone().sub(planePoint); const dist = toPoint.dot(normal); return v.clone().sub(normal.clone().multiplyScalar(2 * dist)); }; const origin = reflect(baseFrame.origin); const right = reflect(baseFrame.origin.clone().add(baseFrame.right)).sub(origin).normalize(); const up = reflect(baseFrame.origin.clone().add(baseFrame.up)).sub(origin).normalize(); const forward = reflect(baseFrame.origin.clone().add(baseFrame.forward)).sub(origin).normalize(); return [makeFrame(origin, forward, up)]; } return []; } function getAttachmentFrames(attachment: AttachmentEntry, instanceIndex?: number | null): Frame[] { if (attachment.generatorId) { const generator = getGeneratorById(attachment.generatorId); if (!generator) return []; const frames = buildGeneratorFrames(generator); const index = instanceIndex ?? attachment.generatorIndex; if (index !== undefined && Number.isFinite(index)) { const idx = Math.max(0, Math.min(frames.length - 1, Math.floor(index))); return frames[idx] ? [frames[idx]] : []; } return frames; } const anchor = getAnchorById(attachment.anchorId); const frame = anchor ? resolveAnchorFrame(anchor) : null; return frame ? [frame] : []; } function estimateModuleRadius(def: HumanoidPartDefinition, scale?: Vec3Like): number { const size = def.size ?? { x: 0.1, y: 0.1, z: 0.1 }; const depth = size.z ?? size.x; const raw = Math.max(size.x, size.y, depth) * 0.6; if (!scale) return raw; const maxScale = Math.max(scale.x ?? 1, scale.y ?? 1, scale.z ?? 1); return raw * maxScale; } function isModuleCompatible(anchor: AnchorEntry, moduleEntry: ModuleEntry): boolean { const anchorProfile = anchor.portProfile ?? 'any'; if (anchorProfile !== 'any') { const moduleProfile = getModulePortProfile(moduleEntry); if (moduleProfile !== 'any' && moduleProfile !== anchorProfile) { return false; } } const accepts = anchor.accepts ?? []; if (accepts.length === 0) return true; const tokenSet = new Set(accepts.map((value) => value.toLowerCase())); if (tokenSet.has(moduleEntry.id.toLowerCase())) return true; if (tokenSet.has(moduleEntry.purpose.toLowerCase())) return true; if (tokenSet.has(moduleEntry.group.toLowerCase())) return true; return false; } let nodeMeshMap = new Map(); let anchorMeshMap = new Map(); let anchorAxesMap = new Map(); let edgeLineMap = new Map(); let attachmentMeshMap = new Map(); let attachmentInstanceMeshes: THREE.Object3D[] = []; let attachmentAxisMeshes: THREE.Line[] = []; let generatorMarkerMeshes: THREE.Mesh[] = []; let generatorMarkerMap = new Map(); let generatorAxisMap = new Map(); let generatorLinkMap = new Map(); let generatorHandleMeshes: THREE.Mesh[] = []; let scenePropMeshMap = new Map(); let raycastNodeTargets: THREE.Object3D[] = []; let raycastAnchorTargets: THREE.Object3D[] = []; let raycastEdgeTargets: THREE.Object3D[] = []; let raycastAttachmentTargets: THREE.Object3D[] = []; let raycastGeneratorTargets: THREE.Object3D[] = []; let raycastScenePropTargets: THREE.Object3D[] = []; let raycastSurfaceTargets: THREE.Object3D[] = []; let isMirroringAttachments = false; let useAttachmentPreviewLod = false; let pendingPresetLodRestore = false; let presetLodRestore: PendingPresetLodRestore | null = null; let pendingPresetLodRestoreToken = 0; function refreshNodeOptions(): void { const options = state.nodes.map((node) => ({ value: node.id, label: formatEditorItemLabel(node.id) })); const buildSelect = (select: HTMLSelectElement, selected?: string) => { select.innerHTML = ''; for (const option of options) { const el = document.createElement('option'); el.value = option.value; el.textContent = option.label; if (selected && selected === option.value) { el.selected = true; } select.appendChild(el); } }; buildSelect(controls.nodeList, controls.nodeList.value); buildSelect(controls.edgeStart, controls.edgeStart.value); buildSelect(controls.edgeEnd, controls.edgeEnd.value); buildSelect(controls.anchorNode, controls.anchorNode.value); } function refreshNodeParentOptions(selectedId?: string, excludeId?: string): void { controls.nodeParent.innerHTML = ''; const none = document.createElement('option'); none.value = ''; none.textContent = 'None'; controls.nodeParent.appendChild(none); for (const node of state.nodes) { if (excludeId && node.id === excludeId) continue; const option = document.createElement('option'); option.value = node.id; option.textContent = formatEditorItemLabel(node.id); if (selectedId && selectedId === node.id) { option.selected = true; } controls.nodeParent.appendChild(option); } } function updateNodeModeInputs(node?: NodeEntry | null): void { if (!node) return; controls.nodeMode.value = node.mode ?? 'static'; refreshNodeParentOptions(node.parentId, node.id); controls.nodeParent.disabled = (node.mode ?? 'static') !== 'relative'; controls.nodePose.value = node.poseJoint ?? ''; controls.nodePose.disabled = false; if (node.mode === 'relative') { controls.nodeX.value = node.position.x.toFixed(2); controls.nodeY.value = node.position.y.toFixed(2); controls.nodeZ.value = node.position.z.toFixed(2); } else { const world = getNodeWorldPosition(node); controls.nodeX.value = world.x.toFixed(2); controls.nodeY.value = world.y.toFixed(2); controls.nodeZ.value = world.z.toFixed(2); } } function updateNodePoseInput(node?: NodeEntry | null): void { if (!node) { controls.nodePose.value = ''; controls.nodePose.disabled = true; return; } controls.nodePose.value = node.poseJoint ?? ''; controls.nodePose.disabled = false; } function applyNodePoseFromInputs(): void { if (state.selection.kind !== 'node' || !state.selection.id) return; const node = getNodeById(state.selection.id); if (!node) return; const value = controls.nodePose.value.trim(); node.poseJoint = value.length > 0 ? value : undefined; if (poseState.clip) { refreshPoseMappingsIfLoaded(); const mapped = getPoseMappedNodeCount(); updatePoseInfo(`Pose mappings updated · ${mapped}/${state.nodes.length} nodes mapped`); } updatePoseDebugInfo(); markPresetCustom(); } function applyNodeModeFromInputs(): void { if (state.selection.kind !== 'node' || !state.selection.id) return; const node = getNodeById(state.selection.id); if (!node) return; const world = resolveNodeWorldTransform(node); const mode = controls.nodeMode.value as 'static' | 'relative'; node.mode = mode; const parentId = controls.nodeParent.value || undefined; node.parentId = mode === 'relative' ? parentId : undefined; updateNodeFromWorld(node, world.position, world.quaternion); updateNodeModeInputs(node); rebuildScene(); refreshUi(); refreshPoseMappingsIfLoaded(); markPresetCustom(); } function refreshEdgeOptions(): void { const options = state.edges.map((edge) => ({ value: edge.id, label: edge.id })); const buildSelect = (select: HTMLSelectElement, selected?: string) => { select.innerHTML = ''; for (const option of options) { const el = document.createElement('option'); el.value = option.value; el.textContent = option.label; if (selected && selected === option.value) { el.selected = true; } select.appendChild(el); } }; buildSelect(controls.edgeList, controls.edgeList.value); buildSelect(controls.anchorEdge, controls.anchorEdge.value); } function refreshAnchorOptions(): void { const options = state.anchors.map((anchor) => ({ value: anchor.id, label: `${anchor.id} (${anchor.type})` })); const buildSelect = (select: HTMLSelectElement, selected?: string) => { select.innerHTML = ''; for (const option of options) { const el = document.createElement('option'); el.value = option.value; el.textContent = option.label; if (selected && selected === option.value) { el.selected = true; } select.appendChild(el); } }; buildSelect(controls.anchorList, controls.anchorList.value); buildSelect(controls.generatorBase, controls.generatorBase.value); buildSelect(controls.attachmentAnchor, controls.attachmentAnchor.value); } function refreshAnchorAttachmentOptions(selected?: string): void { if (!controls.anchorAttachment) return; controls.anchorAttachment.innerHTML = ''; for (const attachment of state.attachments) { const option = document.createElement('option'); option.value = attachment.id; option.textContent = attachment.id; if (selected && selected === attachment.id) { option.selected = true; } controls.anchorAttachment.appendChild(option); } } function refreshGeneratorOptions(): void { controls.generatorList.innerHTML = ''; for (const generator of state.generators) { const option = document.createElement('option'); option.value = generator.id; option.textContent = `${generator.id} (${generator.type})`; controls.generatorList.appendChild(option); } } function refreshAttachmentGeneratorOptions(selected?: string): void { if (!controls.attachmentGenerator) return; controls.attachmentGenerator.innerHTML = ''; const none = document.createElement('option'); none.value = ''; none.textContent = 'None'; controls.attachmentGenerator.appendChild(none); for (const generator of state.generators) { const option = document.createElement('option'); option.value = generator.id; option.textContent = `${generator.id} (${generator.type})`; if (selected && selected === generator.id) { option.selected = true; } controls.attachmentGenerator.appendChild(option); } } function updateAttachmentGeneratorInputs(attachment?: AttachmentEntry | null): void { if (!controls.attachmentGenerator || !controls.attachmentGeneratorIndex) return; const generatorId = attachment?.generatorId ?? ''; refreshAttachmentGeneratorOptions(generatorId); if (attachment?.generatorIndex !== undefined && Number.isFinite(attachment.generatorIndex)) { controls.attachmentGeneratorIndex.value = String(Math.floor(attachment.generatorIndex)); } else { controls.attachmentGeneratorIndex.value = ''; } const disabled = !attachment; controls.attachmentGenerator.disabled = disabled; controls.attachmentGeneratorIndex.disabled = disabled || !controls.attachmentGenerator.value; } function applyAttachmentGeneratorFromInputs(): void { if (!controls.attachmentGenerator || !controls.attachmentGeneratorIndex) return; const id = controls.attachmentList.value; if (!id) return; const attachment = getAttachmentById(id); if (!attachment) return; const generatorId = controls.attachmentGenerator.value || undefined; attachment.generatorId = generatorId; const indexValue = controls.attachmentGeneratorIndex.value.trim(); if (generatorId && indexValue.length > 0) { const parsed = Number.parseInt(indexValue, 10); attachment.generatorIndex = Number.isFinite(parsed) ? parsed : undefined; } else { attachment.generatorIndex = undefined; } updateAttachmentGeneratorInputs(attachment); refreshAttachmentOptions(); rebuildDerivedGroups(); markPresetCustom(); } function updateGeneratorInputs(generator?: GeneratorEntry | null): void { if (!generator) return; controls.generatorType.value = generator.type; if (generator.type === 'alongEdge') { const params = generator.params as GeneratorParamsAlong; controls.generatorMode.value = params.mode; controls.generatorCount.value = String(params.count ?? 1); controls.generatorSpacing.value = String(params.spacing ?? 0.12); controls.generatorStart.value = String(params.startLen ?? 0); controls.generatorEnd.value = String(params.endLen ?? 0); } else if (generator.type === 'radial') { const params = generator.params as GeneratorParamsRadial; controls.generatorCount.value = String(params.count ?? 6); controls.generatorRadius.value = String(params.radius ?? 0.2); controls.generatorAngle.value = String(params.startAngleDeg ?? 0); controls.generatorPlane.value = params.axis ?? 'y'; } else { const params = generator.params as GeneratorParamsMirror; controls.generatorPlane.value = params.axis ?? 'x'; controls.generatorPlaneOffset.value = String(params.offset ?? 0); } } function applyGeneratorFromInputs(): void { const id = controls.generatorList.value; if (!id) return; const generator = getGeneratorById(id); if (!generator) return; if (generator.type === 'alongEdge') { const mode = controls.generatorMode.value as GeneratorParamsAlong['mode']; const count = Math.max(1, Math.floor(parseNumber(controls.generatorCount.value, 6))); const spacing = Math.max(0, parseNumber(controls.generatorSpacing.value, 0.12)); generator.params = { mode, count, spacing, startLen: Math.max(0, parseNumber(controls.generatorStart.value, 0)), endLen: Math.max(0, parseNumber(controls.generatorEnd.value, 0)) }; } else if (generator.type === 'radial') { generator.params = { count: Math.max(1, Math.floor(parseNumber(controls.generatorCount.value, 6))), radius: Math.max(0, parseNumber(controls.generatorRadius.value, 0.2)), startAngleDeg: parseNumber(controls.generatorAngle.value, 0), axis: (controls.generatorPlane.value as 'x' | 'y' | 'z') ?? 'y' }; } else { generator.params = { axis: (controls.generatorPlane.value as 'x' | 'y' | 'z') ?? 'x', offset: parseNumber(controls.generatorPlaneOffset.value, 0) }; } rebuildDerivedGroups(); markPresetCustom(); } function refreshAttachmentOptions(): void { const selected = controls.attachmentList.value; controls.attachmentList.innerHTML = ''; for (const attachment of state.attachments) { const module = getModuleById(attachment.moduleId); const generatorLabel = attachment.generatorId ? ` · gen:${attachment.generatorId}${attachment.generatorIndex !== undefined ? `#${attachment.generatorIndex}` : ''}` : ''; const option = document.createElement('option'); option.value = attachment.id; option.textContent = `${formatEditorItemLabel(attachment.id)} (${module?.id ?? attachment.moduleId} @ ${formatEditorItemLabel(attachment.anchorId)}${generatorLabel})`; if (selected && selected === attachment.id) { option.selected = true; } controls.attachmentList.appendChild(option); } refreshAnchorAttachmentOptions(controls.anchorAttachment?.value ?? undefined); } function updateAttachmentTransformInputs(attachment?: AttachmentEntry | null): void { const offset = attachment?.offset ?? { x: 0, y: 0, z: 0 }; const rotation = attachment?.rotation ?? { x: 0, y: 0, z: 0 }; const scale = attachment?.scale ?? { x: 1, y: 1, z: 1 }; const disabled = !attachment; controls.attachmentOffsetX.value = offset.x.toFixed(2); controls.attachmentOffsetY.value = offset.y.toFixed(2); controls.attachmentOffsetZ.value = offset.z.toFixed(2); controls.attachmentRotX.value = rotation.x.toFixed(1); controls.attachmentRotY.value = rotation.y.toFixed(1); controls.attachmentRotZ.value = rotation.z.toFixed(1); controls.attachmentScaleX.value = scale.x.toFixed(2); controls.attachmentScaleY.value = scale.y.toFixed(2); controls.attachmentScaleZ.value = scale.z.toFixed(2); [ controls.attachmentOffsetX, controls.attachmentOffsetY, controls.attachmentOffsetZ, controls.attachmentRotX, controls.attachmentRotY, controls.attachmentRotZ, controls.attachmentScaleX, controls.attachmentScaleY, controls.attachmentScaleZ, controls.attachmentGenerator ?? undefined, controls.attachmentGeneratorIndex ?? undefined, controls.attachmentRotateYawNeg, controls.attachmentRotateYawPos, controls.attachmentRotatePitchNeg, controls.attachmentRotatePitchPos, controls.attachmentRotateRollNeg, controls.attachmentRotateRollPos, controls.attachmentTransformReset, controls.attachmentPrimitive, controls.attachmentSizeX, controls.attachmentSizeY, controls.attachmentSizeZ, controls.attachmentSizeMinor, controls.attachmentSizeSegments, controls.attachmentSizeRings, controls.attachmentTaperXTop, controls.attachmentTaperXBottom, controls.attachmentTaperZTop, controls.attachmentTaperZBottom, controls.attachmentChamferEdge, controls.attachmentChamferCorner, controls.attachmentProfileKind, controls.attachmentProfileIntensity, controls.attachmentShapeReset ].forEach((el) => { if (!el) return; el.disabled = disabled; }); updateAttachmentDiagnosticInfo(attachment); } function updateAttachmentShapeInputs(attachment?: AttachmentEntry | null): void { const moduleEntry = attachment ? getModuleById(attachment.moduleId) : undefined; const base = moduleEntry?.def; const shape = attachment?.shape; const primitive = (shape?.primitive ?? '') as string; const size = shape?.size ?? base?.size ?? { x: 0.12, y: 0.12, z: 0.12, minor: 0.03, segments: 16, rings: 12 }; controls.attachmentPrimitive.value = primitive; controls.attachmentSizeX.value = (size.x ?? 0.12).toFixed(2); controls.attachmentSizeY.value = (size.y ?? 0.12).toFixed(2); controls.attachmentSizeZ.value = (size.z ?? size.x ?? 0.12).toFixed(2); controls.attachmentSizeMinor.value = (size.minor ?? size.x * 0.25).toFixed(2); controls.attachmentSizeSegments.value = String(size.segments ?? 16); controls.attachmentSizeRings.value = String(size.rings ?? 12); setOptionalInputValue(controls.attachmentTaperXTop, shape?.taper?.xTop ?? 0); setOptionalInputValue(controls.attachmentTaperXBottom, shape?.taper?.xBottom ?? 0); setOptionalInputValue(controls.attachmentTaperZTop, shape?.taper?.zTop ?? 0); setOptionalInputValue(controls.attachmentTaperZBottom, shape?.taper?.zBottom ?? 0); setOptionalInputValue(controls.attachmentChamferEdge, shape?.chamfer?.edge ?? 0); setOptionalInputValue(controls.attachmentChamferCorner, shape?.chamfer?.corner ?? 0); if (controls.attachmentProfileKind) { controls.attachmentProfileKind.value = shape?.profile?.kind ?? 'none'; } setOptionalInputValue(controls.attachmentProfileIntensity, shape?.profile?.intensity ?? 0); } function updateSculptInputs(attachment?: AttachmentEntry | null): void { const moduleEntry = attachment ? getModuleById(attachment.moduleId) : undefined; const baseDef = moduleEntry?.def; const sculpt = attachment?.sculpt; const size = sculpt?.size ?? baseDef?.size ?? { x: 0.14, y: 0.14, z: 0.18 }; controls.sculptEnable.checked = sculpt?.enabled ?? false; controls.sculptPrimitive.value = sculpt?.primitive ?? 'box'; controls.sculptSizeX.value = (size.x ?? 0.14).toFixed(2); controls.sculptSizeY.value = (size.y ?? 0.14).toFixed(2); controls.sculptSizeZ.value = (size.z ?? size.x ?? 0.18).toFixed(2); controls.sculptRoundness.value = (sculpt?.roundness ?? 0.02).toFixed(2); controls.sculptBulgeEnable.checked = sculpt?.bulge?.enabled ?? false; controls.sculptBulgeRadius.value = (sculpt?.bulge?.radius ?? 0.08).toFixed(2); controls.sculptBulgeSmooth.value = (sculpt?.bulge?.smooth ?? 0.04).toFixed(2); const bulgeOffset = sculpt?.bulge?.offset ?? vec3(0, 0.06, 0); controls.sculptBulgeOffsetX.value = bulgeOffset.x.toFixed(2); controls.sculptBulgeOffsetY.value = bulgeOffset.y.toFixed(2); controls.sculptBulgeOffsetZ.value = bulgeOffset.z.toFixed(2); controls.sculptCutEnable.checked = sculpt?.cut?.enabled ?? false; controls.sculptCutRadius.value = (sculpt?.cut?.radius ?? 0.05).toFixed(2); const cutOffset = sculpt?.cut?.offset ?? vec3(0, 0, 0.06); controls.sculptCutOffsetX.value = cutOffset.x.toFixed(2); controls.sculptCutOffsetY.value = cutOffset.y.toFixed(2); controls.sculptCutOffsetZ.value = cutOffset.z.toFixed(2); const disabled = !attachment; const sculptDisabled = disabled || !controls.sculptEnable.checked; controls.sculptEnable.disabled = disabled; controls.sculptReset.disabled = disabled; [ controls.sculptPrimitive, controls.sculptSizeX, controls.sculptSizeY, controls.sculptSizeZ, controls.sculptRoundness, controls.sculptBulgeEnable, controls.sculptBulgeRadius, controls.sculptBulgeSmooth, controls.sculptBulgeOffsetX, controls.sculptBulgeOffsetY, controls.sculptBulgeOffsetZ, controls.sculptCutEnable, controls.sculptCutRadius, controls.sculptCutOffsetX, controls.sculptCutOffsetY, controls.sculptCutOffsetZ ].forEach((el) => { el.disabled = sculptDisabled; }); } function swapSideToken(value: string): string | null { if (value.endsWith('-l')) return `${value.slice(0, -2)}-r`; if (value.endsWith('-r')) return `${value.slice(0, -2)}-l`; if (value.endsWith('_l')) return `${value.slice(0, -2)}_r`; if (value.endsWith('_r')) return `${value.slice(0, -2)}_l`; if (value.endsWith('-left')) return `${value.slice(0, -5)}-right`; if (value.endsWith('-right')) return `${value.slice(0, -6)}-left`; if (value.endsWith('_left')) return `${value.slice(0, -5)}_right`; if (value.endsWith('_right')) return `${value.slice(0, -6)}_left`; return null; } function findMirrorAttachment(attachment: AttachmentEntry): AttachmentEntry | undefined { const idSwap = swapSideToken(attachment.id); if (idSwap) { const byId = getAttachmentById(idSwap); if (byId) return byId; } const anchorSwap = swapSideToken(attachment.anchorId); if (anchorSwap) { return state.attachments.find( (entry) => entry.anchorId === anchorSwap && entry.moduleId === attachment.moduleId ); } return undefined; } function findMirrorAnchorId(anchorId: string): string | null { const swap = swapSideToken(anchorId); if (!swap) return null; return getAnchorById(swap) ? swap : null; } function findMirrorAttachmentForUpdate(source: AttachmentEntry): AttachmentEntry | undefined { const idSwap = swapSideToken(source.id); if (idSwap) { const byId = getAttachmentById(idSwap); if (byId) return byId; } const mirrorAnchorId = findMirrorAnchorId(source.anchorId); if (!mirrorAnchorId) return undefined; return state.attachments.find( (entry) => entry.anchorId === mirrorAnchorId && entry.moduleId === source.moduleId ); } function syncMirrorAttachment(source: AttachmentEntry): void { if (!state.mirrorAttachments || isMirroringAttachments) return; const mirrorAnchorId = findMirrorAnchorId(source.anchorId); if (!mirrorAnchorId) return; const mirror = findMirrorAttachmentForUpdate(source); if (mirror) { isMirroringAttachments = true; mirror.anchorId = mirrorAnchorId; mirror.moduleId = source.moduleId; mirror.portId = source.portId; mirror.generatorId = source.generatorId; mirror.generatorIndex = source.generatorIndex; applyMirrorAttachmentTransform(source, mirror); applyMirrorAttachmentShape(source, mirror); applyMirrorAttachmentSculpt(source, mirror); isMirroringAttachments = false; return; } const created = createMirrorAttachment(source); if (created) { isMirroringAttachments = true; state.attachments.push(created); isMirroringAttachments = false; } } function createMirrorAttachment(source: AttachmentEntry): AttachmentEntry | null { const mirrorAnchorId = findMirrorAnchorId(source.anchorId); if (!mirrorAnchorId) return null; const existing = state.attachments.find( (entry) => entry.anchorId === mirrorAnchorId && entry.moduleId === source.moduleId ); if (existing) return null; const idSwap = swapSideToken(source.id); const mirror: AttachmentEntry = { id: idSwap ?? makeId('attachment'), anchorId: mirrorAnchorId, moduleId: source.moduleId, portId: source.portId, generatorId: source.generatorId, generatorIndex: source.generatorIndex }; applyMirrorAttachmentTransform(source, mirror); applyMirrorAttachmentShape(source, mirror); applyMirrorAttachmentSculpt(source, mirror); return mirror; } function mirrorOffset(offset?: Vec3Like): Vec3Like | undefined { if (!offset) return undefined; return vec3(-offset.x, offset.y, offset.z); } function mirrorRotation(rotation?: Vec3Like): Vec3Like | undefined { if (!rotation) return undefined; const euler = new THREE.Euler( THREE.MathUtils.degToRad(rotation.x ?? 0), THREE.MathUtils.degToRad(rotation.y ?? 0), THREE.MathUtils.degToRad(rotation.z ?? 0), 'XYZ' ); const quat = new THREE.Quaternion().setFromEuler(euler); const rot = new THREE.Matrix4().makeRotationFromQuaternion(quat); const mirror = new THREE.Matrix4().makeScale(-1, 1, 1); const mirrored = mirror.clone().multiply(rot).multiply(mirror); const mirroredQuat = new THREE.Quaternion().setFromRotationMatrix(mirrored); const mirroredEuler = new THREE.Euler().setFromQuaternion(mirroredQuat, 'XYZ'); return vec3( THREE.MathUtils.radToDeg(mirroredEuler.x), THREE.MathUtils.radToDeg(mirroredEuler.y), THREE.MathUtils.radToDeg(mirroredEuler.z) ); } function mirrorSculptOffset(offset?: Vec3Like): Vec3Like | undefined { if (!offset) return undefined; return vec3(-offset.x, offset.y, offset.z); } function cloneSculpt(sculpt?: AttachmentEntry['sculpt']): AttachmentEntry['sculpt'] | undefined { if (!sculpt) return undefined; return { enabled: sculpt.enabled, primitive: sculpt.primitive, size: { ...sculpt.size }, roundness: sculpt.roundness, bulge: { enabled: sculpt.bulge.enabled, radius: sculpt.bulge.radius, smooth: sculpt.bulge.smooth, offset: sculpt.bulge.offset ? { ...sculpt.bulge.offset } : vec3(0, 0, 0) }, cut: { enabled: sculpt.cut.enabled, radius: sculpt.cut.radius, offset: sculpt.cut.offset ? { ...sculpt.cut.offset } : vec3(0, 0, 0) } }; } function applyMirrorAttachmentTransform(source: AttachmentEntry, target: AttachmentEntry): void { target.mirrored = true; target.offset = mirrorOffset(source.offset); target.rotation = mirrorRotation(source.rotation); target.scale = source.scale ? { ...source.scale } : undefined; } function applyMirrorAttachmentShape(source: AttachmentEntry, target: AttachmentEntry): void { target.shape = cloneAttachmentShape(source.shape); } function applyMirrorAttachmentSculpt(source: AttachmentEntry, target: AttachmentEntry): void { if (!source.sculpt) { target.sculpt = undefined; return; } const sculpt = cloneSculpt(source.sculpt); if (sculpt) { sculpt.bulge.offset = mirrorSculptOffset(sculpt.bulge.offset) ?? sculpt.bulge.offset; sculpt.cut.offset = mirrorSculptOffset(sculpt.cut.offset) ?? sculpt.cut.offset; } target.sculpt = sculpt; } function updateAnchorParentControls(anchor?: AnchorEntry | null): void { const canDetach = Boolean( anchor && anchor.type === 'surface' && anchor.surface?.parentAttachmentId && anchor.surface?.localPoint && anchor.surface?.localNormal ); controls.detachAnchorParent.disabled = !canDetach; } function updateAnchorMetaInputs(anchor?: AnchorEntry | null): void { if (!controls.anchorProfile || !controls.anchorExportSocket) return; const profile = anchor?.portProfile ?? 'any'; controls.anchorProfile.value = profile; controls.anchorExportSocket.checked = anchor?.exportSocket ?? false; const disabled = !anchor; controls.anchorProfile.disabled = disabled; controls.anchorExportSocket.disabled = disabled; } function updateAnchorTypeInputs(anchor?: AnchorEntry | null): void { if (!anchor) return; controls.anchorType.value = anchor.type; if (anchor.type === 'node' && anchor.nodeId) { controls.anchorNode.value = anchor.nodeId; } else if (anchor.type === 'edge' && anchor.edgeId) { controls.anchorEdge.value = anchor.edgeId; if (anchor.s !== undefined) { controls.anchorS.value = String(anchor.s); } if (anchor.len !== undefined) { controls.anchorLen.value = String(anchor.len); } } else if (anchor.type === 'attachment' && anchor.attachmentId) { controls.anchorAttachment.value = anchor.attachmentId; if (anchor.attachmentFace) { controls.anchorAttachmentFace.value = anchor.attachmentFace; } } updateAnchorTypeUi(); } function updateAnchorTypeUi(): void { const type = controls.anchorType.value as AnchorEntry['type']; controls.anchorNode.disabled = type !== 'node'; controls.anchorEdge.disabled = type !== 'edge'; controls.anchorS.disabled = type !== 'edge'; controls.anchorLen.disabled = type !== 'edge'; controls.anchorAttachment.disabled = type !== 'attachment'; controls.anchorAttachmentFace.disabled = type !== 'attachment'; } function applyAnchorMetaFromInputs(): void { if (!controls.anchorProfile || !controls.anchorExportSocket) return; const id = controls.anchorList.value; if (!id) return; const anchor = getAnchorById(id); if (!anchor) return; const profile = controls.anchorProfile.value as PortProfile; anchor.portProfile = profile === 'any' ? undefined : profile; anchor.exportSocket = controls.anchorExportSocket.checked; rebuildDerivedGroups(); refreshUi(); markPresetCustom(); } function resolveSurfaceWorldData(anchor: AnchorEntry): { point: Vec3Like; normal: Vec3Like } | null { if (anchor.type !== 'surface' || !anchor.surface) return null; if ( anchor.surface.parentAttachmentId && anchor.surface.localPoint && anchor.surface.localNormal ) { const attachment = getAttachmentById(anchor.surface.parentAttachmentId); if (attachment) { const attachmentMatrix = getAttachmentWorldMatrix(attachment); if (attachmentMatrix) { const worldPoint = toThree(anchor.surface.localPoint).applyMatrix4(attachmentMatrix); const normalMatrix = new THREE.Matrix3().getNormalMatrix(attachmentMatrix); const worldNormal = toThree(anchor.surface.localNormal).applyMatrix3(normalMatrix).normalize(); return { point: fromThree(worldPoint), normal: fromThree(worldNormal) }; } } } return { point: anchor.surface.point, normal: anchor.surface.normal }; } function detachSurfaceAnchorParent(anchorId: string): void { const anchor = getAnchorById(anchorId); if (!anchor || anchor.type !== 'surface' || !anchor.surface?.parentAttachmentId) return; const data = resolveSurfaceWorldData(anchor); if (!data) return; anchor.surface.point = data.point; anchor.surface.normal = data.normal; delete anchor.surface.parentAttachmentId; delete anchor.surface.localPoint; delete anchor.surface.localNormal; rebuildDerivedGroups(); refreshUi(); markPresetCustom(); setStatus(`Surface anchor ${anchorId} detached to world.`); } function detachSurfaceAnchorsFromAttachment(attachmentId: string): void { for (const anchor of state.anchors) { if (anchor.type !== 'surface' || !anchor.surface?.parentAttachmentId) continue; if (anchor.surface.parentAttachmentId !== attachmentId) continue; const data = resolveSurfaceWorldData(anchor); if (!data) continue; anchor.surface.point = data.point; anchor.surface.normal = data.normal; delete anchor.surface.parentAttachmentId; delete anchor.surface.localPoint; delete anchor.surface.localNormal; } } function applyAttachmentTransformFromInputs(): void { const id = controls.attachmentList.value; if (!id) return; const attachment = getAttachmentById(id); if (!attachment) return; attachment.offset = readAttachmentOffset(); attachment.rotation = readAttachmentRotation(); attachment.scale = readAttachmentScale(); if (state.mirrorAttachments && !isMirroringAttachments) { const mirror = findMirrorAttachment(attachment); if (mirror) { isMirroringAttachments = true; applyMirrorAttachmentTransform(attachment, mirror); isMirroringAttachments = false; } } updateAttachmentTransformInputs(attachment); rebuildDerivedGroups(); markPresetCustom(); } function applyAttachmentShapeFromInputs(): void { const id = controls.attachmentList.value; if (!id) return; const attachment = getAttachmentById(id); if (!attachment) return; const primitiveValue = controls.attachmentPrimitive.value.trim(); const size: PartSize = { x: parseNumber(controls.attachmentSizeX.value, 0.12), y: parseNumber(controls.attachmentSizeY.value, 0.12), z: parseNumber(controls.attachmentSizeZ.value, 0.12), minor: parseNumber(controls.attachmentSizeMinor.value, 0.03), segments: Math.max(6, Math.floor(parseNumber(controls.attachmentSizeSegments.value, 16))), rings: Math.max(4, Math.floor(parseNumber(controls.attachmentSizeRings.value, 12))) }; attachment.shape = { primitive: primitiveValue ? (primitiveValue as PartPrimitive) : undefined, size: primitiveValue ? size : undefined, taper: { xTop: parseOptionalInputValue(controls.attachmentTaperXTop, 0), xBottom: parseOptionalInputValue(controls.attachmentTaperXBottom, 0), zTop: parseOptionalInputValue(controls.attachmentTaperZTop, 0), zBottom: parseOptionalInputValue(controls.attachmentTaperZBottom, 0) }, chamfer: { edge: parseOptionalInputValue(controls.attachmentChamferEdge, 0), corner: parseOptionalInputValue(controls.attachmentChamferCorner, 0) }, profile: { kind: (controls.attachmentProfileKind?.value ?? 'none') as ShapeProfileKind, intensity: parseOptionalInputValue(controls.attachmentProfileIntensity, 0) } }; if (!attachment.shape.primitive && !hasAttachmentShapeModifiers(attachment.shape)) { attachment.shape = undefined; } if (state.mirrorAttachments && !isMirroringAttachments) { const mirror = findMirrorAttachment(attachment); if (mirror) { isMirroringAttachments = true; applyMirrorAttachmentShape(attachment, mirror); isMirroringAttachments = false; } } updateAttachmentShapeInputs(attachment); rebuildDerivedGroups(); markPresetCustom(); } function applyAttachmentSculptFromInputs(): void { const id = controls.attachmentList.value; if (!id) return; const attachment = getAttachmentById(id); if (!attachment) return; if (!controls.sculptEnable.checked) { attachment.sculpt = undefined; updateSculptInputs(attachment); rebuildDerivedGroups(); markPresetCustom(); return; } const size: PartSize = { x: parseNumber(controls.sculptSizeX.value, 0.14), y: parseNumber(controls.sculptSizeY.value, 0.14), z: parseNumber(controls.sculptSizeZ.value, 0.18) }; attachment.sculpt = { enabled: true, primitive: controls.sculptPrimitive.value as SculptPrimitive, size, roundness: Math.max(0, parseNumber(controls.sculptRoundness.value, 0.02)), bulge: { enabled: controls.sculptBulgeEnable.checked, radius: Math.max(0.01, parseNumber(controls.sculptBulgeRadius.value, 0.08)), smooth: Math.max(0.001, parseNumber(controls.sculptBulgeSmooth.value, 0.04)), offset: readSculptOffset(controls.sculptBulgeOffsetX, controls.sculptBulgeOffsetY, controls.sculptBulgeOffsetZ) }, cut: { enabled: controls.sculptCutEnable.checked, radius: Math.max(0.01, parseNumber(controls.sculptCutRadius.value, 0.05)), offset: readSculptOffset(controls.sculptCutOffsetX, controls.sculptCutOffsetY, controls.sculptCutOffsetZ) } }; if (state.mirrorAttachments && !isMirroringAttachments) { const mirror = findMirrorAttachment(attachment); if (mirror) { isMirroringAttachments = true; applyMirrorAttachmentSculpt(attachment, mirror); isMirroringAttachments = false; } } updateSculptInputs(attachment); rebuildDerivedGroups(); markPresetCustom(); } function resetAttachmentSculpt(): void { const id = controls.attachmentList.value; if (!id) return; const attachment = getAttachmentById(id); if (!attachment) return; attachment.sculpt = undefined; if (state.mirrorAttachments && !isMirroringAttachments) { const mirror = findMirrorAttachment(attachment); if (mirror) { isMirroringAttachments = true; mirror.sculpt = undefined; isMirroringAttachments = false; } else { syncMirrorAttachment(attachment); } } updateSculptInputs(attachment); rebuildDerivedGroups(); markPresetCustom(); } function resetAttachmentShapeOverride(): void { const id = controls.attachmentList.value; if (!id) return; const attachment = getAttachmentById(id); if (!attachment) return; attachment.shape = undefined; if (state.mirrorAttachments && !isMirroringAttachments) { const mirror = findMirrorAttachment(attachment); if (mirror) { isMirroringAttachments = true; mirror.shape = undefined; isMirroringAttachments = false; } else { syncMirrorAttachment(attachment); } } updateAttachmentShapeInputs(attachment); rebuildDerivedGroups(); markPresetCustom(); } function nudgeAnchorOffset(dx: number, dy: number, dz: number): void { if (state.selection.kind !== 'anchor' || !state.selection.id) return; const anchor = getAnchorById(state.selection.id); if (!anchor) return; const offset = anchor.offset ? { ...anchor.offset } : vec3(0, 0, 0); offset.x += dx; offset.y += dy; offset.z += dz; const isZero = Math.abs(offset.x) < 1e-5 && Math.abs(offset.y) < 1e-5 && Math.abs(offset.z) < 1e-5; anchor.offset = isZero ? undefined : offset; rebuildDerivedGroups(); markPresetCustom(); } function nudgeAttachmentRotation(axis: 'x' | 'y' | 'z', deltaDeg: number): void { const id = controls.attachmentList.value; if (!id) return; const attachment = getAttachmentById(id); if (!attachment) return; const rotation = attachment.rotation ?? { x: 0, y: 0, z: 0 }; rotation[axis] = (rotation[axis] ?? 0) + deltaDeg; attachment.rotation = rotation; if (state.mirrorAttachments && !isMirroringAttachments) { const mirror = findMirrorAttachment(attachment); if (mirror) { isMirroringAttachments = true; applyMirrorAttachmentTransform(attachment, mirror); isMirroringAttachments = false; } else { syncMirrorAttachment(attachment); } } updateAttachmentTransformInputs(attachment); rebuildDerivedGroups(); markPresetCustom(); } function resetAttachmentTransform(): void { const id = controls.attachmentList.value; if (!id) return; const attachment = getAttachmentById(id); if (!attachment) return; attachment.offset = undefined; attachment.rotation = undefined; attachment.scale = undefined; if (state.mirrorAttachments && !isMirroringAttachments) { const mirror = findMirrorAttachment(attachment); if (mirror) { isMirroringAttachments = true; mirror.offset = undefined; mirror.rotation = undefined; mirror.scale = undefined; isMirroringAttachments = false; } else { syncMirrorAttachment(attachment); } } updateAttachmentTransformInputs(attachment); rebuildDerivedGroups(); markPresetCustom(); } function refreshModuleOptions(): void { const filter = controls.moduleFilter.value.trim().toLowerCase(); const anchor = controls.attachmentAnchor.value ? getAnchorById(controls.attachmentAnchor.value) : undefined; const showIncompatible = controls.showIncompatible.checked; if (controls.moduleProfileBadges) { controls.moduleProfileBadges.dataset.activeProfile = anchor?.portProfile ?? 'any'; } controls.moduleList.innerHTML = ''; let compatibleCount = 0; let totalCount = 0; for (const moduleEntry of moduleCatalog) { if (filter && !moduleEntry.id.toLowerCase().includes(filter) && !moduleEntry.purpose.toLowerCase().includes(filter)) { continue; } const compatible = !anchor || isModuleCompatible(anchor, moduleEntry); if (!compatible && !showIncompatible) continue; const profile = getModulePortProfile(moduleEntry); const option = document.createElement('option'); option.value = moduleEntry.id; option.disabled = !compatible; option.textContent = `${compatible ? 'OK' : 'X'} ${moduleEntry.label} (${moduleEntry.purpose}, ${profile})`; controls.moduleList.appendChild(option); totalCount += 1; if (compatible) compatibleCount += 1; } if (!anchor) { controls.moduleCompat.textContent = 'Compatibility: pick an anchor.'; } else { const accepts = anchor.accepts.length ? anchor.accepts.join(', ') : 'Any'; const profile = anchor.portProfile ?? 'any'; controls.moduleCompat.textContent = `Compatibility: ${compatibleCount}/${totalCount} · accepts: ${accepts} · profile: ${profile}`; } } function updateEdgeInfo(): void { const edgeId = controls.edgeList.value; const edge = edgeId ? getEdgeById(edgeId) : undefined; if (!edge) { controls.edgeInfo.textContent = 'No edge selected.'; return; } const cache = getEdgeCache(edge); const total = cache?.totalLength ?? 0; const text = [ `Edge: ${edge.id}`, `Nodes: ${edge.a} -> ${edge.b}`, `Curve: ${edge.curve}`, `Radius: ${edge.radius.toFixed(3)}`, `Arc Length: ${total.toFixed(3)}` ].join('\n'); controls.edgeInfo.textContent = text; } function refreshUiNow(): void { refreshNodeOptions(); refreshNodeParentOptions(controls.nodeParent.value, controls.nodeList.value || undefined); refreshEdgeOptions(); refreshAnchorOptions(); refreshAnchorAttachmentOptions(controls.anchorAttachment?.value ?? undefined); refreshGeneratorOptions(); refreshAttachmentOptions(); refreshScenePropOptions(); refreshModuleOptions(); const attachment = controls.attachmentList.value ? getAttachmentById(controls.attachmentList.value) : undefined; updateAttachmentTransformInputs(attachment); updateAttachmentShapeInputs(attachment); updateSculptInputs(attachment); updateAttachmentGeneratorInputs(attachment); if (controls.mirrorAttachments) { controls.mirrorAttachments.checked = state.mirrorAttachments; } const generator = controls.generatorList.value ? getGeneratorById(controls.generatorList.value) : undefined; if (generator) { updateGeneratorInputs(generator); } const node = controls.nodeList.value ? getNodeById(controls.nodeList.value) : undefined; if (node) { updateNodeModeInputs(node); } else { updateNodePoseInput(null); } updatePoseDebugInfo(); const anchor = controls.anchorList.value ? getAnchorById(controls.anchorList.value) : undefined; updateAnchorTypeInputs(anchor); updateAnchorParentControls(anchor); updateAnchorMetaInputs(anchor); updateEdgeInfo(); updateRuntimeTargetTriggerOptions(); updateRuntimeTargetTriggerInputs(); const runtimeTargetMetadata = getRuntimeTargetMetadata(); const runtimeTargetPreviewEnabled = Boolean(runtimeTargetMetadata); if (controls.exportRuntimeTarget) controls.exportRuntimeTarget.disabled = !runtimeTargetPreviewEnabled; if (controls.runtimeTargetActivate) controls.runtimeTargetActivate.disabled = !runtimeTargetPreviewEnabled; if (controls.runtimeTargetHit) controls.runtimeTargetHit.disabled = !runtimeTargetPreviewEnabled; if (controls.runtimeTargetRecover) controls.runtimeTargetRecover.disabled = !runtimeTargetPreviewEnabled; if (controls.runtimeTargetAutoRecover) controls.runtimeTargetAutoRecover.disabled = !runtimeTargetPreviewEnabled; updateRuntimeTargetTimelineControls(); updatePanelVisibility(); refreshWeaponQaInfo(); updateRuntimeTargetPreviewInfo(); updateRuntimeTargetTimelineInfo(); updateRecipePreview(); } function refreshUi(options?: { immediate?: boolean }): void { if (options?.immediate) { if (refreshUiTimer !== null) { window.clearTimeout(refreshUiTimer); refreshUiTimer = null; } refreshUiPending = false; refreshUiNow(); return; } refreshUiPending = true; if (refreshUiTimer !== null) return; refreshUiTimer = window.setTimeout(() => { refreshUiTimer = null; if (!refreshUiPending) return; refreshUiPending = false; refreshUiNow(); }, UI_REFRESH_DEBOUNCE_MS); } function getRecipeSourceSummary(): string { if (controls.presetSelect.value === 'custom') { return `Source: custom (base preset ${currentBasePresetName})\nReload preset to pull latest code changes for that preset.`; } return `Source: preset ${controls.presetSelect.value}`; } function updateRecipePreview(force = false): void { if (!force) { if (bootHydrationInProgress) { return; } recipePreviewPending = true; if (recipePreviewTimer !== null) { window.clearTimeout(recipePreviewTimer); } recipePreviewTimer = window.setTimeout(() => { recipePreviewTimer = null; if (!recipePreviewPending) return; recipePreviewPending = false; updateRecipePreview(true); }, RECIPE_PREVIEW_DEBOUNCE_MS); return; } if (recipePreviewTimer !== null) { window.clearTimeout(recipePreviewTimer); recipePreviewTimer = null; } recipePreviewPending = false; const doc = serializeRecipe(); const json = JSON.stringify(doc, null, 2); const preview = json.length > 2000 ? `${json.slice(0, 2000)}\n...` : json; controls.recipePreview.textContent = `${getRecipeSourceSummary()}\n\n${preview}`; try { if (hasUserEditedRecipe && !bootHydrationInProgress) { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(doc)); window.localStorage.setItem(STORAGE_BASE_PRESET_KEY, currentBasePresetName); } } catch { // ignore storage errors } } function markPresetCustom(): void { hasUserEditedRecipe = true; if (controls.presetSelect.value !== 'custom') { controls.presetSelect.value = 'custom'; } } function focusPoint(point: THREE.Vector3, radius = 0.8): void { orbit.target.copy(point); camera.position.copy(point.clone().add(new THREE.Vector3(radius, radius * 0.6, radius * 1.1))); orbit.update(); } function focusPresetViewport(name: string): void { if (name === 'weapon-marksman-standalone') { focusPoint(new THREE.Vector3(0, 0.08, 0.12), 0.52); return; } if (name === 'leaper-arc') { focusPoint(new THREE.Vector3(0, 0.72, 0.06), 1.34); return; } if (name === 'drone-heavy-quadcannon') { focusPoint(new THREE.Vector3(0, 0.96, 0.08), 1.3); return; } if (name === 'drone-scout-recon') { focusPoint(new THREE.Vector3(0, 0.92, 0.02), 1.06); return; } if (name === 'flip-target') { focusPoint(new THREE.Vector3(0, 0.92, 0.02), 1.02); return; } if (name === 'flip-target-heavy') { focusPoint(new THREE.Vector3(0, 0.96, 0.02), 1.14); return; } if (name === 'flip-target-scout') { focusPoint(new THREE.Vector3(0, 0.98, 0.04), 0.96); return; } if (name === 'flip-target-breacher') { focusPoint(new THREE.Vector3(0, 1.0, 0.04), 1.22); } } function getSceneContentBounds(): THREE.Box3 { const bounds = new THREE.Box3(); const source = moduleGroup.children.length > 0 ? moduleGroup : nodeGroup; if (source.children.length === 0) { bounds.set(new THREE.Vector3(-0.5, 0, -0.5), new THREE.Vector3(0.5, 1, 0.5)); return bounds; } bounds.setFromObject(source); if (Number.isFinite(bounds.min.x) && Number.isFinite(bounds.max.x)) { return bounds; } bounds.set(new THREE.Vector3(-0.5, 0, -0.5), new THREE.Vector3(0.5, 1, 0.5)); return bounds; } function getDefaultScenePropScale(kind: ScenePropKind, bounds: THREE.Box3): Vec3Like { const size = bounds.getSize(new THREE.Vector3()); switch (kind) { case 'ground': return vec3(Math.max(2.8, size.x + 1.2), 0.16, Math.max(2.8, size.z + 1.2)); case 'terrain': return vec3(Math.max(2.8, size.x + 1.6), 1, Math.max(2.8, size.z + 1.6)); case 'ramp': return vec3(Math.max(1.8, size.x * 0.9), 0.18, Math.max(1.4, size.z * 0.7)); case 'box': return vec3(Math.max(0.7, size.x * 0.35), 0.34, Math.max(0.7, size.z * 0.35)); case 'steps': return vec3(Math.max(1.1, size.x * 0.55), 0.54, Math.max(1.2, size.z * 0.45)); case 'rock': return vec3(Math.max(0.65, size.x * 0.28), 0.38, Math.max(0.65, size.z * 0.28)); default: return vec3(1, 0.2, 1); } } function createScenePropEntry(kind: ScenePropKind, underUnit: boolean): ScenePropEntry { const bounds = getSceneContentBounds(); const center = bounds.getCenter(new THREE.Vector3()); const scale = getDefaultScenePropScale(kind, bounds); const topY = bounds.min.y - 0.01; const position = underUnit ? vec3(center.x, kind === 'terrain' ? topY : topY - scale.y * 0.5, center.z) : vec3(0, -scale.y * 0.5, 0); const rotation = kind === 'ramp' ? vec3(-18, 0, 0) : undefined; return { id: `scene-prop-${state.nextIds.sceneProp++}`, kind, position, rotation, scale, terrain: kind === 'terrain' ? { preset: 'rolling', amplitude: 0.12, frequency: 1.35, resolution: 24 } : undefined }; } function buildTerrainPatchGeometry(settings: ScenePropTerrainSettings): THREE.BufferGeometry { const resolution = THREE.MathUtils.clamp(Math.round(settings.resolution), 4, 64); const geometry = new THREE.PlaneGeometry(1, 1, resolution, resolution); geometry.rotateX(-Math.PI / 2); const position = geometry.attributes.position; for (let index = 0; index < position.count; index += 1) { const x = position.getX(index); const z = position.getZ(index); let y = 0; switch (settings.preset) { case 'ridge': { const ridgeA = Math.sin(x * settings.frequency * Math.PI * 2.8); const ridgeB = Math.sin((x + z * 0.18) * settings.frequency * Math.PI * 5.4); y = settings.amplitude * ((ridgeA * 0.8) + (ridgeB * 0.15)); break; } case 'crater': { const radius = Math.sqrt((x * x) + (z * z)); const bowl = -Math.max(0, 1 - radius * 1.9); const rim = Math.exp(-Math.pow(radius - 0.34, 2) * 32); const ripples = Math.sin((x - z * 0.2) * settings.frequency * Math.PI * 2.1) * 0.08; y = settings.amplitude * ((bowl * 0.95) + (rim * 0.65) + ripples); break; } case 'side-slope': { const slope = x * 0.95; const stepNoise = Math.sin(z * settings.frequency * Math.PI * 1.35) * 0.22; const crossNoise = Math.sin(x * settings.frequency * Math.PI * 0.9) * 0.08; y = settings.amplitude * (slope + stepNoise + crossNoise); break; } case 'custom': case 'rolling': default: { const waveA = Math.sin(x * settings.frequency * Math.PI * 2.2); const waveB = Math.cos(z * settings.frequency * Math.PI * 1.7); const waveC = Math.sin((x - z * 0.65) * settings.frequency * Math.PI * 1.3); y = settings.amplitude * ((waveA * 0.5) + (waveB * 0.35) + (waveC * 0.25)); break; } } position.setY(index, y); } position.needsUpdate = true; geometry.computeVertexNormals(); return geometry; } function createScenePropObject(prop: ScenePropEntry): THREE.Object3D { const root = new THREE.Group(); root.name = prop.id; root.userData = { id: prop.id, kind: 'scene-prop' }; root.layers.set(LAYER_SCENE_PROP); root.position.set(prop.position.x, prop.position.y, prop.position.z); if (prop.rotation) { root.rotation.set( THREE.MathUtils.degToRad(prop.rotation.x), THREE.MathUtils.degToRad(prop.rotation.y), THREE.MathUtils.degToRad(prop.rotation.z) ); } root.scale.set(prop.scale.x, prop.scale.y, prop.scale.z); const addMesh = ( geometry: THREE.BufferGeometry, position?: THREE.Vector3, rotation?: THREE.Euler, scale?: THREE.Vector3 ): void => { const mesh = new THREE.Mesh(geometry, scenePropMaterial); if (position) mesh.position.copy(position); if (rotation) mesh.rotation.copy(rotation); if (scale) mesh.scale.copy(scale); mesh.castShadow = false; mesh.receiveShadow = true; mesh.userData = { id: prop.id, kind: 'scene-prop' }; mesh.layers.set(LAYER_SCENE_PROP); root.add(mesh); }; switch (prop.kind) { case 'ground': addMesh(new THREE.BoxGeometry(1, 1, 1)); break; case 'terrain': addMesh(buildTerrainPatchGeometry(prop.terrain ?? { preset: 'rolling', amplitude: 0.12, frequency: 1.35, resolution: 24 })); break; case 'ramp': addMesh( new THREE.BoxGeometry(1, 1, 1), new THREE.Vector3(0, 0.05, 0), new THREE.Euler(-Math.PI * 0.11, 0, 0), new THREE.Vector3(1, 1, 1) ); break; case 'box': addMesh(new THREE.BoxGeometry(1, 1, 1)); break; case 'steps': { addMesh(new THREE.BoxGeometry(1, 0.34, 0.42), new THREE.Vector3(0, -0.28, 0.28)); addMesh(new THREE.BoxGeometry(1, 0.34, 0.36), new THREE.Vector3(0, -0.02, -0.02)); addMesh(new THREE.BoxGeometry(1, 0.34, 0.3), new THREE.Vector3(0, 0.24, -0.28)); break; } case 'rock': { addMesh(new THREE.IcosahedronGeometry(0.55, 1), new THREE.Vector3(0, 0.02, 0), new THREE.Euler(0.35, 0.18, 0.12)); addMesh(new THREE.BoxGeometry(0.42, 0.24, 0.38), new THREE.Vector3(-0.12, -0.18, 0.08), new THREE.Euler(0.24, -0.3, 0.16)); break; } } return root; } function rebuildSceneProps(): void { clearGroup(scenePropGroup, false, true); scenePropMeshMap = new Map(); for (const prop of state.sceneProps) { const object = createScenePropObject(prop); scenePropGroup.add(object); scenePropMeshMap.set(prop.id, object); } refreshRaycastTargets(); applyVisualState(); updateVisibility(); } function applyTerrainPresetInputs(preset: TerrainPatchPreset): void { if (preset === 'custom') return; const config = TERRAIN_PATCH_PRESETS[preset]; controls.sceneTerrainPreset.value = preset; controls.sceneTerrainAmplitude.value = config.amplitude.toFixed(2); controls.sceneTerrainFrequency.value = config.frequency.toFixed(2); controls.sceneTerrainResolution.value = String(config.resolution); } function readSceneTerrainInputs(): ScenePropTerrainSettings { const preset = (controls.sceneTerrainPreset.value as TerrainPatchPreset) || 'rolling'; return { preset, amplitude: THREE.MathUtils.clamp(parseNumber(controls.sceneTerrainAmplitude.value, 0.12), 0, 1), frequency: THREE.MathUtils.clamp(parseNumber(controls.sceneTerrainFrequency.value, 1.35), 0.1, 8), resolution: THREE.MathUtils.clamp(Math.round(parseNumber(controls.sceneTerrainResolution.value, 24)), 4, 64) }; } function updateScenePropInfo(prop?: ScenePropEntry): void { if (!prop) { controls.scenePropInfo.textContent = 'Editor-only terrain/props. Spawn under the unit, then use the gizmo to place and rotate.'; controls.sceneTerrainApply.disabled = true; return; } controls.sceneTerrainApply.disabled = prop.kind !== 'terrain'; if (prop.kind === 'terrain' && prop.terrain) { controls.sceneTerrainPreset.value = prop.terrain.preset ?? 'rolling'; controls.sceneTerrainAmplitude.value = prop.terrain.amplitude.toFixed(2); controls.sceneTerrainFrequency.value = prop.terrain.frequency.toFixed(2); controls.sceneTerrainResolution.value = String(prop.terrain.resolution); } const rotation = prop.rotation ?? vec3(0, 0, 0); controls.scenePropInfo.textContent = [ `Prop: ${prop.id}`, `Kind: ${prop.kind}`, `Pos: ${prop.position.x.toFixed(2)} ${prop.position.y.toFixed(2)} ${prop.position.z.toFixed(2)}`, `Rot: ${rotation.x.toFixed(0)} ${rotation.y.toFixed(0)} ${rotation.z.toFixed(0)}`, `Scale: ${prop.scale.x.toFixed(2)} ${prop.scale.y.toFixed(2)} ${prop.scale.z.toFixed(2)}`, ...(prop.kind === 'terrain' && prop.terrain ? [`Terrain: ${prop.terrain.preset} · amp ${prop.terrain.amplitude.toFixed(2)} · freq ${prop.terrain.frequency.toFixed(2)} · res ${prop.terrain.resolution}`] : []) ].join('\n'); } function refreshScenePropOptions(): void { const selected = controls.scenePropList.value || state.selection.id; controls.scenePropList.innerHTML = ''; if (state.sceneProps.length === 0) { const option = document.createElement('option'); option.value = ''; option.textContent = 'No terrain / props'; controls.scenePropList.appendChild(option); controls.scenePropList.disabled = true; controls.scenePropFocus.disabled = true; controls.scenePropDelete.disabled = true; updateScenePropInfo(); return; } controls.scenePropList.disabled = false; for (const prop of state.sceneProps) { const option = document.createElement('option'); option.value = prop.id; option.textContent = `${prop.kind} · ${prop.id}`; if (prop.id === selected) { option.selected = true; } controls.scenePropList.appendChild(option); } const active = getScenePropById(controls.scenePropList.value) ?? state.sceneProps[0]; controls.scenePropList.value = active.id; controls.scenePropFocus.disabled = false; controls.scenePropDelete.disabled = false; updateScenePropInfo(active); } function selectScenePropInstance(id: string): void { const prop = getScenePropById(id); if (!prop) return; setSelection('scene-prop', id); controls.scenePropList.value = id; updateScenePropInfo(prop); refreshTransformTarget(); setStatus(`Selected terrain/prop ${id}.`); } function spawnSceneProp(kind: ScenePropKind, underUnit: boolean): void { const prop = createScenePropEntry(kind, underUnit); if (kind === 'terrain') { prop.terrain = readSceneTerrainInputs(); } state.sceneProps.push(prop); rebuildSceneProps(); refreshUi(); selectScenePropInstance(prop.id); } function deleteScenePropById(id: string): void { const index = state.sceneProps.findIndex((entry) => entry.id === id); if (index < 0) return; state.sceneProps.splice(index, 1); if (state.selection.kind === 'scene-prop' && state.selection.id === id) { setSelection(null); } rebuildSceneProps(); refreshUi(); setStatus(`Deleted terrain/prop ${id}.`); } function clearSceneProps(): void { state.sceneProps = []; if (state.selection.kind === 'scene-prop') { setSelection(null); } rebuildSceneProps(); refreshUi(); setStatus('Cleared editor terrain/props.'); } function getBallFootAnchors(): AnchorEntry[] { return state.anchors.filter((anchor) => anchor.tags.includes('ball_foot')); } function resolveBaseNodeWorldTransform(nodeId: string): { position: THREE.Vector3; quaternion: THREE.Quaternion } | null { const node = getNodeById(nodeId); if (!node) return null; return resolveNodeWorldTransform(node, new Set(), false); } function getDistanceForTwoBoneBend( upperLength: number, lowerLength: number, bendRad: number ): number { const clampedBend = THREE.MathUtils.clamp(bendRad, 0, Math.PI - 1e-4); const distanceSq = upperLength * upperLength + lowerLength * lowerLength + 2 * upperLength * lowerLength * Math.cos(clampedBend); return Math.sqrt(Math.max(distanceSq, 1e-8)); } function clampDirectionToFrameEnvelope( referenceFrame: Frame, desiredDirection: THREE.Vector3, limitsDeg: { yawMin: number; yawMax: number; pitchMin: number; pitchMax: number; } ): THREE.Vector3 { const direction = desiredDirection.clone().normalize(); const local = new THREE.Vector3( direction.dot(referenceFrame.right), direction.dot(referenceFrame.up), direction.dot(referenceFrame.forward) ).normalize(); const yaw = Math.atan2(local.x, Math.max(1e-5, local.z)); const pitch = Math.atan2(local.y, Math.max(1e-5, Math.sqrt(local.x * local.x + local.z * local.z))); const clampedYaw = THREE.MathUtils.clamp( yaw, THREE.MathUtils.degToRad(limitsDeg.yawMin), THREE.MathUtils.degToRad(limitsDeg.yawMax) ); const clampedPitch = THREE.MathUtils.clamp( pitch, THREE.MathUtils.degToRad(limitsDeg.pitchMin), THREE.MathUtils.degToRad(limitsDeg.pitchMax) ); const cosPitch = Math.cos(clampedPitch); return referenceFrame.right.clone().multiplyScalar(Math.sin(clampedYaw) * cosPitch) .add(referenceFrame.up.clone().multiplyScalar(Math.sin(clampedPitch))) .add(referenceFrame.forward.clone().multiplyScalar(Math.cos(clampedYaw) * cosPitch)) .normalize(); } function makeTwistSuppressedQuaternion( referenceFrame: Frame, direction: THREE.Vector3, preferredUp: THREE.Vector3, suppression = 0.85 ): THREE.Quaternion { const forward = direction.clone().normalize(); const refForward = referenceFrame.forward.clone().normalize(); const refUp = referenceFrame.up.clone().normalize(); const alignQuat = new THREE.Quaternion().setFromUnitVectors(refForward, forward); const twistFreeUp = refUp.clone().applyQuaternion(alignQuat).projectOnPlane(forward); const preferredProjectedUp = preferredUp.clone().projectOnPlane(forward); const safeTwistFreeUp = twistFreeUp.lengthSq() > 1e-5 ? twistFreeUp.normalize() : referenceFrame.right.clone().cross(forward).normalize(); const safePreferredUp = preferredProjectedUp.lengthSq() > 1e-5 ? preferredProjectedUp.normalize() : safeTwistFreeUp.clone(); const blendedUp = safeTwistFreeUp.clone().lerp(safePreferredUp, THREE.MathUtils.clamp(1 - suppression, 0, 1)).normalize(); return frameToQuaternion(makeFrame(referenceFrame.origin.clone(), forward, blendedUp)); } function getPoleAlignedUp( direction: THREE.Vector3, bendNormal: THREE.Vector3, fallbackUp: THREE.Vector3 ): THREE.Vector3 { const forward = direction.clone().normalize(); if (forward.lengthSq() <= 1e-6) { return fallbackUp.clone().normalize(); } const projectHint = (hint: THREE.Vector3): THREE.Vector3 | null => { const projected = hint.clone().projectOnPlane(forward); if (projected.lengthSq() <= 1e-6) return null; return projected.normalize(); }; let up = new THREE.Vector3().crossVectors(bendNormal, forward); if (up.lengthSq() <= 1e-6) { up = new THREE.Vector3().crossVectors(forward, bendNormal); } let resolvedUp = projectHint(up); const projectedFallback = projectHint(fallbackUp); if (!resolvedUp && projectedFallback) { resolvedUp = projectedFallback.clone(); } if (!resolvedUp) { resolvedUp = projectHint(Math.abs(forward.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0)) ?? new THREE.Vector3(0, 1, 0); } if (projectedFallback && resolvedUp.dot(projectedFallback) < 0) { resolvedUp.multiplyScalar(-1); } return resolvedUp.normalize(); } function clampQuaternionToReferenceFrame( referenceFrame: Frame, quaternion: THREE.Quaternion, limitsDeg: { yawMin: number; yawMax: number; pitchMin: number; pitchMax: number; rollMin: number; rollMax: number; } ): THREE.Quaternion { const referenceQuat = frameToQuaternion(referenceFrame); const localQuat = referenceQuat.clone().invert().multiply(quaternion); const localEuler = new THREE.Euler().setFromQuaternion(localQuat, 'YXZ'); localEuler.x = THREE.MathUtils.clamp( localEuler.x, THREE.MathUtils.degToRad(limitsDeg.pitchMin), THREE.MathUtils.degToRad(limitsDeg.pitchMax) ); localEuler.y = THREE.MathUtils.clamp( localEuler.y, THREE.MathUtils.degToRad(limitsDeg.yawMin), THREE.MathUtils.degToRad(limitsDeg.yawMax) ); localEuler.z = THREE.MathUtils.clamp( localEuler.z, THREE.MathUtils.degToRad(limitsDeg.rollMin), THREE.MathUtils.degToRad(limitsDeg.rollMax) ); return referenceQuat.multiply(new THREE.Quaternion().setFromEuler(localEuler)); } function solveTwoBoneJoint( start: THREE.Vector3, end: THREE.Vector3, upperLength: number, lowerLength: number, bendNormal: THREE.Vector3, referenceMid: THREE.Vector3, options?: { preferredUpperDirection?: THREE.Vector3; minBendRad?: number; maxBendRad?: number; } ): THREE.Vector3 { const startToEnd = end.clone().sub(start); const rawDistance = startToEnd.length(); if (rawDistance <= 1e-5) { return referenceMid.clone(); } const defaultMaxReach = Math.max(upperLength + lowerLength - 1e-4, 1e-4); const defaultMinReach = Math.max(Math.abs(upperLength - lowerLength) + 1e-4, 1e-4); const minBendDistance = options?.maxBendRad != null ? getDistanceForTwoBoneBend(upperLength, lowerLength, options.maxBendRad) : defaultMinReach; const maxBendDistance = options?.minBendRad != null ? getDistanceForTwoBoneBend(upperLength, lowerLength, options.minBendRad) : defaultMaxReach; const minReach = Math.max(defaultMinReach, Math.min(minBendDistance, maxBendDistance)); const maxReach = Math.min(defaultMaxReach, Math.max(minBendDistance, maxBendDistance)); const distance = THREE.MathUtils.clamp(rawDistance, minReach, maxReach); const axis = startToEnd.clone().normalize(); let planeTangent = new THREE.Vector3().crossVectors(bendNormal, axis); if (planeTangent.lengthSq() <= 1e-5) { planeTangent = referenceMid.clone().sub(start).sub(axis.clone().multiplyScalar(referenceMid.clone().sub(start).dot(axis))); } if (planeTangent.lengthSq() <= 1e-5) { planeTangent = Math.abs(axis.y) < 0.92 ? new THREE.Vector3().crossVectors(new THREE.Vector3(0, 1, 0), axis) : new THREE.Vector3().crossVectors(new THREE.Vector3(1, 0, 0), axis); } planeTangent.normalize(); const planeBitangent = new THREE.Vector3().crossVectors(axis, planeTangent).normalize(); const along = ((upperLength * upperLength) - (lowerLength * lowerLength) + (distance * distance)) / (2 * distance); const heightSq = Math.max(0, (upperLength * upperLength) - (along * along)); const height = Math.sqrt(heightSq); const projectedReference = referenceMid.clone().sub(start); const sideSign = Math.sign(projectedReference.dot(planeTangent)) || 1; let circleDirection = planeTangent.clone().multiplyScalar(sideSign); const preferredUpperDirection = options?.preferredUpperDirection; if (preferredUpperDirection && preferredUpperDirection.lengthSq() > 1e-5) { const projectedPreferred = preferredUpperDirection.clone() .sub(axis.clone().multiplyScalar(preferredUpperDirection.dot(axis))); if (projectedPreferred.lengthSq() > 1e-5) { circleDirection = projectedPreferred.normalize(); } } if (circleDirection.dot(planeTangent) * sideSign < 0 && !preferredUpperDirection) { circleDirection = planeTangent.clone().multiplyScalar(sideSign); } if (circleDirection.lengthSq() <= 1e-5) { circleDirection = planeBitangent.lengthSq() > 1e-5 ? planeBitangent : planeTangent.clone(); } return start .clone() .add(axis.multiplyScalar(along)) .add(circleDirection.multiplyScalar(height)); } function averagePoints(points: THREE.Vector3[]): THREE.Vector3 | null { if (points.length === 0) return null; const sum = new THREE.Vector3(); for (const point of points) { sum.add(point); } return sum.multiplyScalar(1 / points.length); } function formatLeaperIkLimitLabel(value: number): string { return `${value.toFixed(0)}°`; } function updateLeaperIkTiltLabels(): void { controls.leaperIkForeAftLimitLabel.textContent = formatLeaperIkLimitLabel(state.leaperIkForeAftLimitDeg); controls.leaperIkLateralLimitLabel.textContent = formatLeaperIkLimitLabel(state.leaperIkLateralLimitDeg); } function formatLeaperTerrainIkInfo(snapshot: LeaperTerrainIkDebugSnapshot): string { if (!snapshot.active) { return 'Leaper terrain IK preview inactive.'; } const lines = [ `Leaper terrain IK preview`, `Lift ${snapshot.bodyLift.toFixed(3)} m`, `Tilt ${snapshot.bodyTiltDeg.toFixed(1)} deg`, `Fore/Aft ${snapshot.foreAftTiltDeg.toFixed(1)} deg Left/Right ${snapshot.lateralTiltDeg.toFixed(1)} deg` ]; for (const row of snapshot.legRows) { lines.push( `${row.label} ${row.hit ? 'hit' : 'miss'} y ${row.contactY.toFixed(3)} dy ${row.deltaY.toFixed(3)} reach ${row.reachRatio.toFixed(3)} normal ${row.normalAngleDeg.toFixed(1)} deg` ); } return lines.join('\n'); } function formatLeaperTerrainIkInfoHtml(snapshot: LeaperTerrainIkDebugSnapshot): string { if (!snapshot.active) { return 'Leaper terrain IK preview inactive.'; } const tone = (warning: boolean, text: string): string => ( warning ? `${text}` : `${text}` ); const escape = (value: string): string => value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>'); const foreAftText = `Fore/Aft ${snapshot.foreAftTiltDeg.toFixed(1)} deg`; const lateralText = `Left/Right ${snapshot.lateralTiltDeg.toFixed(1)} deg`; const lines = [ `Leaper terrain IK preview`, `Lift ${snapshot.bodyLift.toFixed(3)} m`, `Tilt ${snapshot.bodyTiltDeg.toFixed(1)} deg`, `${tone(snapshot.foreAftClamped, foreAftText)} ${tone(snapshot.lateralClamped, lateralText)}` ]; for (const row of snapshot.legRows) { const missed = !row.hit; const overreach = row.reachRatio > 0.82; const statusColor = missed ? '#ff7b72' : overreach ? '#ffb86b' : '#c7f0d8'; const reachColor = overreach ? '#ffb86b' : '#d9ecff'; lines.push( `${escape(row.label)} ${missed ? 'miss' : 'hit'} ` + `y ${row.contactY.toFixed(3)} dy ${row.deltaY.toFixed(3)} ` + `reach ${row.reachRatio.toFixed(3)} ` + `normal ${row.normalAngleDeg.toFixed(1)} deg` ); } return lines.join('\n'); } function updateLeaperTerrainIkInfo(snapshot: LeaperTerrainIkDebugSnapshot): void { leaperTerrainIkDebugSnapshot = snapshot; controls.leaperIkInfo.innerHTML = formatLeaperTerrainIkInfoHtml(snapshot); } function buildLeaperTerrainPreviewOverrides(): { overrides: Map; snapshot: LeaperTerrainIkDebugSnapshot; } { const overrides = new Map(); const snapshot: LeaperTerrainIkDebugSnapshot = { active: false, bodyLift: 0, bodyTiltDeg: 0, foreAftTiltDeg: 0, lateralTiltDeg: 0, foreAftClamped: false, lateralClamped: false, legRows: [] }; if (!state.enableLeaperTerrainIk || currentBasePresetName !== 'leaper-arc') { return { overrides, snapshot }; } const legIds = ['fl', 'fr', 'bl', 'br'] as const; const coreFrontBase = resolveBaseNodeWorldTransform('node-core-front'); const coreBackBase = resolveBaseNodeWorldTransform('node-core-back'); const dorsalBase = resolveBaseNodeWorldTransform('node-dorsal'); const eyeBase = resolveBaseNodeWorldTransform('node-eye'); if (!coreFrontBase || !coreBackBase || !dorsalBase || !eyeBase) { return { overrides, snapshot }; } const motionPreset = characterSandboxState.leaperMotionPreset; const motionEnabled = characterSandboxState.enabled; const motionElapsedSec = getLeaperSandboxMotionElapsedSec(); const sampleLeaperMotion = (legId: (typeof legIds)[number]) => sampleLeaperSandboxMotion(motionPreset, motionElapsedSec, legId, motionEnabled); const legInputs: Array<{ legId: LeaperArcTerrainIkLegId; shoulderBase: { position: THREE.Vector3; quaternion: THREE.Quaternion }; kneeBase: { position: THREE.Vector3; quaternion: THREE.Quaternion }; ankleBase: { position: THREE.Vector3; quaternion: THREE.Quaternion }; footBase: { position: THREE.Vector3; quaternion: THREE.Quaternion }; }> = []; const sampleGround = (x: number, z: number, fallbackY: number) => { const probe = raycastFootProbe(new THREE.Vector3(x, fallbackY, z)); return { height: probe.hit.y, hit: !probe.missed, normal: probe.normal.clone(), source: probe.missed ? 'fallback' : 'raycast', confidence: probe.missed ? 0 : 1 }; }; for (const legId of legIds) { const shoulderBase = resolveBaseNodeWorldTransform(`node-shoulder-${legId}`); const kneeBase = resolveBaseNodeWorldTransform(`node-knee-${legId}`); const ankleBase = resolveBaseNodeWorldTransform(`node-ankle-${legId}`); const footBase = resolveBaseNodeWorldTransform(`node-foot-${legId}`); if (!shoulderBase || !kneeBase || !ankleBase || !footBase) continue; legInputs.push({ legId, shoulderBase, kneeBase, ankleBase, footBase }); } if (legInputs.length === 0) { return { overrides, snapshot }; } const solverInputLegs = legInputs.map((leg) => { const sideSign = (leg.legId.endsWith('l') ? -1 : 1) as -1 | 1; const rowIndex = leg.legId.startsWith('f') ? 0 : 1; const legMotion = sampleLeaperMotion(leg.legId); const pinGround = sampleGround( leg.footBase.position.x, leg.footBase.position.z, leg.footBase.position.y ); const pinnedAnchorWorld = new THREE.Vector3( leg.footBase.position.x, pinGround.height + (pinGround.hit ? LEAPER_ARC_TERRAIN_IK_UNIT_EDITOR_PROFILE.groundContactOffset : 0), leg.footBase.position.z ); return { key: leg.legId, legId: leg.legId, rowIndex, sideSign, shoulderBase: leg.shoulderBase.position.clone(), kneeBase: leg.kneeBase.position.clone(), ankleBase: leg.ankleBase.position.clone(), footBase: leg.footBase.position.clone(), pinnedAnchorWorld: legMotion.footPinned ? pinnedAnchorWorld : null, legMotion }; }); const bodyForwardHint = coreFrontBase.position.clone().sub(coreBackBase.position).normalize(); const solved = solveLeaperArcTerrainIk({ legs: solverInputLegs, sampleGround, fallbackBodyForward: bodyForwardHint, config: buildLeaperArcTerrainIkUnitEditorConfig({ foreAftTiltLimitDeg: state.leaperIkForeAftLimitDeg, lateralTiltLimitDeg: state.leaperIkLateralLimitDeg }) }); if (!solved.body) { return { overrides, snapshot }; } const transformBodyNode = ( baseTransform: { position: THREE.Vector3; quaternion: THREE.Quaternion } ): { position: THREE.Vector3; quaternion: THREE.Quaternion } => { const relative = baseTransform.position.clone() .sub(solved.body!.baseBodyOrigin) .applyQuaternion(solved.body!.bodyDeltaQuaternion); return { position: solved.body!.targetBodyOrigin.clone().add(relative), quaternion: solved.body!.bodyDeltaQuaternion.clone().multiply(baseTransform.quaternion) }; }; for (const [nodeId, baseTransform] of [ ['node-core-front', coreFrontBase], ['node-core-back', coreBackBase], ['node-dorsal', dorsalBase], ['node-eye', eyeBase] ] as const) { const next = transformBodyNode(baseTransform); overrides.set(nodeId, next); } snapshot.active = true; snapshot.bodyLift = solved.body.bodyLift; snapshot.foreAftTiltDeg = THREE.MathUtils.radToDeg(solved.body.clampedForeAftTilt); snapshot.lateralTiltDeg = THREE.MathUtils.radToDeg(solved.body.clampedLateralTilt); snapshot.foreAftClamped = Math.abs(solved.body.rawForeAftTilt - solved.body.clampedForeAftTilt) > 1e-4; snapshot.lateralClamped = Math.abs(solved.body.rawLateralTilt - solved.body.clampedLateralTilt) > 1e-4; snapshot.bodyTiltDeg = THREE.MathUtils.radToDeg( Math.acos(THREE.MathUtils.clamp(solved.body.blendedNormal.dot(new THREE.Vector3(0, 1, 0)), -1, 1)) ); for (const leg of legInputs) { const pose = solved.byLegKey.get(leg.legId); if (!pose) continue; overrides.set(`node-shoulder-${leg.legId}`, { position: pose.shoulder, quaternion: pose.shoulderQuaternion }); overrides.set(`node-knee-${leg.legId}`, { position: pose.knee, quaternion: pose.kneeQuaternion }); overrides.set(`node-ankle-${leg.legId}`, { position: pose.ankle, quaternion: pose.ankleQuaternion }); overrides.set(`node-foot-${leg.legId}`, { position: pose.foot, quaternion: pose.footQuaternion }); const upperLength = leg.shoulderBase.position.distanceTo(leg.kneeBase.position); const lowerLength = leg.kneeBase.position.distanceTo(leg.ankleBase.position); snapshot.legRows.push({ label: leg.legId.toUpperCase(), hit: pose.groundHit, contactY: pose.foot.y, deltaY: pose.foot.y - leg.footBase.position.y, reachRatio: pose.shoulder.distanceTo(pose.ankle) / Math.max(upperLength + lowerLength, 1e-5), normalAngleDeg: THREE.MathUtils.radToDeg( Math.acos(THREE.MathUtils.clamp(pose.footUp.clone().normalize().dot(new THREE.Vector3(0, 1, 0)), -1, 1)) ) }); } return { overrides, snapshot }; } function areNodePreviewOverridesEqual( a: Map, b: Map, epsilon = 1e-4 ): boolean { if (a.size !== b.size) return false; for (const [id, entryA] of a.entries()) { const entryB = b.get(id); if (!entryB) return false; if (entryA.position && entryB.position) { if (entryA.position.distanceToSquared(entryB.position) > epsilon * epsilon) return false; } else if (entryA.position || entryB.position) { return false; } if (entryA.quaternion && entryB.quaternion) { const dot = Math.abs(entryA.quaternion.dot(entryB.quaternion)); if (1 - dot > epsilon) return false; } else if (entryA.quaternion || entryB.quaternion) { return false; } } return true; } function updateLeaperTerrainPreview(): boolean { const { overrides: nextOverrides, snapshot } = buildLeaperTerrainPreviewOverrides(); updateLeaperTerrainIkInfo(snapshot); if (areNodePreviewOverridesEqual(nodePreviewOverrideMap, nextOverrides)) { return false; } nodePreviewOverrideMap = nextOverrides; syncNodeMeshesFromData(); rebuildSkeletonLinks(); invalidateEdgeCache(); syncDerivedTransforms(true); applyVisualState(); markRenderDirty(2); return true; } function getFootProbeLabel(anchor: AnchorEntry): string { const id = anchor.id.toLowerCase(); if (id.endsWith('-fl')) return 'FL'; if (id.endsWith('-fr')) return 'FR'; if (id.endsWith('-bl')) return 'BL'; if (id.endsWith('-br')) return 'BR'; const hasFront = anchor.tags.includes('front'); const hasBack = anchor.tags.includes('rear') || anchor.tags.includes('back'); const hasLeft = anchor.tags.includes('left'); const hasRight = anchor.tags.includes('right'); if ((hasFront || hasBack) && (hasLeft || hasRight)) { return `${hasFront ? 'F' : 'B'}${hasLeft ? 'L' : 'R'}`; } return anchor.id.replace(/^anchor-/, '').slice(0, 6).toUpperCase(); } function getFootProbeLabelTexture(label: string): THREE.CanvasTexture { const cached = footProbeLabelTextureCache.get(label); if (cached) return cached; const canvas = document.createElement('canvas'); canvas.width = 96; canvas.height = 48; const ctx = canvas.getContext('2d'); if (!ctx) { const fallback = new THREE.CanvasTexture(canvas); footProbeLabelTextureCache.set(label, fallback); return fallback; } ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(8, 15, 26, 0.82)'; ctx.strokeStyle = 'rgba(120, 184, 255, 0.9)'; ctx.lineWidth = 3; ctx.fillRect(4, 4, canvas.width - 8, canvas.height - 8); ctx.strokeRect(4, 4, canvas.width - 8, canvas.height - 8); ctx.fillStyle = '#eaf4ff'; ctx.font = 'bold 26px Segoe UI'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(label, canvas.width * 0.5, canvas.height * 0.54); const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; footProbeLabelTextureCache.set(label, texture); return texture; } function createFootProbeLabelSprite(label: string): THREE.Sprite { const material = new THREE.SpriteMaterial({ map: getFootProbeLabelTexture(label), transparent: true, depthWrite: false, depthTest: false }); const sprite = new THREE.Sprite(material); sprite.scale.set(0.18, 0.09, 1); return sprite; } function raycastFootProbe(origin: THREE.Vector3): { start: THREE.Vector3; hit: THREE.Vector3; normal: THREE.Vector3; missed: boolean; } { const start = origin.clone().add(new THREE.Vector3(0, 1.5, 0)); const fallback = new THREE.Vector3(origin.x, 0, origin.z); if (raycastScenePropTargets.length === 0) { return { start: origin.clone(), hit: fallback, normal: new THREE.Vector3(0, 1, 0), missed: true }; } const probeRaycaster = new THREE.Raycaster(start, new THREE.Vector3(0, -1, 0), 0, 4); probeRaycaster.layers.mask = 0; probeRaycaster.layers.enable(LAYER_SCENE_PROP); const hits = probeRaycaster.intersectObjects(raycastScenePropTargets, true); if (hits.length === 0) { return { start: origin.clone(), hit: fallback, normal: new THREE.Vector3(0, 1, 0), missed: true }; } const hit = hits[0]; const normalMatrix = new THREE.Matrix3().getNormalMatrix(hit.object.matrixWorld); const normal = hit.face?.normal.clone().applyMatrix3(normalMatrix).normalize() ?? new THREE.Vector3(0, 1, 0); return { start: origin.clone(), hit: hit.point.clone(), normal, missed: false }; } function updateFootProbeOverlay(): void { clearGroup(footProbeGroup, false, true); if (!state.showFootProbes) return; const anchors = getBallFootAnchors(); if (anchors.length === 0) return; for (const anchor of anchors) { const frame = resolveAnchorFrame(anchor); if (!frame) continue; const result = raycastFootProbe(frame.origin.clone()); const startMarker = new THREE.Mesh(footProbeMarkerGeometry, footProbeStartMaterial); startMarker.position.copy(result.start); footProbeGroup.add(startMarker); const hitMarker = new THREE.Mesh( footProbeMarkerGeometry, result.missed ? footProbeMissMarkerMaterial : footProbeHitMaterial ); hitMarker.position.copy(result.hit); footProbeGroup.add(hitMarker); const contactLine = new THREE.Line( new THREE.BufferGeometry().setFromPoints([result.start, result.hit]), result.missed ? footProbeMissMaterial : footProbeLineMaterial ); footProbeGroup.add(contactLine); const normalTip = result.hit.clone().add(result.normal.clone().multiplyScalar(0.18)); const normalLine = new THREE.Line( new THREE.BufferGeometry().setFromPoints([result.hit, normalTip]), footProbeNormalMaterial ); footProbeGroup.add(normalLine); if (state.showFootProbeLabels) { const labelSprite = createFootProbeLabelSprite(getFootProbeLabel(anchor)); labelSprite.position.copy(result.hit).add(new THREE.Vector3(0, 0.09, 0)); footProbeGroup.add(labelSprite); } } } function applySceneTerrainToSelected(): void { if (state.selection.kind !== 'scene-prop' || !state.selection.id) return; const prop = getScenePropById(state.selection.id); if (!prop || prop.kind !== 'terrain') return; prop.terrain = readSceneTerrainInputs(); rebuildSceneProps(); refreshUi(); selectScenePropInstance(prop.id); setStatus(`Updated terrain patch ${prop.id}.`); } function focusSelection(): void { if (state.selection.kind === 'node' && state.selection.id) { const node = getNodeById(state.selection.id); if (node) { focusPoint(getNodeWorldPosition(node)); } return; } if (state.selection.kind === 'edge' && state.selection.id) { const edge = getEdgeById(state.selection.id); if (edge) { const curve = buildCurve(edge); if (curve) focusPoint(curve.getPointAt(0.5)); } return; } if (state.selection.kind === 'anchor' && state.selection.id) { const anchor = getAnchorById(state.selection.id); const frame = anchor ? resolveAnchorFrame(anchor) : null; if (frame) focusPoint(frame.origin); return; } if (state.selection.kind === 'attachment' && state.selection.id) { const mesh = getAttachmentSelectionMesh(state.selection.id, state.attachmentInstanceIndex); if (mesh) { mesh.updateMatrixWorld(true); const pos = new THREE.Vector3(); pos.setFromMatrixPosition(mesh.matrixWorld); focusPoint(pos); } return; } if (state.selection.kind === 'scene-prop' && state.selection.id) { const object = scenePropMeshMap.get(state.selection.id); if (object) { object.updateMatrixWorld(true); const bounds = new THREE.Box3().setFromObject(object); const center = bounds.getCenter(new THREE.Vector3()); const size = bounds.getSize(new THREE.Vector3()); focusPoint(center, Math.max(size.length() * 0.6, 0.7)); } } } function buildProxySdf(): { mesh: ProceduralMesh; stats: string } | null { const sdfFns: SdfFn[] = []; const bounds = { min: { x: Infinity, y: Infinity, z: Infinity }, max: { x: -Infinity, y: -Infinity, z: -Infinity } }; const expand = (p: Vec3Like, r: number) => { bounds.min.x = Math.min(bounds.min.x, p.x - r); bounds.min.y = Math.min(bounds.min.y, p.y - r); bounds.min.z = Math.min(bounds.min.z, p.z - r); bounds.max.x = Math.max(bounds.max.x, p.x + r); bounds.max.y = Math.max(bounds.max.y, p.y + r); bounds.max.z = Math.max(bounds.max.z, p.z + r); }; const expandPoint = (p: Vec3Like) => { bounds.min.x = Math.min(bounds.min.x, p.x); bounds.min.y = Math.min(bounds.min.y, p.y); bounds.min.z = Math.min(bounds.min.z, p.z); bounds.max.x = Math.max(bounds.max.x, p.x); bounds.max.y = Math.max(bounds.max.y, p.y); bounds.max.z = Math.max(bounds.max.z, p.z); }; const expandWithMatrix = (min: Vec3Like, max: Vec3Like, matrix: THREE.Matrix4) => { const corners = [ new THREE.Vector3(min.x, min.y, min.z), new THREE.Vector3(min.x, min.y, max.z), new THREE.Vector3(min.x, max.y, min.z), new THREE.Vector3(min.x, max.y, max.z), new THREE.Vector3(max.x, min.y, min.z), new THREE.Vector3(max.x, min.y, max.z), new THREE.Vector3(max.x, max.y, min.z), new THREE.Vector3(max.x, max.y, max.z) ]; for (const corner of corners) { corner.applyMatrix4(matrix); expandPoint(fromThree(corner)); } }; for (const edge of state.edges) { const a = getNodeById(edge.a); const b = getNodeById(edge.b); if (!a || !b) continue; const va = fromThree(getNodeWorldPosition(a)); const vb = fromThree(getNodeWorldPosition(b)); sdfFns.push(sdfCapsule(va, vb, edge.radius)); expand(va, edge.radius); expand(vb, edge.radius); } for (const node of state.nodes) { const radius = DEFAULT_NODE_RADIUS * 1.2; const world = fromThree(getNodeWorldPosition(node)); sdfFns.push(translate(sdfSphere(radius), world)); expand(world, radius); } for (const attachment of state.attachments) { const moduleEntry = getModuleById(attachment.moduleId); if (!moduleEntry) continue; const frames = getAttachmentFrames(attachment); if (frames.length === 0) continue; for (const frame of frames) { if (attachment.sculpt?.enabled) { const sculpt = buildSculptSdf(attachment.sculpt, moduleEntry.def.size ?? { x: 0.14, y: 0.14, z: 0.18 }); if (sculpt) { const matrix = buildAttachmentMatrix(attachment, frame, moduleEntry); const inverse = matrix.clone().invert(); const e = inverse.elements; const localSdf = sculpt.sdf; const worldSdf: SdfFn = (p) => { const x = p.x; const y = p.y; const z = p.z; const lx = e[0] * x + e[4] * y + e[8] * z + e[12]; const ly = e[1] * x + e[5] * y + e[9] * z + e[13]; const lz = e[2] * x + e[6] * y + e[10] * z + e[14]; return localSdf({ x: lx, y: ly, z: lz }); }; sdfFns.push(worldSdf); expandWithMatrix(sculpt.bounds.min, sculpt.bounds.max, matrix); continue; } } const def = resolveAttachmentDefinition(attachment, moduleEntry); const radius = estimateModuleRadius(def, attachment.scale); const matrix = buildAttachmentMatrix(attachment, frame, moduleEntry); const worldPos = new THREE.Vector3(); worldPos.setFromMatrixPosition(matrix); const pos = fromThree(worldPos); sdfFns.push(translate(sdfSphere(radius), pos)); expand(pos, radius); } } if (sdfFns.length === 0) { return null; } const sdf = sdfFns.reduce((acc, fn) => union(acc, fn)); const resolution = parseNumber(controls.bakeResolution.value, 36); const mesh = tryMeshSdf(sdf, bounds, { resolution, materialSlot: MaterialSlot.TEAM_PRIMARY }); if (!mesh) { queueSdfMesherHydration(); return null; } const stats = `Resolution: ${resolution}\nTriangles: ${mesh.indices.length / 3}\nBounds: ${ (bounds.max.x - bounds.min.x).toFixed(2) } x ${(bounds.max.y - bounds.min.y).toFixed(2)} x ${(bounds.max.z - bounds.min.z).toFixed(2)}`; return { mesh, stats }; } function buildSocketExport(): SocketExport[] { const filterRaw = controls.socketTagFilter?.value.trim() ?? ''; const filterTags = filterRaw.length > 0 ? filterRaw.split(',').map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0) : []; const includeGenerators = controls.socketIncludeGenerators?.checked ?? false; const matchesFilter = (tags: string[]): boolean => { if (filterTags.length === 0) return true; const set = new Set(tags.map((tag) => tag.toLowerCase())); return filterTags.some((tag) => set.has(tag)); }; const sockets: SocketExport[] = []; for (const anchor of state.anchors) { if (!anchor.exportSocket) continue; if (!matchesFilter(anchor.tags)) continue; const frame = resolveAnchorFrame(anchor); if (!frame) continue; sockets.push({ id: anchor.id, frame: { origin: fromThree(frame.origin), right: fromThree(frame.right), up: fromThree(frame.up), forward: fromThree(frame.forward) }, tags: [...anchor.tags] }); } if (includeGenerators) { for (const generator of state.generators) { const baseAnchor = getAnchorById(generator.baseAnchorId); if (!baseAnchor || !baseAnchor.exportSocket) continue; const generatorTags = [...baseAnchor.tags, `generator:${generator.id}`, `generator:${generator.type}`]; if (!matchesFilter(generatorTags)) continue; const frames = buildGeneratorFrames(generator); frames.forEach((frame, index) => { sockets.push({ id: `${baseAnchor.id}:${generator.id}:${index}`, frame: { origin: fromThree(frame.origin), right: fromThree(frame.right), up: fromThree(frame.up), forward: fromThree(frame.forward) }, tags: generatorTags }); }); } } return sockets; } function bakeProxy(): void { const result = buildProxySdf(); if (!result) { if (!sdfMesherModule) { setStatus('SDF mesher is loading in background. Try bake again in a moment.'); controls.bakeStats.textContent = 'SDF mesher loading...'; return; } setStatus('Nothing to bake yet. Add nodes/edges or modules.'); controls.bakeStats.textContent = 'No bake yet.'; return; } const geometry = proceduralMeshToThreeGeometry(result.mesh); geometry.computeBoundingSphere(); if (state.proxyObject && (state.proxyObject as THREE.Mesh).geometry) { (state.proxyObject as THREE.Mesh).geometry.dispose(); } const mesh = new THREE.Mesh(geometry, proxyMaterial); mesh.layers.set(LAYER_ATTACHMENT); state.proxyMesh = result.mesh; state.proxyObject = mesh; lastSocketExport = buildSocketExport(); const socketLine = `Sockets: ${lastSocketExport.length}`; controls.bakeStats.textContent = `${result.stats}\n${socketLine}`; rebuildScene(); setStatus(`Proxy SDF baked. ${socketLine}.`); } function exportProxyObj(): void { if (!state.proxyMesh) { setStatus('Bake a proxy before export.'); return; } const objText = proceduralMeshToObjText(state.proxyMesh); const blob = new Blob([objText], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'unit-editor-v2-proxy.obj'; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); setStatus('Exported OBJ.'); } function exportSockets(): void { if (!lastSocketExport.length) { lastSocketExport = buildSocketExport(); } if (!lastSocketExport.length) { setStatus('No sockets selected for export.'); return; } const blob = new Blob([JSON.stringify(lastSocketExport, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'unit-editor-v2-sockets.json'; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); setStatus('Exported sockets JSON.'); } function exportRuntimeTargetPackage(): void { const metadata = state.recipeMetadata?.runtimeTarget; if (!metadata) { setStatus('Current recipe has no runtime target metadata.'); return; } const sockets = buildSocketExport(); if (!sockets.length) { setStatus('No exported sockets available for runtime target package.'); return; } const payload: RuntimeTargetExportPackage = { recipeId: metadata.id, basePreset: currentBasePresetName, sockets, metadata: JSON.parse(JSON.stringify(metadata)) as RuntimeTargetMetadata }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${metadata.id}.runtime-target.json`; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); setStatus(`Exported runtime target package: ${metadata.id}.`); } interface RecipeDocument { version: 1; nodes: NodeEntry[]; edges: EdgeEntry[]; anchors: AnchorEntry[]; generators: GeneratorEntry[]; attachments: AttachmentEntry[]; metadata?: RecipeMetadata; } let currentBasePresetName = 'basic'; function cloneRecipeMetadata(metadata: RecipeMetadata | null | undefined): RecipeMetadata | undefined { if (!metadata) return undefined; return JSON.parse(JSON.stringify(metadata)) as RecipeMetadata; } function serializeRecipe(): RecipeDocument { return { version: 1, nodes: state.nodes, edges: state.edges, anchors: state.anchors, generators: state.generators, attachments: state.attachments, metadata: cloneRecipeMetadata(state.recipeMetadata) }; } function applyRecipe( doc: RecipeDocument, options?: { deferSceneRebuild?: boolean; deferUiRefresh?: boolean } ): void { stopPosePlayback(true); state.nodes = doc.nodes ?? []; state.edges = doc.edges ?? []; state.anchors = doc.anchors ?? []; state.generators = doc.generators ?? []; state.recipeMetadata = cloneRecipeMetadata(doc.metadata) ?? null; state.attachments = (doc.attachments ?? []).map((attachment) => { if (!attachment.sculpt?.enabled) return attachment; const moduleEntry = getModuleById(attachment.moduleId); const fallback = moduleEntry?.def.size ?? { x: 0.14, y: 0.14, z: 0.18 }; const sculpt = sanitizeSculpt(attachment.sculpt, fallback); return sculpt === attachment.sculpt ? attachment : { ...attachment, sculpt }; }); lastSocketExport = []; state.selection = { kind: null }; state.hover = { kind: null }; state.attachmentInstanceIndex = null; state.edgeDraftStart = null; state.dragState = null; state.anchorDragState = null; state.generatorDragState = null; state.pendingDeselect = false; invalidateEdgeCache(); syncNextIds(); updateToolbarState(); if (!options?.deferSceneRebuild) { rebuildScene(); } if (!options?.deferUiRefresh) { refreshUi(); } poseState.snapshot.clear(); if (poseState.clip) { rebuildPoseMappings(); updatePoseInfo(`Pose mappings updated · ${getPoseMappedNodeCount()}/${state.nodes.length} nodes mapped`); updatePoseDebugInfo(); } } function mergeAttachmentShape(base: AttachmentEntry['shape'], patch: AttachmentEntry['shape']): AttachmentEntry['shape'] { return { ...base, ...patch, taper: { ...base?.taper, ...patch?.taper }, chamfer: { ...base?.chamfer, ...patch?.chamfer }, profile: { ...base?.profile, ...patch?.profile } }; } function patchHumanAttachmentShape( attachments: AttachmentEntry[], attachmentId: string, patch: AttachmentEntry['shape'] ): void { const attachment = attachments.find((entry) => entry.id === attachmentId); if (!attachment) return; attachment.shape = attachment.shape ? mergeAttachmentShape(attachment.shape, patch) : patch; } function applyHumanPresetShapeRefinement(doc: RecipeDocument): void { const reanchor = (attachmentId: string, anchorId: string): void => { const attachment = doc.attachments.find((entry) => entry.id === attachmentId); if (!attachment) return; attachment.anchorId = anchorId; }; const retuneTransform = ( attachmentId: string, patch: { offset?: Vec3Like; rotation?: Vec3Like; scale?: Vec3Like } ): void => { const attachment = doc.attachments.find((entry) => entry.id === attachmentId); if (!attachment) return; if (patch.offset) attachment.offset = { ...patch.offset }; if (patch.rotation) attachment.rotation = { ...patch.rotation }; if (patch.scale) attachment.scale = { ...patch.scale }; }; const retuneSculpt = ( attachmentId: string, patch: NonNullable ): void => { const attachment = doc.attachments.find((entry) => entry.id === attachmentId); if (!attachment || !attachment.sculpt) return; attachment.sculpt = { ...attachment.sculpt, ...patch, size: { ...attachment.sculpt.size, ...patch.size }, bulge: { ...attachment.sculpt.bulge, ...patch.bulge, offset: { ...attachment.sculpt.bulge?.offset, ...patch.bulge?.offset } }, cut: { ...attachment.sculpt.cut, ...patch.cut, offset: { ...attachment.sculpt.cut?.offset, ...patch.cut?.offset } } }; }; const blockedPrefix = ['detail_weapon_', 'detail_backpack_', 'detail_helmet_']; const blockedExact = new Set([ 'detail_helmet_optic_lens', 'detail_helmet_sensor_block', 'detail_helmet_comm_mast', 'detail_helmet_comm_tip', 'detail_helmet_brow_plate', 'detail_helmet_visor', 'core_helmet_visor', 'detail_shoulder_trim', 'detail_forearm_plate', 'detail_thigh_plate', 'detail_knee_plate', 'detail_calf_plate', 'detail_weapon_receiver', 'detail_weapon_barrel', 'detail_weapon_stock' ]); doc.attachments = doc.attachments.filter((attachment) => { if (blockedExact.has(attachment.moduleId)) return false; return !blockedPrefix.some((prefix) => attachment.moduleId.startsWith(prefix)); }); reanchor('attach-torso-core', 'anchor-spine-mid'); // `anchor-spine-mid` uses the spine tangent as its forward axis. Rotate the torso shell // so its height follows the spine while its front still faces character-forward. retuneTransform('attach-pelvis-core', { offset: { x: 0, y: -0.025, z: -0.004 } }); retuneTransform('attach-abdomen-block', { offset: { x: 0, y: -0.052, z: 0.036 } }); retuneTransform('attach-torso-core', { offset: { x: 0, y: 0.056, z: 0.016 }, rotation: { x: 90, y: 0, z: 0 } }); retuneTransform('attach-neck-column', { offset: { x: 0, y: 0.014, z: -0.004 } }); retuneTransform('attach-head-shell', { offset: { x: 0, y: 0.012, z: -0.004 }, scale: { x: 0.9, y: 0.94, z: 0.9 } }); retuneTransform('attach-shoulder-joint-l', { offset: { x: 0, y: -0.012, z: -0.01 }, scale: { x: 0.84, y: 0.84, z: 0.84 } }); retuneTransform('attach-shoulder-joint-r', { offset: { x: 0, y: -0.012, z: -0.01 }, scale: { x: 0.84, y: 0.84, z: 0.84 } }); retuneTransform('attach-hip-joint-l', { offset: { x: 0, y: 0.016, z: 0 }, scale: { x: 0.84, y: 0.84, z: 0.84 } }); retuneTransform('attach-hip-joint-r', { offset: { x: 0, y: 0.016, z: 0 }, scale: { x: 0.84, y: 0.84, z: 0.84 } }); retuneTransform('attach-foot-l', { offset: { x: 0, y: -0.01, z: 0.04 }, rotation: { x: 0, y: 180, z: 0 }, scale: { x: 1.0, y: 0.9, z: 1.22 } }); retuneTransform('attach-foot-r', { offset: { x: 0, y: -0.01, z: 0.04 }, rotation: { x: 0, y: 180, z: 0 }, scale: { x: 1.0, y: 0.9, z: 1.22 } }); retuneSculpt('attach-pelvis-core', { enabled: true, primitive: 'roundedBox', size: { x: 0.28, y: 0.15, z: 0.16 }, roundness: 0.025, bulge: { enabled: true, radius: 0.07, smooth: 0.035, offset: { x: 0, y: 0.012, z: 0.01 } }, cut: { enabled: false, radius: 0.06, offset: { x: 0, y: 0, z: 0 } } }); retuneSculpt('attach-torso-core', { enabled: true, primitive: 'roundedBox', size: { x: 0.24, y: 0.3, z: 0.16 }, roundness: 0.022, bulge: { enabled: true, radius: 0.09, smooth: 0.04, offset: { x: 0, y: 0.05, z: 0.024 } }, cut: { enabled: true, radius: 0.07, offset: { x: 0, y: -0.02, z: 0.068 } } }); reanchor('attach-shoulder-joint-l', 'anchor-clavicle-l'); reanchor('attach-shoulder-joint-r', 'anchor-clavicle-r'); reanchor('attach-head-shell', 'anchor-head'); const profileOverrides: Array<{ id: string; patch: AttachmentEntry['shape'] }> = [ { id: 'attach-pelvis-core', patch: { size: { x: 0.29, y: 0.15, z: 0.16 }, taper: { xTop: 0.06, xBottom: 0.2, zTop: 0.04, zBottom: 0.12 }, chamfer: { edge: 0.28, corner: 0.22 }, profile: { kind: 'torso', intensity: 0.64 } } }, { id: 'attach-abdomen-block', patch: { size: { x: 0.18, y: 0.13, z: 0.13 }, taper: { xTop: 0.04, xBottom: 0.12, zTop: 0.08, zBottom: 0.04 }, chamfer: { edge: 0.22, corner: 0.18 }, profile: { kind: 'torso', intensity: 0.5 } } }, { id: 'attach-torso-core', patch: { size: { x: 0.24, y: 0.3, z: 0.16 }, taper: { xTop: 0.18, xBottom: 0.02, zTop: 0.08, zBottom: 0.14 }, chamfer: { edge: 0.28, corner: 0.24 }, profile: { kind: 'torso', intensity: 0.82 } } }, { id: 'attach-neck-column', patch: { size: { x: 0.048, y: 0.1, segments: 16 }, taper: { xTop: 0.12, xBottom: 0.08, zTop: 0.12, zBottom: 0.08 }, profile: { kind: 'limb', intensity: 0.52 } } }, { id: 'attach-head-shell', patch: { primitive: 'sphere', size: { x: 0.116, y: 0.128, segments: 20, rings: 14 }, chamfer: { edge: 0.18, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.28 } } }, { id: 'attach-upper-arm-l', patch: { size: { x: 0.046, y: 0.3, segments: 18, rings: 12 }, taper: { xTop: 0.1, xBottom: 0.16, zTop: 0.11, zBottom: 0.08 }, profile: { kind: 'limb', intensity: 0.64 }, chamfer: { edge: 0.2, corner: 0.16 } } }, { id: 'attach-upper-arm-r', patch: { size: { x: 0.046, y: 0.3, segments: 18, rings: 12 }, taper: { xTop: 0.1, xBottom: 0.16, zTop: 0.11, zBottom: 0.08 }, profile: { kind: 'limb', intensity: 0.68 }, chamfer: { edge: 0.2, corner: 0.16 } } }, { id: 'attach-forearm-l', patch: { size: { x: 0.042, y: 0.28, segments: 18, rings: 12 }, taper: { xTop: 0.06, xBottom: 0.14, zTop: 0.09, zBottom: 0.12 }, profile: { kind: 'limb', intensity: 0.66 }, chamfer: { edge: 0.18, corner: 0.14 } } }, { id: 'attach-forearm-r', patch: { size: { x: 0.042, y: 0.28, segments: 18, rings: 12 }, taper: { xTop: 0.06, xBottom: 0.14, zTop: 0.09, zBottom: 0.12 }, profile: { kind: 'limb', intensity: 0.66 }, chamfer: { edge: 0.18, corner: 0.14 } } }, { id: 'attach-shoulder-joint-l', patch: { size: { x: 0.052, y: 0.052, z: 0.052 }, taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.02, zBottom: 0.02 }, chamfer: { edge: 0.22, corner: 0.24 }, profile: { kind: 'limb', intensity: 0.6 } } }, { id: 'attach-shoulder-joint-r', patch: { size: { x: 0.052, y: 0.052, z: 0.052 }, taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.02, zBottom: 0.02 }, chamfer: { edge: 0.22, corner: 0.24 }, profile: { kind: 'limb', intensity: 0.6 } } }, { id: 'attach-upper-leg-l', patch: { size: { x: 0.052, y: 0.34, segments: 18, rings: 12 }, taper: { xTop: 0.08, xBottom: 0.16, zTop: 0.08, zBottom: 0.14 }, profile: { kind: 'limb', intensity: 0.74 }, chamfer: { edge: 0.16, corner: 0.14 } } }, { id: 'attach-upper-leg-r', patch: { size: { x: 0.052, y: 0.34, segments: 18, rings: 12 }, taper: { xTop: 0.08, xBottom: 0.16, zTop: 0.08, zBottom: 0.14 }, profile: { kind: 'limb', intensity: 0.74 }, chamfer: { edge: 0.16, corner: 0.14 } } }, { id: 'attach-hip-joint-l', patch: { size: { x: 0.064, y: 0.064, z: 0.064 }, taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.02, zBottom: 0.02 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'limb', intensity: 0.5 } } }, { id: 'attach-hip-joint-r', patch: { size: { x: 0.064, y: 0.064, z: 0.064 }, taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.02, zBottom: 0.02 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'limb', intensity: 0.5 } } }, { id: 'attach-lower-leg-l', patch: { size: { x: 0.05, y: 0.31, segments: 18, rings: 12 }, taper: { xTop: 0.1, xBottom: 0.2, zTop: 0.07, zBottom: 0.15 }, profile: { kind: 'limb', intensity: 0.76 }, chamfer: { edge: 0.18, corner: 0.16 } } }, { id: 'attach-lower-leg-r', patch: { size: { x: 0.05, y: 0.31, segments: 18, rings: 12 }, taper: { xTop: 0.1, xBottom: 0.2, zTop: 0.07, zBottom: 0.15 }, profile: { kind: 'limb', intensity: 0.76 }, chamfer: { edge: 0.18, corner: 0.16 } } }, { id: 'attach-hand-l', patch: { size: { x: 0.082, y: 0.05, z: 0.11, segments: 16, rings: 10 }, taper: { xTop: 0.12, xBottom: 0.06, zTop: 0.16, zBottom: 0.1 }, profile: { kind: 'limb', intensity: 0.42 }, chamfer: { edge: 0.2, corner: 0.16 } } }, { id: 'attach-hand-r', patch: { size: { x: 0.082, y: 0.05, z: 0.11, segments: 16, rings: 10 }, taper: { xTop: 0.12, xBottom: 0.06, zTop: 0.16, zBottom: 0.1 }, profile: { kind: 'limb', intensity: 0.42 }, chamfer: { edge: 0.2, corner: 0.16 } } }, { id: 'attach-foot-l', patch: { size: { x: 0.1, y: 0.048, z: 0.195 }, taper: { xTop: 0.03, xBottom: 0.12, zTop: 0.18, zBottom: 0.06 }, profile: { kind: 'plate', intensity: 0.68 }, chamfer: { edge: 0.22, corner: 0.16 } } }, { id: 'attach-foot-r', patch: { size: { x: 0.1, y: 0.048, z: 0.195 }, taper: { xTop: 0.03, xBottom: 0.12, zTop: 0.18, zBottom: 0.06 }, profile: { kind: 'plate', intensity: 0.68 }, chamfer: { edge: 0.22, corner: 0.16 } } } ]; for (const { id, patch } of profileOverrides) { patchHumanAttachmentShape(doc.attachments, id, patch); } } function syncNextIds(): void { const countId = (items: { id: string }[], prefix: string): number => { const max = items.reduce((acc, item) => { const match = item.id.match(/\d+$/); const value = match ? Number.parseInt(match[0], 10) : 0; return Math.max(acc, value); }, 0); return max + 1; }; state.nextIds.node = countId(state.nodes, 'node'); state.nextIds.edge = countId(state.edges, 'edge'); state.nextIds.anchor = countId(state.anchors, 'anchor'); state.nextIds.generator = countId(state.generators, 'generator'); state.nextIds.attachment = countId(state.attachments, 'attachment'); state.nextIds.sceneProp = countId(state.sceneProps, 'scene-prop'); } function rebuildDerivedGroups(): void { const profile = state.profileGeneration ? createGenerationProfile('rebuildDerivedGroups') : null; activeGenerationProfile = profile; if (profile) { profile.counters.set('edges', state.edges.length); profile.counters.set('anchors', state.anchors.length); profile.counters.set('generators', state.generators.length); profile.counters.set('attachments', state.attachments.length); } let stageStart = profile ? performance.now() : 0; try { resetRuntimeTargetPreview(); clearGroup(edgeGroup, false, true); clearGroup(anchorGroup, false, true); clearGroup(generatorGroup, false, true); clearGroup(attachmentAxisGroup, false, true); clearLeaperRuntimePreview(); disposeModuleOverrides(); proxyGroup.clear(); edgeLineMap = new Map(); anchorMeshMap = new Map(); anchorAxesMap = new Map(); attachmentMeshMap = new Map(); attachmentInstanceMeshes = []; attachmentAxisMeshes = []; generatorMarkerMeshes = []; generatorMarkerMap = new Map(); generatorAxisMap = new Map(); generatorLinkMap = new Map(); generatorGizmoGroup.clear(); generatorHandleMeshes = []; for (const edge of state.edges) { const curve = buildCurve(edge); if (!curve) continue; const points = curve.getPoints(EDGE_RENDER_SAMPLES); const geometry = new THREE.BufferGeometry().setFromPoints(points); const material = state.selection.kind === 'edge' && state.selection.id === edge.id ? edgeSelectedMaterial : edgeMaterial; const line = new THREE.Line(geometry, material); line.userData = { id: edge.id, kind: 'edge' }; line.layers.set(LAYER_EDGE); edgeGroup.add(line); edgeLineMap.set(edge.id, line); } if (profile) { recordGenerationStage(profile, 'edges', performance.now() - stageStart); stageStart = performance.now(); } for (const anchor of state.anchors) { const frame = resolveAnchorFrame(anchor); if (!frame) continue; const anchorMesh = new THREE.Mesh(anchorGeometry, anchorMaterial); anchorMesh.position.copy(frame.origin); anchorMesh.userData = { id: anchor.id, kind: 'anchor' }; anchorMesh.layers.set(LAYER_ANCHOR); const axes = new THREE.AxesHelper(0.08); axes.position.copy(frame.origin); axes.setRotationFromMatrix(frameToMatrix(frame)); anchorGroup.add(anchorMesh); anchorGroup.add(axes); anchorMeshMap.set(anchor.id, anchorMesh); anchorAxesMap.set(anchor.id, axes); } if (profile) { recordGenerationStage(profile, 'anchors', performance.now() - stageStart); stageStart = performance.now(); } if (state.showGenerators) { for (const generator of state.generators) { const frames = buildGeneratorFrames(generator); const baseAnchor = getAnchorById(generator.baseAnchorId); const baseFrame = baseAnchor ? resolveAnchorFrame(baseAnchor) : null; const markerList: THREE.Mesh[] = []; const axisList: THREE.Line[] = []; const linkList: THREE.Line[] = []; frames.forEach((frame, index) => { const marker = new THREE.Mesh(generatorGeometry, generatorMaterial); marker.position.copy(frame.origin); marker.userData = { id: generator.id, kind: 'generator', index }; marker.layers.set(LAYER_GENERATOR); generatorGroup.add(marker); generatorMarkerMeshes.push(marker); markerList.push(marker); const tip = frame.origin.clone().add(frame.forward.clone().multiplyScalar(GENERATOR_AXIS_LENGTH)); const axisGeom = new THREE.BufferGeometry().setFromPoints([frame.origin, tip]); const axisLine = new THREE.Line(axisGeom, generatorLineMaterial); axisLine.userData = { id: generator.id, kind: 'generator-axis', index }; generatorGroup.add(axisLine); axisList.push(axisLine); if (baseFrame && baseFrame.origin.distanceTo(frame.origin) > 0.001) { const linkGeom = new THREE.BufferGeometry().setFromPoints([baseFrame.origin, frame.origin]); const linkLine = new THREE.Line(linkGeom, generatorLinkMaterial); linkLine.computeLineDistances(); linkLine.userData = { id: generator.id, kind: 'generator-link', index }; generatorGroup.add(linkLine); linkList.push(linkLine); } }); generatorMarkerMap.set(generator.id, markerList); generatorAxisMap.set(generator.id, axisList); if (linkList.length > 0) { generatorLinkMap.set(generator.id, linkList); } if (generator.type === 'mirror') { const params = generator.params as GeneratorParamsMirror; const normal = params.axis === 'x' ? new THREE.Vector3(1, 0, 0) : params.axis === 'z' ? new THREE.Vector3(0, 0, 1) : new THREE.Vector3(0, 1, 0); const plane = new THREE.Plane(normal, -params.offset); const helper = new THREE.PlaneHelper(plane, 0.6, 0x4f9f7a); generatorGroup.add(helper); } } } if (profile) { recordGenerationStage(profile, 'generators', performance.now() - stageStart); stageStart = performance.now(); } rebuildGeneratorGizmo(); const buildAttachmentAxisLine = ( origin: THREE.Vector3, direction: THREE.Vector3, material: THREE.LineBasicMaterial, meta: { id: string; index: number } ): THREE.Line => { const tip = origin.clone().add(direction.clone().multiplyScalar(ATTACHMENT_AXIS_LENGTH)); const geometry = new THREE.BufferGeometry().setFromPoints([origin, tip]); const line = new THREE.Line(geometry, material); line.userData = { id: meta.id, kind: 'attachment-axis', generatorIndex: meta.index }; line.layers.set(LAYER_ATTACHMENT); return line; }; for (const attachment of state.attachments) { const moduleEntry = getModuleById(attachment.moduleId); if (!moduleEntry) continue; const frames = getAttachmentFrames(attachment); if (frames.length === 0) continue; if (profile) { bumpGenerationCounter(profile, 'attachmentFrames', frames.length); } frames.forEach((frame, index) => { const mesh = getModuleMeshForAttachment(attachment, moduleEntry); const matrix = buildAttachmentMatrix(attachment, frame, moduleEntry); matrix.decompose(mesh.position, mesh.quaternion, mesh.scale); mesh.updateMatrix(); mesh.visible = !(currentBasePresetName === 'leaper-arc' && isLeaperRuntimeBodyAttachment(attachment)); mesh.userData = { ...(mesh.userData ?? {}), id: attachment.id, kind: 'attachment', generatorIndex: index }; mesh.layers.set(LAYER_ATTACHMENT); moduleGroup.add(mesh); attachmentInstanceMeshes.push(mesh); if ( !attachmentMeshMap.has(attachment.id) || (attachment.generatorIndex !== undefined && attachment.generatorIndex === index) ) { attachmentMeshMap.set(attachment.id, mesh); } if (state.showAttachmentAxes && mesh.visible) { const upLine = buildAttachmentAxisLine( frame.origin, frame.up, attachmentAxisUpMaterial, { id: attachment.id, index } ); const forwardLine = buildAttachmentAxisLine( frame.origin, frame.forward, attachmentAxisForwardMaterial, { id: attachment.id, index } ); attachmentAxisGroup.add(upLine, forwardLine); attachmentAxisMeshes.push(upLine, forwardLine); } }); } if (profile) { recordGenerationStage(profile, 'attachments', performance.now() - stageStart); } rebuildLeaperRuntimePreview(); proxyGroup.visible = state.showProxy; if (state.proxyObject && state.showProxy) { proxyGroup.add(state.proxyObject); } refreshRaycastTargets(); captureRuntimeTargetPreviewBaseTransforms(); applyVisualState(); } finally { if (profile) { finishGenerationProfile(profile); } activeGenerationProfile = null; } } function finalizeSceneRebuildSelection(): void { updateVisibility(); refreshRaycastTargets(); safeDetachTransform(); if (state.selection.kind === 'node' && state.selection.id) { refreshTransformTarget(); } else if (state.selection.kind === 'attachment' && state.selection.id) { refreshTransformTarget(); } else if (state.selection.kind === 'scene-prop' && state.selection.id) { refreshTransformTarget(); } else { transform.detach(); } } function rebuildScene(options?: { deferDerivedRebuild?: boolean; deferDerivedUntilInteractive?: boolean; chunkNodeBuild?: boolean; onComplete?: () => void; }): void { safeDetachTransform(); transform.detach(); clearGroup(nodeGroup, false, false); nodeMeshMap = new Map(); const buildNodeMesh = (node: NodeEntry): void => { const mesh = new THREE.Mesh(nodeGeometry, nodeMaterial); const world = resolveNodeWorldTransform(node); mesh.position.copy(world.position); mesh.quaternion.copy(world.quaternion); mesh.userData = { id: node.id, kind: 'node' }; mesh.layers.set(LAYER_NODE); nodeGroup.add(mesh); nodeMeshMap.set(node.id, mesh); }; const finalizeRebuild = (): void => { rebuildSkeletonLinks(); if (options?.deferDerivedRebuild) { if (options.deferDerivedUntilInteractive) { scheduleAfterFirstInteractiveFrame(() => { rebuildDerivedGroups(); finalizeSceneRebuildSelection(); }); finalizeSceneRebuildSelection(); options.onComplete?.(); return; } scheduleIdleTask(() => { rebuildDerivedGroups(); finalizeSceneRebuildSelection(); options.onComplete?.(); }); return; } rebuildDerivedGroups(); finalizeSceneRebuildSelection(); options?.onComplete?.(); }; if (!options?.chunkNodeBuild || state.nodes.length <= 24) { for (const node of state.nodes) { buildNodeMesh(node); } finalizeRebuild(); return; } const nodes = state.nodes.slice(); let cursor = 0; const processNodeChunk = (deadline: IdleTaskDeadline): void => { let processed = 0; while (cursor < nodes.length) { buildNodeMesh(nodes[cursor]!); cursor += 1; processed += 1; if (processed >= 32 && !deadline.didTimeout && deadline.timeRemaining() <= 1) { break; } } if (cursor < nodes.length) { scheduleIdleTask(processNodeChunk); return; } finalizeRebuild(); }; scheduleIdleTask(processNodeChunk); } function rebuildAfterMove(): void { invalidateEdgeCache(); rebuildSkeletonLinks(); rebuildDerivedGroups(); updateVisibility(); refreshUi(); } function removeAnchors(anchorIds: Set): void { if (anchorIds.size === 0) return; state.anchors = state.anchors.filter((anchor) => !anchorIds.has(anchor.id)); state.attachments = state.attachments.filter((attachment) => !anchorIds.has(attachment.anchorId)); state.generators = state.generators.filter((gen) => !anchorIds.has(gen.baseAnchorId)); if (state.hover.kind === 'attachment' && state.hover.id && !getAttachmentById(state.hover.id)) { setHover(null); } } function addNodeFromUi(): void { const position = vec3( parseNumber(controls.nodeX.value, 0), parseNumber(controls.nodeY.value, 0), parseNumber(controls.nodeZ.value, 0) ); addNodeAtPosition(position, 'Added node'); } function addNodeAtPosition(position: Vec3Like, statusPrefix = 'Added node'): NodeEntry { const node: NodeEntry = { id: makeId('node'), position, rotation: vec3(0, 0, 0), mode: 'static', tags: [] }; state.nodes.push(node); setSelection('node', node.id); rebuildScene(); refreshPoseMappingsIfLoaded(); refreshUi(); markPresetCustom(); setStatus(`${statusPrefix} ${node.id}.`); return node; } function deleteNodeById(id: string): void { state.nodes = state.nodes.filter((node) => node.id !== id); const removedEdges = new Set(state.edges.filter((edge) => edge.a === id || edge.b === id).map((edge) => edge.id)); state.edges = state.edges.filter((edge) => edge.a !== id && edge.b !== id); const removedAnchors = new Set( state.anchors .filter((anchor) => anchor.nodeId === id || (anchor.edgeId && removedEdges.has(anchor.edgeId))) .map((anchor) => anchor.id) ); removeAnchors(removedAnchors); setSelection(null); if ( (state.hover.kind === 'node' && state.hover.id === id) || (state.hover.kind === 'edge' && state.hover.id && removedEdges.has(state.hover.id)) || (state.hover.kind === 'anchor' && state.hover.id && removedAnchors.has(state.hover.id)) ) { setHover(null); } rebuildScene(); refreshPoseMappingsIfLoaded(); refreshUi(); markPresetCustom(); setStatus(`Deleted node ${id}.`); } function duplicateNodeById(id: string): void { const node = getNodeById(id); if (!node) return; const offset = vec3(0.12, 0, 0.12); const clone = addNodeAtPosition( { x: node.position.x + offset.x, y: node.position.y + offset.y, z: node.position.z + offset.z }, 'Duplicated node' ); clone.tags = [...node.tags]; clone.rotation = node.rotation ? { ...node.rotation } : vec3(0, 0, 0); clone.mode = node.mode ?? 'static'; clone.parentId = node.parentId; setSelection('node', clone.id); controls.nodeList.value = clone.id; rebuildScene(); refreshUi(); } function duplicateEdgeById(id: string): void { const edge = getEdgeById(id); if (!edge) return; const clone: EdgeEntry = { id: makeId('edge'), a: edge.a, b: edge.b, radius: edge.radius, curve: edge.curve, controlOffset: { ...edge.controlOffset } }; state.edges.push(clone); setSelection('edge', clone.id); controls.edgeList.value = clone.id; rebuildScene(); refreshUi(); markPresetCustom(); setStatus(`Duplicated edge ${id}.`); } function deleteNodeFromUi(): void { const id = controls.nodeList.value; if (!id) return; deleteNodeById(id); } function addEdgeFromUi(): void { const a = controls.edgeStart.value; const b = controls.edgeEnd.value; if (!a || !b || a === b) { setStatus('Select two different nodes for the edge.'); return; } addEdgeBetweenNodes(a, b, 'Added edge'); } function addEdgeBetweenNodes(a: string, b: string, statusPrefix = 'Added edge'): EdgeEntry { const edge: EdgeEntry = { id: makeId('edge'), a, b, radius: Math.max(0.01, parseNumber(controls.edgeRadius.value, 0.08)), curve: (controls.edgeCurve.value as EdgeCurveType) ?? 'catmull', controlOffset: vec3( parseNumber(controls.edgeControlX.value, 0), parseNumber(controls.edgeControlY.value, 0.12), parseNumber(controls.edgeControlZ.value, 0) ) }; state.edges.push(edge); setSelection('edge', edge.id); rebuildScene(); refreshUi(); markPresetCustom(); setStatus(`${statusPrefix} ${edge.id}.`); return edge; } function deleteEdgeById(id: string): void { state.edges = state.edges.filter((edge) => edge.id !== id); const removedAnchors = new Set(state.anchors.filter((anchor) => anchor.edgeId === id).map((anchor) => anchor.id)); removeAnchors(removedAnchors); if (state.selection.kind === 'edge' && state.selection.id === id) { setSelection(null); } if ( (state.hover.kind === 'edge' && state.hover.id === id) || (state.hover.kind === 'anchor' && state.hover.id && removedAnchors.has(state.hover.id)) ) { setHover(null); } rebuildScene(); refreshUi(); markPresetCustom(); setStatus(`Deleted edge ${id}.`); } function deleteEdgeFromUi(): void { const id = controls.edgeList.value; if (!id) return; deleteEdgeById(id); } let lastSurfaceHit: { point: Vec3Like; normal: Vec3Like; parentAttachmentId?: string; localPoint?: Vec3Like; localNormal?: Vec3Like; } | null = null; function addAnchorFromUi(): void { const type = controls.anchorType.value as AnchorEntry['type']; const tags = parseCsv(controls.anchorTags.value); const accepts = parseCsv(controls.anchorAccepts.value); const radialAngle = parseNumber(controls.anchorAngle.value, 0); const orientation = controls.anchorRule.value as OrientationRule; const offset = readAnchorOffset(); const portProfile = controls.anchorProfile ? (controls.anchorProfile.value as PortProfile) : 'any'; const exportSocket = controls.anchorExportSocket ? controls.anchorExportSocket.checked : false; if (type === 'node') { const nodeId = controls.anchorNode.value; if (!nodeId) { setStatus('Select a node for the anchor.'); return; } state.anchors.push({ id: makeId('anchor'), type, nodeId, orientation, radialAngle, offset, portProfile: portProfile === 'any' ? undefined : portProfile, exportSocket, tags, accepts }); } else if (type === 'edge') { const edgeId = controls.anchorEdge.value; if (!edgeId) { setStatus('Select an edge for the anchor.'); return; } const s = parseNumber(controls.anchorS.value, 0.5); const len = parseNumber(controls.anchorLen.value, 0); state.anchors.push({ id: makeId('anchor'), type, edgeId, s, len: len > 0 ? len : undefined, orientation, radialAngle, offset, portProfile: portProfile === 'any' ? undefined : portProfile, exportSocket, tags, accepts }); } else if (type === 'surface') { if (!lastSurfaceHit) { setStatus('Pick a surface point first.'); return; } state.anchors.push({ id: makeId('anchor'), type, orientation, radialAngle, offset, portProfile: portProfile === 'any' ? undefined : portProfile, exportSocket, tags, accepts, surface: { point: lastSurfaceHit.point, normal: lastSurfaceHit.normal, parentAttachmentId: lastSurfaceHit.parentAttachmentId, localPoint: lastSurfaceHit.localPoint, localNormal: lastSurfaceHit.localNormal } }); lastSurfaceHit = null; } else if (type === 'attachment') { const attachmentId = controls.anchorAttachment.value; if (!attachmentId) { setStatus('Select an attachment for the anchor.'); return; } const face = controls.anchorAttachmentFace.value as AnchorFace; state.anchors.push({ id: makeId('anchor'), type, attachmentId, attachmentFace: face, orientation, radialAngle, offset, portProfile: portProfile === 'any' ? undefined : portProfile, exportSocket, tags, accepts }); } rebuildScene(); refreshUi(); markPresetCustom(); setStatus('Anchor added.'); } function addSurfaceAnchorFromHit( point: Vec3Like, normal: Vec3Like, parentAttachmentId?: string, localPoint?: Vec3Like, localNormal?: Vec3Like ): void { const tags = parseCsv(controls.anchorTags.value); const accepts = parseCsv(controls.anchorAccepts.value); const radialAngle = parseNumber(controls.anchorAngle.value, 0); const orientation = controls.anchorRule.value as OrientationRule; const offset = readAnchorOffset(); const portProfile = controls.anchorProfile ? (controls.anchorProfile.value as PortProfile) : 'any'; const exportSocket = controls.anchorExportSocket ? controls.anchorExportSocket.checked : false; state.anchors.push({ id: makeId('anchor'), type: 'surface', orientation, radialAngle, offset, portProfile: portProfile === 'any' ? undefined : portProfile, exportSocket, tags, accepts, surface: { point, normal, parentAttachmentId, localPoint, localNormal } }); rebuildScene(); refreshUi(); markPresetCustom(); setStatus('Surface anchor added.'); } function deleteAnchorById(id: string): void { removeAnchors(new Set([id])); if (state.selection.kind === 'anchor' && state.selection.id === id) { setSelection(null); } if (state.hover.kind === 'anchor' && state.hover.id === id) { setHover(null); } rebuildScene(); refreshUi(); markPresetCustom(); setStatus(`Deleted anchor ${id}.`); } function duplicateAnchorById(id: string): void { const anchor = getAnchorById(id); if (!anchor) return; const clone: AnchorEntry = { id: makeId('anchor'), type: anchor.type, nodeId: anchor.nodeId, edgeId: anchor.edgeId, s: anchor.s, len: anchor.len, orientation: anchor.orientation, radialAngle: anchor.radialAngle, offset: anchor.offset ? { ...anchor.offset } : undefined, portProfile: anchor.portProfile, exportSocket: anchor.exportSocket, tags: [...anchor.tags], accepts: [...anchor.accepts], surface: anchor.surface ? { point: { ...anchor.surface.point }, normal: { ...anchor.surface.normal }, parentAttachmentId: anchor.surface.parentAttachmentId, localPoint: anchor.surface.localPoint ? { ...anchor.surface.localPoint } : undefined, localNormal: anchor.surface.localNormal ? { ...anchor.surface.localNormal } : undefined } : undefined, lookAtNodeId: anchor.lookAtNodeId, proximalNodeId: anchor.proximalNodeId, distalNodeId: anchor.distalNodeId, upNodeId: anchor.upNodeId }; state.anchors.push(clone); setSelection('anchor', clone.id); controls.anchorList.value = clone.id; rebuildScene(); refreshUi(); markPresetCustom(); setStatus(`Duplicated anchor ${id}.`); } function deleteAnchorFromUi(): void { const id = controls.anchorList.value; if (!id) return; deleteAnchorById(id); } function addGeneratorFromUi(): void { const base = controls.generatorBase.value; if (!base) { setStatus('Select a base anchor for the generator.'); return; } const type = controls.generatorType.value as GeneratorType; const mode = (controls.generatorMode?.value as GeneratorParamsAlong['mode']) ?? 'spacing'; let params: GeneratorParams; if (type === 'alongEdge') { const count = Math.max(1, Math.floor(parseNumber(controls.generatorCount.value, 6))); const spacing = Math.max(0, parseNumber(controls.generatorSpacing.value, 0.12)); params = { mode, count, spacing, startLen: Math.max(0, parseNumber(controls.generatorStart.value, 0)), endLen: Math.max(0, parseNumber(controls.generatorEnd.value, 0)) }; } else if (type === 'radial') { params = { count: Math.max(1, Math.floor(parseNumber(controls.generatorCount.value, 6))), radius: Math.max(0, parseNumber(controls.generatorRadius.value, 0.2)), startAngleDeg: parseNumber(controls.generatorAngle.value, 0), axis: (controls.generatorPlane.value as 'x' | 'y' | 'z') ?? 'y' }; } else { params = { axis: (controls.generatorPlane.value as 'x' | 'y' | 'z') ?? 'x', offset: parseNumber(controls.generatorPlaneOffset.value, 0) }; } state.generators.push({ id: makeId('generator'), type, baseAnchorId: base, params }); rebuildDerivedGroups(); refreshUi(); markPresetCustom(); setStatus('Generator added.'); } function deleteGeneratorFromUi(): void { const id = controls.generatorList.value; if (!id) return; state.generators = state.generators.filter((gen) => gen.id !== id); rebuildDerivedGroups(); refreshUi(); markPresetCustom(); setStatus(`Deleted generator ${id}.`); } function attachModuleFromUi(): void { const anchorId = controls.attachmentAnchor.value; const moduleId = controls.moduleList.value; if (!anchorId || !moduleId) { setStatus('Select an anchor and module.'); return; } const generatorId = controls.attachmentGenerator?.value || undefined; const generatorIndexValue = controls.attachmentGeneratorIndex?.value.trim() ?? ''; const generatorIndex = generatorId && generatorIndexValue.length > 0 ? Number.parseInt(generatorIndexValue, 10) : undefined; const selectedId = controls.attachmentList.value; const selected = selectedId ? getAttachmentById(selectedId) : undefined; if (selected && state.selection.kind === 'attachment' && state.selection.id === selected.id) { selected.anchorId = anchorId; selected.moduleId = moduleId; selected.portId = 'mount'; selected.generatorId = generatorId; selected.generatorIndex = Number.isFinite(generatorIndex) ? generatorIndex : undefined; syncMirrorAttachment(selected); rebuildScene(); selectAttachmentInstance(selected.id, selected.generatorIndex ?? null); refreshUi(); markPresetCustom(); setStatus(`Module ${selected.id} updated.`); return; } const attachment: AttachmentEntry = { id: makeId('attachment'), anchorId, moduleId, portId: 'mount', generatorId, generatorIndex: Number.isFinite(generatorIndex) ? generatorIndex : undefined }; state.attachments.push(attachment); syncMirrorAttachment(attachment); rebuildScene(); selectAttachmentInstance(attachment.id, attachment.generatorIndex ?? null); refreshUi(); markPresetCustom(); setStatus(`Module ${formatEditorItemLabel(attachment.id)} attached.`); } function detachModuleFromUi(): void { const id = controls.attachmentList.value; if (!id) return; deleteAttachmentById(id); } function deleteAttachmentById(id: string): void { detachSurfaceAnchorsFromAttachment(id); state.attachments = state.attachments.filter((attachment) => attachment.id !== id); if (state.selection.kind === 'attachment' && state.selection.id === id) { setSelection(null); } if (state.hover.kind === 'attachment' && state.hover.id === id) { setHover(null); } rebuildDerivedGroups(); refreshUi(); markPresetCustom(); setStatus(`Detached ${id}.`); } function duplicateAttachmentById(id: string): void { const attachment = getAttachmentById(id); if (!attachment) return; const clone: AttachmentEntry = { id: makeId('attachment'), anchorId: attachment.anchorId, moduleId: attachment.moduleId, portId: attachment.portId, generatorId: attachment.generatorId, generatorIndex: attachment.generatorIndex, offset: attachment.offset ? { ...attachment.offset } : undefined, rotation: attachment.rotation ? { ...attachment.rotation } : undefined, scale: attachment.scale ? { ...attachment.scale } : undefined, shape: cloneAttachmentShape(attachment.shape), sculpt: attachment.sculpt ? { enabled: attachment.sculpt.enabled, primitive: attachment.sculpt.primitive, size: { ...attachment.sculpt.size }, roundness: attachment.sculpt.roundness, bulge: { enabled: attachment.sculpt.bulge.enabled, radius: attachment.sculpt.bulge.radius, smooth: attachment.sculpt.bulge.smooth, offset: { ...attachment.sculpt.bulge.offset } }, cut: { enabled: attachment.sculpt.cut.enabled, radius: attachment.sculpt.cut.radius, offset: { ...attachment.sculpt.cut.offset } } } : undefined }; state.attachments.push(clone); setSelection('attachment', clone.id); controls.attachmentList.value = clone.id; rebuildDerivedGroups(); refreshUi(); markPresetCustom(); setStatus(`Duplicated attachment ${id}.`); } function resetRecipe(): void { const doc = createDefaultRecipe(); clearEditorCaches(); applyRecipe(doc); currentBasePresetName = 'basic'; controls.presetSelect.value = 'basic'; setMode('select'); void autoSelectPoseForCurrentArchetype({ load: true }); setStatus('Recipe reset to Basic preset.'); } function clearEditorCaches(): void { moduleMeshCache.clear(); attachmentMeshCache.clear(); sculptMeshCache.clear(); } function forceReloadCurrentPreset(): void { const presetName = controls.presetSelect.value !== 'custom' ? controls.presetSelect.value : currentBasePresetName; if (!presetName || presetName === 'custom') { setStatus('Select a preset to force reload it.'); return; } stopPosePlayback(true); localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_BASE_PRESET_KEY); clearEditorCaches(); const preset = buildPresetRecipe(presetName); if (!preset) { setStatus(`Unable to reload preset: ${presetName}.`); return; } currentBasePresetName = presetName; applyRecipe(preset); focusPresetViewport(presetName); controls.presetSelect.value = presetName; setMode('select'); void autoSelectPoseForCurrentArchetype({ load: true }); setStatus(`Preset force reloaded: ${presetName}.`); } function resetBaseSkeleton(): void { const presetName = currentBasePresetName; const preset = buildPresetRecipe(presetName); if (!preset) { setStatus('No base preset available for skeleton reset.'); return; } stopPosePlayback(true); clearEditorCaches(); state.nodes = preset.nodes ?? []; state.edges = preset.edges ?? []; state.anchors = preset.anchors ?? []; state.generators = preset.generators ?? []; state.attachments = state.attachments.filter((attachment) => state.anchors.some((anchor) => anchor.id === attachment.anchorId) ); lastSocketExport = []; state.selection = { kind: null }; state.hover = { kind: null }; state.attachmentInstanceIndex = null; state.edgeDraftStart = null; state.dragState = null; state.anchorDragState = null; state.generatorDragState = null; state.pendingDeselect = false; invalidateEdgeCache(); syncNextIds(); rebuildScene(); refreshUi(); markPresetCustom(); setMode('select'); setStatus(`Skeleton reset from preset: ${presetName}.`); } const commanderPlacementScaleToVec = (scale?: HumanoidPlacement['scale']): Vec3Like | undefined => { if (scale === undefined || scale === null) return undefined; if (typeof scale === 'number') { return { x: scale, y: scale, z: scale }; } return { x: scale.x, y: scale.y, z: scale.z }; }; const commanderPlacementRotationToDeg = (rotation?: HumanoidPlacement['rotation']): Vec3Like | undefined => { if (!rotation) return undefined; return { x: THREE.MathUtils.radToDeg(rotation.x), y: THREE.MathUtils.radToDeg(rotation.y), z: THREE.MathUtils.radToDeg(rotation.z) }; }; const applyPlacementAxisRotation = ( rotation: Vec3Like | undefined, axis?: HumanoidPlacement['axis'] ): Vec3Like | undefined => { const base = rotation ?? { x: 0, y: 0, z: 0 }; if (!axis || axis === 'y') return rotation ?? base; if (axis === 'x') { return { x: base.x, y: base.y, z: base.z - 90 }; } if (axis === 'z') { return { x: base.x + 90, y: base.y, z: base.z }; } return rotation ?? base; }; const rotateVecByEulerDegrees = (value: Vec3Like, rotation?: Vec3Like): Vec3Like => { if (!rotation) return { ...value }; const euler = new THREE.Euler( THREE.MathUtils.degToRad(rotation.x ?? 0), THREE.MathUtils.degToRad(rotation.y ?? 0), THREE.MathUtils.degToRad(rotation.z ?? 0), 'XYZ' ); const vec = new THREE.Vector3(value.x, value.y, value.z).applyEuler(euler); return { x: vec.x, y: vec.y, z: vec.z }; }; const getModuleHalfExtents = ( moduleEntry: ModuleEntry, scale?: Vec3Like ): Vec3Like => { const size = moduleEntry.def.size ?? { x: 0.1, y: 0.1, z: 0.1 }; const sx = scale?.x ?? 1; const sy = scale?.y ?? 1; const sz = scale?.z ?? 1; const primitive = moduleEntry.def.primitive; if (primitive === 'sphere') { const r = (size.x ?? 0.1); return { x: r * sx, y: r * sy, z: r * sz }; } if (primitive === 'capsule' || primitive === 'cylinder' || primitive === 'cone') { const r = size.x ?? 0.05; const h = size.y ?? r * 2; return { x: r * sx, y: (h * 0.5) * sy, z: r * sz }; } if (primitive === 'torus') { const r = size.x ?? 0.1; const tube = size.y ?? 0.03; const radius = r + tube; return { x: radius * sx, y: tube * sy, z: radius * sz }; } const w = size.x ?? 0.1; const h = size.y ?? w; const d = size.z ?? w; return { x: (w * 0.5) * sx, y: (h * 0.5) * sy, z: (d * 0.5) * sz }; }; const getModuleFaceOffset = ( moduleEntry: ModuleEntry, scale: Vec3Like | undefined, face: AnchorFace ): Vec3Like => { const half = getModuleHalfExtents(moduleEntry, scale); if (face === 'top') return { x: 0, y: half.y, z: 0 }; if (face === 'bottom') return { x: 0, y: -half.y, z: 0 }; if (face === 'left') return { x: -half.x, y: 0, z: 0 }; if (face === 'right') return { x: half.x, y: 0, z: 0 }; if (face === 'front') return { x: 0, y: 0, z: half.z }; if (face === 'back') return { x: 0, y: 0, z: -half.z }; return { x: 0, y: 0, z: 0 }; }; const buildCommanderAutoSculpt = (entry: ModuleEntry): AttachmentEntry['sculpt'] | undefined => { const size = entry.def.size ?? { x: 0.14, y: 0.14, z: 0.14 }; const x = size.x ?? 0.14; const y = size.y ?? size.x ?? 0.14; const z = size.z ?? size.x ?? 0.14; const maxDim = Math.max(x, y, z); if (maxDim < 0.08) return undefined; const purpose = entry.def.purpose; const useSculpt = purpose === 'housing' || purpose === 'armor_plate' || purpose === 'helmet' || purpose === 'backpack'; if (!useSculpt) return undefined; const roundness = Math.min(0.06, maxDim * 0.22); const bulgeRadius = Math.max(0.02, maxDim * 0.35); const bulgeSmooth = Math.max(0.01, maxDim * 0.18); const cutRadius = Math.max(0.02, maxDim * 0.3); const bulgeEnabled = purpose === 'housing' || purpose === 'backpack' || purpose === 'helmet'; const cutEnabled = purpose === 'armor_plate'; return { enabled: true, primitive: 'roundedBox', size: { x, y, z }, roundness, bulge: { enabled: bulgeEnabled, radius: bulgeRadius, smooth: bulgeSmooth, offset: vec3(0, maxDim * 0.12, maxDim * 0.08) }, cut: { enabled: cutEnabled, radius: cutRadius, offset: vec3(0, 0, maxDim * 0.2) } }; }; const extractCommanderPlacementSide = (label?: string): 'l' | 'r' | null => { if (!label) return null; const trimmed = label.trim(); if (!trimmed) return null; const first = trimmed[0].toLowerCase(); if (first === 'l') return 'l'; if (first === 'r') return 'r'; return null; }; const resolveCommanderAnchorId = ( placement: HumanoidPlacement, resolvedAnchor?: HumanoidAnchorSpec ): string | null => { const sourceLabel = resolvedAnchor?.to ?? placement.anchor?.to ?? placement.id ?? placement.partId; const fallbackLabel = placement.id ?? placement.partId; const side = extractCommanderPlacementSide(sourceLabel) ?? extractCommanderPlacementSide(fallbackLabel) ?? extractCommanderPlacementSide(placement.anchor?.to); const baseName = (sourceLabel ?? '').replace(/^[LR]/i, ''); const normalized = baseName.toLowerCase(); const withSide = (base: string): string | null => (side ? `anchor-${base}-${side}` : null); if (normalized.includes('centerweapon')) { return 'anchor-torso'; } if ( normalized.includes('pelvis') || normalized.includes('waist') ) { return 'anchor-pelvis'; } if (normalized.includes('abdomen')) { return 'anchor-abdomen'; } if (normalized.includes('spine')) { return 'anchor-spine-mid'; } if ( normalized.includes('torso') || normalized.includes('chest') || normalized.includes('sternum') || normalized.includes('cuirass') || normalized.includes('ribcage') ) { return 'anchor-torso'; } if (normalized.includes('neck')) { return 'anchor-neck'; } if ( normalized.includes('head') || normalized.includes('helmet') || normalized.includes('visor') || normalized.includes('crown') || normalized.includes('jaw') || normalized.includes('occipital') ) { return 'anchor-head'; } if ( normalized.includes('shoulder') || normalized.includes('clavicle') || normalized.includes('yoke') ) { return withSide('shoulder'); } if ( normalized.includes('bicep') || normalized.includes('tricep') || normalized.includes('upperarm') ) { return withSide('upper-arm'); } if (normalized.includes('elbow')) { return withSide('elbow'); } if ( normalized.includes('forearm') || normalized.includes('cuff') ) { return withSide('forearm'); } if ( normalized.includes('hand') || normalized.includes('palm') || normalized.includes('wrist') ) { return withSide('hand'); } if (normalized.includes('weapon')) { return withSide('shoulder'); } if (normalized.includes('hip')) { return withSide('hip'); } if ( normalized.includes('upperleg') || normalized.includes('thigh') ) { return withSide('upper-leg'); } if (normalized.includes('knee')) { return withSide('knee'); } if ( normalized.includes('lowerleg') || normalized.includes('calf') ) { return withSide('lower-leg'); } if ( normalized.includes('foot') || normalized.includes('ankle') ) { return withSide('foot'); } return null; }; const resolveCommanderFallbackAnchor = ( volumeType: string, placement: HumanoidPlacement, moduleEntry: ModuleEntry | undefined, sideSign: number ): string => { const label = `${volumeType} ${placement.id ?? ''} ${placement.partId}`.toLowerCase(); const side = sideSign < 0 ? 'l' : sideSign > 0 ? 'r' : ''; const sideAnchor = (base: string, fallback: string): string => side ? `${base}-${side}` : fallback; if (label.includes('shoulder') || volumeType.includes('shoulder_pad')) { return sideAnchor('anchor-shoulder', 'anchor-torso'); } if (label.includes('hip')) { return sideAnchor('anchor-hip', 'anchor-pelvis'); } if (label.includes('knee')) { return sideAnchor('anchor-knee', 'anchor-upper-leg'); } if (label.includes('upperleg') || label.includes('thigh')) { return sideAnchor('anchor-upper-leg', 'anchor-pelvis'); } if (label.includes('lowerleg') || label.includes('calf')) { return sideAnchor('anchor-lower-leg', 'anchor-upper-leg'); } if (label.includes('foot') || label.includes('ankle')) { return sideAnchor('anchor-foot', 'anchor-lower-leg'); } if (label.includes('forearm') || label.includes('cuff')) { return sideAnchor('anchor-forearm', 'anchor-upper-arm'); } if (label.includes('hand') || label.includes('wrist')) { return sideAnchor('anchor-hand', 'anchor-forearm'); } if (label.includes('upperarm') || label.includes('bicep') || label.includes('tricep')) { return sideAnchor('anchor-upper-arm', 'anchor-shoulder'); } if (label.includes('neck')) { return 'anchor-neck'; } if ( label.includes('head') || label.includes('helmet') || label.includes('sensor') || label.includes('antenna') ) { return 'anchor-head'; } if (label.includes('backpack') || volumeType.includes('backpack') || moduleEntry?.def.purpose === 'backpack') { return 'anchor-torso'; } if (label.includes('engine') || volumeType.includes('engine')) { return 'anchor-torso'; } if (label.includes('cockpit') || volumeType.includes('cockpit')) { return 'anchor-torso'; } if (label.includes('armor') || volumeType.includes('armor_plate') || moduleEntry?.def.purpose === 'armor_plate') { return sideAnchor('anchor-shoulder', 'anchor-torso'); } if (label.includes('weapon') || moduleEntry?.def.purpose === 'weapon') { return sideAnchor('anchor-hand', 'anchor-torso'); } return 'anchor-torso'; }; const resolveCommanderVolumeRootAnchor = ( volumeType: string, volumeIndex: number, sideSign: number ): string => { const normalized = volumeType.toLowerCase().replace(/_/g, ''); const side = sideSign < 0 ? 'l' : sideSign > 0 ? 'r' : ''; const sideAnchor = (base: string, fallback: string): string => side ? `${base}-${side}` : fallback; if (normalized.includes('hull')) { return 'anchor-pelvis'; } if (normalized.includes('shoulderpad')) { return 'anchor-torso'; } if (normalized.includes('armorplate')) { return 'anchor-torso'; } if (normalized.includes('backpack') || normalized.includes('engine')) { return 'anchor-spine-mid'; } if (normalized.includes('cockpit')) { return 'anchor-torso'; } if (normalized.includes('sensor') || normalized.includes('antenna') || normalized.includes('light')) { return 'anchor-head'; } if (normalized.includes('weaponmount')) { return 'anchor-torso'; } if (normalized.includes('shoulder')) { return 'anchor-torso'; } if (normalized.includes('hip') || normalized.includes('leg')) { return 'anchor-pelvis'; } if (normalized.includes('arm')) { return 'anchor-torso'; } return 'anchor-torso'; }; const buildCommanderAttachmentId = (placement: HumanoidPlacement, index: number): string => { const rawId = placement.id ?? `${placement.partId}_${index}`; const normalizedId = rawId .replace(/_/g, '-') .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .toLowerCase(); return `attach-${normalizedId}`; }; const commanderPlacementToAttachment = ( placement: HumanoidPlacement, index: number, options?: { anchorId?: string | null; attachmentId?: string; resolvedAnchor?: HumanoidAnchorSpec; moduleEntry?: ModuleEntry; } ): AttachmentEntry | null => { if (placement.hidden) return null; const resolvedAnchor = options?.resolvedAnchor ?? resolveHumanoidAnchorSpec(placement.anchor, 'humanoid_command'); const anchorId = options?.anchorId ?? resolveCommanderAnchorId(placement, resolvedAnchor); if (!anchorId) return null; let offset = placement.anchor ? (resolvedAnchor?.offset ? { ...resolvedAnchor.offset } : { ...placement.offset }) : { ...placement.offset }; const rotation = applyPlacementAxisRotation( commanderPlacementRotationToDeg(placement.rotation), placement.axis ); const scale = commanderPlacementScaleToVec(placement.scale); if (resolvedAnchor?.selfAnchor && options?.moduleEntry) { const selfOffset = getModuleFaceOffset(options.moduleEntry, scale, resolvedAnchor.selfAnchor); const rotatedSelf = rotateVecByEulerDegrees(selfOffset, rotation); offset = { x: offset.x - rotatedSelf.x, y: offset.y - rotatedSelf.y, z: offset.z - rotatedSelf.z }; } const attachmentId = options?.attachmentId ?? buildCommanderAttachmentId(placement, index); const attachment: AttachmentEntry = { id: attachmentId, anchorId, moduleId: placement.partId, portId: 'mount', offset, rotation, scale }; if (!attachment.sculpt && options?.moduleEntry) { const sculpt = buildCommanderAutoSculpt(options.moduleEntry); if (sculpt) { attachment.sculpt = sculpt; } } return attachment; }; const buildCommanderConvertedAttachments = (): { attachments: AttachmentEntry[]; anchors: AnchorEntry[] } => { const template = getTemplateForUnit('Commander'); if (!template) return { attachments: [], anchors: [] }; const volumes = [ template.volumeHierarchy.primary, ...template.volumeHierarchy.secondary, ...template.volumeHierarchy.tertiary ].filter(Boolean) as string[]; const anchors: AnchorEntry[] = []; const anchorCache = new Map(); const ensureAttachmentAnchor = (attachmentId: string, face: AnchorFace): string => { const safeFace = face ?? 'center'; const key = `${attachmentId}:${safeFace}`; const existing = anchorCache.get(key); if (existing) return existing; const anchorId = `anchor-attach-${attachmentId}-${safeFace}`.replace(/[^a-z0-9_-]/gi, '-'); anchors.push({ id: anchorId, type: 'attachment', attachmentId, attachmentFace: safeFace, orientation: 'alongEdge', radialAngle: 0, tags: ['attachment'], accepts: [] }); anchorCache.set(key, anchorId); return anchorId; }; const attachments: AttachmentEntry[] = []; const volumeCounts = new Map(); volumes.forEach((volumeType) => { const index = volumeCounts.get(volumeType) ?? 0; volumeCounts.set(volumeType, index + 1); const recipe = getHumanoidVolumeRecipe(volumeType, 'humanoid_command'); if (!recipe) return; if (recipe.id.startsWith('weapon_mount') && index % 3 !== 2) return; const mirrorSlot = index % 3; const sideSign = mirrorSlot === 0 ? -1 : mirrorSlot === 1 ? 1 : 0; const lateralOffset = recipe.mirrorByIndex ? sideSign * (recipe.lateralSpread ?? 0) : 0; const volumeKey = `${volumeType}-${index}`.replace(/[^a-z0-9_-]/gi, '-'); const attachmentIdByPlacement = new Map(); const rootPlacementIndex = Math.max(0, recipe.placements.findIndex((placement) => !placement.anchor)); const normalizedVolume = volumeType.toLowerCase().replace(/_/g, ''); const shouldIncludePlacement = (placement: HumanoidPlacement, moduleEntry?: ModuleEntry): boolean => { if (!moduleEntry) return false; if (normalizedVolume.includes('weaponmount')) return false; const purpose = moduleEntry.def.purpose; if (purpose === 'limb_segment' || purpose === 'joint' || purpose === 'weapon') return false; const id = placement.partId.toLowerCase(); if ( id.includes('upper_arm') || id.includes('forearm') || id.includes('upper_leg') || id.includes('lower_leg') || id.includes('hand') || id.includes('foot') || id.includes('hip_joint') || id.includes('shoulder_joint') ) { return false; } return true; }; recipe.placements.forEach((placement, placementIndex) => { const moduleEntry = getModuleById(placement.partId); if (!shouldIncludePlacement(placement, moduleEntry)) return; const placementKey = placement.id ?? `${placement.partId}_${placementIndex}`; const attachmentId = `attach-${volumeKey}-${placementKey}` .replace(/_/g, '-') .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .toLowerCase(); attachmentIdByPlacement.set(placementKey, attachmentId); }); const rootPlacementKey = recipe.placements[rootPlacementIndex] ? (recipe.placements[rootPlacementIndex].id ?? `${recipe.placements[rootPlacementIndex].partId}_${rootPlacementIndex}`) : `${volumeType}-root`; const rootAttachmentId = attachmentIdByPlacement.get(rootPlacementKey); const rootAnchorId = resolveCommanderVolumeRootAnchor(volumeType, index, sideSign); const rootAttachmentAnchorId = rootAttachmentId ? ensureAttachmentAnchor(rootAttachmentId, 'center') : null; recipe.placements.forEach((placement, placementIndex) => { const moduleEntry = getModuleById(placement.partId); if (!shouldIncludePlacement(placement, moduleEntry)) return; const resolvedAnchor = resolveHumanoidAnchorSpec(placement.anchor, 'humanoid_command'); const placementKey = placement.id ?? `${placement.partId}_${placementIndex}`; const attachmentId = attachmentIdByPlacement.get(placementKey) ?? buildCommanderAttachmentId(placement, placementIndex); let anchorId: string | null = null; const isRoot = placementIndex === rootPlacementIndex; let useAnchorSpec = true; if (isRoot) { anchorId = rootAnchorId; useAnchorSpec = false; } else if (resolvedAnchor?.to) { const targetAttachmentId = attachmentIdByPlacement.get(resolvedAnchor.to); if (targetAttachmentId) { anchorId = ensureAttachmentAnchor(targetAttachmentId, resolvedAnchor.targetAnchor ?? 'center'); } } if (!anchorId) { anchorId = rootAttachmentAnchorId ?? rootAnchorId; useAnchorSpec = false; } const adjustedPlacement = { ...placement, offset: { x: placement.offset.x + (isRoot ? lateralOffset : 0), y: placement.offset.y, z: placement.offset.z } }; const attachmentPlacement = useAnchorSpec ? adjustedPlacement : { ...adjustedPlacement, anchor: undefined }; const attachment = commanderPlacementToAttachment(attachmentPlacement, placementIndex, { anchorId, attachmentId, resolvedAnchor: useAnchorSpec ? resolvedAnchor : undefined, moduleEntry }); if (attachment) attachments.push(attachment); }); }); return { attachments, anchors }; }; const buildCommanderVolumeAttachmentCluster = ( volumeType: string, rootAnchorId: string, idPrefix: string ): { attachments: AttachmentEntry[]; anchors: AnchorEntry[] } => { const recipe = getHumanoidVolumeRecipe(volumeType, 'humanoid_command'); if (!recipe) return { attachments: [], anchors: [] }; const anchors: AnchorEntry[] = []; const attachments: AttachmentEntry[] = []; const anchorCache = new Map(); const attachmentIdByPlacement = new Map(); const ensureAttachmentAnchor = (attachmentId: string, face: AnchorFace): string => { const safeFace = face ?? 'center'; const key = `${attachmentId}:${safeFace}`; const existing = anchorCache.get(key); if (existing) return existing; const anchorId = `${idPrefix}-anchor-${attachmentId}-${safeFace}`.replace(/[^a-z0-9_-]/gi, '-'); anchors.push({ id: anchorId, type: 'attachment', attachmentId, attachmentFace: safeFace, orientation: 'alongEdge', radialAngle: 0, tags: ['attachment', volumeType], accepts: [] }); anchorCache.set(key, anchorId); return anchorId; }; recipe.placements.forEach((placement, placementIndex) => { const placementKey = placement.id ?? `${placement.partId}_${placementIndex}`; const attachmentId = `${idPrefix}-${placementKey}` .replace(/_/g, '-') .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .toLowerCase(); attachmentIdByPlacement.set(placementKey, attachmentId); }); const rootPlacementIndex = Math.max(0, recipe.placements.findIndex((placement) => !placement.anchor)); const rootPlacement = recipe.placements[rootPlacementIndex]; const rootPlacementKey = rootPlacement ? (rootPlacement.id ?? `${rootPlacement.partId}_${rootPlacementIndex}`) : `${idPrefix}-root`; const rootAttachmentId = attachmentIdByPlacement.get(rootPlacementKey); const rootAttachmentAnchorId = rootAttachmentId ? ensureAttachmentAnchor(rootAttachmentId, 'center') : null; recipe.placements.forEach((placement, placementIndex) => { const moduleEntry = getModuleById(placement.partId); if (!moduleEntry || placement.hidden) return; const resolvedAnchor = resolveHumanoidAnchorSpec(placement.anchor, 'humanoid_command'); const placementKey = placement.id ?? `${placement.partId}_${placementIndex}`; const attachmentId = attachmentIdByPlacement.get(placementKey) ?? `${idPrefix}-${placementKey}`.replace(/_/g, '-').toLowerCase(); let anchorId: string | null = null; let useAnchorSpec = true; if (placementIndex === rootPlacementIndex) { anchorId = rootAnchorId; useAnchorSpec = false; } else if (resolvedAnchor?.to) { const targetAttachmentId = attachmentIdByPlacement.get(resolvedAnchor.to); if (targetAttachmentId) { anchorId = ensureAttachmentAnchor(targetAttachmentId, resolvedAnchor.targetAnchor ?? 'center'); } } if (!anchorId) { anchorId = rootAttachmentAnchorId ?? rootAnchorId; useAnchorSpec = false; } const attachment = commanderPlacementToAttachment( useAnchorSpec ? placement : { ...placement, anchor: undefined }, placementIndex, { anchorId, attachmentId, resolvedAnchor: useAnchorSpec ? resolvedAnchor : undefined, moduleEntry } ); if (attachment) { attachments.push(attachment); } }); return { attachments, anchors }; }; const cloneVec3Like = (value?: Vec3Like): Vec3Like | undefined => (value ? { ...value } : undefined); const addVec3Like = (a: Vec3Like, b?: Vec3Like): Vec3Like => vec3(a.x + (b?.x ?? 0), a.y + (b?.y ?? 0), a.z + (b?.z ?? 0)); const multiplyVec3Like = (a: Vec3Like, b?: Vec3Like): Vec3Like => vec3(a.x * (b?.x ?? 1), a.y * (b?.y ?? 1), a.z * (b?.z ?? 1)); const subtractVec3Like = (a: Vec3Like, b: Vec3Like): Vec3Like => vec3(a.x - b.x, a.y - b.y, a.z - b.z); const maxVec3ComponentDelta = (a: Vec3Like, b: Vec3Like): number => Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z)); const lengthVec3Like = (value: Vec3Like): number => Math.hypot(value.x, value.y, value.z); const cloneWeaponPlacement = (placement: WeaponDesignPlacement): WeaponDesignPlacement => ({ ...placement, offset: cloneVec3Like(placement.offset), plannedCenter: cloneVec3Like(placement.plannedCenter), plannedDimensions: cloneVec3Like(placement.plannedDimensions), silhouetteMaxDimensions: cloneVec3Like(placement.silhouetteMaxDimensions), rotation: cloneVec3Like(placement.rotation), scale: cloneVec3Like(placement.scale) }); const getWeaponPlacementCenter = ( spec: WeaponDesignSpec, placement: WeaponDesignPlacement ): Vec3Like => addVec3Like(spec.sections[placement.section].origin, placement.offset); const computeWeaponPlanScale = ( def: HumanoidPartDefinition, targetDimensions: Vec3Like ): Vec3Like => { const baseDimensions = estimateWeaponModuleDimensions(def); return vec3( targetDimensions.x / Math.max(baseDimensions.x, 0.001), targetDimensions.y / Math.max(baseDimensions.y, 0.001), targetDimensions.z / Math.max(baseDimensions.z, 0.001) ); }; const compileWeaponPlanPart = ( spec: WeaponDesignSpec, plan: WeaponPlan, part: WeaponPartPlan ): WeaponDesignPlacement => { const section = spec.sections[part.section]; const stationStart = Math.min(part.stationStart, part.stationEnd); const stationEnd = Math.max(part.stationStart, part.stationEnd); const plannedDimensions = part.targetDimensions ? { ...part.targetDimensions } : vec3( part.width ?? 0.05, Math.max(0.01, stationEnd - stationStart), part.height ?? 0.05 ); const plannedCenter = vec3( part.centerX ?? 0, part.centerY, (stationStart + stationEnd) * 0.5 ); const moduleEntry = getModuleById(part.moduleId); return { id: part.id, moduleId: part.moduleId, section: part.section, offset: subtractVec3Like(plannedCenter, section.origin), rotation: cloneVec3Like(part.rotation), scale: moduleEntry ? computeWeaponPlanScale(moduleEntry.def, plannedDimensions) : undefined, planId: plan.id, plannedProfile: part.profile, plannedDimensions, plannedCenter, silhouetteMaxDimensions: cloneVec3Like(part.silhouetteMaxDimensions) }; }; const getWeaponBasePlacements = (spec: WeaponDesignSpec): WeaponDesignPlacement[] => { const plannedPlacements = spec.plan ? spec.plan.parts.map((part) => compileWeaponPlanPart(spec, spec.plan!, part)) : []; return [ ...plannedPlacements, ...spec.placements.map((placement) => cloneWeaponPlacement(placement)) ]; }; const formatWeaponPlannedProfile = (profile?: WeaponPlannedProfile): string => { switch (profile) { case 'precision_cradle': return 'precision cradle'; case 'precision_stock': return 'precision stock'; case 'precision_receiver_upper': return 'precision receiver upper'; case 'precision_receiver_lower': return 'precision receiver lower'; case 'precision_rail': return 'precision rail'; case 'precision_magwell': return 'precision magwell'; case 'precision_magwell_throat': return 'precision magwell throat'; case 'box_magazine': return 'box magazine'; case 'precision_barrel': return 'precision barrel'; case 'precision_shroud': return 'precision shroud'; case 'precision_cooling_jacket': return 'precision cooling jacket'; case 'precision_muzzle': return 'precision muzzle'; case 'precision_muzzle_brake': return 'precision muzzle brake'; case 'precision_optic_tube': return 'precision optic tube'; case 'precision_optic_clamp': return 'precision optic clamp'; case 'skeletal_optic_stanchion': return 'skeletal optic stanchion'; default: return 'planned part'; } }; const buildWeaponPlanDeltaLines = ( spec: WeaponDesignSpec, placements: WeaponDesignPlacement[] ): string[] => placements .filter((placement) => placement.plannedDimensions && placement.plannedCenter) .map((placement) => { const moduleEntry = getModuleById(placement.moduleId); if (!moduleEntry) { return `${placement.id} (${formatWeaponPlannedProfile(placement.plannedProfile)}): missing module`; } const actualDimensions = estimateWeaponModuleDimensions(moduleEntry.def, placement.scale); const actualCenter = getWeaponPlacementCenter(spec, placement); const parts: string[] = []; if (maxVec3ComponentDelta(placement.plannedDimensions!, actualDimensions) >= 0.005) { parts.push(`size ${formatDiagVec3(placement.plannedDimensions!)} -> ${formatDiagVec3(actualDimensions)}`); } if (maxVec3ComponentDelta(placement.plannedCenter!, actualCenter) >= 0.005) { parts.push(`ctr ${formatDiagVec3(placement.plannedCenter!)} -> ${formatDiagVec3(actualCenter)}`); } return `${placement.id} (${formatWeaponPlannedProfile(placement.plannedProfile)}): ${parts.length > 0 ? parts.join(' | ') : 'on plan'}`; }); const buildWeaponSilhouetteLimitLines = (placements: WeaponDesignPlacement[]): string[] => placements .filter((placement) => placement.silhouetteMaxDimensions) .flatMap((placement) => { const moduleEntry = getModuleById(placement.moduleId); if (!moduleEntry) { return [`${placement.id}: missing module for silhouette check`]; } const actualDimensions = estimateWeaponModuleDimensions(moduleEntry.def, placement.scale); const limits = placement.silhouetteMaxDimensions!; const breaches: string[] = []; if (actualDimensions.x > limits.x + 0.003) { breaches.push(`w ${actualDimensions.x.toFixed(2)} > ${limits.x.toFixed(2)}`); } if (actualDimensions.y > limits.y + 0.003) { breaches.push(`l ${actualDimensions.y.toFixed(2)} > ${limits.y.toFixed(2)}`); } if (actualDimensions.z > limits.z + 0.003) { breaches.push(`h ${actualDimensions.z.toFixed(2)} > ${limits.z.toFixed(2)}`); } return breaches.length > 0 ? [`${placement.id} (${formatWeaponPlannedProfile(placement.plannedProfile)}): ${breaches.join(' | ')}`] : []; }); const createWeaponAttachment = ( id: string, anchorId: string, moduleId: string, offset: Vec3Like, options?: { rotation?: Vec3Like; scale?: Vec3Like; } ): AttachmentEntry => ({ id, anchorId, moduleId, portId: 'mount', offset: { ...offset }, rotation: cloneVec3Like(options?.rotation ?? DEFAULT_WEAPON_ROTATION), scale: cloneVec3Like(options?.scale) }); const getWeaponPlannedShapeHint = (profile?: WeaponPlannedProfile): AttachmentEntry['shape'] | undefined => { switch (profile) { case 'precision_cradle': return { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.08, zBottom: 0.03 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'precision_stock': return { taper: { xTop: 0.08, xBottom: 0.24, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.2, corner: 0.22 }, profile: { kind: 'hardSurface', intensity: 0.86 } }; case 'precision_receiver_upper': return { taper: { xTop: 0.03, xBottom: 0.01, zTop: 0.06, zBottom: 0.02 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'precision_receiver_lower': return { taper: { xTop: 0.02, xBottom: 0.03, zTop: 0.05, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.2 } }; case 'precision_rail': return { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.04, zBottom: 0.04 }, chamfer: { edge: 0.05, corner: 0.06 }, profile: { kind: 'hardSurface', intensity: 0.12 } }; case 'precision_magwell': return { taper: { xTop: 0.03, xBottom: 0.02, zTop: 0.08, zBottom: 0.04 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.16 } }; case 'precision_magwell_throat': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.12 } }; case 'box_magazine': return { taper: { xTop: 0.01, xBottom: 0.03, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.2 } }; case 'precision_barrel': return { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.08, zBottom: 0.08 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.36 } }; case 'precision_shroud': return { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.18, zBottom: 0.06 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.72 } }; case 'precision_cooling_jacket': return { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.16, zBottom: 0.06 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.68 } }; case 'precision_muzzle': return { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.18, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.68 } }; case 'precision_muzzle_brake': return { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.2, zBottom: 0.08 }, chamfer: { edge: 0.2, corner: 0.22 }, profile: { kind: 'hardSurface', intensity: 0.78 } }; case 'precision_optic_tube': return { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.04, zBottom: 0.04 }, chamfer: { edge: 0.04, corner: 0.05 }, profile: { kind: 'hardSurface', intensity: 0.12 } }; case 'precision_optic_clamp': return { taper: { xTop: 0.03, xBottom: 0.02, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.16 } }; case 'skeletal_optic_stanchion': return { taper: { xTop: 0.05, xBottom: 0.02, zTop: 0.08, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'marksman_cradle': return { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.08, zBottom: 0.03 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.2 } }; case 'marksman_stock': return { taper: { xTop: 0.03, xBottom: 0.1, zTop: 0.1, zBottom: 0.04 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.58 } }; case 'marksman_rail': return { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.04, zBottom: 0.04 }, chamfer: { edge: 0.05, corner: 0.06 }, profile: { kind: 'hardSurface', intensity: 0.14 } }; case 'marksman_magwell': return { taper: { xTop: 0.03, xBottom: 0.02, zTop: 0.08, zBottom: 0.04 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'marksman_magwell_throat': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.14 } }; case 'marksman_magazine': return { taper: { xTop: 0.01, xBottom: 0.03, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.22 } }; case 'marksman_shroud': return { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.14, zBottom: 0.04 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.72 } }; case 'marksman_cooling_jacket': return { taper: { xTop: 0.04, xBottom: 0.01, zTop: 0.1, zBottom: 0.03 }, chamfer: { edge: 0.1, corner: 0.12 }, profile: { kind: 'hardSurface', intensity: 0.62 } }; case 'marksman_optic_clamp': return { taper: { xTop: 0.03, xBottom: 0.02, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'marksman_optic_stanchion': return { taper: { xTop: 0.05, xBottom: 0.02, zTop: 0.08, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.2 } }; case 'marksman_shoulder_pad': return { taper: { xTop: 0.03, xBottom: 0.05, zTop: 0.06, zBottom: 0.04 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.16 } }; case 'marksman_stock_bridge': return { taper: { xTop: 0.03, xBottom: 0.04, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.12 } }; case 'marksman_action_cover': return { taper: { xTop: 0.03, xBottom: 0.04, zTop: 0.08, zBottom: 0.04 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'marksman_action_spine': return { taper: { xTop: 0.02, xBottom: 0.03, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.14 } }; case 'marksman_breach': return { taper: { xTop: 0.03, xBottom: 0.04, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.24 } }; case 'marksman_receiver_upper': return { taper: { xTop: 0.03, xBottom: 0.01, zTop: 0.06, zBottom: 0.02 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.24 } }; case 'marksman_receiver_lower': return { taper: { xTop: 0.02, xBottom: 0.03, zTop: 0.05, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.22 } }; case 'marksman_jacket': return { taper: { xTop: 0.02, xBottom: 0.03, zTop: 0.05, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'marksman_barrel': return { taper: { xTop: 0.01, xBottom: 0.01, zTop: 0.04, zBottom: 0.01 }, chamfer: { edge: 0.04, corner: 0.06 }, profile: { kind: 'hardSurface', intensity: 0.42 } }; case 'marksman_mount_block': return { taper: { xTop: 0.08, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.78 } }; case 'marksman_bridge': return { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'plate', intensity: 0.76 } }; case 'marksman_saddle': return { taper: { xTop: 0.08, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.8 } }; case 'marksman_collar': return { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.16, zBottom: 0.06 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.82 } }; case 'marksman_throat_collar': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.08, zBottom: 0.03 }, chamfer: { edge: 0.1, corner: 0.12 }, profile: { kind: 'hardSurface', intensity: 0.68 } }; case 'marksman_muzzle_collar': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.08, zBottom: 0.03 }, chamfer: { edge: 0.1, corner: 0.12 }, profile: { kind: 'hardSurface', intensity: 0.66 } }; case 'marksman_fairing': return { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'plate', intensity: 0.82 } }; case 'marksman_brace': return { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.14, zBottom: 0.06 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'plate', intensity: 0.76 } }; case 'marksman_shroud_clamp': return { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.14, zBottom: 0.06 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.82 } }; case 'marksman_gas_block': return { taper: { xTop: 0.03, xBottom: 0.01, zTop: 0.06, zBottom: 0.03 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.16 } }; case 'marksman_gas_block_saddle': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.04, zBottom: 0.02 }, chamfer: { edge: 0.05, corner: 0.06 }, profile: { kind: 'hardSurface', intensity: 0.1 } }; case 'marksman_optic_tube': return { taper: { xTop: 0.03, xBottom: 0.02, zTop: 0.05, zBottom: 0.03 }, chamfer: { edge: 0.05, corner: 0.06 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'marksman_optic_ring': return { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.12, zBottom: 0.06 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.74 } }; case 'marksman_front_sight': return { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.18, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.82 } }; case 'marksman_front_sight_base': return { taper: { xTop: 0.08, xBottom: 0.03, zTop: 0.14, zBottom: 0.06 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.78 } }; case 'marksman_front_sight_post': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.14, zBottom: 0.04 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'plate', intensity: 0.86 } }; case 'marksman_rear_sight': return { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.8 } }; case 'marksman_optic_lens_front': return { taper: { xTop: 0.1, xBottom: 0.02, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.62 } }; case 'marksman_optic_lens_rear': return { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.58 } }; case 'marksman_action_port_cover': return { taper: { xTop: 0.03, xBottom: 0.02, zTop: 0.14, zBottom: 0.06 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.86 } }; case 'marksman_recoil_spring': return { taper: { xTop: 0.01, xBottom: 0.01, zTop: 0.03, zBottom: 0.03 }, chamfer: { edge: 0.03, corner: 0.04 }, profile: { kind: 'hardSurface', intensity: 0.08 } }; case 'marksman_muzzle': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.08, zBottom: 0.03 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.56 } }; case 'marksman_muzzle_brake': return { taper: { xTop: 0.12, xBottom: 0.03, zTop: 0.22, zBottom: 0.08 }, chamfer: { edge: 0.2, corner: 0.22 }, profile: { kind: 'hardSurface', intensity: 0.94 } }; case 'marksman_muzzle_baffle': return { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.12, zBottom: 0.04 }, chamfer: { edge: 0.1, corner: 0.12 }, profile: { kind: 'plate', intensity: 0.8 } }; case 'marksman_muzzle_vent_insert': return { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.12, zBottom: 0.04 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.82 } }; case 'marksman_counterweight': return { taper: { xTop: 0.08, xBottom: 0.04, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.72 } }; case 'marksman_rail_clamp': return { taper: { xTop: 0.08, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.8 } }; case 'marksman_rear_sight_base': return { taper: { xTop: 0.06, xBottom: 0.03, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'plate', intensity: 0.78 } }; case 'marksman_bolt_housing': return { taper: { xTop: 0.03, xBottom: 0.02, zTop: 0.08, zBottom: 0.04 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.18 } }; case 'marksman_mag_brace': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.05, zBottom: 0.03 }, chamfer: { edge: 0.05, corner: 0.06 }, profile: { kind: 'hardSurface', intensity: 0.12 } }; case 'marksman_mag_latch': return { taper: { xTop: 0.02, xBottom: 0.01, zTop: 0.04, zBottom: 0.02 }, chamfer: { edge: 0.05, corner: 0.06 }, profile: { kind: 'hardSurface', intensity: 0.1 } }; default: return undefined; } }; const isLocalMarksmanWeaponPlannedProfile = ( profile: WeaponPlannedProfile ): profile is LocalMarksmanWeaponPlannedProfile => profile === 'marksman_cradle' || profile === 'marksman_stock' || profile === 'marksman_rail' || profile === 'marksman_magwell' || profile === 'marksman_magwell_throat' || profile === 'marksman_magazine' || profile === 'marksman_shroud' || profile === 'marksman_cooling_jacket' || profile === 'marksman_optic_clamp' || profile === 'marksman_optic_stanchion' || profile === 'marksman_shoulder_pad' || profile === 'marksman_stock_bridge' || profile === 'marksman_action_cover' || profile === 'marksman_action_spine' || profile === 'marksman_breach' || profile === 'marksman_jacket' || profile === 'marksman_barrel' || profile === 'marksman_receiver_upper' || profile === 'marksman_receiver_lower' || profile === 'marksman_mount_block' || profile === 'marksman_bridge' || profile === 'marksman_saddle' || profile === 'marksman_collar' || profile === 'marksman_throat_collar' || profile === 'marksman_muzzle_collar' || profile === 'marksman_fairing' || profile === 'marksman_brace' || profile === 'marksman_shroud_clamp' || profile === 'marksman_gas_block' || profile === 'marksman_gas_block_saddle' || profile === 'marksman_optic_tube' || profile === 'marksman_optic_ring' || profile === 'marksman_front_sight_base' || profile === 'marksman_front_sight_post' || profile === 'marksman_front_sight' || profile === 'marksman_rear_sight' || profile === 'marksman_optic_lens_front' || profile === 'marksman_optic_lens_rear' || profile === 'marksman_action_port_cover' || profile === 'marksman_recoil_spring' || profile === 'marksman_muzzle' || profile === 'marksman_muzzle_brake' || profile === 'marksman_muzzle_baffle' || profile === 'marksman_muzzle_vent_insert' || profile === 'marksman_counterweight' || profile === 'marksman_rail_clamp' || profile === 'marksman_rear_sight_base' || profile === 'marksman_bolt_housing' || profile === 'marksman_mag_brace' || profile === 'marksman_mag_latch'; const getPromotedMarksmanPlannedShapeHint = ( profile: WeaponPlannedProfile, spec: WeaponDesignSpec ): { taper?: NonNullable['taper']; chamfer?: NonNullable['chamfer']; profile?: NonNullable['profile']; } | null => { const marksmanModule = weaponIterationModules?.marksman; if (!marksmanModule) { return null; } return marksmanModule.getMarksmanPlannedShapeHint(profile as any, spec as any) ?? null; }; const getPromotedMarksmanPlannedExtrusion = ( profile: WeaponPlannedProfile, spec: WeaponDesignSpec ): NonNullable['plannedWeaponExtrusion'] | null => { const marksmanModule = weaponIterationModules?.marksman; if (!marksmanModule) { return null; } const extrusion = marksmanModule.getMarksmanPlannedExtrusion(profile as any, spec as any); if (!extrusion) { return null; } return extrusion as NonNullable['plannedWeaponExtrusion']; }; const getStructuredWeaponPlannedShapeHint = ( spec: WeaponDesignSpec, profile?: WeaponPlannedProfile ): AttachmentEntry['shape'] | undefined => { if (!profile) return undefined; if (isLocalMarksmanWeaponPlannedProfile(profile)) { return getWeaponPlannedShapeHint(profile); } if (spec.archetype === 'marksman') { const promoted = getPromotedMarksmanPlannedShapeHint(profile, spec); if (promoted) { return { taper: promoted.taper ? { ...promoted.taper } : undefined, chamfer: promoted.chamfer ? { ...promoted.chamfer } : undefined, profile: promoted.profile ? { ...promoted.profile } : undefined }; } } return getWeaponPlannedShapeHint(profile); }; const getStructuredWeaponPlannedExtrusion = ( spec: WeaponDesignSpec, profile?: WeaponPlannedProfile ): NonNullable['plannedWeaponExtrusion'] | undefined => { if (!profile) return undefined; if (isLocalMarksmanWeaponPlannedProfile(profile)) { return undefined; } if (spec.archetype !== 'marksman') { return undefined; } const extrusion = getPromotedMarksmanPlannedExtrusion(profile, spec); if (!extrusion) { return undefined; } return { points: extrusion.points.map((point) => ({ ...point })), bevelScale: extrusion.bevelScale, bevelThicknessScale: extrusion.bevelThicknessScale }; }; const buildWeaponPlannedShape = ( profile: WeaponPlannedProfile, primitive?: PartPrimitive ): AttachmentEntry['shape'] => { const hint = getWeaponPlannedShapeHint(profile); return { primitive: primitive ?? 'box', plannedWeaponProfile: profile, taper: hint?.taper ? { ...hint.taper } : undefined, chamfer: hint?.chamfer ? { ...hint.chamfer } : undefined, profile: hint?.profile ? { ...hint.profile } : undefined }; }; const getStructuredWeaponAttachmentShapeOverride = ( spec: WeaponDesignSpec, attachmentId: string, primitive?: PartPrimitive ): AttachmentEntry['shape'] | undefined => { if (spec.archetype !== 'marksman') { return undefined; } switch (attachmentId) { case 'marksman-hardpoint': return buildWeaponPlannedShape('marksman_mount_block', primitive); case 'marksman-cradle': return buildWeaponPlannedShape('marksman_cradle', primitive); case 'marksman-stock': return buildWeaponPlannedShape('marksman_stock', primitive); case 'marksman-rail': return buildWeaponPlannedShape('marksman_rail', primitive); case 'marksman-magwell': return buildWeaponPlannedShape('marksman_magwell', primitive); case 'marksman-magwell-throat': return buildWeaponPlannedShape('marksman_magwell_throat', primitive); case 'marksman-magazine': return buildWeaponPlannedShape('marksman_magazine', primitive); case 'marksman-shroud': return buildWeaponPlannedShape('marksman_shroud', primitive); case 'marksman-cooling-jacket': return buildWeaponPlannedShape('marksman_cooling_jacket', primitive); case 'marksman-optic-clamp-port': case 'marksman-optic-clamp-starboard': return buildWeaponPlannedShape('marksman_optic_clamp', primitive); case 'marksman-optic-stanchion-rear': case 'marksman-optic-stanchion-front': return buildWeaponPlannedShape('marksman_optic_stanchion', primitive); case 'marksman-shoulder-piece': return buildWeaponPlannedShape('marksman_shoulder_pad', primitive); case 'marksman-stock-bridge': return buildWeaponPlannedShape('marksman_stock_bridge', primitive); case 'marksman-breach': return buildWeaponPlannedShape('marksman_breach', primitive); case 'marksman-jacket': return buildWeaponPlannedShape('marksman_jacket', primitive); case 'marksman-throat-jacket': return buildWeaponPlannedShape('marksman_jacket', primitive); case 'marksman-barrel': return buildWeaponPlannedShape('marksman_barrel', primitive); case 'marksman-receiver-upper': return buildWeaponPlannedShape('marksman_receiver_upper', primitive); case 'marksman-receiver-lower': return buildWeaponPlannedShape('marksman_receiver_lower', primitive); case 'marksman-action-cover': return buildWeaponPlannedShape('marksman_action_cover', primitive); case 'marksman-action-spine': return buildWeaponPlannedShape('marksman_action_spine', primitive); case 'marksman-rail-clamp-rear': case 'marksman-rail-clamp-front': return buildWeaponPlannedShape('marksman_rail_clamp', primitive); case 'marksman-rear-sight-base': return buildWeaponPlannedShape('marksman_rear_sight_base', primitive); case 'marksman-bolt-housing': return buildWeaponPlannedShape('marksman_bolt_housing', primitive); case 'marksman-optic-tube': return buildWeaponPlannedShape('marksman_optic_tube', primitive); case 'marksman-optic-ring-front': case 'marksman-optic-ring-rear': return buildWeaponPlannedShape('marksman_optic_ring', primitive); case 'marksman-gas-block': return buildWeaponPlannedShape('marksman_gas_block', primitive); case 'marksman-gas-block-saddle': return buildWeaponPlannedShape('marksman_gas_block_saddle', primitive); case 'marksman-shroud-clamp-port': case 'marksman-shroud-clamp-starboard': return buildWeaponPlannedShape('marksman_shroud_clamp', primitive); case 'marksman-muzzle': return buildWeaponPlannedShape('marksman_muzzle', primitive); case 'marksman-muzzle-brake': return buildWeaponPlannedShape('marksman_muzzle_brake', primitive); case 'marksman-muzzle-baffle-port': case 'marksman-muzzle-baffle-starboard': return buildWeaponPlannedShape('marksman_muzzle_baffle', primitive); case 'marksman-muzzle-vent-port': case 'marksman-muzzle-vent-starboard': return buildWeaponPlannedShape('marksman_muzzle_vent_insert', primitive); case 'marksman-front-sight-base': return buildWeaponPlannedShape('marksman_front_sight_base', primitive); case 'marksman-front-sight-post': return buildWeaponPlannedShape('marksman_front_sight_post', primitive); case 'marksman-front-sight': return buildWeaponPlannedShape('marksman_front_sight', primitive); case 'marksman-rear-sight': return buildWeaponPlannedShape('marksman_rear_sight', primitive); case 'marksman-optic-front-lens': return buildWeaponPlannedShape('marksman_optic_lens_front', primitive); case 'marksman-optic-rear-lens': return buildWeaponPlannedShape('marksman_optic_lens_rear', primitive); case 'marksman-action-port-cover': return buildWeaponPlannedShape('marksman_action_port_cover', primitive); case 'marksman-mag-brace': return buildWeaponPlannedShape('marksman_mag_brace', primitive); case 'marksman-mag-latch': return buildWeaponPlannedShape('marksman_mag_latch', primitive); case 'marksman-recoil-spring': return buildWeaponPlannedShape('marksman_recoil_spring', primitive); case 'marksman-counterweight': return buildWeaponPlannedShape('marksman_counterweight', primitive); case 'marksman-optic-riser': case 'marksman-throat-bridge': return buildWeaponPlannedShape('marksman_bridge', primitive); case 'marksman-optic-yoke': case 'marksman-rear-saddle': return buildWeaponPlannedShape('marksman_saddle', primitive); case 'marksman-throat-collar': return buildWeaponPlannedShape('marksman_throat_collar', primitive); case 'marksman-muzzle-collar': return buildWeaponPlannedShape('marksman_muzzle_collar', primitive); case 'marksman-stock-fairing': case 'marksman-magwell-bridge': return buildWeaponPlannedShape('marksman_fairing', primitive); case 'marksman-action-yoke': case 'marksman-rear-sight-saddle': case 'marksman-lower-keel': return buildWeaponPlannedShape('marksman_brace', primitive); default: return undefined; } }; const getPlannedWeaponExtrusionCacheToken = ( extrusion?: NonNullable['plannedWeaponExtrusion'] ): string => { if (!extrusion) return 'default'; return [ extrusion.bevelScale, extrusion.bevelThicknessScale, ...extrusion.points.flatMap((point) => [point.u, point.v]) ].join(','); }; const validateWeaponDesignSpec = (spec: WeaponDesignSpec): void => { const basePlacements = getWeaponBasePlacements(spec); const ids = new Set(); for (const placement of basePlacements) { if (ids.has(placement.id)) { throw new Error(`Duplicate weapon placement id "${placement.id}" in ${spec.archetype}.`); } ids.add(placement.id); } for (const section of ['rear', 'core', 'front'] as WeaponDesignSectionId[]) { if (!spec.sections[section]) { throw new Error(`Weapon spec "${spec.archetype}" is missing section "${section}".`); } } for (const transition of spec.transitionRules) { const transitionId = `transition-${transition.id}`; if (ids.has(transitionId)) { throw new Error(`Duplicate weapon transition id "${transition.id}" in ${spec.archetype}.`); } ids.add(transitionId); for (const targetId of transition.influence?.targetPlacementIds ?? []) { if (!ids.has(targetId)) { throw new Error(`Weapon transition "${transition.id}" references missing target "${targetId}" in ${spec.archetype}.`); } } } const baseIds = new Set(basePlacements.map((placement) => placement.id)); for (const subMass of spec.subMassRules) { for (const placementId of subMass.expectedPlacementIds) { if (!baseIds.has(placementId)) { throw new Error(`Weapon sub-mass "${subMass.id}" references missing placement "${placementId}" in ${spec.archetype}.`); } } } }; const resolveWeaponDesignPlacements = ( spec: WeaponDesignSpec, applyTransitionInfluence = true ): WeaponDesignPlacement[] => { validateWeaponDesignSpec(spec); const adjustedPlacements = getWeaponBasePlacements(spec); if (!applyTransitionInfluence) { return adjustedPlacements; } const placementById = new Map(adjustedPlacements.map((placement) => [placement.id, placement])); for (const transition of spec.transitionRules) { const influence = transition.influence; if (!influence) continue; for (const targetId of influence.targetPlacementIds) { const placement = placementById.get(targetId); if (!placement) continue; const baseScale = placement.scale ?? vec3(1, 1, 1); placement.scale = multiplyVec3Like(baseScale, influence.scaleMultiplier); if (influence.offsetShift) { placement.offset = addVec3Like(placement.offset ?? vec3(0, 0, 0), influence.offsetShift); } } } return adjustedPlacements; }; const buildWeaponAttachmentsFromResolvedPlacements = ( spec: WeaponDesignSpec, placements: WeaponDesignPlacement[] ): AttachmentEntry[] => { const mountSection = spec.sections.mount; const buildBaseAttachment = ( sectionId: WeaponDesignSectionId, id: string, moduleId: string, offset?: Vec3Like, rotation?: Vec3Like, scale?: Vec3Like ): AttachmentEntry => { const section = spec.sections[sectionId]; const anchorId = section.anchorId ?? mountSection.anchorId ?? 'anchor-hand-r'; const baseRotation = section.rotation ?? mountSection.rotation ?? DEFAULT_WEAPON_ROTATION; return createWeaponAttachment( id, anchorId, moduleId, addVec3Like(section.origin, offset), { rotation: rotation ?? baseRotation, scale } ); }; const buildPlacementAttachment = (placement: WeaponDesignPlacement): AttachmentEntry => { const section = spec.sections[placement.section]; const plannedRotation = placement.plannedProfile ? (placement.rotation ?? section.rotation ?? vec3(0, 0, 0)) : placement.rotation; const attachment = buildBaseAttachment( placement.section, `attach-weapon-${placement.id}`, placement.moduleId, placement.offset, plannedRotation, placement.scale ); const moduleEntry = getModuleById(placement.moduleId); if (moduleEntry?.def.primitive) { let shape: AttachmentEntry['shape'] = { primitive: moduleEntry.def.primitive, plannedWeaponProfile: placement.plannedProfile }; if (placement.plannedDimensions) { shape.size = { x: placement.plannedDimensions.x, // Weapon plans store dimensions as width, length, height. Planned profile // geometry consumes width, height, length, so swap the last two axes here. y: placement.plannedDimensions.z, z: placement.plannedDimensions.y }; const basePlanScale = computeWeaponPlanScale(moduleEntry.def, placement.plannedDimensions); const influencedScale = placement.scale ?? basePlanScale; const relativeScale = vec3( influencedScale.x / Math.max(basePlanScale.x, 0.001), // Planned weapon geometry uses width, height, length while the weapon // plan scale tracks width, length, height. Swap the last two axes so // transition/influence scaling lands on the intended geometry axes. influencedScale.z / Math.max(basePlanScale.z, 0.001), influencedScale.y / Math.max(basePlanScale.y, 0.001) ); const isIdentityScale = maxVec3ComponentDelta(relativeScale, vec3(1, 1, 1)) < 1e-4; attachment.scale = isIdentityScale ? undefined : relativeScale; } const shapeHint = getStructuredWeaponPlannedShapeHint(spec, placement.plannedProfile); if (shapeHint) { shape = mergeAttachmentShape(shape, shapeHint) ?? shape; } const extrusion = getStructuredWeaponPlannedExtrusion(spec, placement.plannedProfile); if (extrusion) { shape.plannedWeaponExtrusion = extrusion; } attachment.shape = shape; } if (moduleEntry?.def.primitive) { const overrideShape = getStructuredWeaponAttachmentShapeOverride( spec, placement.id, moduleEntry.def.primitive ); if (overrideShape) { const overrideChangesProfile = Boolean( overrideShape.plannedWeaponProfile && overrideShape.plannedWeaponProfile !== attachment.shape?.plannedWeaponProfile ); attachment.shape = attachment.shape ? mergeAttachmentShape(attachment.shape, overrideShape) : overrideShape; if (overrideChangesProfile && attachment.shape && !overrideShape.plannedWeaponExtrusion) { // Local marksman profile overrides must drop any previously attached promoted // extrusion, otherwise the live render path keeps using the old profile mesh. delete attachment.shape.plannedWeaponExtrusion; } } } return attachment; }; const buildTransitionAttachment = (transition: WeaponTransitionRule): AttachmentEntry => { const attachment = buildBaseAttachment( transition.section, `attach-weapon-transition-${transition.id}`, transition.moduleId, transition.offset, transition.rotation, transition.scale ); const moduleEntry = getModuleById(transition.moduleId); if (moduleEntry?.def.primitive) { attachment.shape = { primitive: moduleEntry.def.primitive }; const overrideShape = getStructuredWeaponAttachmentShapeOverride( spec, transition.id, moduleEntry.def.primitive ); if (overrideShape) { attachment.shape = mergeAttachmentShape(attachment.shape, overrideShape); } } return attachment; }; return [ ...placements.map((placement) => buildPlacementAttachment(placement)), ...spec.transitionRules.map((transition) => buildTransitionAttachment(transition)) ]; }; const buildWeaponAttachmentsFromSpec = (spec: WeaponDesignSpec): AttachmentEntry[] => buildWeaponAttachmentsFromResolvedPlacements(spec, resolveWeaponDesignPlacements(spec)); const estimateWeaponModuleDimensions = (def: HumanoidPartDefinition, scale?: Vec3Like): Vec3Like => { const size = def.size ?? { x: 0.1, y: 0.1, z: 0.1 }; const dimensions = vec3( size.x ?? 0.1, size.y ?? size.x ?? 0.1, size.z ?? size.x ?? 0.1 ); return multiplyVec3Like(dimensions, scale ?? vec3(1, 1, 1)); }; const buildWeaponSectionEnvelopes = ( spec: WeaponDesignSpec, placements: WeaponDesignPlacement[] ): Map => { const envelopes = new Map(); for (const placement of placements) { const section = spec.sections[placement.section]; const moduleEntry = getModuleById(placement.moduleId); if (!section || !moduleEntry) continue; const center = addVec3Like(section.origin, placement.offset); const dimensions = estimateWeaponModuleDimensions(moduleEntry.def, placement.scale); const half = vec3(dimensions.x * 0.5, dimensions.y * 0.5, dimensions.z * 0.5); const min = subtractVec3Like(center, half); const max = addVec3Like(center, half); const existing = envelopes.get(placement.section); if (!existing) { envelopes.set(placement.section, { count: 1, min, max, size: subtractVec3Like(max, min), center }); continue; } existing.count += 1; existing.min = vec3( Math.min(existing.min.x, min.x), Math.min(existing.min.y, min.y), Math.min(existing.min.z, min.z) ); existing.max = vec3( Math.max(existing.max.x, max.x), Math.max(existing.max.y, max.y), Math.max(existing.max.z, max.z) ); existing.size = subtractVec3Like(existing.max, existing.min); existing.center = vec3( (existing.min.x + existing.max.x) * 0.5, (existing.min.y + existing.max.y) * 0.5, (existing.min.z + existing.max.z) * 0.5 ); } return envelopes; }; const formatWeaponEnvelopeDelta = ( before: WeaponSectionEnvelope, after: WeaponSectionEnvelope ): string => { const sizeChanged = maxVec3ComponentDelta(before.size, after.size) >= 0.005; const centerChanged = maxVec3ComponentDelta(before.center, after.center) >= 0.005; const parts: string[] = []; if (sizeChanged) { parts.push(`size ${formatDiagVec3(before.size)} -> ${formatDiagVec3(after.size)}`); } if (centerChanged) { parts.push(`ctr ${formatDiagVec3(before.center)} -> ${formatDiagVec3(after.center)}`); } return parts.length > 0 ? parts.join(' | ') : 'stable'; }; const computeWeaponArchetypeQaScore = (spec: WeaponDesignSpec): WeaponArchetypeQaScore => { const basePlacements = resolveWeaponDesignPlacements(spec, false); const influencedPlacements = resolveWeaponDesignPlacements(spec, true); const baseEnvelopes = buildWeaponSectionEnvelopes(spec, basePlacements); const influencedEnvelopes = buildWeaponSectionEnvelopes(spec, influencedPlacements); const influenceHits = new Map(); let scaleShift = 0; let offsetShift = 0; for (const transition of spec.transitionRules) { const influence = transition.influence; if (!influence) continue; const scaleMultiplier = influence.scaleMultiplier ?? vec3(1, 1, 1); const scaleDelta = subtractVec3Like(scaleMultiplier, vec3(1, 1, 1)); const offsetDelta = influence.offsetShift ?? vec3(0, 0, 0); for (const targetId of influence.targetPlacementIds) { influenceHits.set(targetId, (influenceHits.get(targetId) ?? 0) + 1); scaleShift += lengthVec3Like(scaleDelta); offsetShift += lengthVec3Like(offsetDelta); } } let changedSections = 0; let sizeShift = 0; let centerShift = 0; for (const [sectionId, before] of baseEnvelopes.entries()) { const after = influencedEnvelopes.get(sectionId); if (!after) continue; const sizeDelta = lengthVec3Like(subtractVec3Like(after.size, before.size)); const centerDelta = lengthVec3Like(subtractVec3Like(after.center, before.center)); sizeShift += sizeDelta; centerShift += centerDelta; if (sizeDelta >= 0.01 || centerDelta >= 0.008) { changedSections += 1; } } const multiHitPlacements = [...influenceHits.values()].filter((count) => count > 1).length; const overCorrection = scaleShift * 0.75 + offsetShift * 12 + sizeShift * 10; const instability = centerShift * 14 + changedSections * 0.35 + multiHitPlacements * 0.6; return { archetype: spec.archetype, score: overCorrection + instability, overCorrection, instability, changedSections, multiHitPlacements }; }; const getWeaponQaRanking = (): WeaponArchetypeQaScore[] => [...STRUCTURED_WEAPON_DESIGN_SPECS.values()] .map((spec) => computeWeaponArchetypeQaScore(spec)) .sort((a, b) => b.score - a.score); // These specs define mass hierarchy first; detail modules are layered on top of // rear/core/front sections instead of being freehanded as isolated offsets. const STRUCTURED_WEAPON_DESIGN_SPECS = new Map([ ['rifle', { archetype: 'rifle', doctrine: { role: 'service rifle', readProfile: 'balanced', balance: 'center-balanced', visualLanguage: 'service', signature: ['straight top line', 'disciplined receiver block', 'light underslung support'] }, holdProfile: RIFLE_WEAPON_HOLD_PROFILE, sections: { mount: { anchorId: 'anchor-hand-r', origin: vec3(0, 0, 0), rotation: DEFAULT_WEAPON_ROTATION }, rear: { origin: vec3(0, 0.004, -0.104) }, core: { origin: vec3(0, 0.024, 0.044) }, front: { origin: vec3(0, 0.024, 0.236) }, top: { origin: vec3(0, 0.074, 0.032) }, bottom: { origin: vec3(0, -0.03, 0.108) }, port: { origin: vec3(-0.048, 0.024, 0.042) }, starboard: { origin: vec3(0.048, 0.024, 0.042) }, support: { origin: vec3(0, 0, 0) } }, placements: [ { id: 'rifle-hardpoint', moduleId: 'detail_weapon_hardpoint', section: 'mount', offset: vec3(0, 0.012, -0.024), scale: vec3(0.94, 0.9, 0.88) }, { id: 'rifle-cradle', moduleId: 'detail_weapon_cradle', section: 'mount', offset: vec3(0, 0.026, 0.006), scale: vec3(0.92, 0.88, 0.82) }, { id: 'rifle-stock', moduleId: 'detail_weapon_stock', section: 'rear', offset: vec3(0, -0.008, -0.008), rotation: vec3(98, 0, 0), scale: vec3(0.96, 0.94, 0.98) }, { id: 'rifle-breach', moduleId: 'detail_weapon_breach', section: 'rear', offset: vec3(0, 0.016, 0.072), scale: vec3(0.94, 0.94, 0.92) }, { id: 'rifle-receiver', moduleId: 'detail_weapon_receiver', section: 'core', offset: vec3(0, -0.004, 0.004), scale: vec3(1.02, 1.0, 1.04) }, { id: 'rifle-jacket', moduleId: 'detail_weapon_jacket', section: 'core', offset: vec3(0, 0.008, 0.072), scale: vec3(1.0, 0.96, 1.0) }, { id: 'rifle-rail', moduleId: 'detail_weapon_rail', section: 'top', offset: vec3(0, -0.008, 0.01), scale: vec3(0.98, 0.96, 0.98) }, { id: 'rifle-rail-clamp', moduleId: 'detail_weapon_hardpoint', section: 'top', offset: vec3(0, -0.016, 0.018), scale: vec3(0.62, 0.58, 0.52) }, { id: 'rifle-rear-sight', moduleId: 'detail_weapon_rear_sight', section: 'top', offset: vec3(0, 0.002, -0.024), scale: vec3(0.96, 0.96, 0.96) }, { id: 'rifle-barrel', moduleId: 'detail_weapon_barrel', section: 'front', offset: vec3(0, -0.006, 0.014), scale: vec3(1.0, 0.96, 1.0) }, { id: 'rifle-shroud', moduleId: 'detail_weapon_shroud', section: 'front', offset: vec3(0, 0.006, -0.036), scale: vec3(0.96, 0.96, 0.98) }, { id: 'rifle-cooling-jacket', moduleId: 'detail_weapon_cooling_jacket', section: 'front', offset: vec3(0, 0.008, -0.014), scale: vec3(0.94, 0.94, 0.96) }, { id: 'rifle-gas-block', moduleId: 'detail_weapon_gas_block', section: 'front', offset: vec3(0, 0.022, -0.074), scale: vec3(0.82, 0.8, 0.8) }, { id: 'rifle-muzzle', moduleId: 'detail_weapon_muzzle', section: 'front', offset: vec3(0, -0.006, 0.122), scale: vec3(0.92, 0.9, 0.9) }, { id: 'rifle-front-sight', moduleId: 'detail_weapon_front_sight', section: 'front', offset: vec3(0, 0.034, 0.104), scale: vec3(0.94, 0.94, 0.94) }, { id: 'rifle-underslung', moduleId: 'detail_weapon_underslung', section: 'bottom', offset: vec3(0, 0.004, -0.018), scale: vec3(0.82, 0.84, 0.88) }, { id: 'rifle-feed', moduleId: 'detail_weapon_feed', section: 'starboard', offset: vec3(-0.004, -0.01, -0.052), scale: vec3(0.82, 0.84, 0.82) }, { id: 'rifle-ejector', moduleId: 'detail_weapon_ejector', section: 'starboard', offset: vec3(0.004, 0.016, -0.06), scale: vec3(0.82, 0.82, 0.8) } ], subMassRules: [ { id: 'rifle-rear-chassis', label: 'Rear chassis', section: 'rear', emphasis: 'primary', expectedPlacementIds: ['rifle-stock', 'rifle-breach'] }, { id: 'rifle-core-body', label: 'Core body', section: 'core', emphasis: 'primary', expectedPlacementIds: ['rifle-receiver', 'rifle-jacket'] }, { id: 'rifle-front-assembly', label: 'Front assembly', section: 'front', emphasis: 'primary', expectedPlacementIds: ['rifle-barrel', 'rifle-shroud', 'rifle-cooling-jacket', 'rifle-gas-block', 'rifle-muzzle'] }, { id: 'rifle-sightline', label: 'Sightline', section: 'top', emphasis: 'secondary', expectedPlacementIds: ['rifle-rail', 'rifle-rail-clamp', 'rifle-rear-sight', 'rifle-front-sight'] }, { id: 'rifle-underbody', label: 'Underbody', section: 'bottom', emphasis: 'support', expectedPlacementIds: ['rifle-underslung', 'rifle-feed', 'rifle-ejector'] } ], transitionRules: [ { id: 'rifle-rear-saddle', label: 'Rear saddle', section: 'rear', moduleId: 'detail_weapon_cradle', style: 'saddle', offset: vec3(0, 0.008, 0.104), scale: vec3(0.82, 0.8, 0.74), influence: { targetPlacementIds: ['rifle-receiver', 'rifle-jacket'], scaleMultiplier: vec3(1.04, 1.02, 1.05), offsetShift: vec3(0, 0.002, -0.006) } }, { id: 'rifle-throat-collar', label: 'Throat collar', section: 'front', moduleId: 'detail_weapon_gas_block', style: 'collar', offset: vec3(0, 0.018, -0.128), scale: vec3(0.74, 0.72, 0.72), influence: { targetPlacementIds: ['rifle-barrel', 'rifle-shroud', 'rifle-cooling-jacket'], scaleMultiplier: vec3(0.96, 0.98, 0.97) } }, { id: 'rifle-top-bridge', label: 'Top bridge', section: 'top', moduleId: 'detail_weapon_rail', style: 'bridge', offset: vec3(0, -0.002, 0.044), scale: vec3(0.8, 0.78, 0.78), influence: { targetPlacementIds: ['rifle-rail', 'rifle-rail-clamp', 'rifle-rear-sight', 'rifle-front-sight'], scaleMultiplier: vec3(1.02, 1.02, 1.02), offsetShift: vec3(0, 0.002, 0.002) } }, { id: 'rifle-lower-brace', label: 'Lower brace', section: 'bottom', moduleId: 'detail_weapon_hardpoint', style: 'brace', offset: vec3(0, 0.004, -0.034), scale: vec3(0.7, 0.68, 0.66), influence: { targetPlacementIds: ['rifle-underslung', 'rifle-feed'], scaleMultiplier: vec3(1.02, 1.01, 0.96), offsetShift: vec3(0, -0.002, -0.004) } } ] }], ['heavy', { archetype: 'heavy', doctrine: { role: 'heavy cannon', readProfile: 'balanced', balance: 'front-heavy', visualLanguage: 'industrial', signature: ['oversized chamber block', 'paired side systems', 'obvious recoil package'] }, holdProfile: HEAVY_WEAPON_HOLD_PROFILE, sections: { mount: { anchorId: 'anchor-hand-r', origin: vec3(0, 0, 0), rotation: DEFAULT_WEAPON_ROTATION }, rear: { origin: vec3(0, 0.006, -0.146) }, core: { origin: vec3(0, 0.032, 0.054) }, front: { origin: vec3(0, 0.032, 0.278) }, top: { origin: vec3(0, 0.09, 0.046) }, bottom: { origin: vec3(0, -0.058, 0.076) }, port: { origin: vec3(-0.07, 0.024, 0.08) }, starboard: { origin: vec3(0.07, 0.024, 0.08) }, support: { origin: vec3(0, 0, 0) } }, placements: [ { id: 'heavy-hardpoint', moduleId: 'detail_weapon_hardpoint', section: 'mount', offset: vec3(0, 0.014, -0.038), scale: vec3(1.12, 1.0, 0.96) }, { id: 'heavy-cradle', moduleId: 'detail_weapon_cradle', section: 'mount', offset: vec3(0, 0.03, 0.012), scale: vec3(1.08, 0.98, 0.88) }, { id: 'heavy-stock', moduleId: 'detail_weapon_stock', section: 'rear', offset: vec3(0, -0.012, -0.004), rotation: vec3(100, 0, 0), scale: vec3(1.08, 1.02, 1.04) }, { id: 'heavy-breach', moduleId: 'detail_weapon_breach', section: 'rear', offset: vec3(0, 0.024, 0.1), scale: vec3(1.14, 1.08, 1.06) }, { id: 'heavy-receiver', moduleId: 'detail_weapon_receiver', section: 'core', offset: vec3(0, -0.004, 0.002), scale: vec3(1.16, 1.1, 1.12) }, { id: 'heavy-jacket', moduleId: 'detail_weapon_jacket', section: 'core', offset: vec3(0, 0.01, 0.078), scale: vec3(1.16, 1.08, 1.12) }, { id: 'heavy-rail', moduleId: 'detail_weapon_rail', section: 'top', offset: vec3(0, -0.004, -0.002), scale: vec3(1.08, 1.04, 1.04) }, { id: 'heavy-recoil-spring', moduleId: 'detail_weapon_recoil_spring', section: 'top', offset: vec3(0, 0.02, -0.134), scale: vec3(1.08, 1.04, 1.0) }, { id: 'heavy-barrel', moduleId: 'detail_weapon_barrel', section: 'front', offset: vec3(0, -0.006, 0.008), scale: vec3(1.18, 1.08, 1.14) }, { id: 'heavy-shroud', moduleId: 'detail_weapon_shroud', section: 'front', offset: vec3(0, 0.01, -0.048), scale: vec3(1.12, 1.08, 1.08) }, { id: 'heavy-cooling-jacket', moduleId: 'detail_weapon_cooling_jacket', section: 'front', offset: vec3(0, 0.014, -0.026), scale: vec3(1.08, 1.06, 1.06) }, { id: 'heavy-muzzle', moduleId: 'detail_weapon_muzzle', section: 'front', offset: vec3(0, -0.006, 0.13), scale: vec3(1.08, 1.04, 1.0) }, { id: 'heavy-muzzle-brake', moduleId: 'detail_weapon_muzzle', section: 'front', offset: vec3(0, -0.006, 0.172), scale: vec3(1.02, 0.98, 0.94) }, { id: 'heavy-counterweight', moduleId: 'detail_weapon_counterweight', section: 'bottom', offset: vec3(0, -0.006, -0.006), scale: vec3(1.04, 1.02, 1.0) }, { id: 'heavy-feed', moduleId: 'detail_weapon_feed', section: 'starboard', offset: vec3(-0.014, 0, -0.086), scale: vec3(1.06, 1.02, 1.02) }, { id: 'heavy-ejector', moduleId: 'detail_weapon_ejector', section: 'starboard', offset: vec3(-0.006, 0.028, -0.098), scale: vec3(0.88, 0.88, 0.84) }, { id: 'heavy-sidecar-starboard', moduleId: 'detail_weapon_sidecar', section: 'starboard', scale: vec3(1.02, 1.0, 0.98) }, { id: 'heavy-sidecar-port', moduleId: 'detail_weapon_sidecar', section: 'port', scale: vec3(1.02, 1.0, 0.98) } ], subMassRules: [ { id: 'heavy-rear-chamber', label: 'Rear chamber', section: 'rear', emphasis: 'primary', expectedPlacementIds: ['heavy-stock', 'heavy-breach'] }, { id: 'heavy-core-body', label: 'Core body', section: 'core', emphasis: 'primary', expectedPlacementIds: ['heavy-receiver', 'heavy-jacket'] }, { id: 'heavy-cannon-front', label: 'Cannon front', section: 'front', emphasis: 'primary', expectedPlacementIds: ['heavy-barrel', 'heavy-shroud', 'heavy-cooling-jacket', 'heavy-muzzle', 'heavy-muzzle-brake'] }, { id: 'heavy-lateral-systems', label: 'Lateral systems', section: 'starboard', emphasis: 'secondary', expectedPlacementIds: ['heavy-feed', 'heavy-ejector', 'heavy-sidecar-starboard', 'heavy-sidecar-port'] }, { id: 'heavy-recoil-pack', label: 'Recoil pack', section: 'top', emphasis: 'support', expectedPlacementIds: ['heavy-rail', 'heavy-recoil-spring', 'heavy-counterweight'] } ], transitionRules: [ { id: 'heavy-rear-saddle', label: 'Rear saddle', section: 'rear', moduleId: 'detail_weapon_cradle', style: 'saddle', offset: vec3(0, 0.006, 0.128), scale: vec3(0.92, 0.86, 0.76), influence: { targetPlacementIds: ['heavy-breach', 'heavy-receiver', 'heavy-jacket'], scaleMultiplier: vec3(1.05, 1.02, 1.06), offsetShift: vec3(0, 0.002, -0.01) } }, { id: 'heavy-throat-collar', label: 'Throat collar', section: 'front', moduleId: 'detail_weapon_jacket', style: 'collar', offset: vec3(0, 0.004, -0.11), scale: vec3(0.82, 0.8, 0.78), influence: { targetPlacementIds: ['heavy-barrel', 'heavy-shroud', 'heavy-cooling-jacket'], scaleMultiplier: vec3(0.97, 0.98, 0.97) } }, { id: 'heavy-port-brace', label: 'Port brace', section: 'port', moduleId: 'detail_weapon_hardpoint', style: 'brace', offset: vec3(0.02, 0.004, -0.03), scale: vec3(0.78, 0.76, 0.72), influence: { targetPlacementIds: ['heavy-sidecar-port'], scaleMultiplier: vec3(0.98, 1.02, 0.96), offsetShift: vec3(0.002, 0, -0.004) } }, { id: 'heavy-starboard-brace', label: 'Starboard brace', section: 'starboard', moduleId: 'detail_weapon_hardpoint', style: 'brace', offset: vec3(-0.02, 0.004, -0.03), scale: vec3(0.78, 0.76, 0.72), influence: { targetPlacementIds: ['heavy-feed', 'heavy-ejector', 'heavy-sidecar-starboard'], scaleMultiplier: vec3(0.98, 1.02, 0.96), offsetShift: vec3(-0.002, 0, -0.004) } } ] }], ['marksman', { archetype: 'marksman', doctrine: { role: 'marksman rifle', readProfile: 'long', balance: 'front-heavy', visualLanguage: 'precision', signature: ['long uninterrupted barrel line', 'elevated optic bridge', 'stepped rear action block'] }, holdProfile: MARKSMAN_WEAPON_HOLD_PROFILE, sections: { mount: { anchorId: 'anchor-hand-r', origin: vec3(0, 0, 0), rotation: DEFAULT_WEAPON_ROTATION }, rear: { origin: vec3(0, 0.004, -0.126) }, core: { origin: vec3(0, 0.028, 0.05) }, front: { origin: vec3(0, 0.024, 0.304) }, top: { origin: vec3(0, 0.09, 0.038) }, bottom: { origin: vec3(0, -0.048, 0.082) }, port: { origin: vec3(-0.03, 0.11, 0.066) }, starboard: { origin: vec3(0.034, 0.012, 0.03) }, support: { origin: vec3(0, 0, 0) } }, plan: { id: 'marksman-precision-layout', spineStart: -0.24, spineEnd: 0.56, parts: [ { id: 'marksman-cradle', moduleId: 'detail_weapon_cradle_precision', section: 'mount', profile: 'precision_cradle', stationStart: -0.064, stationEnd: 0.074, centerY: 0.02, width: 0.05, height: 0.058 }, { id: 'marksman-stock', moduleId: 'detail_weapon_stock_precision', section: 'rear', profile: 'precision_stock', stationStart: -0.222, stationEnd: -0.082, centerY: 0.002, width: 0.04, height: 0.094, rotation: vec3(100, 0, 0) }, { id: 'marksman-receiver-upper', moduleId: 'detail_weapon_receiver_precision', section: 'core', profile: 'precision_receiver_upper', stationStart: 0.01, stationEnd: 0.152, centerY: 0.036, width: 0.067, height: 0.094 }, { id: 'marksman-receiver-lower', moduleId: 'detail_weapon_receiver_lower', section: 'core', profile: 'precision_receiver_lower', stationStart: -0.014, stationEnd: 0.102, centerY: 0.01, targetDimensions: vec3(0.066, 0.092, 0.094), silhouetteMaxDimensions: vec3(0.072, 0.11, 0.098) }, { id: 'marksman-rail', moduleId: 'detail_weapon_rail', section: 'top', profile: 'precision_rail', stationStart: -0.062, stationEnd: 0.174, centerY: 0.082, targetDimensions: vec3(0.042, 0.236, 0.046), silhouetteMaxDimensions: vec3(0.048, 0.244, 0.05) }, { id: 'marksman-optic-clamp-port', moduleId: 'detail_weapon_hardpoint', section: 'port', profile: 'precision_optic_clamp', stationStart: 0.09, stationEnd: 0.126, centerX: 0.01, centerY: 0.008, rotation: vec3(0, 0, 90), targetDimensions: vec3(0.034, 0.048, 0.04), silhouetteMaxDimensions: vec3(0.038, 0.054, 0.044) }, { id: 'marksman-optic-clamp-starboard', moduleId: 'detail_weapon_hardpoint', section: 'starboard', profile: 'precision_optic_clamp', stationStart: 0.09, stationEnd: 0.126, centerX: -0.01, centerY: 0.008, rotation: vec3(0, 0, 90), targetDimensions: vec3(0.034, 0.048, 0.04), silhouetteMaxDimensions: vec3(0.038, 0.054, 0.044) }, { id: 'marksman-optic-tube', moduleId: 'detail_scope_tube', section: 'top', profile: 'precision_optic_tube', stationStart: -0.094, stationEnd: 0.21, centerY: 0.112, rotation: vec3(0, 0, 0), targetDimensions: vec3(0.046, 0.318, 0.042), silhouetteMaxDimensions: vec3(0.05, 0.334, 0.046) }, { id: 'marksman-magwell', moduleId: 'detail_weapon_hardpoint', section: 'bottom', profile: 'precision_magwell', stationStart: -0.018, stationEnd: 0.042, centerY: -0.036, targetDimensions: vec3(0.046, 0.072, 0.06), silhouetteMaxDimensions: vec3(0.05, 0.078, 0.066) }, { id: 'marksman-magwell-throat', moduleId: 'detail_weapon_jacket', section: 'bottom', profile: 'precision_magwell_throat', stationStart: 0.002, stationEnd: 0.066, centerY: -0.048, targetDimensions: vec3(0.034, 0.078, 0.064), silhouetteMaxDimensions: vec3(0.038, 0.084, 0.07) }, { id: 'marksman-magazine', moduleId: 'detail_weapon_mag_box', section: 'bottom', profile: 'box_magazine', stationStart: -0.008, stationEnd: 0.064, centerY: -0.082, targetDimensions: vec3(0.044, 0.146, 0.07), silhouetteMaxDimensions: vec3(0.048, 0.152, 0.074) }, { id: 'marksman-barrel', moduleId: 'detail_weapon_barrel', section: 'front', profile: 'precision_barrel', stationStart: 0.126, stationEnd: 0.532, centerY: 0.02, targetDimensions: vec3(0.05, 0.408, 0.03), silhouetteMaxDimensions: vec3(0.054, 0.42, 0.034) }, { id: 'marksman-shroud', moduleId: 'detail_weapon_shroud', section: 'front', profile: 'precision_shroud', stationStart: 0.146, stationEnd: 0.302, centerY: 0.028, targetDimensions: vec3(0.048, 0.188, 0.046), silhouetteMaxDimensions: vec3(0.052, 0.196, 0.05) }, { id: 'marksman-cooling-jacket', moduleId: 'detail_weapon_cooling_jacket', section: 'front', profile: 'precision_cooling_jacket', stationStart: 0.182, stationEnd: 0.354, centerY: 0.028, targetDimensions: vec3(0.042, 0.168, 0.042), silhouetteMaxDimensions: vec3(0.046, 0.176, 0.046) }, { id: 'marksman-muzzle', moduleId: 'detail_weapon_muzzle', section: 'front', profile: 'precision_muzzle', stationStart: 0.448, stationEnd: 0.5, centerY: 0.019, targetDimensions: vec3(0.022, 0.086, 0.022), silhouetteMaxDimensions: vec3(0.024, 0.092, 0.024) }, { id: 'marksman-muzzle-brake', moduleId: 'detail_weapon_muzzle', section: 'front', profile: 'precision_muzzle_brake', stationStart: 0.504, stationEnd: 0.548, centerY: 0.02, targetDimensions: vec3(0.036, 0.08, 0.036), silhouetteMaxDimensions: vec3(0.04, 0.086, 0.04) }, { id: 'marksman-optic-stanchion-rear', moduleId: 'detail_weapon_hardpoint', section: 'top', profile: 'skeletal_optic_stanchion', stationStart: 0.018, stationEnd: 0.046, centerY: 0.086, width: 0.022, height: 0.048 }, { id: 'marksman-optic-stanchion-front', moduleId: 'detail_weapon_hardpoint', section: 'top', profile: 'skeletal_optic_stanchion', stationStart: 0.123, stationEnd: 0.149, centerY: 0.086, width: 0.021, height: 0.046 } ] }, placements: [ { id: 'marksman-hardpoint', moduleId: 'detail_weapon_hardpoint', section: 'mount', offset: vec3(0, 0.012, -0.034), scale: vec3(0.86, 0.8, 0.78) }, { id: 'marksman-shoulder-piece', moduleId: 'detail_weapon_buttpad', section: 'rear', offset: vec3(0, -0.006, -0.086), rotation: vec3(96, 0, 0), scale: vec3(0.62, 0.5, 0.42) }, { id: 'marksman-stock-bridge', moduleId: 'detail_weapon_jacket', section: 'rear', offset: vec3(0, 0.006, 0.018), scale: vec3(0.5, 0.44, 0.58) }, { id: 'marksman-breach', moduleId: 'detail_weapon_breach', section: 'rear', offset: vec3(0, 0.012, 0.072), scale: vec3(0.82, 0.72, 0.76) }, { id: 'marksman-jacket', moduleId: 'detail_weapon_jacket', section: 'core', offset: vec3(0, -0.002, 0.074), scale: vec3(0.66, 0.58, 0.76) }, { id: 'marksman-action-cover', moduleId: 'detail_weapon_breach', section: 'top', offset: vec3(0, 0.002, -0.016), scale: vec3(0.5, 0.42, 0.52) }, { id: 'marksman-action-spine', moduleId: 'detail_weapon_jacket', section: 'top', offset: vec3(0, 0.003, 0.018), scale: vec3(0.5, 0.46, 0.72) }, { id: 'marksman-rail-clamp-rear', moduleId: 'detail_weapon_hardpoint', section: 'top', offset: vec3(0, -0.012, -0.026), scale: vec3(0.48, 0.42, 0.38) }, { id: 'marksman-rail-clamp-front', moduleId: 'detail_weapon_hardpoint', section: 'top', offset: vec3(0, -0.012, 0.126), scale: vec3(0.46, 0.4, 0.36) }, { id: 'marksman-optic-front-lens', moduleId: 'detail_scope_lens', section: 'top', offset: vec3(0, 0.022, 0.204), scale: vec3(0.88, 0.88, 0.88) }, { id: 'marksman-optic-rear-lens', moduleId: 'detail_scope_lens', section: 'top', offset: vec3(0, 0.022, -0.09), scale: vec3(0.78, 0.78, 0.78) }, { id: 'marksman-rear-sight-base', moduleId: 'detail_weapon_hardpoint', section: 'top', offset: vec3(0, -0.002, -0.046), scale: vec3(0.48, 0.38, 0.34) }, { id: 'marksman-bolt-housing', moduleId: 'detail_weapon_sidecar', section: 'starboard', offset: vec3(-0.004, 0.01, -0.004), scale: vec3(0.46, 0.42, 0.58) }, { id: 'marksman-gas-block', moduleId: 'detail_weapon_gas_block', section: 'front', offset: vec3(0, 0.02, -0.126), scale: vec3(0.62, 0.58, 0.6) }, { id: 'marksman-front-sight', moduleId: 'detail_weapon_front_sight', section: 'front', offset: vec3(0, 0.02, 0.164), scale: vec3(0.34, 0.38, 0.22) }, { id: 'marksman-rear-sight', moduleId: 'detail_weapon_rear_sight', section: 'top', offset: vec3(0, 0.004, -0.036), scale: vec3(0.62, 0.62, 0.62) }, { id: 'marksman-mag-brace', moduleId: 'detail_weapon_mag_brace', section: 'bottom', offset: vec3(0, -0.002, -0.028), rotation: vec3(0, 0, 0), scale: vec3(0.46, 0.42, 0.38) }, { id: 'marksman-mag-latch', moduleId: 'detail_weapon_hardpoint', section: 'starboard', offset: vec3(-0.012, 0.004, -0.05), rotation: vec3(0, 0, 90), scale: vec3(0.38, 0.32, 0.28) }, { id: 'marksman-recoil-spring', moduleId: 'detail_weapon_recoil_spring', section: 'top', offset: vec3(0, 0.014, -0.092), rotation: vec3(0, 0, 0), scale: vec3(0.82, 0.82, 0.82) }, { id: 'marksman-counterweight', moduleId: 'detail_weapon_counterweight', section: 'bottom', offset: vec3(0, -0.01, 0.026), scale: vec3(0.46, 0.42, 0.42) } ], subMassRules: [ { id: 'marksman-rear-action', label: 'Rear action', section: 'rear', emphasis: 'primary', expectedPlacementIds: ['marksman-stock', 'marksman-shoulder-piece', 'marksman-stock-bridge', 'marksman-breach'] }, { id: 'marksman-core-body', label: 'Core body', section: 'core', emphasis: 'primary', expectedPlacementIds: ['marksman-receiver-upper', 'marksman-receiver-lower', 'marksman-jacket', 'marksman-action-cover', 'marksman-action-spine', 'marksman-bolt-housing'] }, { id: 'marksman-long-front', label: 'Long front', section: 'front', emphasis: 'primary', expectedPlacementIds: ['marksman-barrel', 'marksman-shroud', 'marksman-cooling-jacket', 'marksman-gas-block', 'marksman-muzzle', 'marksman-muzzle-brake', 'marksman-front-sight'] }, { id: 'marksman-optic-stack', label: 'Optic stack', section: 'top', emphasis: 'secondary', expectedPlacementIds: ['marksman-rail', 'marksman-rail-clamp-rear', 'marksman-rail-clamp-front', 'marksman-optic-clamp-port', 'marksman-optic-clamp-starboard', 'marksman-optic-stanchion-rear', 'marksman-optic-stanchion-front', 'marksman-optic-tube', 'marksman-optic-front-lens', 'marksman-optic-rear-lens', 'marksman-rear-sight-base', 'marksman-rear-sight'] }, { id: 'marksman-balance-keel', label: 'Balance keel', section: 'bottom', emphasis: 'support', expectedPlacementIds: ['marksman-magwell', 'marksman-magwell-throat', 'marksman-magazine', 'marksman-mag-brace', 'marksman-mag-latch', 'marksman-counterweight', 'marksman-recoil-spring'] } ], transitionRules: [ { id: 'marksman-optic-riser', label: 'Optic riser', section: 'top', moduleId: 'detail_weapon_hardpoint', style: 'bridge', offset: vec3(0, 0.008, 0.05), scale: vec3(0.28, 0.44, 0.2), influence: { targetPlacementIds: ['marksman-optic-clamp-port', 'marksman-optic-clamp-starboard', 'marksman-optic-stanchion-rear', 'marksman-optic-stanchion-front', 'marksman-optic-tube', 'marksman-optic-front-lens', 'marksman-optic-rear-lens'], scaleMultiplier: vec3(1.005, 1.015, 1.01), offsetShift: vec3(0, 0.001, 0.002) } }, { id: 'marksman-optic-yoke', label: 'Optic yoke', section: 'top', moduleId: 'detail_weapon_hardpoint', style: 'saddle', offset: vec3(0, 0, 0.024), scale: vec3(0.24, 0.34, 0.18), influence: { targetPlacementIds: ['marksman-receiver-upper', 'marksman-action-cover', 'marksman-rail', 'marksman-rail-clamp-rear', 'marksman-rail-clamp-front'], scaleMultiplier: vec3(1.005, 1.01, 1.015), offsetShift: vec3(0, 0.001, -0.002) } }, { id: 'marksman-throat-collar', label: 'Throat collar', section: 'front', moduleId: 'detail_weapon_gas_block', style: 'collar', offset: vec3(0, 0, -0.148), scale: vec3(0.54, 0.42, 0.46), influence: { targetPlacementIds: ['marksman-barrel', 'marksman-shroud', 'marksman-cooling-jacket'], scaleMultiplier: vec3(0.94, 0.98, 0.96), offsetShift: vec3(0, 0, -0.006) } }, { id: 'marksman-rear-saddle', label: 'Rear saddle', section: 'rear', moduleId: 'detail_weapon_cradle', style: 'saddle', offset: vec3(0, 0.004, 0.09), scale: vec3(0.6, 0.5, 0.42), influence: { targetPlacementIds: ['marksman-shoulder-piece', 'marksman-stock-bridge', 'marksman-jacket'], scaleMultiplier: vec3(1.005, 1.0, 1.01), offsetShift: vec3(0, 0.001, -0.003) } }, { id: 'marksman-stock-fairing', label: 'Stock fairing', section: 'rear', moduleId: 'detail_weapon_jacket', style: 'fairing', offset: vec3(0, 0.003, 0.03), scale: vec3(0.5, 0.44, 0.46), influence: { targetPlacementIds: ['marksman-stock', 'marksman-breach'], scaleMultiplier: vec3(1.0, 1.0, 1.005), offsetShift: vec3(0, 0.001, -0.002) } }, { id: 'marksman-action-yoke', label: 'Action yoke', section: 'starboard', moduleId: 'detail_weapon_hardpoint', style: 'brace', offset: vec3(-0.01, 0.014, -0.014), scale: vec3(0.46, 0.38, 0.36), influence: { targetPlacementIds: ['marksman-action-cover', 'marksman-bolt-housing'], scaleMultiplier: vec3(1.0, 1.0, 1.01), offsetShift: vec3(-0.001, 0.001, -0.001) } }, { id: 'marksman-rear-sight-saddle', label: 'Rear sight saddle', section: 'top', moduleId: 'detail_weapon_hardpoint', style: 'brace', offset: vec3(0, 0.001, -0.046), scale: vec3(0.44, 0.34, 0.3), influence: { targetPlacementIds: ['marksman-rail-clamp-rear', 'marksman-rear-sight-base', 'marksman-rear-sight'], scaleMultiplier: vec3(1.02, 1.02, 1.02), offsetShift: vec3(0, 0.001, -0.003) } }, { id: 'marksman-magwell-bridge', label: 'Magwell bridge', section: 'bottom', moduleId: 'detail_weapon_jacket', style: 'fairing', offset: vec3(0, 0.002, -0.046), scale: vec3(0.56, 0.48, 0.5), influence: { targetPlacementIds: ['marksman-receiver-lower', 'marksman-jacket', 'marksman-magwell', 'marksman-magwell-throat', 'marksman-mag-brace'], scaleMultiplier: vec3(1.01, 1.015, 1.02), offsetShift: vec3(0, -0.001, -0.003) } }, { id: 'marksman-muzzle-collar', label: 'Muzzle collar', section: 'front', moduleId: 'detail_weapon_gas_block', style: 'collar', offset: vec3(0, -0.004, 0.186), scale: vec3(0.4, 0.32, 0.36), influence: { targetPlacementIds: ['marksman-muzzle', 'marksman-muzzle-brake', 'marksman-front-sight'], scaleMultiplier: vec3(0.98, 1.02, 0.98), offsetShift: vec3(0, 0.002, -0.002) } }, { id: 'marksman-lower-keel', label: 'Lower keel', section: 'bottom', moduleId: 'detail_weapon_counterweight', style: 'brace', offset: vec3(0, 0.002, -0.042), scale: vec3(0.44, 0.36, 0.34), influence: { targetPlacementIds: ['marksman-magazine', 'marksman-mag-latch', 'marksman-counterweight'], scaleMultiplier: vec3(1.005, 1.01, 0.99), offsetShift: vec3(0, -0.001, -0.003) } } ] }], ['beam', { archetype: 'beam', doctrine: { role: 'beam carbine', readProfile: 'balanced', balance: 'center-balanced', visualLanguage: 'energy', signature: ['emitter-heavy front', 'paired energy pods', 'clean lower silhouette'] }, holdProfile: BEAM_WEAPON_HOLD_PROFILE, sections: { mount: { anchorId: 'anchor-hand-r', origin: vec3(0, 0, 0), rotation: DEFAULT_WEAPON_ROTATION }, rear: { origin: vec3(0, 0.008, -0.062) }, core: { origin: vec3(0, 0.03, 0.07) }, front: { origin: vec3(0, 0.03, 0.272) }, top: { origin: vec3(0, 0.092, 0.05) }, bottom: { origin: vec3(0, -0.05, 0.044) }, port: { origin: vec3(-0.06, 0.024, 0.072) }, starboard: { origin: vec3(0.06, 0.024, 0.072) }, support: { origin: vec3(0, 0, 0) } }, placements: [ { id: 'beam-hardpoint', moduleId: 'detail_weapon_hardpoint', section: 'mount', offset: vec3(0, 0.012, -0.012), scale: vec3(0.98, 0.94, 0.9) }, { id: 'beam-cradle', moduleId: 'detail_weapon_cradle', section: 'mount', offset: vec3(0, 0.026, 0.028), scale: vec3(0.96, 0.92, 0.84) }, { id: 'beam-receiver', moduleId: 'detail_weapon_receiver', section: 'core', offset: vec3(0, -0.006, -0.006), scale: vec3(1.08, 0.98, 1.12) }, { id: 'beam-jacket', moduleId: 'detail_weapon_jacket', section: 'core', offset: vec3(0, 0.008, 0.05), scale: vec3(1.12, 1.0, 1.08) }, { id: 'beam-rail', moduleId: 'detail_weapon_rail', section: 'top', offset: vec3(0, -0.008, 0.002), scale: vec3(1.12, 1.0, 1.02) }, { id: 'beam-recoil-spring', moduleId: 'detail_weapon_recoil_spring', section: 'rear', offset: vec3(0, 0.014, 0.008), scale: vec3(1.04, 1.0, 1.0) }, { id: 'beam-barrel', moduleId: 'detail_weapon_barrel', section: 'front', offset: vec3(0, -0.006, -0.002), scale: vec3(0.92, 0.92, 1.1) }, { id: 'beam-shroud', moduleId: 'detail_weapon_shroud', section: 'front', offset: vec3(0, 0.008, -0.048), scale: vec3(1.12, 1.08, 1.14) }, { id: 'beam-cooling-jacket', moduleId: 'detail_weapon_cooling_jacket', section: 'front', offset: vec3(0, 0.012, -0.022), scale: vec3(1.08, 1.04, 1.08) }, { id: 'beam-emitter-core', moduleId: 'detail_weapon_gas_block', section: 'front', offset: vec3(0, 0.02, -0.106), scale: vec3(0.94, 0.88, 0.86) }, { id: 'beam-emitter-ring', moduleId: 'detail_weapon_gas_block', section: 'front', offset: vec3(0, 0.02, 0.038), scale: vec3(0.88, 0.84, 0.82) }, { id: 'beam-muzzle', moduleId: 'detail_weapon_muzzle', section: 'front', offset: vec3(0, -0.006, 0.118), scale: vec3(1.04, 1.0, 0.96) }, { id: 'beam-sidecar-port', moduleId: 'detail_weapon_sidecar', section: 'port', scale: vec3(0.92, 0.96, 0.94) }, { id: 'beam-sidecar-starboard', moduleId: 'detail_weapon_sidecar', section: 'starboard', scale: vec3(0.92, 0.96, 0.94) }, { id: 'beam-feed', moduleId: 'detail_weapon_feed', section: 'bottom', offset: vec3(0, 0.024, 0.006), scale: vec3(1.02, 0.98, 1.02) }, { id: 'beam-counterweight', moduleId: 'detail_weapon_counterweight', section: 'bottom', offset: vec3(0, -0.002, -0.054), scale: vec3(0.94, 0.9, 0.92) }, { id: 'beam-front-sight', moduleId: 'detail_weapon_front_sight', section: 'front', offset: vec3(0, 0.04, 0.082), scale: vec3(0.82, 0.82, 0.82) }, { id: 'beam-rear-sight', moduleId: 'detail_weapon_rear_sight', section: 'top', offset: vec3(0, -0.002, -0.046), scale: vec3(0.84, 0.84, 0.84) } ], subMassRules: [ { id: 'beam-core-cell', label: 'Core cell', section: 'core', emphasis: 'primary', expectedPlacementIds: ['beam-receiver', 'beam-jacket'] }, { id: 'beam-emitter-front', label: 'Emitter front', section: 'front', emphasis: 'primary', expectedPlacementIds: ['beam-barrel', 'beam-shroud', 'beam-cooling-jacket', 'beam-emitter-core', 'beam-emitter-ring', 'beam-muzzle', 'beam-front-sight'] }, { id: 'beam-power-pods', label: 'Power pods', section: 'port', emphasis: 'secondary', expectedPlacementIds: ['beam-sidecar-port', 'beam-sidecar-starboard'] }, { id: 'beam-spine', label: 'Spine', section: 'top', emphasis: 'secondary', expectedPlacementIds: ['beam-rail', 'beam-recoil-spring', 'beam-rear-sight'] }, { id: 'beam-underbody', label: 'Underbody', section: 'bottom', emphasis: 'support', expectedPlacementIds: ['beam-feed', 'beam-counterweight'] } ], transitionRules: [ { id: 'beam-emitter-throat', label: 'Emitter throat', section: 'front', moduleId: 'detail_weapon_cooling_jacket', style: 'collar', offset: vec3(0, 0.004, -0.098), scale: vec3(0.8, 0.8, 0.8), influence: { targetPlacementIds: ['beam-barrel', 'beam-shroud', 'beam-cooling-jacket'], scaleMultiplier: vec3(0.94, 0.98, 0.96) } }, { id: 'beam-power-spine', label: 'Power spine', section: 'top', moduleId: 'detail_weapon_rail', style: 'bridge', offset: vec3(0, 0.01, 0.056), scale: vec3(0.84, 0.8, 0.8), influence: { targetPlacementIds: ['beam-rail', 'beam-recoil-spring', 'beam-rear-sight'], scaleMultiplier: vec3(1.04, 1.03, 1.02), offsetShift: vec3(0, 0.004, 0.004) } }, { id: 'beam-battery-brace', label: 'Battery brace', section: 'bottom', moduleId: 'detail_weapon_jacket', style: 'brace', offset: vec3(0, 0.014, -0.01), scale: vec3(0.76, 0.74, 0.74), influence: { targetPlacementIds: ['beam-feed', 'beam-counterweight'], scaleMultiplier: vec3(1.03, 1.0, 0.96), offsetShift: vec3(0, -0.002, -0.004) } } ] }], ['launcher', { archetype: 'launcher', doctrine: { role: 'shoulder launcher', readProfile: 'long', balance: 'front-heavy', visualLanguage: 'industrial', signature: ['tube-dominant body', 'shoulder-mounted stance', 'visible support grip'] }, holdProfile: LAUNCHER_WEAPON_HOLD_PROFILE, sections: { mount: { anchorId: 'anchor-shoulder-l', origin: vec3(-0.04, 0.04, 0.06), rotation: vec3(82, -8, -16) }, rear: { anchorId: 'anchor-shoulder-l', origin: vec3(-0.056, 0.068, 0.028), rotation: vec3(82, -8, -16) }, core: { anchorId: 'anchor-shoulder-l', origin: vec3(-0.09, 0.074, 0.156), rotation: vec3(82, -8, -16) }, front: { anchorId: 'anchor-shoulder-l', origin: vec3(-0.132, 0.07, 0.34), rotation: vec3(82, -8, -16) }, top: { anchorId: 'anchor-shoulder-l', origin: vec3(-0.03, 0.162, 0.1), rotation: vec3(82, -8, -16) }, bottom: { anchorId: 'anchor-shoulder-l', origin: vec3(-0.1, 0.008, 0.238), rotation: vec3(82, -8, -16) }, port: { anchorId: 'anchor-shoulder-l', origin: vec3(-0.152, 0.104, 0.18), rotation: vec3(82, -8, -16) }, starboard: { anchorId: 'anchor-shoulder-l', origin: vec3(-0.008, 0.1, 0.18), rotation: vec3(82, -8, -16) }, support: { anchorId: 'anchor-hand-r', origin: vec3(0.018, 0.008, 0.022), rotation: vec3(92, 0, 0) } }, placements: [ { id: 'launcher-hardpoint', moduleId: 'detail_weapon_hardpoint', section: 'mount', offset: vec3(0.014, -0.01, -0.04), scale: vec3(1.08, 1.0, 0.96) }, { id: 'launcher-cradle', moduleId: 'detail_weapon_cradle', section: 'mount', offset: vec3(-0.01, 0.012, 0.02), scale: vec3(1.06, 0.98, 0.88) }, { id: 'launcher-breach', moduleId: 'detail_weapon_breach', section: 'rear', scale: vec3(1.08, 1.02, 1.0) }, { id: 'launcher-receiver', moduleId: 'detail_weapon_receiver', section: 'core', scale: vec3(1.18, 1.06, 1.12) }, { id: 'launcher-shroud', moduleId: 'detail_weapon_shroud', section: 'front', offset: vec3(0.022, 0.008, -0.08), scale: vec3(1.18, 1.12, 1.14) }, { id: 'launcher-barrel', moduleId: 'detail_weapon_barrel', section: 'front', scale: vec3(1.2, 1.12, 1.18) }, { id: 'launcher-underslung', moduleId: 'detail_weapon_underslung', section: 'bottom', scale: vec3(1.08, 1.02, 1.04) }, { id: 'launcher-muzzle', moduleId: 'detail_weapon_muzzle', section: 'front', offset: vec3(-0.016, -0.006, 0.106), scale: vec3(1.12, 1.08, 1.0) }, { id: 'launcher-feed-port', moduleId: 'detail_weapon_feed', section: 'port', scale: vec3(1.04, 1.0, 1.0) }, { id: 'launcher-feed-starboard', moduleId: 'detail_weapon_feed', section: 'starboard', scale: vec3(1.04, 1.0, 1.0) }, { id: 'launcher-sidecar', moduleId: 'detail_weapon_sidecar', section: 'top', offset: vec3(-0.006, -0.016, 0.048), scale: vec3(0.94, 0.9, 1.04) }, { id: 'launcher-rear-sight', moduleId: 'detail_weapon_rear_sight', section: 'top', scale: vec3(0.96, 0.96, 0.96) }, { id: 'launcher-grip', moduleId: 'detail_weapon_stock', section: 'support', scale: vec3(0.82, 0.78, 0.82) } ], subMassRules: [ { id: 'launcher-shoulder-block', label: 'Shoulder block', section: 'rear', emphasis: 'primary', expectedPlacementIds: ['launcher-breach', 'launcher-receiver'] }, { id: 'launcher-tube-body', label: 'Tube body', section: 'front', emphasis: 'primary', expectedPlacementIds: ['launcher-shroud', 'launcher-barrel', 'launcher-underslung', 'launcher-muzzle'] }, { id: 'launcher-ammo-saddles', label: 'Ammo saddles', section: 'port', emphasis: 'secondary', expectedPlacementIds: ['launcher-feed-port', 'launcher-feed-starboard', 'launcher-sidecar'] }, { id: 'launcher-aimline', label: 'Aimline', section: 'top', emphasis: 'secondary', expectedPlacementIds: ['launcher-rear-sight'] }, { id: 'launcher-support', label: 'Support', section: 'support', emphasis: 'support', expectedPlacementIds: ['launcher-grip'] } ], transitionRules: [ { id: 'launcher-shoulder-saddle', label: 'Shoulder saddle', section: 'rear', moduleId: 'detail_weapon_cradle', style: 'saddle', offset: vec3(0.014, -0.002, 0.018), scale: vec3(0.84, 0.82, 0.74), influence: { targetPlacementIds: ['launcher-breach', 'launcher-receiver', 'launcher-shroud'], scaleMultiplier: vec3(1.04, 1.02, 1.04), offsetShift: vec3(0, 0.002, -0.008) } }, { id: 'launcher-tube-collar', label: 'Tube collar', section: 'front', moduleId: 'detail_weapon_gas_block', style: 'collar', offset: vec3(0.012, 0.016, -0.026), scale: vec3(0.82, 0.8, 0.78), influence: { targetPlacementIds: ['launcher-shroud', 'launcher-barrel', 'launcher-muzzle'], scaleMultiplier: vec3(0.97, 0.99, 0.98) } }, { id: 'launcher-grip-brace', label: 'Grip brace', section: 'support', moduleId: 'detail_weapon_hardpoint', style: 'brace', offset: vec3(0, 0.006, -0.028), scale: vec3(0.74, 0.72, 0.72), influence: { targetPlacementIds: ['launcher-grip'], scaleMultiplier: vec3(1.02, 1.04, 0.98) } } ] }], ['forearm-pod', { archetype: 'forearm-pod', doctrine: { role: 'integrated forearm pod', readProfile: 'compact', balance: 'center-balanced', visualLanguage: 'integrated', signature: ['armored shell first', 'compact in-line barrel', 'forearm-hugging mass'] }, holdProfile: FOREARM_POD_WEAPON_HOLD_PROFILE, sections: { mount: { anchorId: 'anchor-forearm-l', origin: vec3(-0.048, 0.002, 0.08), rotation: vec3(88, -90, 2) }, rear: { anchorId: 'anchor-forearm-l', origin: vec3(-0.068, -0.014, 0.108), rotation: vec3(88, -90, 2) }, core: { anchorId: 'anchor-forearm-l', origin: vec3(-0.052, 0.01, 0.132), rotation: vec3(88, -90, 2) }, front: { anchorId: 'anchor-forearm-l', origin: vec3(-0.052, 0.008, 0.228), rotation: vec3(88, -90, 2) }, top: { anchorId: 'anchor-forearm-l', origin: vec3(-0.05, 0.042, 0.19), rotation: vec3(88, -90, 2) }, bottom: { anchorId: 'anchor-forearm-l', origin: vec3(-0.022, -0.012, 0.144), rotation: vec3(88, -90, 2) }, port: { anchorId: 'anchor-forearm-l', origin: vec3(-0.078, -0.026, 0.112), rotation: vec3(88, -90, 2) }, starboard: { anchorId: 'anchor-forearm-l', origin: vec3(-0.012, 0.022, 0.118), rotation: vec3(88, -90, 2) }, support: { origin: vec3(0, 0, 0) } }, placements: [ { id: 'pod-shell', moduleId: 'detail_forearm_weapon_pod', section: 'core', offset: vec3(0.006, -0.006, -0.044), scale: vec3(0.98, 1.02, 0.98) }, { id: 'pod-hardpoint', moduleId: 'detail_weapon_hardpoint', section: 'mount', offset: vec3(0.002, -0.01, -0.03), scale: vec3(0.9, 0.88, 0.84) }, { id: 'pod-cradle', moduleId: 'detail_weapon_cradle', section: 'mount', offset: vec3(-0.002, 0.006, 0.018), scale: vec3(0.92, 0.9, 0.82) }, { id: 'pod-receiver', moduleId: 'detail_weapon_receiver', section: 'core', scale: vec3(0.96, 0.94, 0.98) }, { id: 'pod-barrel', moduleId: 'detail_weapon_barrel', section: 'front', offset: vec3(0, -0.004, -0.002), scale: vec3(0.92, 0.9, 0.98) }, { id: 'pod-shroud', moduleId: 'detail_weapon_shroud', section: 'front', offset: vec3(0, 0.006, -0.044), scale: vec3(0.94, 0.96, 0.96) }, { id: 'pod-gas-block', moduleId: 'detail_weapon_gas_block', section: 'front', offset: vec3(0, 0.018, -0.066), scale: vec3(0.76, 0.76, 0.76) }, { id: 'pod-muzzle', moduleId: 'detail_weapon_muzzle', section: 'front', offset: vec3(0, -0.004, 0.076), scale: vec3(0.88, 0.86, 0.86) }, { id: 'pod-feed', moduleId: 'detail_weapon_feed', section: 'bottom', scale: vec3(0.88, 0.86, 0.9) }, { id: 'pod-counterweight', moduleId: 'detail_weapon_counterweight', section: 'port', scale: vec3(0.86, 0.84, 0.84) }, { id: 'pod-ejector', moduleId: 'detail_weapon_ejector', section: 'starboard', scale: vec3(0.8, 0.8, 0.78) }, { id: 'pod-front-sight', moduleId: 'detail_weapon_front_sight', section: 'top', offset: vec3(0, -0.002, 0.084), scale: vec3(0.82, 0.82, 0.82) } ], subMassRules: [ { id: 'pod-shell-body', label: 'Shell body', section: 'core', emphasis: 'primary', expectedPlacementIds: ['pod-shell', 'pod-receiver'] }, { id: 'pod-firing-line', label: 'Firing line', section: 'front', emphasis: 'primary', expectedPlacementIds: ['pod-barrel', 'pod-shroud', 'pod-gas-block', 'pod-muzzle', 'pod-front-sight'] }, { id: 'pod-mount-package', label: 'Mount package', section: 'mount', emphasis: 'secondary', expectedPlacementIds: ['pod-hardpoint', 'pod-cradle'] }, { id: 'pod-balance-pack', label: 'Balance pack', section: 'bottom', emphasis: 'support', expectedPlacementIds: ['pod-feed', 'pod-counterweight', 'pod-ejector'] } ], transitionRules: [ { id: 'pod-shell-fairing', label: 'Shell fairing', section: 'core', moduleId: 'detail_weapon_jacket', style: 'fairing', offset: vec3(0.002, 0.004, 0.012), scale: vec3(0.8, 0.8, 0.78), influence: { targetPlacementIds: ['pod-shell', 'pod-receiver', 'pod-shroud'], scaleMultiplier: vec3(1.03, 1.04, 1.02), offsetShift: vec3(0, 0.002, -0.004) } }, { id: 'pod-muzzle-collar', label: 'Muzzle collar', section: 'front', moduleId: 'detail_weapon_gas_block', style: 'collar', offset: vec3(0, 0.012, -0.012), scale: vec3(0.72, 0.72, 0.7), influence: { targetPlacementIds: ['pod-barrel', 'pod-shroud', 'pod-muzzle'], scaleMultiplier: vec3(0.96, 0.98, 0.97) } }, { id: 'pod-forearm-brace', label: 'Forearm brace', section: 'rear', moduleId: 'detail_weapon_cradle', style: 'brace', offset: vec3(0.012, -0.002, -0.02), scale: vec3(0.76, 0.74, 0.72), influence: { targetPlacementIds: ['pod-hardpoint', 'pod-cradle', 'pod-shell'], scaleMultiplier: vec3(1.02, 1.02, 1.04), offsetShift: vec3(0, -0.002, -0.006) } } ] }] ]); function hydratePromotedWeaponSpecs(modules: WeaponIterationModules): void { STRUCTURED_WEAPON_DESIGN_SPECS.set('marksman', modules.marksman.getActiveMarksmanWeaponSpec() as WeaponDesignSpec); STRUCTURED_WEAPON_DESIGN_SPECS.set('rifle', modules.rifle.getActiveRifleWeaponSpec() as WeaponDesignSpec); STRUCTURED_WEAPON_DESIGN_SPECS.set('heavy', modules.heavy.getActiveHeavyWeaponSpec() as WeaponDesignSpec); } const WEAPON_QA_SECTION_COLORS: Record = { mount: 0xe15759, rear: 0x9c755f, core: 0x4e79a7, front: 0xf28e2b, top: 0x76b7b2, bottom: 0x59a14f, port: 0xb07aa1, starboard: 0xedc948, support: 0xff9da7 }; const WEAPON_QA_SUBSYSTEM_COLORS: Record = { action: 0xff6b6b, optic: 0x57e7ff, magazine: 0x8bd450 }; const stripHumanoidWeaponAttachments = (attachments: AttachmentEntry[]): AttachmentEntry[] => attachments.filter((attachment) => !attachment.id.startsWith('attach-weapon-')); const stripHumanoidWeaponAnchors = (anchors: AnchorEntry[]): AnchorEntry[] => anchors.filter((anchor) => !anchor.id.startsWith('anchor-weapon-')); const STANDALONE_WEAPON_ROOT_ANCHOR_IDS = new Set(['anchor-hand-r', 'anchor-weapon-root']); const normalizeStandaloneWeaponAttachments = (attachments: AttachmentEntry[]): string => JSON.stringify( attachments .filter((attachment) => attachment.id.startsWith('attach-weapon-')) .map((attachment) => ({ id: attachment.id, anchorId: attachment.anchorId, moduleId: attachment.moduleId, portId: attachment.portId, generatorId: attachment.generatorId, generatorIndex: attachment.generatorIndex, mirrored: attachment.mirrored, offset: attachment.offset ? cloneVec3Like(attachment.offset) : undefined, rotation: attachment.rotation ? cloneVec3Like(attachment.rotation) : undefined, scale: attachment.scale ? cloneVec3Like(attachment.scale) : undefined, shape: cloneAttachmentShape(attachment.shape) })) .sort((a, b) => a.id.localeCompare(b.id)) ); const normalizeStandaloneWeaponAnchors = (anchors: AnchorEntry[]): string => JSON.stringify( anchors .filter((anchor) => anchor.id.startsWith('anchor-weapon-') || STANDALONE_WEAPON_ROOT_ANCHOR_IDS.has(anchor.id)) .map((anchor) => ({ id: anchor.id, type: anchor.type, attachmentId: anchor.attachmentId, nodeId: anchor.nodeId, proximalNodeId: anchor.proximalNodeId, distalNodeId: anchor.distalNodeId, upNodeId: anchor.upNodeId, attachmentFace: anchor.attachmentFace, orientation: anchor.orientation, radialAngle: anchor.radialAngle, offset: anchor.offset ? cloneVec3Like(anchor.offset) : undefined, tags: [...anchor.tags], accepts: [...anchor.accepts] })) .sort((a, b) => a.id.localeCompare(b.id)) ); function reconcileStandaloneWeaponCustomRecipe( doc: RecipeDocument, basePresetName: string ): { doc: RecipeDocument; reconciled: boolean } { if (!isStandaloneWeaponPreset(basePresetName)) { return { doc, reconciled: false }; } const preset = buildPresetRecipe(basePresetName); if (!preset) { return { doc, reconciled: false }; } const presetWeaponAttachmentIds = new Set( (preset.attachments ?? []) .filter((attachment) => attachment.id.startsWith('attach-weapon-')) .map((attachment) => attachment.id) ); const currentWeaponAttachmentIds = new Set( (doc.attachments ?? []) .filter((attachment) => attachment.id.startsWith('attach-weapon-')) .map((attachment) => attachment.id) ); const presetWeaponAnchorIds = new Set( (preset.anchors ?? []) .filter((anchor) => anchor.id.startsWith('anchor-weapon-') || STANDALONE_WEAPON_ROOT_ANCHOR_IDS.has(anchor.id)) .map((anchor) => anchor.id) ); const currentWeaponAnchorIds = new Set( (doc.anchors ?? []) .filter((anchor) => anchor.id.startsWith('anchor-weapon-') || STANDALONE_WEAPON_ROOT_ANCHOR_IDS.has(anchor.id)) .map((anchor) => anchor.id) ); const missingWeaponAttachments = [...presetWeaponAttachmentIds].some((id) => !currentWeaponAttachmentIds.has(id)); const missingWeaponAnchors = [...presetWeaponAnchorIds].some((id) => !currentWeaponAnchorIds.has(id)); const weaponAttachmentSliceChanged = normalizeStandaloneWeaponAttachments(doc.attachments ?? []) !== normalizeStandaloneWeaponAttachments(preset.attachments ?? []); const weaponAnchorSliceChanged = normalizeStandaloneWeaponAnchors(doc.anchors ?? []) !== normalizeStandaloneWeaponAnchors(preset.anchors ?? []); if (!missingWeaponAttachments && !missingWeaponAnchors && !weaponAttachmentSliceChanged && !weaponAnchorSliceChanged) { return { doc, reconciled: false }; } const attachments = [ ...(doc.attachments ?? []).filter((attachment) => !attachment.id.startsWith('attach-weapon-')), ...(preset.attachments ?? []).map((attachment) => ({ ...attachment, offset: cloneVec3Like(attachment.offset), rotation: cloneVec3Like(attachment.rotation), scale: cloneVec3Like(attachment.scale), shape: cloneAttachmentShape(attachment.shape), sculpt: attachment.sculpt })) ]; const anchors = [ ...(doc.anchors ?? []).filter( (anchor) => !anchor.id.startsWith('anchor-weapon-') && !STANDALONE_WEAPON_ROOT_ANCHOR_IDS.has(anchor.id) ), ...(preset.anchors ?? []) .filter((anchor) => anchor.id.startsWith('anchor-weapon-') || STANDALONE_WEAPON_ROOT_ANCHOR_IDS.has(anchor.id)) .map((anchor) => ({ ...anchor, offset: cloneVec3Like(anchor.offset) })) ]; return { doc: { ...doc, anchors, attachments }, reconciled: true }; } const createWeaponHoldAnchor = ( id: string, attachmentId: string, attachmentFace: AnchorFace, tags: string[] ): AnchorEntry => ({ id, type: 'attachment', attachmentId, attachmentFace, orientation: 'alongEdge', radialAngle: 0, tags, accepts: ['limb_segment', 'connector', 'weapon'] }); const createWeaponFunctionalAnchor = ( id: string, attachmentId: string, attachmentFace: AnchorFace, tags: string[], offset?: Vec3Like ): AnchorEntry => ({ id, type: 'attachment', attachmentId, attachmentFace, orientation: 'alongEdge', radialAngle: 0, offset: cloneVec3Like(offset), tags, accepts: ['weapon', 'connector', 'sensor'] }); const buildHumanoidWeaponAnchors = (archetype: HumanoidWeaponArchetype): AnchorEntry[] => { switch (archetype) { case 'rifle': return [ createWeaponHoldAnchor('anchor-weapon-grip-r', 'attach-weapon-rifle-hardpoint', 'back', ['weapon', 'grip', 'right']), createWeaponHoldAnchor('anchor-weapon-support-l', 'attach-weapon-rifle-underslung', 'bottom', ['weapon', 'support', 'left']), createWeaponHoldAnchor('anchor-weapon-shoulder', 'attach-weapon-rifle-stock', 'back', ['weapon', 'shoulder']) ]; case 'marksman': return [ createWeaponHoldAnchor('anchor-weapon-grip-r', 'attach-weapon-marksman-hardpoint', 'back', ['weapon', 'grip', 'right']), createWeaponHoldAnchor('anchor-weapon-support-l', 'attach-weapon-marksman-cooling-jacket', 'bottom', ['weapon', 'support', 'left']), createWeaponHoldAnchor('anchor-weapon-magwell', 'attach-weapon-marksman-magwell', 'front', ['weapon', 'magwell', 'left']), createWeaponHoldAnchor('anchor-weapon-shoulder', 'attach-weapon-marksman-shoulder-piece', 'back', ['weapon', 'shoulder']) ]; case 'beam': return [ createWeaponHoldAnchor('anchor-weapon-grip-r', 'attach-weapon-beam-hardpoint', 'back', ['weapon', 'grip', 'right']), createWeaponHoldAnchor('anchor-weapon-support-l', 'attach-weapon-beam-feed', 'bottom', ['weapon', 'support', 'left']) ]; case 'heavy': return [ createWeaponHoldAnchor('anchor-weapon-grip-r', 'attach-weapon-heavy-hardpoint', 'back', ['weapon', 'grip', 'right']), createWeaponHoldAnchor('anchor-weapon-support-l', 'attach-weapon-heavy-counterweight', 'bottom', ['weapon', 'support', 'left']), createWeaponHoldAnchor('anchor-weapon-shoulder', 'attach-weapon-heavy-stock', 'back', ['weapon', 'shoulder']) ]; case 'launcher': return [ createWeaponHoldAnchor('anchor-weapon-grip-r', 'attach-weapon-launcher-grip', 'back', ['weapon', 'grip', 'right']), createWeaponHoldAnchor('anchor-weapon-support-l', 'attach-weapon-launcher-underslung', 'bottom', ['weapon', 'support', 'left']), createWeaponHoldAnchor('anchor-weapon-shoulder', 'attach-weapon-launcher-breach', 'back', ['weapon', 'shoulder']) ]; case 'forearm-pod': return [ createWeaponHoldAnchor('anchor-weapon-grip-r', 'attach-weapon-pod-hardpoint', 'back', ['weapon', 'grip', 'right']), createWeaponHoldAnchor('anchor-weapon-support-l', 'attach-weapon-pod-shroud', 'bottom', ['weapon', 'support', 'left']) ]; default: return []; } }; const buildStandaloneWeaponFunctionalAnchors = (archetype: HumanoidWeaponArchetype): AnchorEntry[] => { switch (archetype) { case 'marksman': return [ createWeaponFunctionalAnchor( 'anchor-weapon-muzzle-flash', 'attach-weapon-marksman-muzzle-brake', 'front', ['weapon', 'fx', 'muzzle'], vec3(0, 0, 0.008) ), createWeaponFunctionalAnchor( 'anchor-weapon-action-port', 'attach-weapon-marksman-action-port-cover', 'right', ['weapon', 'action', 'eject'], vec3(0.006, 0.002, 0.004) ), createWeaponFunctionalAnchor( 'anchor-weapon-bolt-closed', 'attach-weapon-marksman-bolt-housing', 'right', ['weapon', 'action', 'bolt', 'closed'], vec3(0.004, 0.002, 0.02) ), createWeaponFunctionalAnchor( 'anchor-weapon-bolt-open', 'attach-weapon-marksman-bolt-housing', 'right', ['weapon', 'action', 'bolt', 'open'], vec3(0.004, 0.002, -0.024) ), createWeaponFunctionalAnchor( 'anchor-weapon-mag-seat', 'attach-weapon-marksman-magazine', 'center', ['weapon', 'magazine', 'seat'], vec3(0, 0, 0) ), createWeaponFunctionalAnchor( 'anchor-weapon-mag-approach', 'attach-weapon-marksman-magazine', 'center', ['weapon', 'magazine', 'approach'], vec3(0, -0.14, -0.016) ), createWeaponFunctionalAnchor( 'anchor-weapon-mag-release', 'attach-weapon-marksman-mag-latch', 'right', ['weapon', 'magazine', 'release'], vec3(0.006, 0.002, 0) ) ]; default: return []; } }; const getActiveWeaponDesignSpec = (): WeaponDesignSpec | null => { const direct = getWeaponPresetArchetype(currentBasePresetName); if (direct) { return STRUCTURED_WEAPON_DESIGN_SPECS.get(direct) ?? null; } if (currentBasePresetName === 'human' || currentBasePresetName === 'humanoid-full') { return STRUCTURED_WEAPON_DESIGN_SPECS.get('rifle') ?? null; } return null; }; const getWeaponQaContext = ( attachment?: AttachmentEntry | null ): { spec: WeaponDesignSpec; placement?: WeaponDesignPlacement; transition?: WeaponTransitionRule; section: WeaponDesignSectionId; subsystem: WeaponQaSubsystem | null; } | null => { if (!attachment) return null; const spec = getActiveWeaponDesignSpec(); if (!spec || !attachment.id.startsWith('attach-weapon-')) return null; const placement = getWeaponBasePlacements(spec).find((entry) => `attach-weapon-${entry.id}` === attachment.id); if (placement) { return { spec, placement, section: placement.section, subsystem: resolveWeaponQaSubsystem(placement.id) }; } const transition = spec.transitionRules.find((entry) => `attach-weapon-transition-${entry.id}` === attachment.id); if (!transition) return null; return { spec, transition, section: transition.section, subsystem: resolveWeaponQaSubsystem(transition.id) }; }; const formatWeaponSectionLabel = (section: WeaponDesignSectionId): string => section.charAt(0).toUpperCase() + section.slice(1); const formatWeaponQaSubsystemLabel = (subsystem: WeaponQaSubsystem): string => subsystem.charAt(0).toUpperCase() + subsystem.slice(1); const resolveWeaponQaSubsystem = (id: string): WeaponQaSubsystem | null => { if (id.includes('optic') || id.includes('scope') || id.includes('sight') || id.includes('rail')) { return 'optic'; } if (id.includes('mag') || id.includes('feed')) { return 'magazine'; } if (id.includes('receiver') || id.includes('breach') || id.includes('action') || id.includes('bolt') || id.includes('sidecar')) { return 'action'; } return null; }; const formatWeaponQaHex = (value: number): string => `#${value.toString(16).padStart(6, '0')}`; const applyWeaponQaOverlay = ( object: THREE.Object3D, context: { section: WeaponDesignSectionId; subsystem: WeaponQaSubsystem | null } ): void => { const color = new THREE.Color(WEAPON_QA_SECTION_COLORS[context.section]); const subsystemColor = context.subsystem ? new THREE.Color(WEAPON_QA_SUBSYSTEM_COLORS[context.subsystem]) : null; const finalColor = subsystemColor ? color.clone().lerp(subsystemColor, 0.6) : color; object.traverse((node) => { const mesh = node as THREE.Mesh; if (!mesh.isMesh || !mesh.material) return; const apply = (material: THREE.Material): THREE.Material => { if (!(material instanceof THREE.MeshStandardMaterial)) return material; const tinted = material.clone(); tinted.color.lerp(finalColor, subsystemColor ? 0.82 : 0.72); tinted.emissive = tinted.emissive.clone().lerp(subsystemColor ?? finalColor, subsystemColor ? 0.48 : 0.35); tinted.emissiveIntensity = subsystemColor ? 0.28 : 0.22; tinted.roughness = Math.max(subsystemColor ? 0.28 : 0.34, tinted.roughness * (subsystemColor ? 0.84 : 0.9)); tinted.metalness = Math.min(subsystemColor ? 0.78 : 0.68, Math.max(tinted.metalness, subsystemColor ? 0.22 : 0.16)); return tinted; }; if (Array.isArray(mesh.material)) { mesh.material = mesh.material.map((material) => apply(material)); return; } mesh.material = apply(mesh.material); }); }; function refreshWeaponQaInfo(): void { controls.weaponQaToggle.checked = state.showWeaponQa; const spec = getActiveWeaponDesignSpec(); if (!spec) { controls.weaponQaInfo.textContent = 'No structured weapon spec active.'; return; } const basePlacements = resolveWeaponDesignPlacements(spec, false); const influencedPlacements = resolveWeaponDesignPlacements(spec, true); const baseEnvelopes = buildWeaponSectionEnvelopes(spec, basePlacements); const influencedEnvelopes = buildWeaponSectionEnvelopes(spec, influencedPlacements); const marksmanReport = spec.archetype === 'marksman' && weaponIterationModules?.marksman ? weaponIterationModules.marksman.evaluateMarksmanFitness(spec as any) : null; const rifleReport = spec.archetype === 'rifle' && weaponIterationModules?.rifle ? weaponIterationModules.rifle.evaluateRifleFitness(spec as any) : null; const heavyReport = spec.archetype === 'heavy' && weaponIterationModules?.heavy ? weaponIterationModules.heavy.evaluateHeavyFitness(spec as any) : null; if ( state.showWeaponQa && (spec.archetype === 'marksman' || spec.archetype === 'rifle' || spec.archetype === 'heavy') && !weaponIterationModules ) { queueWeaponIterationHydration(); } const ranking = getWeaponQaRanking() .map((entry) => { if (spec.archetype === 'marksman' && entry.archetype === 'marksman' && marksmanReport) { return { ...entry, score: marksmanReport.qa.score, overCorrection: marksmanReport.qa.overCorrection, instability: marksmanReport.qa.instability, changedSections: marksmanReport.qa.changedSections, multiHitPlacements: marksmanReport.qa.multiHitPlacements }; } if (spec.archetype === 'rifle' && entry.archetype === 'rifle' && rifleReport) { return { ...entry, score: rifleReport.qa.score, overCorrection: rifleReport.qa.overCorrection, instability: rifleReport.qa.instability, changedSections: rifleReport.qa.changedSections, multiHitPlacements: rifleReport.qa.multiHitPlacements }; } if (spec.archetype === 'heavy' && entry.archetype === 'heavy' && heavyReport) { return { ...entry, score: heavyReport.qa.score, overCorrection: heavyReport.qa.overCorrection, instability: heavyReport.qa.instability, changedSections: heavyReport.qa.changedSections, multiHitPlacements: heavyReport.qa.multiHitPlacements }; } return entry; }) .sort((a, b) => b.score - a.score); const activeRank = ranking.findIndex((entry) => entry.archetype === spec.archetype); const activeScore = marksmanReport?.qa ?? rifleReport?.qa ?? heavyReport?.qa ?? ranking[activeRank] ?? computeWeaponArchetypeQaScore(spec); const attachedIds = new Set(state.attachments.map((attachment) => attachment.id)); const expectedPlacements = basePlacements; const sectionCounts = new Map(); const subsystemCounts = new Map(); for (const placement of expectedPlacements) { const current = sectionCounts.get(placement.section) ?? { expected: 0, actual: 0 }; current.expected += 1; const isAttached = attachedIds.has(`attach-weapon-${placement.id}`); if (isAttached) { current.actual += 1; } sectionCounts.set(placement.section, current); const subsystem = resolveWeaponQaSubsystem(placement.id); if (!subsystem) continue; const subsystemCurrent = subsystemCounts.get(subsystem) ?? { expected: 0, actual: 0 }; subsystemCurrent.expected += 1; if (isAttached) { subsystemCurrent.actual += 1; } subsystemCounts.set(subsystem, subsystemCurrent); } const coverageLines = (Object.keys(spec.sections) as WeaponDesignSectionId[]) .filter((section) => sectionCounts.has(section)) .map((section) => { const counts = sectionCounts.get(section)!; const color = formatWeaponQaHex(WEAPON_QA_SECTION_COLORS[section]); return `${formatWeaponSectionLabel(section)} ${counts.actual}/${counts.expected} ${color}`; }); const subsystemLines = (['action', 'optic', 'magazine'] as WeaponQaSubsystem[]) .filter((subsystem) => subsystemCounts.has(subsystem)) .map((subsystem) => { const counts = subsystemCounts.get(subsystem)!; const color = formatWeaponQaHex(WEAPON_QA_SUBSYSTEM_COLORS[subsystem]); return `${formatWeaponQaSubsystemLabel(subsystem)} ${counts.actual}/${counts.expected} ${color}`; }); const missing = expectedPlacements .filter((placement) => !attachedIds.has(`attach-weapon-${placement.id}`)) .map((placement) => placement.id); const missingTransitions = spec.transitionRules .filter((transition) => !attachedIds.has(`attach-weapon-transition-${transition.id}`)) .map((transition) => transition.id); const transitionLines = spec.transitionRules.map((transition) => { const attachmentId = `attach-weapon-transition-${transition.id}`; const targetCount = transition.influence?.targetPlacementIds.length ?? 0; return `${transition.label}: ${attachedIds.has(attachmentId) ? 'ok' : 'missing'} (${transition.style}${targetCount > 0 ? ` -> ${targetCount}` : ''})`; }); const subMassLines = spec.subMassRules.map((subMass) => { const actual = subMass.expectedPlacementIds.filter((placementId) => attachedIds.has(`attach-weapon-${placementId}`) ).length; return `${subMass.label}: ${actual}/${subMass.expectedPlacementIds.length} ${subMass.emphasis}`; }); const envelopeLines = (Object.keys(spec.sections) as WeaponDesignSectionId[]) .filter((section) => baseEnvelopes.has(section) && influencedEnvelopes.has(section)) .map((section) => { const before = baseEnvelopes.get(section)!; const after = influencedEnvelopes.get(section)!; return { changed: formatWeaponEnvelopeDelta(before, after) !== 'stable', text: `${formatWeaponSectionLabel(section)}: ${formatWeaponEnvelopeDelta(before, after)}` }; }); const changedEnvelopeLines = envelopeLines.filter((entry) => entry.changed).map((entry) => entry.text); const plannedDeltaLines = buildWeaponPlanDeltaLines(spec, influencedPlacements); const silhouetteLimitLines = buildWeaponSilhouetteLimitLines(influencedPlacements); const rankingLines = ranking.map((entry, index) => `${index + 1}. ${entry.archetype} score ${entry.score.toFixed(2)} | over ${entry.overCorrection.toFixed(2)} | inst ${entry.instability.toFixed(2)} | sec ${entry.changedSections} | multi ${entry.multiHitPlacements}` ); const structuredFitnessReport = marksmanReport ?? rifleReport ?? heavyReport; const heuristicLines = structuredFitnessReport ? structuredFitnessReport.heuristics.map((entry) => `${entry.name}: ${entry.passed ? 'pass' : 'fail'} (${entry.value.toFixed(3)} / ${entry.threshold.toFixed(3)})`) : []; const holdProfile = spec.holdProfile; const holdLines = [ `Hand dir: L ${holdProfile.handDirection.leftLateral.toFixed(2)} | R ${holdProfile.handDirection.rightLateral.toFixed(2)} | V ${holdProfile.handDirection.vertical.toFixed(2)} | F ${holdProfile.handDirection.forward.toFixed(2)}`, `Pole: lat ${holdProfile.supportPole.lateral.toFixed(2)} | up ${holdProfile.supportPole.vertical.toFixed(2)} | fwd ${holdProfile.supportPole.forward.toFixed(2)}`, `Pole bias: lat ${holdProfile.supportPole.biasLateral.toFixed(2)} | up ${holdProfile.supportPole.biasVertical.toFixed(2)} | fwd ${holdProfile.supportPole.biasForward.toFixed(2)}`, `Shoulder offset: ${formatDiagVec3(holdProfile.shoulderOffset, 2)}`, `Torso keep-out: radii ${formatDiagVec3(holdProfile.torsoClearance.radiiScale, 2)} | min ${formatDiagVec3(holdProfile.torsoClearance.minRadii, 2)}`, `Torso rules: elbow ${holdProfile.torsoClearance.elbowLateralRatio.toFixed(2)}/${holdProfile.torsoClearance.elbowForwardRatio.toFixed(2)} | wrist ${holdProfile.torsoClearance.wristLateralRatio.toFixed(2)}/${holdProfile.torsoClearance.wristForwardRatio.toFixed(2)} | pad ${holdProfile.torsoClearance.padding.toFixed(2)}` ]; const sourceLine = controls.presetSelect.value === 'custom' ? `Source: custom (base preset ${currentBasePresetName})` : `Source: preset ${controls.presetSelect.value}`; const staleCustomWarning = controls.presetSelect.value === 'custom' && missing.length >= 4 && (missingTransitions.length >= 2 || isStandaloneWeaponPreset(currentBasePresetName)) ? `Likely stale custom recipe: this view is missing ${missing.length} weapon placements and ${missingTransitions.length} transitions from the current ${spec.archetype} spec. Click Reload Preset to pull latest weapon edits.` : null; const lines = [ `Archetype: ${spec.archetype}`, sourceLine, `Role: ${spec.doctrine.role}`, `Read: ${spec.doctrine.readProfile}`, `Balance: ${spec.doctrine.balance}`, `Language: ${spec.doctrine.visualLanguage}`, `Signature: ${spec.doctrine.signature.join(' | ')}`, `Risk rank: ${activeRank + 1}/${ranking.length} score ${activeScore.score.toFixed(2)} | over ${activeScore.overCorrection.toFixed(2)} | inst ${activeScore.instability.toFixed(2)}`, `Overlay: ${state.showWeaponQa ? 'on' : 'off'}`, ...(staleCustomWarning ? ['Warning:', staleCustomWarning] : []), 'Coverage:', ...coverageLines, 'Subsystem colors:', ...(subsystemLines.length > 0 ? subsystemLines : ['None']), 'Hold profile:', ...holdLines, 'Sub-masses:', ...subMassLines, 'Envelope deltas:', ...(changedEnvelopeLines.length > 0 ? changedEnvelopeLines : ['No section envelope changes.']), 'Planned parts:', ...(plannedDeltaLines.length > 0 ? plannedDeltaLines : ['No planned parts.']), 'Silhouette limits:', ...(silhouetteLimitLines.length > 0 ? silhouetteLimitLines : ['All planned parts within silhouette limits.']), ...(structuredFitnessReport ? [ `Gate: ${structuredFitnessReport.passesGate ? 'pass' : 'fail'}`, `Recommended focus: ${structuredFitnessReport.recommendedFocus}`, `${spec.archetype === 'marksman' ? 'Hybrid' : spec.archetype === 'rifle' ? 'Rifle' : 'Heavy'} heuristics:`, ...heuristicLines ] : []), 'Archetype ranking:', ...rankingLines, 'Transitions:', ...transitionLines ]; if (missing.length > 0) { lines.push(`Missing: ${missing.join(', ')}`); } else { lines.push('Missing: none'); } controls.weaponQaInfo.textContent = lines.join('\n'); } const buildHumanoidWeaponAttachments = (archetype: HumanoidWeaponArchetype): AttachmentEntry[] => { const structuredSpec = STRUCTURED_WEAPON_DESIGN_SPECS.get(archetype); if (structuredSpec) { return buildWeaponAttachmentsFromSpec(structuredSpec); } const attachments: AttachmentEntry[] = []; const add = ( id: string, moduleId: string, offset: Vec3Like, options?: { anchorId?: string; rotation?: Vec3Like; scale?: Vec3Like; } ): void => { attachments.push( createWeaponAttachment( `attach-weapon-${id}`, options?.anchorId ?? 'anchor-hand-r', moduleId, offset, options ) ); }; switch (archetype) { case 'rifle': add('rifle-hardpoint', 'detail_weapon_hardpoint', vec3(0, 0.012, -0.024), { scale: vec3(0.94, 0.9, 0.88) }); add('rifle-cradle', 'detail_weapon_cradle', vec3(0, 0.026, 0.004), { scale: vec3(0.92, 0.88, 0.82) }); add('rifle-receiver', 'detail_weapon_receiver', vec3(0, 0.02, 0.05), { scale: vec3(1.02, 1.0, 1.04) }); add('rifle-breach', 'detail_weapon_breach', vec3(0, 0.02, -0.032), { scale: vec3(0.94, 0.94, 0.92) }); add('rifle-stock', 'detail_weapon_stock', vec3(0, -0.004, -0.112), { rotation: vec3(98, 0, 0), scale: vec3(0.96, 0.94, 0.98) }); add('rifle-jacket', 'detail_weapon_jacket', vec3(0, 0.03, 0.118), { scale: vec3(1.0, 0.96, 1.0) }); add('rifle-rail', 'detail_weapon_rail', vec3(0, 0.066, 0.038), { scale: vec3(0.98, 0.96, 0.98) }); add('rifle-barrel', 'detail_weapon_barrel', vec3(0, 0.018, 0.244), { scale: vec3(1.0, 0.96, 1.0) }); add('rifle-shroud', 'detail_weapon_shroud', vec3(0, 0.03, 0.194), { scale: vec3(0.96, 0.96, 0.98) }); add('rifle-cooling-jacket', 'detail_weapon_cooling_jacket', vec3(0, 0.032, 0.216), { scale: vec3(0.94, 0.94, 0.96) }); add('rifle-gas-block', 'detail_weapon_gas_block', vec3(0, 0.046, 0.156), { scale: vec3(0.82, 0.8, 0.8) }); add('rifle-muzzle', 'detail_weapon_muzzle', vec3(0, 0.018, 0.35), { scale: vec3(0.92, 0.9, 0.9) }); add('rifle-front-sight', 'detail_weapon_front_sight', vec3(0, 0.058, 0.332), { scale: vec3(0.94, 0.94, 0.94) }); add('rifle-rear-sight', 'detail_weapon_rear_sight', vec3(0, 0.076, 0.008), { scale: vec3(0.96, 0.96, 0.96) }); add('rifle-underslung', 'detail_weapon_underslung', vec3(0, -0.03, 0.108), { scale: vec3(0.88, 0.88, 0.9) }); add('rifle-feed', 'detail_weapon_feed', vec3(0.046, 0.018, -0.004), { scale: vec3(0.88, 0.88, 0.88) }); add('rifle-ejector', 'detail_weapon_ejector', vec3(0.05, 0.04, -0.014), { scale: vec3(0.82, 0.82, 0.8) }); break; case 'marksman': add('marksman-hardpoint', 'detail_weapon_hardpoint', vec3(0, 0.014, -0.03), { scale: vec3(1.0, 0.94, 0.9) }); add('marksman-cradle', 'detail_weapon_cradle', vec3(0, 0.03, 0.006), { scale: vec3(0.96, 0.9, 0.84) }); add('marksman-receiver', 'detail_weapon_receiver', vec3(0, 0.024, 0.052), { scale: vec3(1.08, 1.02, 1.08) }); add('marksman-breach', 'detail_weapon_breach', vec3(0, 0.024, -0.038), { scale: vec3(1.02, 0.98, 0.98) }); add('marksman-stock', 'detail_weapon_stock', vec3(0, 0, -0.132), { rotation: vec3(100, 0, 0), scale: vec3(1.02, 0.98, 1.02) }); add('marksman-rail', 'detail_weapon_rail', vec3(0, 0.082, 0.04), { scale: vec3(1.14, 1.02, 1.04) }); add('marksman-sidecar', 'detail_weapon_sidecar', vec3(0, 0.112, 0.07), { scale: vec3(0.86, 0.78, 1.16) }); add('marksman-barrel', 'detail_weapon_barrel', vec3(0, 0.022, 0.308), { scale: vec3(1.16, 1.02, 1.12) }); add('marksman-shroud', 'detail_weapon_shroud', vec3(0, 0.032, 0.246), { scale: vec3(1.02, 0.98, 1.04) }); add('marksman-gas-block', 'detail_weapon_gas_block', vec3(0, 0.05, 0.19), { scale: vec3(0.84, 0.8, 0.8) }); add('marksman-muzzle', 'detail_weapon_muzzle', vec3(0, 0.022, 0.432), { scale: vec3(0.9, 0.88, 0.88) }); add('marksman-front-sight', 'detail_weapon_front_sight', vec3(0, 0.064, 0.41), { scale: vec3(0.88, 0.88, 0.88) }); add('marksman-rear-sight', 'detail_weapon_rear_sight', vec3(0, 0.088, -0.008), { scale: vec3(0.86, 0.86, 0.86) }); add('marksman-feed', 'detail_weapon_feed', vec3(0.032, -0.014, 0.03), { scale: vec3(0.96, 0.92, 1.0) }); add('marksman-recoil-spring', 'detail_weapon_recoil_spring', vec3(0, 0.104, -0.092), { scale: vec3(1.04, 1.02, 1.0) }); add('marksman-counterweight', 'detail_weapon_counterweight', vec3(0, -0.048, 0.082), { scale: vec3(0.94, 0.92, 0.92) }); break; case 'beam': add('beam-hardpoint', 'detail_weapon_hardpoint', vec3(0, 0.012, -0.012), { scale: vec3(0.98, 0.94, 0.9) }); add('beam-cradle', 'detail_weapon_cradle', vec3(0, 0.026, 0.028), { scale: vec3(0.96, 0.92, 0.84) }); add('beam-receiver', 'detail_weapon_receiver', vec3(0, 0.024, 0.064), { scale: vec3(1.08, 0.98, 1.12) }); add('beam-jacket', 'detail_weapon_jacket', vec3(0, 0.038, 0.12), { scale: vec3(1.12, 1.0, 1.08) }); add('beam-rail', 'detail_weapon_rail', vec3(0, 0.084, 0.052), { scale: vec3(1.12, 1.0, 1.02) }); add('beam-barrel', 'detail_weapon_barrel', vec3(0, 0.024, 0.27), { scale: vec3(0.92, 0.92, 1.1) }); add('beam-shroud', 'detail_weapon_shroud', vec3(0, 0.038, 0.224), { scale: vec3(1.12, 1.08, 1.14) }); add('beam-cooling-jacket', 'detail_weapon_cooling_jacket', vec3(0, 0.042, 0.25), { scale: vec3(1.08, 1.04, 1.08) }); add('beam-emitter-core', 'detail_weapon_gas_block', vec3(0, 0.05, 0.166), { scale: vec3(0.94, 0.88, 0.86) }); add('beam-emitter-ring', 'detail_weapon_gas_block', vec3(0, 0.05, 0.31), { scale: vec3(0.88, 0.84, 0.82) }); add('beam-muzzle', 'detail_weapon_muzzle', vec3(0, 0.024, 0.39), { scale: vec3(1.04, 1.0, 0.96) }); add('beam-sidecar-port', 'detail_weapon_sidecar', vec3(-0.06, 0.022, 0.072), { scale: vec3(0.92, 0.96, 0.94) }); add('beam-sidecar-starboard', 'detail_weapon_sidecar', vec3(0.06, 0.022, 0.072), { scale: vec3(0.92, 0.96, 0.94) }); add('beam-feed', 'detail_weapon_feed', vec3(0, -0.026, 0.05), { scale: vec3(1.02, 0.98, 1.02) }); add('beam-counterweight', 'detail_weapon_counterweight', vec3(0, -0.052, -0.01), { scale: vec3(0.94, 0.9, 0.92) }); add('beam-recoil-spring', 'detail_weapon_recoil_spring', vec3(0, 0.106, -0.062), { scale: vec3(1.04, 1.0, 1.0) }); add('beam-front-sight', 'detail_weapon_front_sight', vec3(0, 0.07, 0.354), { scale: vec3(0.82, 0.82, 0.82) }); add('beam-rear-sight', 'detail_weapon_rear_sight', vec3(0, 0.09, 0.004), { scale: vec3(0.84, 0.84, 0.84) }); break; case 'heavy': add('heavy-hardpoint', 'detail_weapon_hardpoint', vec3(0, 0.014, -0.038), { scale: vec3(1.12, 1.0, 0.96) }); add('heavy-cradle', 'detail_weapon_cradle', vec3(0, 0.03, 0.012), { scale: vec3(1.08, 0.98, 0.88) }); add('heavy-receiver', 'detail_weapon_receiver', vec3(0, 0.028, 0.056), { scale: vec3(1.16, 1.1, 1.12) }); add('heavy-breach', 'detail_weapon_breach', vec3(0, 0.03, -0.046), { scale: vec3(1.14, 1.08, 1.06) }); add('heavy-stock', 'detail_weapon_stock', vec3(0, -0.006, -0.15), { rotation: vec3(100, 0, 0), scale: vec3(1.08, 1.02, 1.04) }); add('heavy-jacket', 'detail_weapon_jacket', vec3(0, 0.042, 0.13), { scale: vec3(1.16, 1.08, 1.12) }); add('heavy-rail', 'detail_weapon_rail', vec3(0, 0.086, 0.044), { scale: vec3(1.08, 1.04, 1.04) }); add('heavy-barrel', 'detail_weapon_barrel', vec3(0, 0.026, 0.28), { scale: vec3(1.18, 1.08, 1.14) }); add('heavy-shroud', 'detail_weapon_shroud', vec3(0, 0.042, 0.23), { scale: vec3(1.12, 1.08, 1.08) }); add('heavy-cooling-jacket', 'detail_weapon_cooling_jacket', vec3(0, 0.046, 0.252), { scale: vec3(1.08, 1.06, 1.06) }); add('heavy-muzzle', 'detail_weapon_muzzle', vec3(0, 0.026, 0.408), { scale: vec3(1.08, 1.04, 1.0) }); add('heavy-muzzle-brake', 'detail_weapon_muzzle', vec3(0, 0.026, 0.45), { scale: vec3(1.02, 0.98, 0.94) }); add('heavy-feed', 'detail_weapon_feed', vec3(0.056, 0.024, -0.008), { scale: vec3(1.06, 1.02, 1.02) }); add('heavy-sidecar-port', 'detail_weapon_sidecar', vec3(-0.068, 0.018, 0.08), { scale: vec3(1.02, 1.0, 0.98) }); add('heavy-sidecar-starboard', 'detail_weapon_sidecar', vec3(0.068, 0.018, 0.08), { scale: vec3(1.02, 1.0, 0.98) }); add('heavy-counterweight', 'detail_weapon_counterweight', vec3(0, -0.064, 0.07), { scale: vec3(1.04, 1.02, 1.0) }); add('heavy-recoil-spring', 'detail_weapon_recoil_spring', vec3(0, 0.11, -0.088), { scale: vec3(1.08, 1.04, 1.0) }); add('heavy-ejector', 'detail_weapon_ejector', vec3(0.064, 0.052, -0.018), { scale: vec3(0.88, 0.88, 0.84) }); break; case 'launcher': add('launcher-hardpoint', 'detail_weapon_hardpoint', vec3(-0.026, 0.03, 0.02), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.08, 1.0, 0.96) }); add('launcher-cradle', 'detail_weapon_cradle', vec3(-0.05, 0.052, 0.08), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.06, 0.98, 0.88) }); add('launcher-receiver', 'detail_weapon_receiver', vec3(-0.084, 0.07, 0.154), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.18, 1.06, 1.12) }); add('launcher-breach', 'detail_weapon_breach', vec3(-0.056, 0.068, 0.02), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.08, 1.02, 1.0) }); add('launcher-shroud', 'detail_weapon_shroud', vec3(-0.11, 0.078, 0.26), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.18, 1.12, 1.14) }); add('launcher-barrel', 'detail_weapon_barrel', vec3(-0.13, 0.064, 0.338), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.2, 1.12, 1.18) }); add('launcher-underslung', 'detail_weapon_underslung', vec3(-0.098, 0.008, 0.238), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.08, 1.02, 1.04) }); add('launcher-muzzle', 'detail_weapon_muzzle', vec3(-0.148, 0.064, 0.446), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.12, 1.08, 1.0) }); add('launcher-feed-port', 'detail_weapon_feed', vec3(-0.152, 0.104, 0.18), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.04, 1.0, 1.0) }); add('launcher-feed-starboard', 'detail_weapon_feed', vec3(-0.008, 0.1, 0.18), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(1.04, 1.0, 1.0) }); add('launcher-sidecar', 'detail_weapon_sidecar', vec3(-0.036, 0.146, 0.148), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(0.94, 0.9, 1.04) }); add('launcher-rear-sight', 'detail_weapon_rear_sight', vec3(-0.03, 0.166, 0.094), { anchorId: 'anchor-shoulder-l', rotation: vec3(82, -8, -16), scale: vec3(0.96, 0.96, 0.96) }); add('launcher-grip', 'detail_weapon_stock', vec3(0.018, 0.008, 0.022), { anchorId: 'anchor-hand-r', rotation: vec3(92, 0, 0), scale: vec3(0.82, 0.78, 0.82) }); break; case 'forearm-pod': add('pod-shell', 'detail_forearm_weapon_pod', vec3(-0.046, 0.004, 0.088), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.98, 1.02, 0.98) }); add('pod-hardpoint', 'detail_weapon_hardpoint', vec3(-0.046, -0.008, 0.05), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.9, 0.88, 0.84) }); add('pod-cradle', 'detail_weapon_cradle', vec3(-0.05, 0.008, 0.098), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.92, 0.9, 0.82) }); add('pod-receiver', 'detail_weapon_receiver', vec3(-0.052, 0.01, 0.13), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.96, 0.94, 0.98) }); add('pod-barrel', 'detail_weapon_barrel', vec3(-0.052, 0.004, 0.226), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.92, 0.9, 0.98) }); add('pod-shroud', 'detail_weapon_shroud', vec3(-0.052, 0.014, 0.184), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.94, 0.96, 0.96) }); add('pod-gas-block', 'detail_weapon_gas_block', vec3(-0.052, 0.026, 0.162), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.76, 0.76, 0.76) }); add('pod-muzzle', 'detail_weapon_muzzle', vec3(-0.052, 0.004, 0.304), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.88, 0.86, 0.86) }); add('pod-feed', 'detail_weapon_feed', vec3(-0.018, -0.012, 0.144), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.88, 0.86, 0.9) }); add('pod-counterweight', 'detail_weapon_counterweight', vec3(-0.078, -0.026, 0.112), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.86, 0.84, 0.84) }); add('pod-ejector', 'detail_weapon_ejector', vec3(-0.012, 0.022, 0.118), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.8, 0.8, 0.78) }); add('pod-front-sight', 'detail_weapon_front_sight', vec3(-0.05, 0.04, 0.274), { anchorId: 'anchor-forearm-l', rotation: vec3(88, -90, 2), scale: vec3(0.82, 0.82, 0.82) }); break; } return attachments; }; const buildHumanoidWeaponPreset = (archetype: HumanoidWeaponArchetype): RecipeDocument | null => { const base = buildPresetRecipe('humanoid-full'); if (!base) return null; base.anchors = [ ...stripHumanoidWeaponAnchors(base.anchors), ...buildHumanoidWeaponAnchors(archetype) ]; base.attachments = [ ...stripHumanoidWeaponAttachments(base.attachments), ...buildHumanoidWeaponAttachments(archetype) ]; return base; }; const buildStandaloneWeaponPreset = (archetype: HumanoidWeaponArchetype): RecipeDocument => ({ version: 1, nodes: [ { id: 'node-weapon-root', position: { x: 0, y: 0.06, z: 0 }, mode: 'static', tags: ['weapon', 'root'] } ], edges: [], anchors: [ { id: 'anchor-hand-r', type: 'node', nodeId: 'node-weapon-root', orientation: 'alongEdge', radialAngle: 0, tags: ['weapon', 'mount', 'right'], accepts: ['weapon', 'connector', 'sensor'] }, { id: 'anchor-weapon-root', type: 'node', nodeId: 'node-weapon-root', orientation: 'alongEdge', radialAngle: 0, tags: ['weapon', 'root'], accepts: ['weapon', 'connector', 'sensor'] }, ...buildHumanoidWeaponAnchors(archetype), ...buildStandaloneWeaponFunctionalAnchors(archetype) ], generators: [], attachments: buildHumanoidWeaponAttachments(archetype) }); function buildPresetRecipe(name: string): RecipeDocument | null { const anchorForNode = ( id: string, nodeId: string, tags: string[] = [], accepts: string[] = [], options?: { orientation?: OrientationRule; radialAngle?: number; proximalNodeId?: string; distalNodeId?: string; upNodeId?: string; } ): AnchorEntry => ({ id, type: 'node', nodeId, orientation: options?.orientation ?? 'alongEdge', radialAngle: options?.radialAngle ?? 0, proximalNodeId: options?.proximalNodeId, distalNodeId: options?.distalNodeId, upNodeId: options?.upNodeId, tags, accepts }); const anchorForEdge = ( id: string, edgeId: string, s: number, tags: string[] = [], accepts: string[] = [] ): AnchorEntry => ({ id, type: 'edge', edgeId, s, orientation: 'alongEdge', radialAngle: 0, tags, accepts }); const anchorForAttachment = ( id: string, attachmentId: string, attachmentFace: AnchorFace, tags: string[] = [], accepts: string[] = [], options?: { offset?: Vec3Like; exportSocket?: boolean } ): AnchorEntry => ({ id, type: 'attachment', attachmentId, attachmentFace, orientation: 'alongEdge', radialAngle: 0, offset: options?.offset ? { ...options.offset } : undefined, exportSocket: options?.exportSocket, tags, accepts }); const createFlipTargetRuntimeMetadata = ( variantId: FlipTargetSharedVariant['id'] ): RuntimeTargetMetadata => { const shared = getFlipTargetSharedVariant(variantId); const animationTriggers = shared.animationTriggers .map(mapSharedFlipTargetAnimationTrigger) .filter((trigger): trigger is RuntimeTargetAnimationTrigger => Boolean(trigger)); const lightTriggers = shared.lightTriggers .map(mapSharedFlipTargetLightTrigger) .filter((trigger): trigger is RuntimeTargetLightTrigger => Boolean(trigger)); const soundTriggers = shared.soundTriggers.map(mapSharedFlipTargetSoundTrigger); return { id: shared.id, category: 'flip-target', variant: shared.variant, hitSurfaceSocketId: FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID['face-front'], recoverSocketId: FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID['face-back'], pivotSocketId: FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID.pivot, serviceSocketId: FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID['base-top'], lightSocketIds: shared.sockets .map((socket) => FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID[socket.id]) .filter((socketId): socketId is string => Boolean(socketId) && socketId !== FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID.pivot && socketId !== FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID['face-front'] && socketId !== FLIP_TARGET_SHARED_TO_EDITOR_SOCKET_ID['face-back']), animationTriggers, lightTriggers, soundTriggers }; }; if (name === 'basic') { return createDefaultRecipe(); } if (name === 'windmill') { return { version: 1, nodes: [ { id: 'node-base', position: { x: 0, y: 0, z: 0 }, tags: [] }, { id: 'node-hub', position: { x: 0, y: 0.6, z: 0 }, tags: [] }, { id: 'node-tip-1', position: { x: 0.45, y: 0.6, z: 0 }, tags: [] }, { id: 'node-tip-2', position: { x: -0.45, y: 0.6, z: 0 }, tags: [] }, { id: 'node-tip-3', position: { x: 0, y: 0.6, z: 0.45 }, tags: [] }, { id: 'node-tip-4', position: { x: 0, y: 0.6, z: -0.45 }, tags: [] } ], edges: [ { id: 'edge-tower', a: 'node-base', b: 'node-hub', radius: 0.09, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-blade-1', a: 'node-hub', b: 'node-tip-1', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-blade-2', a: 'node-hub', b: 'node-tip-2', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-blade-3', a: 'node-hub', b: 'node-tip-3', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-blade-4', a: 'node-hub', b: 'node-tip-4', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ], anchors: [ anchorForNode('anchor-hub', 'node-hub', ['hub'], ['detail']), anchorForNode('anchor-tip-1', 'node-tip-1', ['blade'], ['detail']), anchorForNode('anchor-tip-2', 'node-tip-2', ['blade'], ['detail']), anchorForNode('anchor-tip-3', 'node-tip-3', ['blade'], ['detail']), anchorForNode('anchor-tip-4', 'node-tip-4', ['blade'], ['detail']) ], generators: [], attachments: [ { id: 'attach-blade-1', anchorId: 'anchor-tip-1', moduleId: 'detail_tool_arm', portId: 'mount' }, { id: 'attach-blade-2', anchorId: 'anchor-tip-2', moduleId: 'detail_tool_arm', portId: 'mount' }, { id: 'attach-blade-3', anchorId: 'anchor-tip-3', moduleId: 'detail_tool_arm', portId: 'mount' }, { id: 'attach-blade-4', anchorId: 'anchor-tip-4', moduleId: 'detail_tool_arm', portId: 'mount' } ] }; } if (name === 'human') { const base = buildPresetRecipe('humanoid-full'); if (!base) return null; applyHumanPresetShapeRefinement(base); return base; } const standaloneWeaponArchetype = STANDALONE_WEAPON_PRESET_ARCHETYPES.get(name); if (standaloneWeaponArchetype) { return buildStandaloneWeaponPreset(standaloneWeaponArchetype); } const weaponArchetype = HUMANOID_WEAPON_PRESET_ARCHETYPES.get(name); if (weaponArchetype) { return buildHumanoidWeaponPreset(weaponArchetype); } if (name === 'humanoid-full') { const staticNode = (id: string, position: Vec3Like, poseJoint?: string): NodeEntry => ({ id, position, mode: 'static', poseJoint, tags: [] }); const relNode = (id: string, parentId: string, position: Vec3Like, poseJoint?: string): NodeEntry => ({ id, position, mode: 'relative', parentId, poseJoint, tags: [] }); return { version: 1, nodes: [ staticNode('node-pelvis', { x: 0, y: 0.1, z: 0 }, 'Hips'), relNode('node-torso', 'node-pelvis', { x: 0, y: 0.34, z: 0 }, 'Spine2'), relNode('node-neck', 'node-torso', { x: 0, y: 0.2, z: 0 }, 'Neck'), relNode('node-head', 'node-neck', { x: 0, y: 0.18, z: 0 }, 'Head'), relNode('node-shoulder-l', 'node-torso', { x: -0.22, y: 0.18, z: 0.02 }), relNode('node-shoulder-r', 'node-torso', { x: 0.22, y: 0.18, z: 0.02 }), relNode('node-elbow-l', 'node-shoulder-l', { x: -0.22, y: -0.18, z: 0.02 }), relNode('node-elbow-r', 'node-shoulder-r', { x: 0.22, y: -0.18, z: 0.02 }), relNode('node-wrist-l', 'node-elbow-l', { x: -0.18, y: -0.18, z: 0.04 }), relNode('node-wrist-r', 'node-elbow-r', { x: 0.18, y: -0.18, z: 0.04 }), relNode('node-hand-l', 'node-wrist-l', { x: -0.08, y: -0.05, z: 0.06 }), relNode('node-hand-r', 'node-wrist-r', { x: 0.08, y: -0.05, z: 0.06 }), relNode('node-hip-l', 'node-pelvis', { x: -0.14, y: -0.08, z: 0 }), relNode('node-hip-r', 'node-pelvis', { x: 0.14, y: -0.08, z: 0 }), relNode('node-knee-l', 'node-hip-l', { x: -0.02, y: -0.32, z: 0.03 }), relNode('node-knee-r', 'node-hip-r', { x: 0.02, y: -0.32, z: 0.03 }), relNode('node-ankle-l', 'node-knee-l', { x: 0, y: -0.3, z: 0.05 }), relNode('node-ankle-r', 'node-knee-r', { x: 0, y: -0.3, z: 0.05 }), relNode('node-foot-l', 'node-ankle-l', { x: 0, y: -0.06, z: 0.14 }), relNode('node-foot-r', 'node-ankle-r', { x: 0, y: -0.06, z: 0.14 }) ], edges: [ { id: 'edge-spine', a: 'node-pelvis', b: 'node-torso', radius: 0.1, curve: 'catmull', controlOffset: { x: 0, y: 0.08, z: 0 } }, { id: 'edge-neck', a: 'node-torso', b: 'node-neck', radius: 0.05, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-head', a: 'node-neck', b: 'node-head', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-shoulder-l', a: 'node-torso', b: 'node-shoulder-l', radius: 0.06, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-shoulder-r', a: 'node-torso', b: 'node-shoulder-r', radius: 0.06, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-upper-arm-l', a: 'node-shoulder-l', b: 'node-elbow-l', radius: 0.045, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-upper-arm-r', a: 'node-shoulder-r', b: 'node-elbow-r', radius: 0.045, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-forearm-l', a: 'node-elbow-l', b: 'node-wrist-l', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-forearm-r', a: 'node-elbow-r', b: 'node-wrist-r', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-hand-l', a: 'node-wrist-l', b: 'node-hand-l', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-hand-r', a: 'node-wrist-r', b: 'node-hand-r', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-hip-l', a: 'node-pelvis', b: 'node-hip-l', radius: 0.07, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-hip-r', a: 'node-pelvis', b: 'node-hip-r', radius: 0.07, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-upper-leg-l', a: 'node-hip-l', b: 'node-knee-l', radius: 0.06, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-upper-leg-r', a: 'node-hip-r', b: 'node-knee-r', radius: 0.06, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-lower-leg-l', a: 'node-knee-l', b: 'node-ankle-l', radius: 0.055, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-lower-leg-r', a: 'node-knee-r', b: 'node-ankle-r', radius: 0.055, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-foot-l', a: 'node-ankle-l', b: 'node-foot-l', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-foot-r', a: 'node-ankle-r', b: 'node-foot-r', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ], anchors: [ anchorForNode('anchor-pelvis', 'node-pelvis', ['pelvis'], ['housing', 'armor_plate', 'connector']), anchorForNode('anchor-torso', 'node-torso', ['torso'], ['housing', 'armor_plate', 'connector', 'backpack']), anchorForNode('anchor-neck', 'node-neck', ['neck'], ['connector']), anchorForNode('anchor-head', 'node-head', ['head'], ['helmet', 'sensor', 'armor_plate']), anchorForNode('anchor-shoulder-l', 'node-shoulder-l', ['shoulder', 'left'], ['joint', 'connector', 'armor_plate']), anchorForNode('anchor-shoulder-r', 'node-shoulder-r', ['shoulder', 'right'], ['joint', 'connector', 'armor_plate']), anchorForNode('anchor-hip-l', 'node-hip-l', ['hip', 'left'], ['joint', 'connector', 'armor_plate']), anchorForNode('anchor-hip-r', 'node-hip-r', ['hip', 'right'], ['joint', 'connector', 'armor_plate']), anchorForNode('anchor-hand-l', 'node-hand-l', ['hand', 'left'], ['weapon', 'armor_plate', 'connector']), anchorForNode('anchor-hand-r', 'node-hand-r', ['hand', 'right'], ['weapon', 'armor_plate', 'connector']), anchorForNode('anchor-foot-l', 'node-foot-l', ['foot', 'left'], ['limb_segment', 'armor_plate', 'connector']), anchorForNode('anchor-foot-r', 'node-foot-r', ['foot', 'right'], ['limb_segment', 'armor_plate', 'connector']), anchorForNode('anchor-knee-l', 'node-knee-l', ['knee', 'left'], ['armor_plate']), anchorForNode('anchor-knee-r', 'node-knee-r', ['knee', 'right'], ['armor_plate']), anchorForEdge('anchor-abdomen', 'edge-spine', 0.3, ['abdomen'], ['housing', 'armor_plate']), anchorForNode('anchor-abdomen-core', 'node-torso', ['abdomen', 'core'], ['housing', 'connector']), anchorForEdge('anchor-spine-mid', 'edge-spine', 0.6, ['spine'], ['armor_plate', 'connector']), anchorForEdge('anchor-neck-base', 'edge-neck', 0.08, ['neck', 'base'], ['connector']), anchorForEdge('anchor-upper-arm-l', 'edge-upper-arm-l', 0.5, ['upper_arm', 'left'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-upper-arm-r', 'edge-upper-arm-r', 0.5, ['upper_arm', 'right'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-forearm-l', 'edge-forearm-l', 0.5, ['forearm', 'left'], ['limb_segment', 'armor_plate', 'weapon']), anchorForEdge('anchor-forearm-r', 'edge-forearm-r', 0.5, ['forearm', 'right'], ['limb_segment', 'armor_plate', 'weapon']), anchorForNode('anchor-elbow-l', 'node-elbow-l', ['elbow', 'left'], ['armor_plate', 'connector']), anchorForNode('anchor-elbow-r', 'node-elbow-r', ['elbow', 'right'], ['armor_plate', 'connector']), anchorForEdge('anchor-clavicle-l', 'edge-shoulder-l', 0.45, ['clavicle', 'left'], ['connector']), anchorForEdge('anchor-clavicle-r', 'edge-shoulder-r', 0.45, ['clavicle', 'right'], ['connector']), anchorForEdge('anchor-upper-leg-l', 'edge-upper-leg-l', 0.5, ['upper_leg', 'left'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-upper-leg-r', 'edge-upper-leg-r', 0.5, ['upper_leg', 'right'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-lower-leg-l', 'edge-lower-leg-l', 0.5, ['lower_leg', 'left'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-lower-leg-r', 'edge-lower-leg-r', 0.5, ['lower_leg', 'right'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-wrist-l', 'edge-hand-l', 0.35, ['wrist', 'left'], ['joint', 'connector', 'weapon']), anchorForEdge('anchor-wrist-r', 'edge-hand-r', 0.35, ['wrist', 'right'], ['joint', 'connector', 'weapon']), anchorForEdge('anchor-hand-bone-l', 'edge-hand-l', 0.72, ['hand', 'left'], ['limb_segment', 'weapon', 'connector']), anchorForEdge('anchor-hand-bone-r', 'edge-hand-r', 0.72, ['hand', 'right'], ['limb_segment', 'weapon', 'connector']), anchorForEdge('anchor-ankle-l', 'edge-lower-leg-l', 0.95, ['ankle', 'left'], ['joint', 'connector']), anchorForEdge('anchor-ankle-r', 'edge-lower-leg-r', 0.95, ['ankle', 'right'], ['joint', 'connector']), anchorForEdge('anchor-foot-bone-l', 'edge-foot-l', 0.48, ['foot', 'left'], ['limb_segment', 'connector']), anchorForEdge('anchor-foot-bone-r', 'edge-foot-r', 0.48, ['foot', 'right'], ['limb_segment', 'connector']), ...buildHumanoidWeaponAnchors('rifle') ], generators: [ { id: 'generator-spine-vents', type: 'alongEdge', baseAnchorId: 'anchor-spine-mid', params: { mode: 'count', count: 3, spacing: 0.12, startLen: 0.06, endLen: 0.28 } }, { id: 'generator-head-sensors', type: 'radial', baseAnchorId: 'anchor-head', params: { count: 4, radius: 0.12, startAngleDeg: 45, axis: 'y' } } ], attachments: [ { id: 'attach-pelvis-core', anchorId: 'anchor-pelvis', moduleId: 'core_pelvis', portId: 'mount', shape: { primitive: 'box', size: { x: 0.24, y: 0.18, z: 0.18 } }, sculpt: { enabled: true, primitive: 'roundedBox', size: { x: 0.24, y: 0.18, z: 0.18 }, roundness: 0.03, bulge: { enabled: true, radius: 0.08, smooth: 0.04, offset: { x: 0, y: 0.03, z: 0.02 } }, cut: { enabled: false, radius: 0.06, offset: { x: 0, y: 0, z: 0 } } } }, { id: 'attach-abdomen-block', anchorId: 'anchor-abdomen-core', moduleId: 'core_abdomen_block', portId: 'mount', offset: { x: 0, y: -0.13, z: 0.07 }, shape: { primitive: 'box', size: { x: 0.2, y: 0.16, z: 0.14 } } }, { id: 'attach-torso-core', anchorId: 'anchor-torso', moduleId: 'core_torso_command', portId: 'mount', shape: { primitive: 'box', size: { x: 0.28, y: 0.34, z: 0.2 } }, sculpt: { enabled: true, primitive: 'roundedBox', size: { x: 0.28, y: 0.34, z: 0.2 }, roundness: 0.03, bulge: { enabled: true, radius: 0.12, smooth: 0.05, offset: { x: 0, y: 0.08, z: 0.04 } }, cut: { enabled: true, radius: 0.08, offset: { x: 0, y: 0.02, z: 0.1 } } } }, { id: 'attach-neck-column', anchorId: 'anchor-neck', moduleId: 'core_neck_column', portId: 'mount', shape: { primitive: 'cylinder', size: { x: 0.035, y: 0.12, segments: 16 } } }, { id: 'attach-head-shell', anchorId: 'anchor-head', moduleId: 'core_head_cowl', portId: 'mount', shape: { primitive: 'sphere', size: { x: 0.12, y: 0.12, segments: 18, rings: 14 } } }, { id: 'attach-helmet-visor', anchorId: 'anchor-head', moduleId: 'core_helmet_visor', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.09 } }, { id: 'attach-helmet-optic', anchorId: 'anchor-head', moduleId: 'detail_helmet_optic_lens', portId: 'mount', offset: { x: 0, y: 0.01, z: 0.12 }, scale: { x: 0.6, y: 0.6, z: 0.6 } }, { id: 'attach-shoulder-joint-l', anchorId: 'anchor-shoulder-l', moduleId: 'core_shoulder_joint', portId: 'mount', shape: { primitive: 'sphere', size: { x: 0.07, y: 0.07 } } }, { id: 'attach-shoulder-joint-r', anchorId: 'anchor-shoulder-r', moduleId: 'core_shoulder_joint', portId: 'mount', shape: { primitive: 'sphere', size: { x: 0.07, y: 0.07 } } }, { id: 'attach-shoulder-trim-l', anchorId: 'anchor-shoulder-l', moduleId: 'detail_shoulder_trim', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.04 } }, { id: 'attach-shoulder-trim-r', anchorId: 'anchor-shoulder-r', moduleId: 'detail_shoulder_trim', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.04 } }, { id: 'attach-upper-arm-l', anchorId: 'anchor-upper-arm-l', moduleId: 'core_upper_arm', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.045, y: 0.26, segments: 16, rings: 12 } } }, { id: 'attach-upper-arm-r', anchorId: 'anchor-upper-arm-r', moduleId: 'core_upper_arm', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.045, y: 0.26, segments: 16, rings: 12 } } }, { id: 'attach-forearm-l', anchorId: 'anchor-forearm-l', moduleId: 'core_forearm', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.04, y: 0.24, segments: 16, rings: 12 } } }, { id: 'attach-forearm-r', anchorId: 'anchor-forearm-r', moduleId: 'core_forearm', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.04, y: 0.24, segments: 16, rings: 12 } } }, { id: 'attach-forearm-plate-l', anchorId: 'anchor-forearm-l', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0.02, y: 0, z: 0.02 } }, { id: 'attach-forearm-plate-r', anchorId: 'anchor-forearm-r', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: -0.02, y: 0, z: 0.02 } }, { id: 'attach-hand-l', anchorId: 'anchor-hand-bone-l', moduleId: 'core_hand_guard', portId: 'mount', rotation: { x: 0, y: 180, z: 0 }, shape: { primitive: 'box', size: { x: 0.08, y: 0.05, z: 0.12 } } }, { id: 'attach-hand-r', anchorId: 'anchor-hand-bone-r', moduleId: 'core_hand_guard', portId: 'mount', rotation: { x: 0, y: 180, z: 0 }, shape: { primitive: 'box', size: { x: 0.08, y: 0.05, z: 0.12 } } }, { id: 'attach-hip-joint-l', anchorId: 'anchor-hip-l', moduleId: 'core_hip_joint', portId: 'mount', shape: { primitive: 'sphere', size: { x: 0.08, y: 0.08 } } }, { id: 'attach-hip-joint-r', anchorId: 'anchor-hip-r', moduleId: 'core_hip_joint', portId: 'mount', shape: { primitive: 'sphere', size: { x: 0.08, y: 0.08 } } }, { id: 'attach-upper-leg-l', anchorId: 'anchor-upper-leg-l', moduleId: 'core_upper_leg', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.055, y: 0.32, segments: 16, rings: 12 } } }, { id: 'attach-upper-leg-r', anchorId: 'anchor-upper-leg-r', moduleId: 'core_upper_leg', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.055, y: 0.32, segments: 16, rings: 12 } } }, { id: 'attach-thigh-plate-l', anchorId: 'anchor-upper-leg-l', moduleId: 'detail_thigh_plate', portId: 'mount', offset: { x: 0.02, y: 0.02, z: 0.02 } }, { id: 'attach-thigh-plate-r', anchorId: 'anchor-upper-leg-r', moduleId: 'detail_thigh_plate', portId: 'mount', offset: { x: -0.02, y: 0.02, z: 0.02 } }, { id: 'attach-knee-plate-l', anchorId: 'anchor-knee-l', moduleId: 'detail_knee_plate', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.04 } }, { id: 'attach-knee-plate-r', anchorId: 'anchor-knee-r', moduleId: 'detail_knee_plate', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.04 } }, { id: 'attach-lower-leg-l', anchorId: 'anchor-lower-leg-l', moduleId: 'core_lower_leg', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.05, y: 0.3, segments: 16, rings: 12 } } }, { id: 'attach-lower-leg-r', anchorId: 'anchor-lower-leg-r', moduleId: 'core_lower_leg', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.05, y: 0.3, segments: 16, rings: 12 } } }, { id: 'attach-calf-plate-l', anchorId: 'anchor-lower-leg-l', moduleId: 'detail_calf_plate', portId: 'mount', offset: { x: 0.02, y: 0, z: -0.02 } }, { id: 'attach-calf-plate-r', anchorId: 'anchor-lower-leg-r', moduleId: 'detail_calf_plate', portId: 'mount', offset: { x: -0.02, y: 0, z: -0.02 } }, { id: 'attach-foot-l', anchorId: 'anchor-foot-bone-l', moduleId: 'core_foot_block', portId: 'mount', rotation: { x: 0, y: 180, z: 0 }, shape: { primitive: 'box', size: { x: 0.1, y: 0.05, z: 0.18 } } }, { id: 'attach-foot-r', anchorId: 'anchor-foot-bone-r', moduleId: 'core_foot_block', portId: 'mount', rotation: { x: 0, y: 180, z: 0 }, shape: { primitive: 'box', size: { x: 0.1, y: 0.05, z: 0.18 } } }, { id: 'attach-backpack', anchorId: 'anchor-torso', moduleId: 'detail_backpack_block', portId: 'mount', offset: { x: 0, y: 0.05, z: -0.15 } }, { id: 'attach-backpack-thruster', anchorId: 'anchor-torso', moduleId: 'detail_backpack_thruster', portId: 'mount', offset: { x: 0, y: -0.02, z: -0.24 }, rotation: { x: 90, y: 0, z: 0 }, shape: { primitive: 'cylinder', size: { x: 0.05, y: 0.12, segments: 16 } } }, ...buildHumanoidWeaponAttachments('rifle') ] }; } if (name === 'commander') { const base = buildPresetRecipe('humanoid-full'); if (!base) return null; const keepModules = new Set([ 'core_pelvis', 'core_abdomen_block', 'core_torso_command', 'core_neck_column', 'core_head_cowl', 'core_shoulder_joint', 'core_upper_arm', 'core_forearm', 'core_hand_guard', 'core_hip_joint', 'core_upper_leg', 'core_lower_leg', 'core_foot_block' ]); const attachments: AttachmentEntry[] = base.attachments .filter((attachment) => keepModules.has(attachment.moduleId)) .map((attachment) => ({ ...attachment, shape: undefined, sculpt: undefined })); const removeAttachments = (ids: string[]): void => { const blocked = new Set(ids); for (let index = attachments.length - 1; index >= 0; index -= 1) { if (blocked.has(attachments[index].id)) { attachments.splice(index, 1); } } }; base.anchors = [ ...base.anchors, { id: 'anchor-torso-backpack', type: 'attachment', attachmentId: 'attach-torso-core', attachmentFace: 'back', orientation: 'alongEdge', radialAngle: 0, tags: ['torso', 'backpack'], accepts: ['backpack', 'connector'] } ]; const tuneShape = (attachmentId: string, shape: AttachmentEntry['shape']): void => { const attachment = attachments.find((entry) => entry.id === attachmentId); if (!attachment) return; attachment.shape = cloneAttachmentShape(shape); }; const tuneTransform = ( attachmentId: string, options: { offset?: Vec3Like; rotation?: Vec3Like; scale?: Vec3Like } ): void => { const attachment = attachments.find((entry) => entry.id === attachmentId); if (!attachment) return; if (options.offset) attachment.offset = { ...options.offset }; if (options.rotation) attachment.rotation = { ...options.rotation }; if (options.scale) attachment.scale = { ...options.scale }; }; // Limb packages must stay mirror-derived; tune one side and reflect it. const tuneMirroredShape = (idBase: string, shape: AttachmentEntry['shape']): void => { tuneShape(`${idBase}-l`, shape); tuneShape(`${idBase}-r`, shape); }; const tuneMirroredTransform = ( idBase: string, options: { offset?: Vec3Like; rotation?: Vec3Like; scale?: Vec3Like } ): void => { tuneTransform(`${idBase}-l`, options); tuneTransform(`${idBase}-r`, { offset: options.offset ? mirrorOffset(options.offset) : undefined, rotation: options.rotation ? mirrorRotation(options.rotation) : undefined, scale: options.scale }); }; const addPair = ( idBase: string, anchorBase: string, moduleId: string, offset?: Vec3Like, rotation?: Vec3Like ): void => { attachments.push({ id: `${idBase}-l`, anchorId: `${anchorBase}-l`, moduleId, portId: 'mount', offset, rotation: rotation ? { x: rotation.x, y: -(rotation.y ?? 0), z: -(rotation.z ?? 0) } : undefined }); attachments.push({ id: `${idBase}-r`, anchorId: `${anchorBase}-r`, moduleId, portId: 'mount', offset, rotation }); }; const addMirroredPair = ( idBase: string, anchorBase: string, moduleId: string, leftOffset: Vec3Like, rightOffset: Vec3Like, leftRotation?: Vec3Like, rightRotation?: Vec3Like, leftScale?: Vec3Like, rightScale?: Vec3Like ): void => { attachments.push({ id: `${idBase}-l`, anchorId: `${anchorBase}-l`, moduleId, portId: 'mount', offset: leftOffset, rotation: leftRotation, scale: leftScale }); attachments.push({ id: `${idBase}-r`, anchorId: `${anchorBase}-r`, moduleId, portId: 'mount', offset: rightOffset, rotation: rightRotation, scale: rightScale }); }; tuneShape('attach-torso-core', { taper: { xTop: 0.2, xBottom: 0.06, zTop: 0.22, zBottom: 0.08 }, chamfer: { edge: 0.24, corner: 0.28 }, profile: { kind: 'hardSurface', intensity: 0.95 } }); tuneShape('attach-pelvis-core', { taper: { xTop: 0.1, xBottom: 0.2, zTop: 0.08, zBottom: 0.18 }, chamfer: { edge: 0.2, corner: 0.24 }, profile: { kind: 'hardSurface', intensity: 0.72 } }); tuneShape('attach-abdomen-block', { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.12, zBottom: 0.05 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.68 } }); tuneShape('attach-neck-column', { taper: { xTop: 0.08, xBottom: 0.08, zTop: 0.12, zBottom: 0.12 }, chamfer: { edge: 0.16, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.52 } }); removeAttachments([ 'attach-head-shell', 'attach-helmet-visor', 'attach-helmet-optic', 'attach-thigh-plate-l', 'attach-thigh-plate-r', 'attach-calf-plate-l', 'attach-calf-plate-r' ]); tuneMirroredShape('attach-upper-arm', { taper: { xTop: 0.2, xBottom: 0.12, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.12, corner: 0.1 }, profile: { kind: 'mechLimb', intensity: 0.8 } }); tuneMirroredShape('attach-forearm', { taper: { xTop: 0.12, xBottom: 0.22, zTop: 0.1, zBottom: 0.18 }, chamfer: { edge: 0.14, corner: 0.12 }, profile: { kind: 'mechLimb', intensity: 0.9 } }); tuneMirroredShape('attach-upper-leg', { taper: { xTop: 0.18, xBottom: 0.26, zTop: 0.12, zBottom: 0.16 }, chamfer: { edge: 0.12, corner: 0.1 }, profile: { kind: 'mechLimb', intensity: 0.78 } }); tuneMirroredShape('attach-lower-leg', { taper: { xTop: 0.14, xBottom: 0.3, zTop: 0.12, zBottom: 0.22 }, chamfer: { edge: 0.16, corner: 0.14 }, profile: { kind: 'mechLimb', intensity: 0.95 } }); tuneMirroredShape('attach-hand', { taper: { xTop: 0.1, xBottom: 0.02, zTop: 0.18, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.18 }, profile: { kind: 'plate', intensity: 0.55 } }); tuneMirroredShape('attach-foot', { taper: { xTop: 0.02, xBottom: 0.08, zTop: 0.22, zBottom: 0.08 }, chamfer: { edge: 0.2, corner: 0.18 }, profile: { kind: 'plate', intensity: 0.7 } }); tuneMirroredShape('attach-shoulder-joint', { chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.55 } }); tuneMirroredShape('attach-hip-joint', { chamfer: { edge: 0.2, corner: 0.22 }, profile: { kind: 'hardSurface', intensity: 0.6 } }); const backpackCluster = buildCommanderVolumeAttachmentCluster('backpack', 'anchor-torso-backpack', 'attach-backpack'); base.anchors = [...base.anchors, ...backpackCluster.anchors]; attachments.push(...backpackCluster.attachments); const helmetCluster = buildCommanderVolumeAttachmentCluster('helmet', 'anchor-head', 'attach-helmet'); base.anchors = [...base.anchors, ...helmetCluster.anchors]; attachments.push(...helmetCluster.attachments); tuneTransform('attach-backpack-pack-body', { offset: { x: 0, y: 0.08, z: -0.08 } }); tuneTransform('attach-backpack-pack-spine', { offset: { x: 0, y: 0.06, z: -0.06 } }); tuneTransform('attach-backpack-pack-thruster', { offset: { x: 0, y: -0.01, z: -0.08 } }); tuneTransform('attach-backpack-pack-radiator-left', { offset: { x: -0.03, y: 0.015, z: -0.012 } }); tuneTransform('attach-backpack-pack-radiator-right', { offset: { x: 0.03, y: 0.015, z: -0.012 } }); tuneTransform('attach-backpack-pack-fin', { offset: { x: 0, y: 0.024, z: -0.018 } }); tuneTransform('attach-backpack-pack-fin-l', { offset: { x: -0.016, y: 0.015, z: -0.012 } }); tuneTransform('attach-backpack-pack-fin-r', { offset: { x: 0.016, y: 0.015, z: -0.012 } }); tuneTransform('attach-backpack-pack-sensor', { offset: { x: 0, y: 0.028, z: 0.006 } }); tuneTransform('attach-backpack-pack-sensor-mast', { offset: { x: 0, y: 0.022, z: 0 } }); tuneTransform('attach-backpack-pack-sensor-head', { offset: { x: 0, y: 0.016, z: 0.002 } }); tuneShape('attach-backpack-pack-body', { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.22, corner: 0.26 }, profile: { kind: 'hardSurface', intensity: 0.82 } }); tuneShape('attach-backpack-pack-spine', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.12, zBottom: 0.12 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.48 } }); tuneShape('attach-backpack-pack-core', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.1, zBottom: 0.06 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.55 } }); tuneShape('attach-backpack-pack-radiator-left', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.7 } }); tuneShape('attach-backpack-pack-radiator-right', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.7 } }); tuneShape('attach-backpack-pack-fin', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.7 } }); tuneShape('attach-backpack-pack-fin-l', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.7 } }); tuneShape('attach-backpack-pack-fin-r', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.7 } }); tuneShape('attach-backpack-pack-nozzle-l', { chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.48 } }); tuneShape('attach-backpack-pack-nozzle-r', { chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.48 } }); tuneShape('attach-helmet-helmet-shell', { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.62 } }); tuneShape('attach-helmet-helmet-crown', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.5 } }); tuneShape('attach-helmet-helmet-visor', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.18, zBottom: 0.14 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.8 } }); tuneShape('attach-helmet-helmet-jaw-guard', { taper: { xTop: 0.04, xBottom: 0.08, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.72 } }); tuneShape('attach-helmet-helmet-occipital-guard', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.58 } }); tuneShape('attach-helmet-helmet-brow-plate', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'plate', intensity: 0.74 } }); tuneShape('attach-helmet-helmet-cheek-plate-l', { taper: { xTop: 0.02, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'plate', intensity: 0.7 } }); tuneShape('attach-helmet-helmet-cheek-plate-r', { taper: { xTop: 0.02, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'plate', intensity: 0.7 } }); tuneShape('attach-helmet-helmet-rear-fin', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.2, zBottom: 0.1 }, chamfer: { edge: 0.12, corner: 0.12 }, profile: { kind: 'plate', intensity: 0.62 } }); tuneShape('attach-helmet-lhelmet-temple-guard', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'plate', intensity: 0.66 } }); tuneShape('attach-helmet-rhelmet-temple-guard', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'plate', intensity: 0.66 } }); tuneShape('attach-helmet-lhelmet-cheek-plate', { taper: { xTop: 0.02, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.74 } }); tuneShape('attach-helmet-rhelmet-cheek-plate', { taper: { xTop: 0.02, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.74 } }); tuneShape('attach-helmet-lhelmet-ear-pod', { chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.54 } }); tuneShape('attach-helmet-rhelmet-ear-pod', { chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.54 } }); tuneShape('attach-backpack-pack-sensor', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.18, zBottom: 0.1 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.46 } }); tuneShape('attach-backpack-pack-sensor-mast', { chamfer: { edge: 0.12, corner: 0.12 }, profile: { kind: 'hardSurface', intensity: 0.34 } }); tuneShape('attach-backpack-pack-sensor-head', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.44 } }); tuneShape('attach-backpack-pack-hardpoint-l', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.14, zBottom: 0.1 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.52 } }); tuneShape('attach-backpack-pack-hardpoint-r', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.14, zBottom: 0.1 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.52 } }); tuneShape('attach-commander-engine-intake', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.16, zBottom: 0.12 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.62 } }); tuneShape('attach-commander-engine-louver-l', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.58 } }); tuneShape('attach-commander-engine-louver-r', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.58 } }); tuneShape('attach-commander-sensor-array', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.56 } }); attachments.push( { id: 'attach-collar-ring', anchorId: 'anchor-neck-base', moduleId: 'core_collar_ring', portId: 'mount', offset: { x: 0, y: -0.01, z: 0.01 } }, { id: 'attach-waist-ring', anchorId: 'anchor-pelvis', moduleId: 'core_waist_ring', portId: 'mount', offset: { x: 0, y: 0.05, z: 0 } }, { id: 'attach-spine-block', anchorId: 'anchor-spine-mid', moduleId: 'core_spine_block', portId: 'mount', offset: { x: 0, y: 0.02, z: -0.02 }, rotation: { x: 90, y: 0, z: 0 } }, { id: 'attach-spine-bus', anchorId: 'anchor-spine-mid', moduleId: 'core_spine_bus', portId: 'mount', offset: { x: 0, y: -0.07, z: 0.03 }, rotation: { x: 90, y: 0, z: 0 } }, { id: 'attach-sternum-truss', anchorId: 'anchor-torso', moduleId: 'core_sternum_truss', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.1 }, rotation: { x: 90, y: 0, z: 0 } }, { id: 'attach-sternum-keel', anchorId: 'anchor-torso', moduleId: 'core_sternum_keel', portId: 'mount', offset: { x: 0, y: -0.01, z: 0.12 }, rotation: { x: 90, y: 0, z: 0 } }, { id: 'attach-sternum-sensor', anchorId: 'anchor-torso', moduleId: 'detail_sternum_sensor', portId: 'mount', offset: { x: 0, y: 0.03, z: 0.145 } }, { id: 'attach-abdomen-frame', anchorId: 'anchor-abdomen', moduleId: 'core_abdomen_frame', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.08 }, rotation: { x: 90, y: 0, z: 0 } }, { id: 'attach-pelvis-buttress', anchorId: 'anchor-pelvis', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: 0, y: -0.01, z: 0.08 } } ); tuneShape('attach-collar-ring', { chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.4 } }); tuneShape('attach-waist-ring', { chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.42 } }); tuneShape('attach-spine-block', { taper: { xTop: 0.06, xBottom: 0.04, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.2, corner: 0.22 }, profile: { kind: 'hardSurface', intensity: 0.7 } }); tuneShape('attach-spine-bus', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.1, zBottom: 0.1 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.45 } }); tuneShape('attach-sternum-truss', { taper: { xTop: 0.08, xBottom: 0.04, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.62 } }); tuneShape('attach-sternum-keel', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.58 } }); tuneShape('attach-abdomen-frame', { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.55 } }); tuneShape('attach-pelvis-buttress', { taper: { xTop: 0.02, xBottom: 0.08, zTop: 0.16, zBottom: 0.12 }, chamfer: { edge: 0.2, corner: 0.22 }, profile: { kind: 'hardSurface', intensity: 0.64 } }); addPair('attach-pelvis-socket-cap', 'anchor-hip', 'detail_pelvis_socket_cap', { x: 0, y: 0.012, z: 0.028 }, { x: 90, y: 0, z: 0 }); attachments.push( { id: 'attach-rib-pod-l', anchorId: 'anchor-torso', moduleId: 'core_rib_pod', portId: 'mount', offset: { x: -0.15, y: 0.025, z: 0.035 }, rotation: { x: 90, y: 0, z: 0 } }, { id: 'attach-rib-pod-r', anchorId: 'anchor-torso', moduleId: 'core_rib_pod', portId: 'mount', offset: { x: 0.15, y: 0.025, z: 0.035 }, rotation: { x: 90, y: 0, z: 0 } } ); addPair('attach-clavicle-plate', 'anchor-clavicle', 'core_clavicle_plate', { x: 0, y: 0.01, z: 0.02 }, { x: 0, y: 90, z: 0 }); addPair('attach-shoulder-yoke', 'anchor-clavicle', 'core_shoulder_yoke', { x: 0, y: -0.015, z: 0 }, { x: 0, y: 90, z: 0 }); addPair('attach-shoulder-actuator', 'anchor-clavicle', 'detail_shoulder_actuator', { x: 0, y: -0.03, z: 0.03 }, { x: 90, y: 0, z: 0 }); addPair('attach-shoulder-cuff', 'anchor-upper-arm', 'core_shoulder_cuff', { x: 0, y: 0, z: -0.08 }, { x: 90, y: 0, z: 0 }); addPair('attach-upper-arm-sleeve', 'anchor-upper-arm', 'detail_hydraulic_sleeve', { x: 0.022, y: -0.012, z: 0 }, { x: 90, y: 0, z: 0 }); addPair('attach-forearm-cuff', 'anchor-forearm', 'core_forearm_cuff', { x: 0, y: 0, z: 0.08 }, { x: 90, y: 0, z: 0 }); addPair('attach-elbow-guard', 'anchor-forearm', 'core_elbow_guard', { x: 0, y: 0, z: -0.09 }, { x: 90, y: 0, z: 0 }); addPair('attach-forearm-rod', 'anchor-forearm', 'detail_hydraulic_rod', { x: -0.018, y: -0.006, z: 0.015 }, { x: 90, y: 0, z: 0 }); addPair('attach-wrist-cuff', 'anchor-wrist', 'core_forearm_cuff', { x: 0, y: 0, z: 0 }, { x: 90, y: 0, z: 0 }); addPair('attach-wrist-cable', 'anchor-wrist', 'detail_tendon_cable', { x: 0.012, y: -0.006, z: 0.015 }, { x: 90, y: 0, z: 0 }); addPair('attach-thigh-sleeve', 'anchor-upper-leg', 'detail_hydraulic_sleeve', { x: 0.024, y: -0.02, z: -0.01 }, { x: 90, y: 0, z: 0 }); addPair('attach-knee-guard', 'anchor-knee', 'core_knee_guard', { x: 0, y: 0, z: 0 }, { x: 90, y: 0, z: 0 }); addPair('attach-calf-rod', 'anchor-lower-leg', 'detail_hydraulic_rod', { x: -0.018, y: 0.01, z: -0.01 }, { x: 90, y: 0, z: 0 }); addPair('attach-calf-cable', 'anchor-lower-leg', 'detail_tendon_cable', { x: 0.018, y: -0.005, z: -0.02 }, { x: 90, y: 0, z: 0 }); addPair('attach-ankle-joint', 'anchor-ankle', 'core_ankle_joint', { x: 0, y: 0, z: 0 }); addPair('attach-boot-collar', 'anchor-ankle', 'core_boot_collar', { x: 0, y: 0.01, z: 0.02 }, { x: 90, y: 0, z: 0 }); addPair('attach-ankle-brace', 'anchor-ankle', 'detail_ankle_brace', { x: 0.018, y: 0, z: 0.022 }, { x: 0, y: 0, z: 0 }); addPair('attach-heel-block', 'anchor-foot-bone', 'core_heel_block', { x: 0, y: -0.02, z: -0.08 }, { x: 90, y: 0, z: 0 }); addPair('attach-sole-plate', 'anchor-foot-bone', 'core_sole_plate', { x: 0, y: -0.045, z: 0.01 }); addPair('attach-toe-cap', 'anchor-foot-bone', 'core_toe_cap', { x: 0, y: -0.005, z: 0.085 }); addPair('attach-sole-ridge', 'anchor-foot-bone', 'detail_sole_ridge', { x: 0, y: -0.026, z: 0.035 }); attachments.push( { id: 'attach-commander-chest', anchorId: 'anchor-torso', moduleId: 'core_chest_plate', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.14 } }, { id: 'attach-commander-cuirass-front', anchorId: 'anchor-torso', moduleId: 'core_cuirass_front', portId: 'mount', offset: { x: 0, y: -0.005, z: 0.12 }, rotation: { x: 8, y: 0, z: 0 }, scale: { x: 1.06, y: 1.06, z: 1.02 } }, { id: 'attach-commander-cuirass-back', anchorId: 'anchor-spine-mid', moduleId: 'core_cuirass_back', portId: 'mount', offset: { x: 0, y: 0.01, z: -0.12 }, rotation: { x: -6, y: 180, z: 0 }, scale: { x: 1.02, y: 1.04, z: 1.02 } }, { id: 'attach-commander-torso-plate', anchorId: 'anchor-abdomen-core', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0, y: 0.01, z: 0.11 }, rotation: { x: 6, y: 0, z: 0 }, scale: { x: 1.1, y: 1.0, z: 1.0 } }, { id: 'attach-commander-torso-ridge', anchorId: 'anchor-torso', moduleId: 'detail_torso_ridge', portId: 'mount', offset: { x: 0, y: 0.015, z: 0.165 }, rotation: { x: 6, y: 0, z: 0 }, scale: { x: 1.02, y: 1.08, z: 1.02 } }, { id: 'attach-commander-vent-l', anchorId: 'anchor-torso', moduleId: 'detail_torso_vent_array', portId: 'mount', offset: { x: -0.125, y: -0.005, z: 0.11 }, rotation: { x: 10, y: -8, z: 0 }, scale: { x: 0.96, y: 0.96, z: 0.96 } }, { id: 'attach-commander-vent-r', anchorId: 'anchor-torso', moduleId: 'detail_torso_vent_array', portId: 'mount', offset: { x: 0.125, y: -0.005, z: 0.11 }, rotation: { x: 10, y: 8, z: 0 }, scale: { x: 0.96, y: 0.96, z: 0.96 } }, { id: 'attach-commander-engine-intake', anchorId: 'anchor-torso-backpack', moduleId: 'detail_engine_intake', portId: 'mount', offset: { x: 0, y: -0.02, z: -0.045 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 0.96, y: 1.0, z: 0.96 } }, { id: 'attach-commander-engine-louver-l', anchorId: 'anchor-torso-backpack', moduleId: 'detail_engine_louver', portId: 'mount', offset: { x: -0.1, y: 0.015, z: -0.02 }, rotation: { x: 90, y: -10, z: 0 }, scale: { x: 0.92, y: 0.96, z: 0.92 } }, { id: 'attach-commander-engine-louver-r', anchorId: 'anchor-torso-backpack', moduleId: 'detail_engine_louver', portId: 'mount', offset: { x: 0.1, y: 0.015, z: -0.02 }, rotation: { x: 90, y: 10, z: 0 }, scale: { x: 0.92, y: 0.96, z: 0.92 } }, { id: 'attach-commander-sensor-array', anchorId: 'anchor-torso-backpack', moduleId: 'detail_sensor_array', portId: 'mount', offset: { x: 0, y: 0.12, z: 0.01 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 0.86, y: 0.9, z: 0.86 } }, { id: 'attach-commander-pelvis-guard', anchorId: 'anchor-pelvis', moduleId: 'core_pelvis_guard', portId: 'mount', offset: { x: 0, y: -0.015, z: 0.095 }, rotation: { x: 4, y: 0, z: 0 }, scale: { x: 1.12, y: 1.06, z: 1.08 } }, { id: 'attach-commander-sternum-crest', anchorId: 'anchor-torso', moduleId: 'detail_abdomen_plate', portId: 'mount', offset: { x: 0, y: 0.018, z: 0.166 }, rotation: { x: 18, y: 0, z: 0 }, scale: { x: 0.86, y: 1.12, z: 1.0 } }, { id: 'attach-commander-centerline-sensor', anchorId: 'anchor-torso', moduleId: 'detail_sternum_sensor', portId: 'mount', offset: { x: 0, y: 0.064, z: 0.176 }, scale: { x: 1.18, y: 1.18, z: 1.18 } }, { id: 'attach-commander-abdomen-core-plate', anchorId: 'anchor-abdomen-core', moduleId: 'detail_abdomen_plate', portId: 'mount', offset: { x: 0, y: -0.016, z: 0.104 }, rotation: { x: 20, y: 0, z: 0 }, scale: { x: 0.98, y: 1.06, z: 0.96 } }, { id: 'attach-commander-backpack-saddle', anchorId: 'anchor-torso-backpack', moduleId: 'detail_backpack_module', portId: 'mount', offset: { x: 0, y: 0.038, z: -0.026 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 0.94, y: 1.08, z: 0.92 } } ); addMirroredPair( 'attach-commander-pectoral-bridge', 'anchor-torso', 'detail_shoulder_trim', { x: -0.094, y: 0.072, z: 0.124 }, { x: 0.094, y: 0.072, z: 0.124 }, { x: 8, y: -38, z: 14 }, { x: 8, y: 38, z: -14 }, { x: 1.02, y: 1.08, z: 1.02 }, { x: 1.02, y: 1.08, z: 1.02 } ); addMirroredPair( 'attach-commander-backpack-clip', 'anchor-torso-backpack', 'detail_backpack_clip', { x: -0.074, y: -0.006, z: 0.014 }, { x: 0.074, y: -0.006, z: 0.014 }, { x: 90, y: -16, z: 0 }, { x: 90, y: 16, z: 0 }, { x: 1.18, y: 1.12, z: 1.1 }, { x: 1.18, y: 1.12, z: 1.1 } ); addMirroredPair( 'attach-commander-shoulder-trim', 'anchor-clavicle', 'detail_shoulder_trim', { x: -0.012, y: 0.02, z: 0.07 }, { x: 0.012, y: 0.02, z: 0.07 }, { x: -10, y: -78, z: 14 }, { x: -10, y: 78, z: -14 }, { x: 1.12, y: 1.12, z: 1.12 }, { x: 1.12, y: 1.12, z: 1.12 } ); addMirroredPair( 'attach-commander-shoulder-guard', 'anchor-clavicle', 'detail_shoulder_guard', { x: -0.02, y: 0.03, z: 0.09 }, { x: 0.02, y: 0.03, z: 0.09 }, { x: -8, y: -84, z: 16 }, { x: -8, y: 84, z: -16 }, { x: 1.18, y: 1.14, z: 1.16 }, { x: 1.18, y: 1.14, z: 1.16 } ); addMirroredPair( 'attach-commander-shoulder-bay', 'anchor-clavicle', 'detail_shoulder_bay', { x: -0.01, y: 0.008, z: 0.045 }, { x: 0.01, y: 0.008, z: 0.045 }, { x: 0, y: -88, z: 8 }, { x: 0, y: 88, z: -8 }, { x: 0.95, y: 1.02, z: 0.98 }, { x: 0.95, y: 1.02, z: 0.98 } ); addMirroredPair( 'attach-commander-shoulder-vane', 'anchor-clavicle', 'detail_shoulder_vane', { x: -0.03, y: 0.065, z: 0.11 }, { x: 0.03, y: 0.065, z: 0.11 }, { x: -4, y: -90, z: 18 }, { x: -4, y: 90, z: -18 }, { x: 1.04, y: 1.08, z: 1.02 }, { x: 1.04, y: 1.08, z: 1.02 } ); addMirroredPair( 'attach-commander-chest-cheek', 'anchor-torso', 'core_chest_cheek_plate', { x: -0.12, y: 0.045, z: 0.11 }, { x: 0.12, y: 0.045, z: 0.11 }, { x: 8, y: -28, z: 10 }, { x: 8, y: 28, z: -10 }, { x: 0.98, y: 1.02, z: 0.98 }, { x: 0.98, y: 1.02, z: 0.98 } ); addMirroredPair( 'attach-commander-torso-flank', 'anchor-torso', 'core_torso_flank', { x: -0.132, y: -0.004, z: 0.05 }, { x: 0.132, y: -0.004, z: 0.05 }, { x: 4, y: -84, z: 10 }, { x: 4, y: 84, z: -10 }, { x: 1.0, y: 1.06, z: 1.0 }, { x: 1.0, y: 1.06, z: 1.0 } ); addMirroredPair( 'attach-commander-lat-plate', 'anchor-abdomen-core', 'core_lat_plate', { x: -0.122, y: 0.01, z: 0.042 }, { x: 0.122, y: 0.01, z: 0.042 }, { x: 6, y: -88, z: 8 }, { x: 6, y: 88, z: -8 }, { x: 0.96, y: 1.04, z: 0.98 }, { x: 0.96, y: 1.04, z: 0.98 } ); addMirroredPair( 'attach-commander-flank-bulkhead', 'anchor-torso', 'core_flank_bulkhead', { x: -0.148, y: -0.002, z: 0.018 }, { x: 0.148, y: -0.002, z: 0.018 }, { x: 2, y: -92, z: 10 }, { x: 2, y: 92, z: -10 }, { x: 1.02, y: 1.08, z: 1.02 }, { x: 1.02, y: 1.08, z: 1.02 } ); addMirroredPair( 'attach-commander-abdomen-side', 'anchor-abdomen-core', 'core_abdomen_side_plate', { x: -0.114, y: -0.004, z: 0.056 }, { x: 0.114, y: -0.004, z: 0.056 }, { x: 10, y: -86, z: 8 }, { x: 10, y: 86, z: -8 }, { x: 0.98, y: 1.04, z: 1.0 }, { x: 0.98, y: 1.04, z: 1.0 } ); addMirroredPair( 'attach-commander-bicep-guard', 'anchor-upper-arm', 'core_bicep_guard', { x: -0.01, y: 0.015, z: -0.01 }, { x: 0.01, y: 0.015, z: -0.01 }, { x: -6, y: -90, z: 8 }, { x: -6, y: 90, z: -8 } ); addMirroredPair( 'attach-commander-tricep-guard', 'anchor-upper-arm', 'core_tricep_guard', { x: 0.008, y: -0.01, z: -0.04 }, { x: -0.008, y: -0.01, z: -0.04 }, { x: 10, y: -90, z: -6 }, { x: 10, y: 90, z: 6 } ); addMirroredPair( 'attach-commander-bicep-plate', 'anchor-upper-arm', 'detail_bicep_plate', { x: -0.016, y: 0.02, z: 0.055 }, { x: 0.016, y: 0.02, z: 0.055 }, { x: -4, y: -90, z: 12 }, { x: -4, y: 90, z: -12 }, { x: 1.08, y: 1.08, z: 1.02 }, { x: 1.08, y: 1.08, z: 1.02 } ); addMirroredPair( 'attach-commander-forearm-plate', 'anchor-forearm', 'detail_forearm_plate', { x: -0.01, y: 0.012, z: 0.025 }, { x: 0.01, y: 0.012, z: 0.025 }, { x: 4, y: -90, z: 6 }, { x: 4, y: 90, z: -6 }, { x: 1.06, y: 1.08, z: 1.02 }, { x: 1.06, y: 1.08, z: 1.02 } ); addMirroredPair( 'attach-commander-elbow-plate', 'anchor-forearm', 'detail_elbow_plate', { x: -0.01, y: 0.01, z: -0.085 }, { x: 0.01, y: 0.01, z: -0.085 }, { x: 4, y: -90, z: 8 }, { x: 4, y: 90, z: -8 }, { x: 1.02, y: 1.04, z: 1.02 }, { x: 1.02, y: 1.04, z: 1.02 } ); addMirroredPair( 'attach-commander-forearm-pod', 'anchor-forearm', 'detail_forearm_weapon_pod', { x: -0.04, y: 0.005, z: 0.08 }, { x: 0.04, y: 0.005, z: 0.08 }, { x: 0, y: -90, z: 0 }, { x: 0, y: 90, z: 0 }, { x: 0.88, y: 0.94, z: 0.88 }, { x: 0.88, y: 0.94, z: 0.88 } ); addMirroredPair( 'attach-commander-forearm-pod-cover', 'anchor-forearm', 'detail_forearm_pod_cover', { x: -0.042, y: 0.012, z: 0.095 }, { x: 0.042, y: 0.012, z: 0.095 }, { x: 4, y: -90, z: 4 }, { x: 4, y: 90, z: -4 }, { x: 0.92, y: 0.98, z: 0.92 }, { x: 0.92, y: 0.98, z: 0.92 } ); addMirroredPair( 'attach-commander-hand-guard', 'anchor-hand-bone', 'core_hand_guard', { x: -0.006, y: 0.004, z: 0.03 }, { x: 0.006, y: 0.004, z: 0.03 }, { x: 0, y: 180, z: 0 }, { x: 0, y: 180, z: 0 }, { x: 1.08, y: 1.0, z: 1.0 }, { x: 1.08, y: 1.0, z: 1.0 } ); addMirroredPair( 'attach-commander-thigh-guard', 'anchor-upper-leg', 'core_thigh_guard', { x: -0.012, y: 0.012, z: 0.03 }, { x: 0.012, y: 0.012, z: 0.03 }, { x: 8, y: -90, z: 6 }, { x: 8, y: 90, z: -6 }, { x: 1.08, y: 1.14, z: 1.06 }, { x: 1.08, y: 1.14, z: 1.06 } ); addMirroredPair( 'attach-commander-knee-plate', 'anchor-knee', 'detail_knee_plate', { x: 0, y: 0.018, z: 0.055 }, { x: 0, y: 0.018, z: 0.055 }, { x: 8, y: 0, z: 0 }, { x: 8, y: 0, z: 0 }, { x: 1.08, y: 1.08, z: 1.08 }, { x: 1.08, y: 1.08, z: 1.08 } ); addMirroredPair( 'attach-commander-calf-guard', 'anchor-lower-leg', 'core_calf_guard', { x: -0.01, y: 0.008, z: -0.01 }, { x: 0.01, y: 0.008, z: -0.01 }, { x: 0, y: -90, z: 8 }, { x: 0, y: 90, z: -8 } ); addMirroredPair( 'attach-commander-shin-detail', 'anchor-lower-leg', 'detail_shin_plate', { x: -0.006, y: 0.006, z: 0.08 }, { x: 0.006, y: 0.006, z: 0.08 }, { x: -2, y: 0, z: 0 }, { x: -2, y: 0, z: 0 }, { x: 1.04, y: 1.1, z: 1.02 }, { x: 1.04, y: 1.1, z: 1.02 } ); addMirroredPair( 'attach-commander-shin-plate', 'anchor-lower-leg', 'core_shin_plate', { x: 0, y: 0.004, z: 0.055 }, { x: 0, y: 0.004, z: 0.055 }, { x: -2, y: 0, z: 0 }, { x: -2, y: 0, z: 0 }, { x: 1.08, y: 1.18, z: 1.08 }, { x: 1.08, y: 1.18, z: 1.08 } ); tuneShape('attach-commander-chest', { taper: { xTop: 0.16, xBottom: 0.06, zTop: 0.24, zBottom: 0.1 }, chamfer: { edge: 0.22, corner: 0.26 }, profile: { kind: 'hardSurface', intensity: 0.9 } }); tuneShape('attach-commander-cuirass-front', { taper: { xTop: 0.18, xBottom: 0.1, zTop: 0.28, zBottom: 0.12 }, chamfer: { edge: 0.22, corner: 0.24 }, profile: { kind: 'hardSurface', intensity: 0.96 } }); tuneShape('attach-commander-cuirass-back', { taper: { xTop: 0.12, xBottom: 0.06, zTop: 0.22, zBottom: 0.16 }, chamfer: { edge: 0.2, corner: 0.22 }, profile: { kind: 'hardSurface', intensity: 0.86 } }); tuneShape('attach-commander-torso-plate', { taper: { xTop: 0.12, xBottom: 0.04, zTop: 0.2, zBottom: 0.08 }, chamfer: { edge: 0.2, corner: 0.22 }, profile: { kind: 'plate', intensity: 0.82 } }); tuneShape('attach-commander-torso-ridge', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.28, zBottom: 0.18 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.86 } }); tuneShape('attach-commander-vent-l', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.62 } }); tuneShape('attach-commander-vent-r', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.62 } }); tuneShape('attach-commander-pelvis-guard', { taper: { xTop: 0.08, xBottom: 0.14, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.8 } }); tuneShape('attach-commander-sternum-crest', { taper: { xTop: 0.02, xBottom: 0.04, zTop: 0.2, zBottom: 0.12 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'plate', intensity: 0.84 } }); tuneShape('attach-commander-centerline-sensor', { chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.56 } }); tuneShape('attach-commander-abdomen-core-plate', { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.18, zBottom: 0.1 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'plate', intensity: 0.8 } }); tuneShape('attach-commander-backpack-saddle', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.72 } }); tuneMirroredShape('attach-clavicle-plate', { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.18, zBottom: 0.1 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.76 } }); tuneMirroredShape('attach-shoulder-yoke', { taper: { xTop: 0.04, xBottom: 0.04, zTop: 0.16, zBottom: 0.12 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.68 } }); tuneMirroredShape('attach-commander-pectoral-bridge', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.78 } }); tuneMirroredShape('attach-commander-backpack-clip', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.12, zBottom: 0.08 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.64 } }); tuneShape('attach-sternum-truss', { taper: { xTop: 0.12, xBottom: 0.06, zTop: 0.2, zBottom: 0.1 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.74 } }); tuneShape('attach-sternum-keel', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.24, zBottom: 0.12 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.72 } }); tuneShape('attach-sternum-sensor', { chamfer: { edge: 0.12, corner: 0.14 }, profile: { kind: 'hardSurface', intensity: 0.5 } }); tuneShape('attach-abdomen-frame', { taper: { xTop: 0.08, xBottom: 0.04, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.64 } }); tuneMirroredShape('attach-commander-shoulder-guard', { taper: { xTop: 0.04, xBottom: 0.02, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'plate', intensity: 0.8 } }); tuneMirroredShape('attach-commander-shoulder-trim', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.72 } }); tuneMirroredShape('attach-commander-bicep-guard', { taper: { xTop: 0.08, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.7 } }); tuneMirroredShape('attach-commander-thigh-guard', { taper: { xTop: 0.04, xBottom: 0.08, zTop: 0.18, zBottom: 0.14 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.84 } }); tuneMirroredShape('attach-commander-calf-guard', { taper: { xTop: 0.04, xBottom: 0.08, zTop: 0.14, zBottom: 0.1 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.74 } }); tuneMirroredShape('attach-commander-shoulder-bay', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.58 } }); tuneMirroredShape('attach-commander-shoulder-vane', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.2, zBottom: 0.12 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.68 } }); tuneMirroredShape('attach-commander-chest-cheek', { taper: { xTop: 0.08, xBottom: 0.04, zTop: 0.18, zBottom: 0.1 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.82 } }); tuneMirroredShape('attach-commander-torso-flank', { taper: { xTop: 0.04, xBottom: 0.04, zTop: 0.16, zBottom: 0.12 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.76 } }); tuneMirroredShape('attach-commander-lat-plate', { taper: { xTop: 0.04, xBottom: 0.04, zTop: 0.14, zBottom: 0.1 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'plate', intensity: 0.72 } }); tuneMirroredShape('attach-commander-flank-bulkhead', { taper: { xTop: 0.04, xBottom: 0.06, zTop: 0.12, zBottom: 0.12 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.78 } }); tuneMirroredShape('attach-commander-abdomen-side', { taper: { xTop: 0.04, xBottom: 0.06, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'plate', intensity: 0.76 } }); tuneMirroredShape('attach-commander-tricep-guard', { taper: { xTop: 0.04, xBottom: 0.08, zTop: 0.14, zBottom: 0.1 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.66 } }); tuneMirroredShape('attach-commander-bicep-plate', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.7 } }); tuneMirroredShape('attach-commander-forearm-plate', { taper: { xTop: 0.04, xBottom: 0.08, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.72 } }); tuneMirroredShape('attach-commander-elbow-plate', { taper: { xTop: 0.02, xBottom: 0.04, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.66 } }); tuneMirroredShape('attach-commander-forearm-pod', { taper: { xTop: 0.06, xBottom: 0.02, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.76 } }); tuneMirroredShape('attach-commander-forearm-pod-cover', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.16, zBottom: 0.1 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.7 } }); tuneMirroredShape('attach-commander-hand-guard', { taper: { xTop: 0.08, xBottom: 0.02, zTop: 0.18, zBottom: 0.08 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.68 } }); tuneMirroredShape('attach-commander-knee-plate', { taper: { xTop: 0.04, xBottom: 0.04, zTop: 0.16, zBottom: 0.08 }, chamfer: { edge: 0.16, corner: 0.18 }, profile: { kind: 'plate', intensity: 0.72 } }); tuneMirroredShape('attach-commander-shin-plate', { taper: { xTop: 0.04, xBottom: 0.08, zTop: 0.18, zBottom: 0.12 }, chamfer: { edge: 0.18, corner: 0.2 }, profile: { kind: 'hardSurface', intensity: 0.78 } }); tuneMirroredShape('attach-commander-shin-detail', { taper: { xTop: 0.02, xBottom: 0.02, zTop: 0.14, zBottom: 0.08 }, chamfer: { edge: 0.14, corner: 0.16 }, profile: { kind: 'plate', intensity: 0.66 } }); tuneTransform('attach-commander-chest', { offset: { x: 0, y: 0.034, z: 0.152 }, rotation: { x: 6, y: 0, z: 0 }, scale: { x: 1.06, y: 1.12, z: 1.04 } }); tuneTransform('attach-commander-cuirass-front', { offset: { x: 0, y: 0.014, z: 0.138 }, rotation: { x: 14, y: 0, z: 0 }, scale: { x: 1.12, y: 1.14, z: 1.08 } }); tuneTransform('attach-commander-cuirass-back', { offset: { x: 0, y: 0.036, z: -0.126 }, rotation: { x: -10, y: 180, z: 0 }, scale: { x: 1.06, y: 1.1, z: 1.08 } }); tuneTransform('attach-commander-torso-plate', { offset: { x: 0, y: 0.01, z: 0.126 }, rotation: { x: 12, y: 0, z: 0 }, scale: { x: 1.14, y: 1.06, z: 1.04 } }); tuneTransform('attach-commander-torso-ridge', { offset: { x: 0, y: 0.036, z: 0.184 }, rotation: { x: 14, y: 0, z: 0 }, scale: { x: 0.96, y: 1.18, z: 1.08 } }); tuneTransform('attach-commander-vent-l', { offset: { x: -0.13, y: 0.008, z: 0.12 }, rotation: { x: 12, y: -12, z: 0 } }); tuneTransform('attach-commander-vent-r', { offset: { x: 0.13, y: 0.008, z: 0.12 }, rotation: { x: 12, y: 12, z: 0 } }); tuneTransform('attach-sternum-truss', { offset: { x: 0, y: 0.018, z: 0.124 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 1.02, y: 1.12, z: 1.06 } }); tuneTransform('attach-sternum-keel', { offset: { x: 0, y: -0.004, z: 0.158 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 0.92, y: 1.12, z: 1.04 } }); tuneTransform('attach-sternum-sensor', { offset: { x: 0, y: 0.042, z: 0.17 }, scale: { x: 1.16, y: 1.16, z: 1.16 } }); tuneTransform('attach-abdomen-frame', { offset: { x: 0, y: 0.004, z: 0.102 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 1.08, y: 1.12, z: 1.04 } }); tuneMirroredTransform('attach-clavicle-plate', { offset: { x: -0.018, y: 0.02, z: 0.062 }, rotation: { x: -4, y: -84, z: 14 }, scale: { x: 1.16, y: 1.04, z: 1.12 } }); tuneMirroredTransform('attach-shoulder-yoke', { offset: { x: -0.028, y: -0.002, z: 0.05 }, rotation: { x: -6, y: -88, z: 16 }, scale: { x: 1.14, y: 1.04, z: 1.08 } }); tuneTransform('attach-backpack-pack-body', { offset: { x: 0, y: 0.062, z: -0.052 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 1.04, y: 1.08, z: 1.02 } }); tuneTransform('attach-backpack-pack-core', { offset: { x: 0, y: 0.028, z: -0.022 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 0.98, y: 1.04, z: 0.98 } }); tuneTransform('attach-backpack-pack-spine', { offset: { x: 0, y: 0.048, z: -0.04 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 0.98, y: 1.12, z: 1.02 } }); tuneTransform('attach-backpack-pack-thruster', { offset: { x: 0, y: -0.016, z: -0.09 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 0.94, y: 1.02, z: 0.94 } }); tuneMirroredTransform('attach-backpack-pack-radiator', { offset: { x: -0.058, y: 0.02, z: -0.032 }, rotation: { x: 90, y: -8, z: 0 }, scale: { x: 0.96, y: 1.06, z: 0.94 } }); tuneMirroredTransform('attach-backpack-pack-hardpoint', { offset: { x: -0.078, y: 0.018, z: -0.01 }, rotation: { x: 90, y: -10, z: 0 }, scale: { x: 0.96, y: 1.04, z: 0.94 } }); tuneMirroredTransform('attach-backpack-pack-nozzle', { offset: { x: -0.04, y: -0.028, z: -0.086 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 0.94, y: 1.0, z: 0.94 } }); tuneTransform('attach-backpack-pack-fin', { offset: { x: 0, y: 0.056, z: -0.026 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 0.96, y: 1.08, z: 0.96 } }); tuneMirroredTransform('attach-backpack-pack-fin', { offset: { x: -0.03, y: 0.03, z: -0.02 }, rotation: { x: 90, y: -10, z: 0 }, scale: { x: 0.94, y: 1.0, z: 0.94 } }); tuneTransform('attach-backpack-pack-sensor', { offset: { x: 0, y: 0.066, z: 0.002 }, scale: { x: 0.84, y: 0.9, z: 0.84 } }); tuneTransform('attach-backpack-pack-sensor-mast', { offset: { x: 0, y: 0.042, z: -0.002 }, scale: { x: 0.92, y: 1.02, z: 0.92 } }); tuneTransform('attach-backpack-pack-sensor-head', { offset: { x: 0, y: 0.026, z: 0.004 }, scale: { x: 0.94, y: 0.96, z: 0.94 } }); tuneTransform('attach-commander-backpack-saddle', { offset: { x: 0, y: 0.046, z: -0.024 }, rotation: { x: 90, y: 0, z: 0 }, scale: { x: 1.0, y: 1.08, z: 0.96 } }); tuneMirroredTransform('attach-commander-backpack-clip', { offset: { x: -0.078, y: -0.01, z: 0.01 }, rotation: { x: 90, y: -14, z: 0 }, scale: { x: 1.18, y: 1.16, z: 1.14 } }); tuneTransform('attach-commander-sternum-crest', { offset: { x: 0, y: 0.018, z: 0.174 }, rotation: { x: 24, y: 0, z: 0 }, scale: { x: 0.84, y: 1.14, z: 1.0 } }); tuneTransform('attach-commander-centerline-sensor', { offset: { x: 0, y: 0.07, z: 0.184 }, scale: { x: 1.22, y: 1.22, z: 1.22 } }); tuneTransform('attach-commander-abdomen-core-plate', { offset: { x: 0, y: -0.018, z: 0.116 }, rotation: { x: 24, y: 0, z: 0 }, scale: { x: 1.02, y: 1.08, z: 0.98 } }); tuneMirroredTransform('attach-commander-pectoral-bridge', { offset: { x: -0.096, y: 0.074, z: 0.132 }, rotation: { x: 10, y: -42, z: 16 }, scale: { x: 1.04, y: 1.1, z: 1.04 } }); tuneMirroredTransform('attach-commander-shoulder-trim', { offset: { x: -0.028, y: 0.024, z: 0.082 }, rotation: { x: -8, y: -78, z: 18 }, scale: { x: 1.14, y: 1.12, z: 1.1 } }); tuneMirroredTransform('attach-commander-shoulder-guard', { offset: { x: -0.032, y: 0.042, z: 0.116 }, rotation: { x: -10, y: -84, z: 20 }, scale: { x: 1.24, y: 1.2, z: 1.2 } }); tuneMirroredTransform('attach-commander-shoulder-bay', { offset: { x: -0.014, y: 0.005, z: 0.05 }, rotation: { x: -4, y: -90, z: 8 } }); tuneMirroredTransform('attach-commander-chest-cheek', { offset: { x: -0.128, y: 0.05, z: 0.12 }, rotation: { x: 10, y: -34, z: 12 }, scale: { x: 1.0, y: 1.06, z: 1.0 } }); tuneMirroredTransform('attach-commander-torso-flank', { offset: { x: -0.138, y: 0.002, z: 0.058 }, rotation: { x: 6, y: -88, z: 12 }, scale: { x: 1.02, y: 1.08, z: 1.02 } }); tuneMirroredTransform('attach-commander-lat-plate', { offset: { x: -0.126, y: 0.014, z: 0.05 }, rotation: { x: 8, y: -92, z: 10 }, scale: { x: 0.98, y: 1.02, z: 1.0 } }); tuneMirroredTransform('attach-commander-flank-bulkhead', { offset: { x: -0.154, y: 0.004, z: 0.026 }, rotation: { x: 4, y: -94, z: 12 }, scale: { x: 1.04, y: 1.08, z: 1.02 } }); tuneMirroredTransform('attach-commander-abdomen-side', { offset: { x: -0.118, y: -0.006, z: 0.062 }, rotation: { x: 12, y: -90, z: 10 }, scale: { x: 1.0, y: 1.06, z: 1.0 } }); tuneMirroredTransform('attach-commander-bicep-guard', { offset: { x: -0.014, y: 0.012, z: 0.012 }, rotation: { x: -8, y: -92, z: 10 }, scale: { x: 1.08, y: 1.08, z: 1.04 } }); tuneMirroredTransform('attach-commander-tricep-guard', { offset: { x: 0.01, y: -0.012, z: -0.05 }, rotation: { x: 12, y: -92, z: -8 }, scale: { x: 1.04, y: 1.02, z: 1.02 } }); tuneMirroredTransform('attach-commander-forearm-pod', { offset: { x: -0.05, y: 0.002, z: 0.09 }, rotation: { x: 2, y: -90, z: 0 }, scale: { x: 0.9, y: 0.96, z: 0.9 } }); tuneMirroredTransform('attach-commander-forearm-pod-cover', { offset: { x: -0.052, y: 0.012, z: 0.105 }, rotation: { x: 6, y: -90, z: 4 } }); tuneMirroredTransform('attach-commander-thigh-guard', { offset: { x: -0.024, y: 0.012, z: 0.058 }, rotation: { x: 10, y: -98, z: 14 }, scale: { x: 1.02, y: 1.1, z: 1.0 } }); tuneMirroredTransform('attach-commander-knee-plate', { offset: { x: -0.004, y: 0.025, z: 0.07 }, rotation: { x: 12, y: -5, z: 4 }, scale: { x: 1.08, y: 1.08, z: 1.06 } }); tuneMirroredTransform('attach-commander-calf-guard', { offset: { x: -0.018, y: 0.01, z: 0.024 }, rotation: { x: -6, y: -100, z: 16 }, scale: { x: 1.04, y: 1.12, z: 1.04 } }); tuneMirroredTransform('attach-commander-shin-plate', { offset: { x: -0.006, y: 0.016, z: 0.078 }, rotation: { x: -4, y: -4, z: 2 }, scale: { x: 1.06, y: 1.16, z: 1.06 } }); tuneMirroredTransform('attach-commander-shin-detail', { offset: { x: -0.004, y: 0.018, z: 0.096 }, rotation: { x: -4, y: -6, z: 2 }, scale: { x: 1.0, y: 1.06, z: 0.98 } }); tuneTransform('attach-helmet-helmet-shell', { offset: { x: 0, y: 0.002, z: 0.008 }, scale: { x: 1.04, y: 1.02, z: 0.98 } }); tuneTransform('attach-helmet-helmet-crown', { offset: { x: 0, y: -0.004, z: -0.016 }, scale: { x: 0.96, y: 0.94, z: 0.92 } }); tuneTransform('attach-helmet-helmet-visor', { offset: { x: 0, y: -0.008, z: 0.018 }, rotation: { x: 7, y: 0, z: 0 }, scale: { x: 1.02, y: 1.0, z: 1.02 } }); tuneTransform('attach-helmet-helmet-jaw-guard', { offset: { x: 0, y: -0.074, z: 0.018 }, rotation: { x: 14, y: 0, z: 0 }, scale: { x: 1.02, y: 1.04, z: 1.0 } }); tuneTransform('attach-helmet-helmet-occipital-guard', { offset: { x: 0, y: -0.026, z: -0.094 }, rotation: { x: -10, y: 0, z: 0 }, scale: { x: 1.0, y: 1.04, z: 1.02 } }); tuneTransform('attach-helmet-helmet-brow-plate', { offset: { x: 0, y: -0.004, z: 0.006 }, rotation: { x: 10, y: 0, z: 0 }, scale: { x: 1.0, y: 1.04, z: 1.02 } }); tuneTransform('attach-helmet-lhelmet-temple-guard', { offset: { x: -0.036, y: -0.008, z: 0.022 }, rotation: { x: 0, y: -12, z: -4 }, scale: { x: 1.02, y: 1.04, z: 1.0 } }); tuneTransform('attach-helmet-rhelmet-temple-guard', { offset: { x: 0.036, y: -0.008, z: 0.022 }, rotation: { x: 0, y: 12, z: 4 }, scale: { x: 1.02, y: 1.04, z: 1.0 } }); tuneTransform('attach-helmet-lhelmet-cheek-plate', { offset: { x: -0.016, y: -0.01, z: 0.012 }, rotation: { x: 4, y: -8, z: -4 }, scale: { x: 1.02, y: 1.02, z: 1.0 } }); tuneTransform('attach-helmet-rhelmet-cheek-plate', { offset: { x: 0.016, y: -0.01, z: 0.012 }, rotation: { x: 4, y: 8, z: 4 }, scale: { x: 1.02, y: 1.02, z: 1.0 } }); tuneTransform('attach-helmet-lhelmet-ear-pod', { offset: { x: -0.042, y: 0.004, z: -0.012 }, scale: { x: 1.0, y: 1.0, z: 0.98 } }); tuneTransform('attach-helmet-rhelmet-ear-pod', { offset: { x: 0.042, y: 0.004, z: -0.012 }, scale: { x: 1.0, y: 1.0, z: 0.98 } }); tuneTransform('attach-helmet-helmet-rear-fin', { offset: { x: 0, y: 0.012, z: -0.032 }, rotation: { x: -6, y: 0, z: 0 }, scale: { x: 1.0, y: 1.06, z: 1.0 } }); base.attachments = attachments; return base; } if (name === 'dog') { return { version: 1, nodes: [ { id: 'node-body-front', position: { x: 0.2, y: 0.25, z: 0 }, tags: [] }, { id: 'node-body-back', position: { x: -0.3, y: 0.25, z: 0 }, tags: [] }, { id: 'node-head', position: { x: 0.55, y: 0.32, z: 0 }, tags: [] }, { id: 'node-tail', position: { x: -0.6, y: 0.35, z: 0 }, tags: [] }, { id: 'node-leg-fl', position: { x: 0.15, y: -0.05, z: 0.12 }, tags: [] }, { id: 'node-leg-fr', position: { x: 0.15, y: -0.05, z: -0.12 }, tags: [] }, { id: 'node-leg-bl', position: { x: -0.35, y: -0.05, z: 0.12 }, tags: [] }, { id: 'node-leg-br', position: { x: -0.35, y: -0.05, z: -0.12 }, tags: [] } ], edges: [ { id: 'edge-body', a: 'node-body-front', b: 'node-body-back', radius: 0.09, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-head', a: 'node-body-front', b: 'node-head', radius: 0.06, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-tail', a: 'node-body-back', b: 'node-tail', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-leg-fl', a: 'node-body-front', b: 'node-leg-fl', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-leg-fr', a: 'node-body-front', b: 'node-leg-fr', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-leg-bl', a: 'node-body-back', b: 'node-leg-bl', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-leg-br', a: 'node-body-back', b: 'node-leg-br', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ], anchors: [ anchorForNode('anchor-head', 'node-head', ['head'], ['sensor']), anchorForNode('anchor-body', 'node-body-front', ['body'], ['core']) ], generators: [], attachments: [ { id: 'attach-head', anchorId: 'anchor-head', moduleId: 'detail_sensor_head', portId: 'mount' }, { id: 'attach-body', anchorId: 'anchor-body', moduleId: 'detail_backpack_block', portId: 'mount' } ] }; } if (name === 'leaper-arc') { const coreFrontNode = LEAPER_ARC_EDITOR_CORE_NODES['node-core-front']; const coreBackNode = LEAPER_ARC_EDITOR_CORE_NODES['node-core-back']; const dorsalNode = LEAPER_ARC_EDITOR_CORE_NODES['node-dorsal']; const eyeNode = LEAPER_ARC_EDITOR_CORE_NODES['node-eye']; const frontSkeleton = LEAPER_ARC_EDITOR_SKELETON.front; const rearSkeleton = LEAPER_ARC_EDITOR_SKELETON.rear; const frontShoulderX = frontSkeleton.shoulder.x; const rearShoulderX = rearSkeleton.shoulder.x; const frontShoulderY = frontSkeleton.shoulder.y; const rearShoulderY = rearSkeleton.shoulder.y; const frontShoulderZ = frontSkeleton.shoulder.z; const rearShoulderZ = rearSkeleton.shoulder.z; const frontKneeX = frontSkeleton.knee.x; const rearKneeX = rearSkeleton.knee.x; const frontKneeY = frontSkeleton.knee.y; const rearKneeY = rearSkeleton.knee.y; const frontKneeZ = frontSkeleton.knee.z; const rearKneeZ = rearSkeleton.knee.z; const frontAnkleX = frontSkeleton.ankle.x; const rearAnkleX = rearSkeleton.ankle.x; const frontAnkleY = frontSkeleton.ankle.y; const rearAnkleY = rearSkeleton.ankle.y; const frontAnkleZ = frontSkeleton.ankle.z; const rearAnkleZ = rearSkeleton.ankle.z; const frontFootX = frontSkeleton.foot.x; const rearFootX = rearSkeleton.foot.x; const frontFootY = frontSkeleton.foot.y; const rearFootY = rearSkeleton.foot.y; const frontFootZ = frontSkeleton.foot.z; const rearFootZ = rearSkeleton.foot.z; const makeLeaperHardShape = ( primitive: PartPrimitive, size: PartSize, taper: NonNullable['taper']>, options?: { chamferEdge?: number; chamferCorner?: number; profileKind?: ShapeProfileKind; profileIntensity?: number; } ): NonNullable => ({ primitive, size, taper, chamfer: { edge: options?.chamferEdge ?? 0.18, corner: options?.chamferCorner ?? 0.2 }, profile: { kind: options?.profileKind ?? 'hardSurface', intensity: options?.profileIntensity ?? 0.84 } }); const makeLeaperRoundedSculpt = ( size: PartSize, options?: { roundness?: number; bulgeRadius?: number; bulgeSmooth?: number; bulgeOffset?: Vec3Like; cutRadius?: number; cutOffset?: Vec3Like; } ): NonNullable => ({ enabled: true, primitive: 'roundedBox', size, roundness: options?.roundness ?? 0.02, bulge: { enabled: (options?.bulgeRadius ?? 0) > 0.001, radius: options?.bulgeRadius ?? 0.04, smooth: options?.bulgeSmooth ?? 0.03, offset: options?.bulgeOffset ?? { x: 0, y: 0, z: 0 } }, cut: { enabled: (options?.cutRadius ?? 0) > 0.001, radius: options?.cutRadius ?? 0.03, offset: options?.cutOffset ?? { x: 0, y: 0, z: 0 } } }); const makeLeaperArmorPadSculpt = ( size: PartSize, options?: { roundness?: number; bulgeRadius?: number; bulgeSmooth?: number; bulgeOffset?: Vec3Like; cutRadius?: number; cutOffset?: Vec3Like; } ): NonNullable => ({ enabled: true, primitive: 'armorPad', size, roundness: options?.roundness ?? 0.018, bulge: { enabled: (options?.bulgeRadius ?? 0) > 0.001, radius: options?.bulgeRadius ?? 0.032, smooth: options?.bulgeSmooth ?? 0.028, offset: options?.bulgeOffset ?? { x: 0, y: 0.006, z: 0.01 } }, cut: { enabled: (options?.cutRadius ?? 0) > 0.001, radius: options?.cutRadius ?? 0.024, offset: options?.cutOffset ?? { x: 0, y: 0, z: 0.02 } } }); const leaperSuffixForDefinition = (rowIndex: 0 | 1, sideSign: -1 | 1) => { if (rowIndex === 0) return sideSign < 0 ? 'fl' : 'fr'; return sideSign < 0 ? 'bl' : 'br'; }; const leaperAnchorIdForDefinition = ( anchorKey: 'shoulder' | 'upper' | 'knee' | 'lower' | 'foot' | 'ball', suffix: string ) => `anchor-${anchorKey}-${suffix}`; const leaperLegAttachments: AttachmentEntry[] = buildLeaperArcAuthoredLegAttachmentDefs().map((definition) => ({ id: definition.id, anchorId: leaperAnchorIdForDefinition( definition.anchorKey, leaperSuffixForDefinition(definition.rowIndex, definition.sideSign) ), moduleId: definition.moduleId, portId: 'mount', mirrored: definition.mirrored, offset: definition.offset, rotation: definition.rotationDeg ? { x: definition.rotationDeg.x, y: definition.rotationDeg.y, z: definition.rotationDeg.z } : undefined, shape: makeLeaperHardShape( definition.shape.primitive, definition.shape.size, definition.shape.taper, { chamferEdge: definition.shape.chamfer.edge, chamferCorner: definition.shape.chamfer.corner, profileKind: definition.shape.profile.kind, profileIntensity: definition.shape.profile.intensity } ), sculpt: definition.sculpt?.kind === 'armor_pad' ? makeLeaperArmorPadSculpt(definition.shape.size, { roundness: definition.sculpt.roundness, bulgeRadius: definition.sculpt.bulgeRadius, bulgeOffset: definition.sculpt.bulgeOffset, cutRadius: definition.sculpt.cutRadius, cutOffset: definition.sculpt.cutOffset }) : undefined })); const leaperLegs = [ { suffix: 'fl', sideSign: -1, front: true }, { suffix: 'fr', sideSign: 1, front: true }, { suffix: 'bl', sideSign: -1, front: false }, { suffix: 'br', sideSign: 1, front: false } ] as const; const legacyLeaperLegAttachments_unused: AttachmentEntry[] = leaperLegs.flatMap((leg) => { const legShell = leg.front ? LEAPER_ARC_SHARED_LEG_SHELL.front : LEAPER_ARC_SHARED_LEG_SHELL.rear; const mirrored = leg.sideSign < 0; const outerX = leg.sideSign * legShell.upperBlade.offset.x; const innerX = -leg.sideSign * (leg.front ? 0.008 : 0.01); const bladeYaw = -leg.sideSign * (leg.front ? 14 : 4); const clampYaw = -leg.sideSign * (leg.front ? 18 : 14); const clampRoll = leg.sideSign * (leg.front ? 24 : 18); const shinYaw = leg.sideSign * (leg.front ? 10 : 6); const shinRoll = leg.sideSign * (leg.front ? 12 : 8); const ankleYaw = leg.sideSign * (leg.front ? 12 : 8); const ankleRoll = leg.sideSign * (leg.front ? 20 : 14); const footYaw = leg.sideSign * (leg.front ? 8 : 6); const footRoll = leg.sideSign * (leg.front ? 14 : 10); const coreUpperYaw = leg.sideSign * (leg.front ? 6 : 4); const coreUpperRoll = -leg.sideSign * (leg.front ? 10 : 8); const coreLowerYaw = leg.sideSign * (leg.front ? 6 : 4); const coreLowerRoll = -leg.sideSign * (leg.front ? 8 : 6); const coreAnkleYaw = leg.sideSign * (leg.front ? 6 : 4); const coreAnkleRoll = -leg.sideSign * (leg.front ? 10 : 8); const coreFootYaw = leg.sideSign * (leg.front ? 4 : 3); const coreFootRoll = -leg.sideSign * (leg.front ? 8 : 6); const upperCoreSize = leg.front ? { x: 0.044, y: 0.586, z: 0.144 } : { x: 0.112, y: 0.35, z: 0.252 }; const lowerCoreSize = leg.front ? { x: 0.054, y: 0.648, z: 0.146 } : { x: 0.118, y: 0.404, z: 0.248 }; const shoulderCoreSize = legShell.shoulderJoint.size; const ankleSize = legShell.ankle.size; const footStemSize = legShell.footStem.size; return [ { id: `attach-shoulder-joint-${leg.suffix}`, anchorId: `anchor-shoulder-${leg.suffix}`, moduleId: 'core_shoulder_yoke', portId: 'mount', mirrored, shape: makeLeaperHardShape( 'wedge', shoulderCoreSize, leg.front ? { xTop: 0.18, xBottom: 0.04, zTop: 0.22, zBottom: 0.08 } : { xTop: 0.12, xBottom: 0.04, zTop: 0.18, zBottom: 0.08 }, { profileKind: 'mechLimb', profileIntensity: 0.94, chamferEdge: 0.22, chamferCorner: 0.24 } ) }, { id: `attach-shoulder-clamp-${leg.suffix}`, anchorId: `anchor-shoulder-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.012, y: 0.004, z: leg.front ? -0.008 : -0.014 }, rotation: { x: -12, y: clampYaw, z: clampRoll }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.038, y: 0.034, z: 0.066 } : { x: 0.052, y: 0.044, z: 0.084 }, leg.front ? { xTop: 0.12, xBottom: 0.04, zTop: 0.12, zBottom: 0.02 } : { xTop: 0.14, xBottom: 0.06, zTop: 0.14, zBottom: 0.04 }, { profileIntensity: 0.82, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: `attach-shoulder-cowl-${leg.suffix}`, anchorId: `anchor-shoulder-${leg.suffix}`, moduleId: 'detail_shoulder_guard', portId: 'mount', offset: { x: leg.sideSign * legShell.shoulderCowl.offset.x, y: legShell.shoulderCowl.offset.y, z: legShell.shoulderCowl.offset.z }, rotation: { x: leg.front ? -10 : -8, y: clampYaw, z: clampRoll + (leg.front ? 0 : -2) }, shape: makeLeaperHardShape( 'wedge', legShell.shoulderCowl.size, leg.front ? { xTop: 0.14, xBottom: 0.04, zTop: 0.2, zBottom: 0.06 } : { xTop: 0.14, xBottom: 0.05, zTop: 0.2, zBottom: 0.08 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.2, chamferCorner: 0.22 } ), sculpt: makeLeaperArmorPadSculpt( legShell.shoulderCowl.size, leg.front ? { bulgeRadius: 0.034, bulgeOffset: { x: 0, y: 0.01, z: 0.016 }, cutRadius: 0.024, cutOffset: { x: 0, y: 0.004, z: 0.032 } } : { bulgeRadius: 0.05, bulgeOffset: { x: 0, y: 0.014, z: 0.022 }, cutRadius: 0.034, cutOffset: { x: 0, y: 0.004, z: 0.038 } } ) }, { id: `attach-shoulder-spur-${leg.suffix}`, anchorId: `anchor-shoulder-${leg.suffix}`, moduleId: 'detail_shoulder_guard', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.024 : 0.036), y: leg.front ? 0.024 : 0.03, z: leg.front ? 0.014 : -0.02 }, rotation: { x: leg.front ? -6 : 6, y: clampYaw + leg.sideSign * (leg.front ? -10 : -6), z: clampRoll + leg.sideSign * (leg.front ? 8 : 4) }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.044, y: 0.066, z: 0.126 } : { x: 0.088, y: 0.11, z: 0.196 }, leg.front ? { xTop: 0.04, xBottom: 0.16, zTop: 0.22, zBottom: 0.05 } : { xTop: 0.04, xBottom: 0.16, zTop: 0.22, zBottom: 0.06 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: `attach-upper-leg-${leg.suffix}`, anchorId: `anchor-upper-${leg.suffix}`, moduleId: 'core_upper_leg', portId: 'mount', mirrored, offset: { x: leg.sideSign * (leg.front ? 0.002 : 0.008), y: leg.front ? 0.004 : 0.006, z: leg.front ? 0.016 : -0.01 }, rotation: { x: leg.front ? -10 : -2, y: 180 + coreUpperYaw * 0.6, z: coreUpperRoll * 0.55 }, shape: makeLeaperHardShape( 'box', upperCoreSize, leg.front ? { xTop: 0.08, xBottom: 0.16, zTop: 0.16, zBottom: 0.05 } : { xTop: 0.08, xBottom: 0.14, zTop: 0.14, zBottom: 0.05 }, { profileKind: 'mechLimb', profileIntensity: 0.98, chamferEdge: 0.22, chamferCorner: 0.24 } ), }, { id: `attach-upper-blade-${leg.suffix}`, anchorId: `anchor-upper-${leg.suffix}`, moduleId: 'detail_thigh_plate', portId: 'mount', offset: { x: outerX, y: legShell.upperBlade.offset.y, z: legShell.upperBlade.offset.z }, rotation: { x: leg.front ? -4 : 2, y: bladeYaw * 0.42, z: 0 }, shape: makeLeaperHardShape( 'box', legShell.upperBlade.size, leg.front ? { xTop: 0.06, xBottom: 0.12, zTop: 0.1, zBottom: 0.04 } : { xTop: 0.06, xBottom: 0.1, zTop: 0.1, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.9, chamferEdge: 0.18, chamferCorner: 0.2 } ), sculpt: makeLeaperArmorPadSculpt( legShell.upperBlade.size, leg.front ? { bulgeRadius: 0.028, bulgeOffset: { x: 0, y: 0.026, z: 0.008 }, cutRadius: 0.018, cutOffset: { x: 0, y: 0.094, z: 0.014 } } : { bulgeRadius: 0.032, bulgeOffset: { x: 0, y: 0.02, z: 0.012 }, cutRadius: 0.02, cutOffset: { x: 0, y: 0.05, z: 0.02 } } ) }, ...(leg.front ? [ { id: `attach-upper-scythe-${leg.suffix}`, anchorId: `anchor-upper-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.024, y: 0.006, z: 0.018 }, rotation: { x: -8, y: bladeYaw * 0.4 + leg.sideSign * -2, z: leg.sideSign * 8 }, shape: makeLeaperHardShape( 'box', { x: 0.014, y: 0.338, z: 0.066 }, { xTop: 0.04, xBottom: 0.08, zTop: 0.06, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.16, chamferCorner: 0.18 } ), sculpt: makeLeaperArmorPadSculpt( { x: 0.014, y: 0.338, z: 0.066 }, { bulgeRadius: 0.024, bulgeOffset: { x: 0, y: 0.026, z: 0.008 }, cutRadius: 0.016, cutOffset: { x: 0, y: 0.096, z: 0.014 } } ) } ] : [ { id: `attach-haunch-block-${leg.suffix}`, anchorId: `anchor-upper-${leg.suffix}`, moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: leg.sideSign * 0.01, y: 0.03, z: -0.018 }, rotation: { x: 8, y: leg.sideSign * 8, z: 0 }, shape: makeLeaperHardShape( 'box', { x: 0.104, y: 0.224, z: 0.242 }, { xTop: 0.14, xBottom: 0.04, zTop: 0.16, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.82, chamferEdge: 0.2, chamferCorner: 0.22 } ), sculpt: makeLeaperArmorPadSculpt( { x: 0.104, y: 0.224, z: 0.242 }, { bulgeRadius: 0.04, bulgeOffset: { x: 0, y: 0.018, z: 0.016 }, cutRadius: 0.028, cutOffset: { x: 0, y: 0.048, z: 0.026 } } ) } ]), { id: `attach-upper-strut-${leg.suffix}`, anchorId: `anchor-upper-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: innerX, y: -0.006, z: leg.front ? -0.004 : -0.008 }, rotation: { x: 0, y: -bladeYaw, z: 0 }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.01, y: 0.298, z: 0.02 } : { x: 0.022, y: 0.216, z: 0.04 }, { xTop: 0.08, xBottom: 0.12, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'hardSurface', profileIntensity: 0.76, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: `attach-upper-carapace-${leg.suffix}`, anchorId: `anchor-upper-${leg.suffix}`, moduleId: 'detail_thigh_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.004 : 0.008), y: leg.front ? 0.022 : 0.016, z: leg.front ? 0.004 : -0.004 }, rotation: { x: leg.front ? -4 : -2, y: bladeYaw * -0.18, z: leg.sideSign * (leg.front ? 3 : 2) }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.032, y: 0.292, z: 0.09 } : { x: 0.05, y: 0.21, z: 0.14 }, leg.front ? { xTop: 0.04, xBottom: 0.1, zTop: 0.08, zBottom: 0.03 } : { xTop: 0.04, xBottom: 0.1, zTop: 0.08, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.16, chamferCorner: 0.18 } ), sculpt: makeLeaperArmorPadSculpt( leg.front ? { x: 0.032, y: 0.292, z: 0.09 } : { x: 0.05, y: 0.21, z: 0.14 }, leg.front ? { bulgeRadius: 0.026, bulgeOffset: { x: 0, y: 0.022, z: 0.01 }, cutRadius: 0.018, cutOffset: { x: 0, y: 0.08, z: 0.016 } } : { bulgeRadius: 0.03, bulgeOffset: { x: 0, y: 0.02, z: 0.014 }, cutRadius: 0.02, cutOffset: { x: 0, y: 0.05, z: 0.02 } } ) }, { id: `attach-knee-joint-${leg.suffix}`, anchorId: `anchor-knee-${leg.suffix}`, moduleId: 'detail_knee_plate', portId: 'mount', shape: makeLeaperHardShape( 'box', legShell.kneeJoint.size, leg.front ? { xTop: 0.1, xBottom: 0.06, zTop: 0.16, zBottom: 0.06 } : { xTop: 0.08, xBottom: 0.04, zTop: 0.14, zBottom: 0.04 }, { profileKind: 'mechLimb', profileIntensity: 0.9, chamferEdge: 0.22, chamferCorner: 0.24 } ) }, { id: `attach-knee-crest-${leg.suffix}`, anchorId: `anchor-knee-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.008, y: 0.012, z: leg.front ? 0.004 : -0.002 }, rotation: { x: leg.front ? -14 : -8, y: -leg.sideSign * 8, z: leg.sideSign * 12 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.028, y: 0.058, z: 0.072 } : { x: 0.044, y: 0.074, z: 0.09 }, leg.front ? { xTop: 0.04, xBottom: 0.12, zTop: 0.18, zBottom: 0.04 } : { xTop: 0.04, xBottom: 0.1, zTop: 0.16, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: `attach-knee-sideplate-${leg.suffix}`, anchorId: `anchor-knee-${leg.suffix}`, moduleId: 'detail_knee_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.016 : 0.022), y: 0.002, z: leg.front ? 0.006 : -0.002 }, rotation: { x: leg.front ? -10 : -6, y: leg.sideSign * (leg.front ? -8 : -6), z: leg.sideSign * 8 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.024, y: 0.066, z: 0.068 } : { x: 0.036, y: 0.09, z: 0.102 }, leg.front ? { xTop: 0.04, xBottom: 0.14, zTop: 0.2, zBottom: 0.04 } : { xTop: 0.04, xBottom: 0.12, zTop: 0.18, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.86, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: `attach-lower-leg-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'core_lower_leg', portId: 'mount', mirrored, offset: { x: leg.sideSign * (leg.front ? 0.004 : 0.01), y: leg.front ? -0.014 : -0.012, z: leg.front ? 0.024 : -0.016 }, rotation: { x: leg.front ? -10 : -2, y: 180 + coreLowerYaw * 0.55, z: coreLowerRoll * 0.5 }, shape: makeLeaperHardShape( 'box', lowerCoreSize, leg.front ? { xTop: 0.08, xBottom: 0.18, zTop: 0.16, zBottom: 0.04 } : { xTop: 0.08, xBottom: 0.16, zTop: 0.14, zBottom: 0.04 }, { profileKind: 'mechLimb', profileIntensity: 0.98, chamferEdge: 0.22, chamferCorner: 0.24 } ), }, { id: `attach-lower-shell-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'detail_calf_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.012 : 0.018), y: leg.front ? -0.008 : -0.006, z: leg.front ? 0.016 : -0.004 }, rotation: { x: leg.front ? -6 : -4, y: 180 + shinYaw * 0.28 + leg.sideSign * -3, z: shinRoll * 0.35 + leg.sideSign * 3 }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.024, y: 0.234, z: 0.072 } : { x: 0.046, y: 0.17, z: 0.118 }, leg.front ? { xTop: 0.04, xBottom: 0.08, zTop: 0.1, zBottom: 0.03 } : { xTop: 0.04, xBottom: 0.08, zTop: 0.08, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.16, chamferCorner: 0.18 } ), sculpt: makeLeaperArmorPadSculpt( leg.front ? { x: 0.024, y: 0.234, z: 0.072 } : { x: 0.046, y: 0.17, z: 0.118 }, leg.front ? { bulgeRadius: 0.024, bulgeOffset: { x: 0, y: 0.014, z: 0.012 }, cutRadius: 0.016, cutOffset: { x: 0, y: 0.048, z: 0.02 } } : { bulgeRadius: 0.03, bulgeOffset: { x: 0, y: 0.01, z: 0.014 }, cutRadius: 0.02, cutOffset: { x: 0, y: 0.04, z: 0.022 } } ) }, { id: `attach-shin-face-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'detail_calf_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.014 : 0.018), y: leg.front ? -0.004 : -0.006, z: leg.front ? 0.018 : 0.006 }, rotation: { x: leg.front ? -4 : -3, y: 180 + shinYaw * 0.35 + leg.sideSign * -2, z: shinRoll * 0.45 + leg.sideSign * 2 }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.028, y: 0.312, z: 0.086 } : { x: 0.044, y: 0.228, z: 0.132 }, leg.front ? { xTop: 0.05, xBottom: 0.1, zTop: 0.08, zBottom: 0.03 } : { xTop: 0.05, xBottom: 0.1, zTop: 0.08, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.9, chamferEdge: 0.16, chamferCorner: 0.18 } ), sculpt: makeLeaperArmorPadSculpt( leg.front ? { x: 0.028, y: 0.312, z: 0.086 } : { x: 0.044, y: 0.228, z: 0.132 }, leg.front ? { bulgeRadius: 0.026, bulgeOffset: { x: 0, y: 0.022, z: 0.01 }, cutRadius: 0.018, cutOffset: { x: 0, y: 0.08, z: 0.016 } } : { bulgeRadius: 0.028, bulgeOffset: { x: 0, y: 0.016, z: 0.014 }, cutRadius: 0.02, cutOffset: { x: 0, y: 0.05, z: 0.022 } } ) }, { id: `attach-shin-keel-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.003, y: leg.front ? -0.016 : -0.018, z: leg.front ? 0.022 : 0.008 }, rotation: { x: leg.front ? 8 : 4, y: 180 + shinYaw * 0.25, z: leg.sideSign * 4 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.018, y: 0.244, z: 0.062 } : { x: 0.026, y: 0.182, z: 0.094 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.2, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.84, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: `attach-lower-blade-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'detail_calf_plate', portId: 'mount', offset: { x: outerX, y: 0.0, z: leg.front ? 0.01 : -0.008 }, rotation: { x: leg.front ? -2 : 2, y: bladeYaw * -0.28, z: 0 }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.022, y: 0.31, z: 0.052 } : { x: 0.06, y: 0.184, z: 0.124 }, leg.front ? { xTop: 0.05, xBottom: 0.08, zTop: 0.06, zBottom: 0.03 } : { xTop: 0.05, xBottom: 0.1, zTop: 0.08, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ), sculpt: makeLeaperArmorPadSculpt( leg.front ? { x: 0.022, y: 0.31, z: 0.052 } : { x: 0.06, y: 0.184, z: 0.124 }, leg.front ? { bulgeRadius: 0.03, bulgeOffset: { x: 0, y: 0.02, z: 0.006 }, cutRadius: 0.018, cutOffset: { x: 0, y: 0.08, z: 0.014 } } : { bulgeRadius: 0.036, bulgeOffset: { x: 0, y: 0.02, z: 0.01 }, cutRadius: 0.024, cutOffset: { x: 0, y: 0.04, z: 0.018 } } ) }, ...(leg.front ? [ { id: `attach-lower-scythe-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.024, y: -0.012, z: 0.016 }, rotation: { x: -6, y: bladeYaw * -0.34, z: leg.sideSign * 8 }, shape: makeLeaperHardShape( 'box', { x: 0.012, y: 0.35, z: 0.058 }, { xTop: 0.04, xBottom: 0.08, zTop: 0.06, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.86, chamferEdge: 0.16, chamferCorner: 0.18 } ), sculpt: makeLeaperArmorPadSculpt( { x: 0.012, y: 0.35, z: 0.058 }, { bulgeRadius: 0.024, bulgeOffset: { x: 0, y: 0.024, z: 0.008 }, cutRadius: 0.016, cutOffset: { x: 0, y: 0.09, z: 0.014 } } ) } ] : [ { id: `attach-shin-block-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: leg.sideSign * 0.014, y: -0.018, z: -0.026 }, rotation: { x: 4, y: leg.sideSign * 6, z: 0 }, shape: makeLeaperHardShape( 'box', { x: 0.09, y: 0.232, z: 0.214 }, { xTop: 0.12, xBottom: 0.04, zTop: 0.16, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.82, chamferEdge: 0.18, chamferCorner: 0.2 } ), sculpt: makeLeaperArmorPadSculpt( { x: 0.09, y: 0.232, z: 0.214 }, { bulgeRadius: 0.034, bulgeOffset: { x: 0, y: 0.018, z: 0.014 }, cutRadius: 0.024, cutOffset: { x: 0, y: 0.048, z: 0.024 } } ) } ]), { id: `attach-lower-strut-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: innerX, y: -0.004, z: leg.front ? -0.008 : -0.012 }, rotation: { x: 0, y: bladeYaw * 0.4, z: 0 }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.01, y: 0.304, z: 0.018 } : { x: 0.022, y: 0.214, z: 0.036 }, { xTop: 0.08, xBottom: 0.12, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'hardSurface', profileIntensity: 0.76, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: `attach-lower-guard-${leg.suffix}`, anchorId: `anchor-lower-${leg.suffix}`, moduleId: 'detail_calf_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.006 : 0.008), y: leg.front ? -0.028 : -0.02, z: leg.front ? 0.008 : -0.008 }, rotation: { x: leg.front ? 4 : 3, y: bladeYaw * -0.14, z: leg.sideSign * (leg.front ? -4 : -3) }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.03, y: 0.248, z: 0.078 } : { x: 0.046, y: 0.184, z: 0.128 }, leg.front ? { xTop: 0.05, xBottom: 0.1, zTop: 0.08, zBottom: 0.03 } : { xTop: 0.05, xBottom: 0.1, zTop: 0.08, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.86, chamferEdge: 0.16, chamferCorner: 0.18 } ), sculpt: makeLeaperArmorPadSculpt( leg.front ? { x: 0.03, y: 0.248, z: 0.078 } : { x: 0.046, y: 0.184, z: 0.128 }, leg.front ? { bulgeRadius: 0.024, bulgeOffset: { x: 0, y: 0.018, z: 0.008 }, cutRadius: 0.016, cutOffset: { x: 0, y: 0.06, z: 0.014 } } : { bulgeRadius: 0.028, bulgeOffset: { x: 0, y: 0.014, z: 0.014 }, cutRadius: 0.02, cutOffset: { x: 0, y: 0.04, z: 0.02 } } ) }, { id: `attach-ankle-${leg.suffix}`, anchorId: `anchor-foot-${leg.suffix}`, moduleId: 'core_ankle_joint', portId: 'mount', mirrored, offset: { x: leg.sideSign * legShell.ankle.offset.x, y: legShell.ankle.offset.y, z: legShell.ankle.offset.z }, rotation: { x: leg.front ? 10 : 6, y: 180 + coreAnkleYaw, z: coreAnkleRoll }, shape: makeLeaperHardShape( 'box', ankleSize, leg.front ? { xTop: 0.1, xBottom: 0.2, zTop: 0.18, zBottom: 0.08 } : { xTop: 0.08, xBottom: 0.16, zTop: 0.16, zBottom: 0.08 }, { profileKind: 'hardSurface', profileIntensity: 0.94, chamferEdge: 0.22, chamferCorner: 0.24 } ), }, { id: `attach-ankle-spur-${leg.suffix}`, anchorId: `anchor-foot-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.016 : 0.02), y: leg.front ? 0.01 : 0.014, z: leg.front ? -0.044 : -0.056 }, rotation: { x: leg.front ? 18 : 14, y: 180 + leg.sideSign * (leg.front ? -12 : -10), z: leg.sideSign * 12 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.02, y: 0.04, z: 0.078 } : { x: 0.03, y: 0.056, z: 0.104 }, { xTop: 0.04, xBottom: 0.12, zTop: 0.22, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: `attach-ankle-collar-${leg.suffix}`, anchorId: `anchor-foot-${leg.suffix}`, moduleId: 'detail_calf_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.004 : 0.008), y: leg.front ? 0.022 : 0.024, z: leg.front ? -0.048 : -0.06 }, rotation: { x: leg.front ? 22 : 16, y: 180 + ankleYaw * 0.7, z: leg.sideSign * (leg.front ? 10 : 8) }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.038, y: 0.036, z: 0.088 } : { x: 0.056, y: 0.048, z: 0.114 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.18, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.86, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: `attach-foot-stem-${leg.suffix}`, anchorId: `anchor-foot-${leg.suffix}`, moduleId: 'core_foot_block', portId: 'mount', mirrored, offset: { x: leg.sideSign * legShell.footStem.offset.x, y: legShell.footStem.offset.y, z: legShell.footStem.offset.z }, rotation: { x: leg.front ? -8 : -4, y: 180 + coreFootYaw * 0.5, z: coreFootRoll * 0.45 }, shape: makeLeaperHardShape( 'box', footStemSize, leg.front ? { xTop: 0.08, xBottom: 0.16, zTop: 0.16, zBottom: 0.04 } : { xTop: 0.08, xBottom: 0.14, zTop: 0.14, zBottom: 0.04 }, { profileKind: 'hardSurface', profileIntensity: 0.94, chamferEdge: 0.2, chamferCorner: 0.22 } ), }, { id: `attach-foot-cowl-${leg.suffix}`, anchorId: `anchor-foot-${leg.suffix}`, moduleId: 'detail_calf_plate', portId: 'mount', offset: { x: leg.sideSign * legShell.footCowl.offset.x, y: legShell.footCowl.offset.y, z: legShell.footCowl.offset.z }, rotation: { x: leg.front ? -8 : -4, y: 180 + footYaw * 0.35 + leg.sideSign * -2, z: footRoll * 0.4 + leg.sideSign * 2 }, shape: makeLeaperHardShape( 'box', legShell.footCowl.size, leg.front ? { xTop: 0.04, xBottom: 0.08, zTop: 0.06, zBottom: 0.03 } : { xTop: 0.05, xBottom: 0.1, zTop: 0.08, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.16, chamferCorner: 0.18 } ), sculpt: makeLeaperArmorPadSculpt( legShell.footCowl.size, leg.front ? { bulgeRadius: 0.02, bulgeOffset: { x: 0, y: 0.002, z: 0.016 }, cutRadius: 0.016, cutOffset: { x: 0, y: 0.002, z: 0.026 } } : { bulgeRadius: 0.026, bulgeOffset: { x: 0, y: 0.004, z: 0.02 }, cutRadius: 0.018, cutOffset: { x: 0, y: 0.002, z: 0.03 } } ) }, { id: `attach-foot-keel-${leg.suffix}`, anchorId: `anchor-foot-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.002, y: leg.front ? -0.01 : -0.012, z: leg.front ? 0.038 : 0.022 }, rotation: { x: leg.front ? 20 : 10, y: 180 + footYaw * 0.6, z: leg.sideSign * 4 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.012, y: 0.014, z: 0.132 } : { x: 0.024, y: 0.024, z: 0.134 }, leg.front ? { xTop: 0.03, xBottom: 0.12, zTop: 0.28, zBottom: 0.04 } : { xTop: 0.04, xBottom: 0.16, zTop: 0.22, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.84, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: `attach-foot-clamp-${leg.suffix}`, anchorId: `anchor-foot-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.006, y: 0.002, z: -0.01 }, rotation: { x: leg.front ? -10 : -8, y: -leg.sideSign * 8, z: 0 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.018, y: 0.016, z: 0.042 } : { x: 0.026, y: 0.02, z: 0.056 }, { xTop: 0.04, xBottom: 0.12, zTop: 0.18, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.82, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: `attach-foot-heel-${leg.suffix}`, anchorId: `anchor-foot-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.002, y: leg.front ? -0.003 : -0.005, z: leg.front ? -0.034 : -0.046 }, rotation: { x: leg.front ? 18 : 12, y: leg.sideSign * (leg.front ? -6 : -5), z: leg.sideSign * 4 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.012, y: 0.01, z: 0.038 } : { x: 0.022, y: 0.018, z: 0.066 }, leg.front ? { xTop: 0.03, xBottom: 0.08, zTop: 0.18, zBottom: 0.04 } : { xTop: 0.04, xBottom: 0.12, zTop: 0.24, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.8, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: `attach-ball-foot-${leg.suffix}`, anchorId: `anchor-ball-${leg.suffix}`, moduleId: 'core_foot_block', portId: 'mount', offset: { x: 0, y: 0.0, z: leg.front ? 0.012 : 0.01 }, rotation: { x: 0, y: 180, z: 0 }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.02, y: 0.014, z: 0.046 } : { x: 0.044, y: 0.028, z: 0.078 }, leg.front ? { xTop: 0.02, xBottom: 0.08, zTop: 0.12, zBottom: 0.03 } : { xTop: 0.06, xBottom: 0.14, zTop: 0.18, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: leg.front ? 0.76 : 0.78, chamferEdge: 0.2, chamferCorner: 0.18 } ), }, { id: `attach-ball-clamp-${leg.suffix}`, anchorId: `anchor-ball-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * 0.004, y: 0.003, z: -0.004 }, rotation: { x: -14, y: -leg.sideSign * 8, z: 0 }, shape: makeLeaperHardShape( 'box', leg.front ? { x: 0.012, y: 0.014, z: 0.026 } : { x: 0.016, y: 0.016, z: 0.034 }, { xTop: 0.06, xBottom: 0.1, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'hardSurface', profileIntensity: 0.72, chamferEdge: 0.12, chamferCorner: 0.14 } ) }, { id: `attach-ball-fork-${leg.suffix}`, anchorId: `anchor-ball-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.008 : 0.004), y: leg.front ? 0.001 : 0.002, z: leg.front ? 0.0 : -0.004 }, rotation: { x: leg.front ? -12 : -6, y: leg.sideSign * (leg.front ? 8 : 4), z: 0 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.01, y: 0.01, z: 0.04 } : { x: 0.018, y: 0.016, z: 0.038 }, leg.front ? { xTop: 0.02, xBottom: 0.06, zTop: 0.14, zBottom: 0.02 } : { xTop: 0.04, xBottom: 0.08, zTop: 0.06, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: leg.front ? 0.82 : 0.78, chamferEdge: 0.12, chamferCorner: 0.14 } ), sculpt: leg.front ? makeLeaperArmorPadSculpt( { x: 0.01, y: 0.01, z: 0.04 }, { bulgeRadius: 0.012, bulgeOffset: { x: 0, y: 0.001, z: 0.01 }, cutRadius: 0.008, cutOffset: { x: 0, y: 0.001, z: 0.014 } } ) : undefined }, { id: `attach-toe-blade-${leg.suffix}`, anchorId: `anchor-ball-${leg.suffix}`, moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: leg.sideSign * (leg.front ? 0.012 : 0.004), y: leg.front ? -0.003 : -0.004, z: leg.front ? 0.052 : 0.046 }, rotation: { x: leg.front ? -26 : -14, y: leg.sideSign * (leg.front ? 12 : 4), z: 0 }, shape: makeLeaperHardShape( 'wedge', leg.front ? { x: 0.01, y: 0.01, z: 0.094 } : { x: 0.018, y: 0.016, z: 0.082 }, leg.front ? { xTop: 0.02, xBottom: 0.06, zTop: 0.16, zBottom: 0.02 } : { xTop: 0.04, xBottom: 0.08, zTop: 0.08, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: leg.front ? 0.88 : 0.84, chamferEdge: 0.12, chamferCorner: 0.14 } ), sculpt: leg.front ? makeLeaperArmorPadSculpt( { x: 0.01, y: 0.01, z: 0.094 }, { bulgeRadius: 0.016, bulgeOffset: { x: 0, y: 0, z: 0.02 }, cutRadius: 0.01, cutOffset: { x: 0, y: 0.002, z: 0.032 } } ) : undefined } ]; }); return { version: 1, nodes: [ { id: 'node-core-front', position: { x: coreFrontNode.x, y: coreFrontNode.y, z: coreFrontNode.z }, tags: ['core', 'front'] }, { id: 'node-core-back', position: { x: coreBackNode.x, y: coreBackNode.y, z: coreBackNode.z }, tags: ['core', 'rear'] }, { id: 'node-dorsal', position: { x: dorsalNode.x, y: dorsalNode.y, z: dorsalNode.z }, tags: ['core', 'dorsal'] }, { id: 'node-eye', position: { x: eyeNode.x, y: eyeNode.y, z: eyeNode.z }, tags: ['sensor', 'ventral'] }, { id: 'node-shoulder-fl', position: { x: -frontShoulderX, y: frontShoulderY, z: frontShoulderZ }, tags: ['leg', 'front', 'left', 'shoulder'] }, { id: 'node-shoulder-fr', position: { x: frontShoulderX, y: frontShoulderY, z: frontShoulderZ }, tags: ['leg', 'front', 'right', 'shoulder'] }, { id: 'node-shoulder-bl', position: { x: -rearShoulderX, y: rearShoulderY, z: rearShoulderZ }, tags: ['leg', 'rear', 'left', 'shoulder'] }, { id: 'node-shoulder-br', position: { x: rearShoulderX, y: rearShoulderY, z: rearShoulderZ }, tags: ['leg', 'rear', 'right', 'shoulder'] }, { id: 'node-knee-fl', position: { x: -frontKneeX, y: frontKneeY, z: frontKneeZ }, tags: ['leg', 'front', 'left', 'knee'] }, { id: 'node-knee-fr', position: { x: frontKneeX, y: frontKneeY, z: frontKneeZ }, tags: ['leg', 'front', 'right', 'knee'] }, { id: 'node-knee-bl', position: { x: -rearKneeX, y: rearKneeY, z: rearKneeZ }, tags: ['leg', 'rear', 'left', 'knee'] }, { id: 'node-knee-br', position: { x: rearKneeX, y: rearKneeY, z: rearKneeZ }, tags: ['leg', 'rear', 'right', 'knee'] }, { id: 'node-ankle-fl', position: { x: -frontAnkleX, y: frontAnkleY, z: frontAnkleZ }, tags: ['leg', 'front', 'left', 'ankle'] }, { id: 'node-ankle-fr', position: { x: frontAnkleX, y: frontAnkleY, z: frontAnkleZ }, tags: ['leg', 'front', 'right', 'ankle'] }, { id: 'node-ankle-bl', position: { x: -rearAnkleX, y: rearAnkleY, z: rearAnkleZ }, tags: ['leg', 'rear', 'left', 'ankle'] }, { id: 'node-ankle-br', position: { x: rearAnkleX, y: rearAnkleY, z: rearAnkleZ }, tags: ['leg', 'rear', 'right', 'ankle'] }, { id: 'node-foot-fl', position: { x: -frontFootX, y: frontFootY, z: frontFootZ }, tags: ['leg', 'front', 'left', 'foot'] }, { id: 'node-foot-fr', position: { x: frontFootX, y: frontFootY, z: frontFootZ }, tags: ['leg', 'front', 'right', 'foot'] }, { id: 'node-foot-bl', position: { x: -rearFootX, y: rearFootY, z: rearFootZ }, tags: ['leg', 'rear', 'left', 'foot'] }, { id: 'node-foot-br', position: { x: rearFootX, y: rearFootY, z: rearFootZ }, tags: ['leg', 'rear', 'right', 'foot'] } ], edges: [ { id: 'edge-spine', a: 'node-core-front', b: 'node-core-back', radius: 0.11, curve: 'catmull', controlOffset: { x: 0, y: 0.04, z: -0.05 } }, { id: 'edge-dorsal-front', a: 'node-dorsal', b: 'node-core-front', radius: 0.062, curve: 'catmull', controlOffset: { x: 0, y: -0.04, z: 0.042 } }, { id: 'edge-dorsal-back', a: 'node-dorsal', b: 'node-core-back', radius: 0.062, curve: 'catmull', controlOffset: { x: 0, y: -0.058, z: -0.056 } }, { id: 'edge-keel', a: 'node-core-front', b: 'node-eye', radius: 0.07, curve: 'catmull', controlOffset: { x: 0, y: -0.11, z: 0.03 } }, { id: 'edge-shoulder-fl', a: 'node-core-front', b: 'node-shoulder-fl', radius: 0.044, curve: 'catmull', controlOffset: { x: 0.014, y: -0.01, z: -0.026 } }, { id: 'edge-shoulder-fr', a: 'node-core-front', b: 'node-shoulder-fr', radius: 0.044, curve: 'catmull', controlOffset: { x: -0.014, y: -0.01, z: -0.026 } }, { id: 'edge-shoulder-bl', a: 'node-core-back', b: 'node-shoulder-bl', radius: 0.048, curve: 'catmull', controlOffset: { x: 0.016, y: -0.012, z: 0.028 } }, { id: 'edge-shoulder-br', a: 'node-core-back', b: 'node-shoulder-br', radius: 0.048, curve: 'catmull', controlOffset: { x: -0.016, y: -0.012, z: 0.028 } }, { id: 'edge-upper-fl', a: 'node-shoulder-fl', b: 'node-knee-fl', radius: 0.034, curve: 'catmull', controlOffset: { x: 0.046, y: 0.034, z: 0.094 } }, { id: 'edge-upper-fr', a: 'node-shoulder-fr', b: 'node-knee-fr', radius: 0.034, curve: 'catmull', controlOffset: { x: -0.046, y: 0.034, z: 0.094 } }, { id: 'edge-upper-bl', a: 'node-shoulder-bl', b: 'node-knee-bl', radius: 0.05, curve: 'catmull', controlOffset: { x: 0.026, y: 0.02, z: -0.052 } }, { id: 'edge-upper-br', a: 'node-shoulder-br', b: 'node-knee-br', radius: 0.05, curve: 'catmull', controlOffset: { x: -0.026, y: 0.02, z: -0.052 } }, { id: 'edge-lower-fl', a: 'node-knee-fl', b: 'node-ankle-fl', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-lower-fr', a: 'node-knee-fr', b: 'node-ankle-fr', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-lower-bl', a: 'node-knee-bl', b: 'node-ankle-bl', radius: 0.042, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-lower-br', a: 'node-knee-br', b: 'node-ankle-br', radius: 0.042, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-foot-fl', a: 'node-ankle-fl', b: 'node-foot-fl', radius: 0.022, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-foot-fr', a: 'node-ankle-fr', b: 'node-foot-fr', radius: 0.022, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-foot-bl', a: 'node-ankle-bl', b: 'node-foot-bl', radius: 0.026, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-foot-br', a: 'node-ankle-br', b: 'node-foot-br', radius: 0.026, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ], anchors: [ anchorForNode( 'anchor-core-front', 'node-core-front', ['core', 'front'], ['core', 'armor_plate', 'detail'], { proximalNodeId: 'node-core-back', distalNodeId: 'node-core-front', upNodeId: 'node-dorsal' } ), anchorForNode( 'anchor-core-back', 'node-core-back', ['core', 'rear'], ['core', 'armor_plate', 'detail'], { proximalNodeId: 'node-core-back', distalNodeId: 'node-core-front', upNodeId: 'node-dorsal' } ), anchorForNode( 'anchor-dorsal', 'node-dorsal', ['core', 'dorsal'], ['armor_plate', 'detail'], { proximalNodeId: 'node-core-back', distalNodeId: 'node-core-front' } ), anchorForNode( 'anchor-eye', 'node-eye', ['sensor', 'eye'], ['sensor', 'detail'], { proximalNodeId: 'node-core-back', distalNodeId: 'node-core-front', upNodeId: 'node-dorsal' } ), anchorForEdge('anchor-spine', 'edge-spine', 0.52, ['core', 'spine'], ['core', 'armor_plate']), anchorForEdge('anchor-keel', 'edge-keel', 0.45, ['core', 'keel'], ['armor_plate', 'detail']), anchorForNode('anchor-shoulder-fl', 'node-shoulder-fl', ['front', 'left', 'shoulder'], ['joint', 'armor_plate', 'detail']), anchorForNode('anchor-shoulder-fr', 'node-shoulder-fr', ['front', 'right', 'shoulder'], ['joint', 'armor_plate', 'detail']), anchorForNode('anchor-shoulder-bl', 'node-shoulder-bl', ['rear', 'left', 'shoulder'], ['joint', 'armor_plate', 'detail']), anchorForNode('anchor-shoulder-br', 'node-shoulder-br', ['rear', 'right', 'shoulder'], ['joint', 'armor_plate', 'detail']), anchorForEdge('anchor-upper-fl', 'edge-upper-fl', 0.5, ['front', 'left', 'upper_leg'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-upper-fr', 'edge-upper-fr', 0.5, ['front', 'right', 'upper_leg'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-upper-bl', 'edge-upper-bl', 0.5, ['rear', 'left', 'upper_leg'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-upper-br', 'edge-upper-br', 0.5, ['rear', 'right', 'upper_leg'], ['limb_segment', 'armor_plate']), anchorForNode('anchor-knee-fl', 'node-knee-fl', ['front', 'left', 'knee'], ['joint', 'armor_plate']), anchorForNode('anchor-knee-fr', 'node-knee-fr', ['front', 'right', 'knee'], ['joint', 'armor_plate']), anchorForNode('anchor-knee-bl', 'node-knee-bl', ['rear', 'left', 'knee'], ['joint', 'armor_plate']), anchorForNode('anchor-knee-br', 'node-knee-br', ['rear', 'right', 'knee'], ['joint', 'armor_plate']), anchorForEdge('anchor-lower-fl', 'edge-lower-fl', 0.5, ['front', 'left', 'lower_leg'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-lower-fr', 'edge-lower-fr', 0.5, ['front', 'right', 'lower_leg'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-lower-bl', 'edge-lower-bl', 0.5, ['rear', 'left', 'lower_leg'], ['limb_segment', 'armor_plate']), anchorForEdge('anchor-lower-br', 'edge-lower-br', 0.5, ['rear', 'right', 'lower_leg'], ['limb_segment', 'armor_plate']), anchorForNode('anchor-ankle-fl', 'node-ankle-fl', ['front', 'left', 'ankle'], ['joint', 'detail']), anchorForNode('anchor-ankle-fr', 'node-ankle-fr', ['front', 'right', 'ankle'], ['joint', 'detail']), anchorForNode('anchor-ankle-bl', 'node-ankle-bl', ['rear', 'left', 'ankle'], ['joint', 'detail']), anchorForNode('anchor-ankle-br', 'node-ankle-br', ['rear', 'right', 'ankle'], ['joint', 'detail']), anchorForEdge('anchor-foot-fl', 'edge-foot-fl', 0.38, ['front', 'left', 'foot'], ['limb_segment', 'detail']), anchorForEdge('anchor-foot-fr', 'edge-foot-fr', 0.38, ['front', 'right', 'foot'], ['limb_segment', 'detail']), anchorForEdge('anchor-foot-bl', 'edge-foot-bl', 0.38, ['rear', 'left', 'foot'], ['limb_segment', 'detail']), anchorForEdge('anchor-foot-br', 'edge-foot-br', 0.38, ['rear', 'right', 'foot'], ['limb_segment', 'detail']), anchorForNode('anchor-ball-fl', 'node-foot-fl', ['front', 'left', 'ball_foot'], ['joint', 'sensor']), anchorForNode('anchor-ball-fr', 'node-foot-fr', ['front', 'right', 'ball_foot'], ['joint', 'sensor']), anchorForNode('anchor-ball-bl', 'node-foot-bl', ['rear', 'left', 'ball_foot'], ['joint', 'sensor']), anchorForNode('anchor-ball-br', 'node-foot-br', ['rear', 'right', 'ball_foot'], ['joint', 'sensor']) ], generators: [], attachments: [ { id: 'attach-leaper-thorax-core', anchorId: 'anchor-spine', moduleId: 'core_torso_command', portId: 'mount', offset: { x: 0, y: 0.002, z: 0.018 }, shape: makeLeaperHardShape( 'box', { x: 0.246, y: 0.162, z: 0.458 }, { xTop: 0.1, xBottom: 0.01, zTop: 0.1, zBottom: 0.03 }, { profileIntensity: 0.98, chamferEdge: 0.24, chamferCorner: 0.26 } ) }, { id: 'attach-leaper-forebody-core', anchorId: 'anchor-core-front', moduleId: 'core_cuirass_front', portId: 'mount', offset: { x: 0, y: -0.032, z: 0.018 }, rotation: { x: -18, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.152, y: 0.096, z: 0.238 }, { xTop: 0.04, xBottom: 0.14, zTop: 0.24, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-rearbody-core', anchorId: 'anchor-core-back', moduleId: 'core_cuirass_back', portId: 'mount', offset: { x: 0, y: -0.002, z: -0.052 }, rotation: { x: 14, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.236, y: 0.132, z: 0.214 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.18, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-hull', anchorId: 'anchor-spine', moduleId: 'core_ribcage_shell', portId: 'mount', offset: { x: 0, y: 0.002, z: 0.03 }, shape: makeLeaperHardShape( 'box', { x: 0.29, y: 0.176, z: 0.622 }, { xTop: 0.04, xBottom: 0.01, zTop: 0.14, zBottom: 0.03 }, { profileIntensity: 1.0, chamferEdge: 0.24, chamferCorner: 0.26 } ) }, { id: 'attach-leaper-belly-core', anchorId: 'anchor-keel', moduleId: 'core_abdomen_frame', portId: 'mount', offset: { x: 0, y: -0.042, z: 0.054 }, rotation: { x: 90, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.092, y: 0.06, z: 0.18 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.24, zBottom: 0.08 }, { profileKind: 'block', profileIntensity: 0.82, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-dorsal-plate', anchorId: 'anchor-dorsal', moduleId: 'detail_torso_plate', portId: 'mount', rotation: { x: 90, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.124, y: 0.082, z: 0.334 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.14, zBottom: 0.06 }, { profileKind: 'plate', profileIntensity: 0.9, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-nose-wedge', anchorId: 'anchor-core-front', moduleId: 'core_cuirass_front', portId: 'mount', offset: { x: 0, y: -0.026, z: 0.118 }, rotation: { x: -24, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.086, y: 0.082, z: 0.216 }, { xTop: 0.02, xBottom: 0.12, zTop: 0.22, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.92, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-keel', anchorId: 'anchor-keel', moduleId: 'core_sternum_keel', portId: 'mount', offset: { x: 0, y: -0.03, z: 0.05 }, rotation: { x: 90, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.082, y: 0.042, z: 0.142 }, { xTop: 0.03, xBottom: 0.14, zTop: 0.2, zBottom: 0.05 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: 'attach-leaper-belly-skid', anchorId: 'anchor-keel', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: -0.056, z: 0.062 }, rotation: { x: 90, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.054, y: 0.02, z: 0.108 }, { xTop: 0.01, xBottom: 0.1, zTop: 0.16, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-spine-rail', anchorId: 'anchor-dorsal', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0, y: 0.012, z: 0.004 }, rotation: { x: 90, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.068, y: 0.054, z: 0.234 }, { xTop: 0.02, xBottom: 0.1, zTop: 0.18, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.9, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: 'attach-leaper-flank-left', anchorId: 'anchor-spine', moduleId: 'core_torso_flank', portId: 'mount', offset: { x: -0.09, y: -0.002, z: 0.018 }, rotation: { x: 18, y: -90, z: 12 }, shape: makeLeaperHardShape( 'wedge', { x: 0.076, y: 0.07, z: 0.192 }, { xTop: 0.04, xBottom: 0.12, zTop: 0.16, zBottom: 0.05 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-flank-right', anchorId: 'anchor-spine', moduleId: 'core_torso_flank', portId: 'mount', offset: { x: 0.09, y: -0.002, z: 0.018 }, rotation: { x: 18, y: 90, z: -12 }, shape: makeLeaperHardShape( 'wedge', { x: 0.076, y: 0.07, z: 0.192 }, { xTop: 0.04, xBottom: 0.12, zTop: 0.16, zBottom: 0.05 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-bridge-left', anchorId: 'anchor-spine', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: -0.076, y: -0.012, z: 0.084 }, rotation: { x: 12, y: -66, z: 14 }, shape: makeLeaperHardShape( 'wedge', { x: 0.062, y: 0.068, z: 0.162 }, { xTop: 0.02, xBottom: 0.12, zTop: 0.14, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.92, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-bridge-right', anchorId: 'anchor-spine', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0.076, y: -0.012, z: 0.084 }, rotation: { x: 12, y: 66, z: -14 }, shape: makeLeaperHardShape( 'wedge', { x: 0.062, y: 0.068, z: 0.162 }, { xTop: 0.02, xBottom: 0.12, zTop: 0.14, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.92, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-shoulder-throat-left', anchorId: 'anchor-spine', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: -0.094, y: -0.016, z: 0.122 }, rotation: { x: 36, y: -58, z: 12 }, shape: makeLeaperHardShape( 'wedge', { x: 0.056, y: 0.06, z: 0.132 }, { xTop: 0.02, xBottom: 0.1, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-shoulder-throat-right', anchorId: 'anchor-spine', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0.094, y: -0.016, z: 0.122 }, rotation: { x: 36, y: 58, z: -12 }, shape: makeLeaperHardShape( 'wedge', { x: 0.056, y: 0.06, z: 0.132 }, { xTop: 0.02, xBottom: 0.1, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-side-skirt-left', anchorId: 'anchor-keel', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: -0.072, y: -0.02, z: 0.016 }, rotation: { x: 96, y: -10, z: 8 }, shape: makeLeaperHardShape( 'wedge', { x: 0.034, y: 0.05, z: 0.144 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.1, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.84, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-side-skirt-right', anchorId: 'anchor-keel', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0.072, y: -0.02, z: 0.016 }, rotation: { x: 96, y: 10, z: -8 }, shape: makeLeaperHardShape( 'wedge', { x: 0.034, y: 0.05, z: 0.144 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.1, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.84, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-shoulder-shell-left', anchorId: 'anchor-spine', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: -0.106, y: 0.016, z: 0.126 }, rotation: { x: 18, y: -100, z: 10 }, shape: makeLeaperHardShape( 'wedge', { x: 0.078, y: 0.074, z: 0.164 }, { xTop: 0.01, xBottom: 0.12, zTop: 0.1, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.9, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-shoulder-shell-right', anchorId: 'anchor-spine', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: 0.106, y: 0.016, z: 0.126 }, rotation: { x: 18, y: 100, z: -10 }, shape: makeLeaperHardShape( 'wedge', { x: 0.078, y: 0.074, z: 0.164 }, { xTop: 0.01, xBottom: 0.12, zTop: 0.1, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.9, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-shoulder-collar-left', anchorId: 'anchor-spine', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: -0.102, y: -0.006, z: 0.126 }, rotation: { x: 32, y: -72, z: 8 }, shape: makeLeaperHardShape( 'wedge', { x: 0.05, y: 0.05, z: 0.104 }, { xTop: 0.01, xBottom: 0.1, zTop: 0.1, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.92, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-shoulder-collar-right', anchorId: 'anchor-spine', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: 0.102, y: -0.006, z: 0.126 }, rotation: { x: 32, y: 72, z: -8 }, shape: makeLeaperHardShape( 'wedge', { x: 0.05, y: 0.05, z: 0.104 }, { xTop: 0.01, xBottom: 0.1, zTop: 0.1, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.92, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-shoulder-gusset-left', anchorId: 'anchor-keel', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: -0.1, y: 0.012, z: 0.108 }, rotation: { x: 92, y: -10, z: 10 }, shape: makeLeaperHardShape( 'wedge', { x: 0.048, y: 0.058, z: 0.138 }, { xTop: 0.02, xBottom: 0.1, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.86, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-shoulder-gusset-right', anchorId: 'anchor-keel', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0.1, y: 0.012, z: 0.108 }, rotation: { x: 92, y: 10, z: -10 }, shape: makeLeaperHardShape( 'wedge', { x: 0.048, y: 0.058, z: 0.138 }, { xTop: 0.02, xBottom: 0.1, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.86, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-haunch-left', anchorId: 'anchor-core-back', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: -0.144, y: 0.024, z: -0.064 }, rotation: { x: 24, y: -94, z: 24 }, shape: makeLeaperHardShape( 'box', { x: 0.142, y: 0.156, z: 0.276 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.18, zBottom: 0.05 }, { profileKind: 'block', profileIntensity: 0.92, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-haunch-right', anchorId: 'anchor-core-back', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: 0.144, y: 0.024, z: -0.064 }, rotation: { x: 24, y: 94, z: -24 }, shape: makeLeaperHardShape( 'box', { x: 0.142, y: 0.156, z: 0.276 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.18, zBottom: 0.05 }, { profileKind: 'block', profileIntensity: 0.92, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-hip-saddle-left', anchorId: 'anchor-core-back', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: -0.118, y: -0.002, z: -0.006 }, rotation: { x: 82, y: -8, z: 18 }, shape: makeLeaperHardShape( 'wedge', { x: 0.094, y: 0.108, z: 0.268 }, { xTop: 0.04, xBottom: 0.14, zTop: 0.18, zBottom: 0.05 }, { profileKind: 'block', profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-hip-saddle-right', anchorId: 'anchor-core-back', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: 0.118, y: -0.002, z: -0.006 }, rotation: { x: 82, y: 8, z: -18 }, shape: makeLeaperHardShape( 'wedge', { x: 0.094, y: 0.108, z: 0.268 }, { xTop: 0.04, xBottom: 0.14, zTop: 0.18, zBottom: 0.05 }, { profileKind: 'block', profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-hip-collar-left', anchorId: 'anchor-core-back', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: -0.154, y: 0.02, z: -0.024 }, rotation: { x: 44, y: -84, z: 24 }, shape: makeLeaperHardShape( 'box', { x: 0.128, y: 0.138, z: 0.228 }, { xTop: 0.04, xBottom: 0.18, zTop: 0.16, zBottom: 0.05 }, { profileKind: 'block', profileIntensity: 0.94, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-hip-collar-right', anchorId: 'anchor-core-back', moduleId: 'core_pelvis_buttress', portId: 'mount', offset: { x: 0.154, y: 0.02, z: -0.024 }, rotation: { x: 44, y: 84, z: -24 }, shape: makeLeaperHardShape( 'box', { x: 0.128, y: 0.138, z: 0.228 }, { xTop: 0.04, xBottom: 0.18, zTop: 0.16, zBottom: 0.05 }, { profileKind: 'block', profileIntensity: 0.94, chamferEdge: 0.2, chamferCorner: 0.22 } ) }, { id: 'attach-leaper-hip-gusset-left', anchorId: 'anchor-keel', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: -0.136, y: 0.014, z: -0.074 }, rotation: { x: 86, y: -8, z: 20 }, shape: makeLeaperHardShape( 'wedge', { x: 0.084, y: 0.09, z: 0.24 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.18, zBottom: 0.05 }, { profileKind: 'block', profileIntensity: 0.9, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-hip-gusset-right', anchorId: 'anchor-keel', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0.136, y: 0.014, z: -0.074 }, rotation: { x: 86, y: 8, z: -20 }, shape: makeLeaperHardShape( 'wedge', { x: 0.084, y: 0.09, z: 0.24 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.18, zBottom: 0.05 }, { profileKind: 'block', profileIntensity: 0.9, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-rear-shell', anchorId: 'anchor-core-back', moduleId: 'core_cuirass_back', portId: 'mount', offset: { x: 0, y: 0.008, z: -0.096 }, rotation: { x: 16, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.266, y: 0.148, z: 0.226 }, { xTop: 0.04, xBottom: 0.16, zTop: 0.16, zBottom: 0.04 }, { profileIntensity: 0.88, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-rear-crown', anchorId: 'anchor-core-back', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0, y: 0.048, z: -0.072 }, rotation: { x: 20, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.096, y: 0.048, z: 0.138 }, { xTop: 0.02, xBottom: 0.12, zTop: 0.16, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: 'attach-leaper-neck-collar', anchorId: 'anchor-core-front', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0, y: -0.016, z: 0.076 }, rotation: { x: -24, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.074, y: 0.034, z: 0.084 }, { xTop: 0.02, xBottom: 0.1, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: 'attach-leaper-eye-housing', anchorId: 'anchor-eye', moduleId: 'detail_sensor_head', portId: 'mount', offset: { x: 0, y: -0.002, z: -0.018 }, rotation: { x: -24, y: 0, z: 0 }, shape: makeLeaperHardShape( 'box', { x: 0.028, y: 0.022, z: 0.042 }, { xTop: 0.04, xBottom: 0.03, zTop: 0.08, zBottom: 0.03 }, { profileIntensity: 0.8, chamferEdge: 0.18, chamferCorner: 0.18 } ) }, { id: 'attach-leaper-eye-lens', anchorId: 'anchor-eye', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0, y: -0.002, z: 0.018 }, shape: makeLeaperHardShape( 'box', { x: 0.01, y: 0.01, z: 0.02 }, { xTop: 0.03, xBottom: 0.05, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'hardSurface', profileIntensity: 0.82, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-eye-collar', anchorId: 'anchor-eye', moduleId: 'detail_sensor_head', portId: 'mount', offset: { x: 0, y: 0.002, z: 0.008 }, rotation: { x: -18, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.02, y: 0.016, z: 0.03 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: 'attach-leaper-temple-left', anchorId: 'anchor-eye', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: -0.016, y: 0.006, z: -0.004 }, rotation: { x: -18, y: -28, z: -6 }, shape: makeLeaperHardShape( 'wedge', { x: 0.014, y: 0.02, z: 0.044 }, { xTop: 0.02, xBottom: 0.08, zTop: 0.14, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-temple-right', anchorId: 'anchor-eye', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0.016, y: 0.006, z: -0.004 }, rotation: { x: -18, y: 28, z: 6 }, shape: makeLeaperHardShape( 'wedge', { x: 0.014, y: 0.02, z: 0.044 }, { xTop: 0.02, xBottom: 0.08, zTop: 0.14, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-brow', anchorId: 'anchor-core-front', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0, y: -0.014, z: 0.112 }, rotation: { x: 98, y: 0, z: 0 }, shape: makeLeaperHardShape( 'wedge', { x: 0.09, y: 0.016, z: 0.112 }, { xTop: 0.01, xBottom: 0.1, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.86, chamferEdge: 0.16, chamferCorner: 0.18 } ) }, { id: 'attach-leaper-jaw-left', anchorId: 'anchor-core-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: -0.022, y: -0.034, z: 0.12 }, rotation: { x: 28, y: -12, z: -4 }, shape: makeLeaperHardShape( 'wedge', { x: 0.014, y: 0.016, z: 0.1 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.2, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.9, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-jaw-right', anchorId: 'anchor-core-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0.022, y: -0.034, z: 0.12 }, rotation: { x: 28, y: 12, z: 4 }, shape: makeLeaperHardShape( 'wedge', { x: 0.014, y: 0.016, z: 0.1 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.2, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.9, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-cheek-left', anchorId: 'anchor-core-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: -0.028, y: -0.022, z: 0.094 }, rotation: { x: 16, y: -18, z: -8 }, shape: makeLeaperHardShape('wedge', { x: 0.02, y: 0.016, z: 0.04 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.16, chamferCorner: 0.18 }) }, { id: 'attach-leaper-cheek-right', anchorId: 'anchor-core-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0.028, y: -0.022, z: 0.094 }, rotation: { x: 16, y: 18, z: 8 }, shape: makeLeaperHardShape('wedge', { x: 0.02, y: 0.016, z: 0.04 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.84, chamferEdge: 0.16, chamferCorner: 0.18 }) }, { id: 'attach-leaper-chine-left', anchorId: 'anchor-core-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: -0.038, y: -0.014, z: 0.078 }, rotation: { x: 24, y: -30, z: -10 }, shape: makeLeaperHardShape('wedge', { x: 0.022, y: 0.014, z: 0.058 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.14, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.16, chamferCorner: 0.18 }) }, { id: 'attach-leaper-chine-right', anchorId: 'anchor-core-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0.038, y: -0.014, z: 0.078 }, rotation: { x: 24, y: 30, z: 10 }, shape: makeLeaperHardShape('wedge', { x: 0.022, y: 0.014, z: 0.058 }, { xTop: 0.01, xBottom: 0.08, zTop: 0.14, zBottom: 0.03 }, { profileKind: 'plate', profileIntensity: 0.88, chamferEdge: 0.16, chamferCorner: 0.18 }) }, { id: 'attach-leaper-emp-pod-left', anchorId: 'anchor-spine', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: -0.058, y: -0.01, z: 0.09 }, rotation: { x: 6, y: -6, z: 6 }, shape: makeLeaperHardShape( 'box', { x: 0.038, y: 0.038, z: 0.088 }, { xTop: 0.04, xBottom: 0.04, zTop: 0.1, zBottom: 0.04 }, { profileIntensity: 0.8, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-emp-pod-right', anchorId: 'anchor-spine', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0.058, y: -0.01, z: 0.09 }, rotation: { x: 6, y: 6, z: -6 }, shape: makeLeaperHardShape( 'box', { x: 0.038, y: 0.038, z: 0.088 }, { xTop: 0.04, xBottom: 0.04, zTop: 0.1, zBottom: 0.04 }, { profileIntensity: 0.8, chamferEdge: 0.18, chamferCorner: 0.2 } ) }, { id: 'attach-leaper-pod-saddle-left', anchorId: 'anchor-spine', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: -0.05, y: 0, z: 0.08 }, rotation: { x: 8, y: -12, z: 6 }, shape: makeLeaperHardShape( 'wedge', { x: 0.034, y: 0.028, z: 0.066 }, { xTop: 0.02, xBottom: 0.08, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.8, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-pod-saddle-right', anchorId: 'anchor-spine', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0.05, y: 0, z: 0.08 }, rotation: { x: 8, y: 12, z: -6 }, shape: makeLeaperHardShape( 'wedge', { x: 0.034, y: 0.028, z: 0.066 }, { xTop: 0.02, xBottom: 0.08, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.8, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-emp-collar-left', anchorId: 'anchor-spine', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: -0.058, y: -0.01, z: 0.102 }, rotation: { x: 6, y: -6, z: 4 }, shape: makeLeaperHardShape( 'wedge', { x: 0.022, y: 0.018, z: 0.042 }, { xTop: 0.02, xBottom: 0.08, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.8, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-emp-collar-right', anchorId: 'anchor-spine', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0.058, y: -0.01, z: 0.102 }, rotation: { x: 6, y: 6, z: -4 }, shape: makeLeaperHardShape( 'wedge', { x: 0.022, y: 0.018, z: 0.042 }, { xTop: 0.02, xBottom: 0.08, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.8, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-emp-fin-left', anchorId: 'anchor-spine', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: -0.072, y: 0.006, z: 0.1 }, rotation: { x: -6, y: -84, z: 12 }, shape: makeLeaperHardShape( 'wedge', { x: 0.026, y: 0.028, z: 0.058 }, { xTop: 0.02, xBottom: 0.08, zTop: 0.14, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.82, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-emp-fin-right', anchorId: 'anchor-spine', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0.072, y: 0.006, z: 0.1 }, rotation: { x: -6, y: 84, z: -12 }, shape: makeLeaperHardShape( 'wedge', { x: 0.026, y: 0.028, z: 0.058 }, { xTop: 0.02, xBottom: 0.08, zTop: 0.14, zBottom: 0.04 }, { profileKind: 'plate', profileIntensity: 0.82, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-emp-muzzle-left', anchorId: 'anchor-spine', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: -0.058, y: -0.006, z: 0.126 }, rotation: { x: 6, y: -6, z: 6 }, shape: makeLeaperHardShape( 'wedge', { x: 0.014, y: 0.014, z: 0.026 }, { xTop: 0.02, xBottom: 0.06, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.78, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-emp-muzzle-right', anchorId: 'anchor-spine', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0.058, y: -0.006, z: 0.126 }, rotation: { x: 6, y: 6, z: -6 }, shape: makeLeaperHardShape( 'wedge', { x: 0.014, y: 0.014, z: 0.026 }, { xTop: 0.02, xBottom: 0.06, zTop: 0.12, zBottom: 0.04 }, { profileKind: 'block', profileIntensity: 0.78, chamferEdge: 0.14, chamferCorner: 0.16 } ) }, { id: 'attach-leaper-emp-emitter-left', anchorId: 'anchor-spine', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: -0.058, y: -0.004, z: 0.118 }, rotation: { x: 6, y: -6, z: 6 }, shape: makeLeaperHardShape( 'box', { x: 0.008, y: 0.008, z: 0.014 }, { xTop: 0.02, xBottom: 0.04, zTop: 0.08, zBottom: 0.04 }, { profileKind: 'hardSurface', profileIntensity: 0.78, chamferEdge: 0.12, chamferCorner: 0.14 } ) }, { id: 'attach-leaper-emp-emitter-right', anchorId: 'anchor-spine', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0.058, y: -0.004, z: 0.118 }, rotation: { x: 6, y: 6, z: -6 }, shape: makeLeaperHardShape( 'box', { x: 0.008, y: 0.008, z: 0.014 }, { xTop: 0.02, xBottom: 0.04, zTop: 0.08, zBottom: 0.04 }, { profileKind: 'hardSurface', profileIntensity: 0.78, chamferEdge: 0.12, chamferCorner: 0.14 } ) }, ...leaperLegAttachments ] }; } if (name === 'gunrange') { return { version: 1, nodes: [ { id: 'node-base', position: { x: 0, y: 0, z: 0 }, tags: [] }, { id: 'node-pole-top', position: { x: 0, y: 0.6, z: 0 }, tags: [] }, { id: 'node-ring-1', position: { x: 0.25, y: 0.85, z: 0 }, tags: [] }, { id: 'node-ring-2', position: { x: 0, y: 0.85, z: 0.25 }, tags: [] }, { id: 'node-ring-3', position: { x: -0.25, y: 0.85, z: 0 }, tags: [] }, { id: 'node-ring-4', position: { x: 0, y: 0.85, z: -0.25 }, tags: [] } ], edges: [ { id: 'edge-pole', a: 'node-base', b: 'node-pole-top', radius: 0.05, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-ring-1', a: 'node-ring-1', b: 'node-ring-2', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-ring-2', a: 'node-ring-2', b: 'node-ring-3', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-ring-3', a: 'node-ring-3', b: 'node-ring-4', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-ring-4', a: 'node-ring-4', b: 'node-ring-1', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ], anchors: [ anchorForNode('anchor-target', 'node-ring-1', ['target'], ['sensor']), anchorForNode('anchor-base', 'node-base', ['base'], ['core']) ], generators: [], attachments: [ { id: 'attach-target', anchorId: 'anchor-target', moduleId: 'detail_scope_lens', portId: 'mount' }, { id: 'attach-base', anchorId: 'anchor-base', moduleId: 'detail_engine_block', portId: 'mount' } ] }; } if (name === 'drone-heavy-quadcannon') { return { version: 1, nodes: [ { id: 'node-core', position: { x: 0, y: 0.92, z: 0.04 }, tags: ['drone', 'core'] }, { id: 'node-nose', position: { x: 0, y: 0.98, z: 0.42 }, tags: ['drone', 'sensor'] }, { id: 'node-tail', position: { x: 0, y: 0.9, z: -0.38 }, tags: ['drone', 'tail'] }, { id: 'node-rotor-fl', position: { x: -0.52, y: 1.02, z: 0.32 }, tags: ['rotor', 'front', 'left'] }, { id: 'node-rotor-fr', position: { x: 0.52, y: 1.02, z: 0.32 }, tags: ['rotor', 'front', 'right'] }, { id: 'node-rotor-bl', position: { x: -0.54, y: 0.98, z: -0.32 }, tags: ['rotor', 'back', 'left'] }, { id: 'node-rotor-br', position: { x: 0.54, y: 0.98, z: -0.32 }, tags: ['rotor', 'back', 'right'] }, { id: 'node-gun-l', position: { x: -0.28, y: 0.72, z: 0.36 }, tags: ['weapon', 'left'] }, { id: 'node-gun-r', position: { x: 0.28, y: 0.72, z: 0.36 }, tags: ['weapon', 'right'] }, { id: 'node-gun-l-tip', position: { x: -0.28, y: 0.7, z: 0.86 }, tags: ['weapon', 'left', 'muzzle'] }, { id: 'node-gun-r-tip', position: { x: 0.28, y: 0.7, z: 0.86 }, tags: ['weapon', 'right', 'muzzle'] }, { id: 'node-belly', position: { x: 0, y: 0.62, z: 0.06 }, tags: ['drone', 'belly'] } ], edges: [ { id: 'edge-spine-front', a: 'node-core', b: 'node-nose', radius: 0.06, curve: 'catmull', controlOffset: { x: 0, y: 0.04, z: 0.04 } }, { id: 'edge-spine-back', a: 'node-core', b: 'node-tail', radius: 0.06, curve: 'catmull', controlOffset: { x: 0, y: -0.02, z: -0.06 } }, { id: 'edge-arm-fl', a: 'node-core', b: 'node-rotor-fl', radius: 0.05, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-arm-fr', a: 'node-core', b: 'node-rotor-fr', radius: 0.05, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-arm-bl', a: 'node-core', b: 'node-rotor-bl', radius: 0.05, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-arm-br', a: 'node-core', b: 'node-rotor-br', radius: 0.05, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-gun-l', a: 'node-gun-l', b: 'node-gun-l-tip', radius: 0.035, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-gun-r', a: 'node-gun-r', b: 'node-gun-r-tip', radius: 0.035, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-gun-mount-l', a: 'node-core', b: 'node-gun-l', radius: 0.04, curve: 'line', controlOffset: { x: -0.04, y: -0.04, z: 0.08 } }, { id: 'edge-gun-mount-r', a: 'node-core', b: 'node-gun-r', radius: 0.04, curve: 'line', controlOffset: { x: 0.04, y: -0.04, z: 0.08 } }, { id: 'edge-belly', a: 'node-core', b: 'node-belly', radius: 0.08, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ], anchors: [ anchorForNode('anchor-heavy-core', 'node-core', ['core', 'hull'], ['core', 'armor_plate', 'detail']), anchorForNode('anchor-heavy-sensor', 'node-nose', ['sensor', 'nose'], ['sensor', 'detail']), anchorForNode('anchor-heavy-tail', 'node-tail', ['tail'], ['detail', 'sensor']), anchorForNode('anchor-heavy-rotor-fl', 'node-rotor-fl', ['rotor', 'left', 'front'], ['engine', 'detail']), anchorForNode('anchor-heavy-rotor-fr', 'node-rotor-fr', ['rotor', 'right', 'front'], ['engine', 'detail']), anchorForNode('anchor-heavy-rotor-bl', 'node-rotor-bl', ['rotor', 'left', 'back'], ['engine', 'detail']), anchorForNode('anchor-heavy-rotor-br', 'node-rotor-br', ['rotor', 'right', 'back'], ['engine', 'detail']), anchorForNode('anchor-heavy-gun-l', 'node-gun-l', ['weapon', 'left'], ['weapon', 'detail']), anchorForNode('anchor-heavy-gun-r', 'node-gun-r', ['weapon', 'right'], ['weapon', 'detail']), anchorForNode('anchor-heavy-belly', 'node-belly', ['belly', 'armor'], ['armor_plate', 'detail']), anchorForEdge('anchor-heavy-backbone', 'edge-spine-back', 0.38, ['backbone'], ['armor_plate', 'detail']) ], generators: [], attachments: [ { id: 'attach-heavy-hull', anchorId: 'anchor-heavy-core', moduleId: 'detail_backpack_block', portId: 'mount', shape: { primitive: 'box', size: { x: 0.66, y: 0.28, z: 0.72 }, taper: { xTop: 0.92, xBottom: 1.08, zTop: 0.94, zBottom: 1.04 }, chamfer: { edge: 0.14, corner: 0.18 }, profile: { kind: 'hardSurface', intensity: 0.84 } } }, { id: 'attach-heavy-armor-top', anchorId: 'anchor-heavy-backbone', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0, y: 0.05, z: -0.02 }, rotation: { x: 90, y: 0, z: 0 }, shape: { primitive: 'box', size: { x: 0.42, y: 0.12, z: 0.6 }, chamfer: { edge: 0.1, corner: 0.12 }, profile: { kind: 'plate', intensity: 0.78 } } }, { id: 'attach-heavy-belly-armor', anchorId: 'anchor-heavy-belly', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: -0.02, z: 0.04 }, shape: { primitive: 'box', size: { x: 0.4, y: 0.16, z: 0.36 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'block', intensity: 0.72 } } }, { id: 'attach-heavy-sensor-head', anchorId: 'anchor-heavy-sensor', moduleId: 'detail_sensor_head', portId: 'mount', offset: { x: 0, y: 0.012, z: 0.014 }, scale: { x: 1.06, y: 1.06, z: 1.06 } }, { id: 'attach-heavy-sensor-eye', anchorId: 'anchor-heavy-sensor', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0, y: 0.008, z: 0.07 }, scale: { x: 1.28, y: 1.28, z: 1.28 } }, { id: 'attach-heavy-tail-fin', anchorId: 'anchor-heavy-tail', moduleId: 'detail_tool_arm', portId: 'mount', rotation: { x: 90, y: 0, z: 0 }, shape: { primitive: 'box', size: { x: 0.12, y: 0.1, z: 0.28 }, chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'plate', intensity: 0.68 } } }, { id: 'attach-heavy-rotor-front-left', anchorId: 'anchor-heavy-rotor-fl', moduleId: 'detail_engine_block', portId: 'mount', shape: { primitive: 'cylinder', size: { x: 0.15, y: 0.16, segments: 18 }, profile: { kind: 'hardSurface', intensity: 0.74 } } }, { id: 'attach-heavy-rotor-front-right', anchorId: 'anchor-heavy-rotor-fr', moduleId: 'detail_engine_block', portId: 'mount', shape: { primitive: 'cylinder', size: { x: 0.15, y: 0.16, segments: 18 }, profile: { kind: 'hardSurface', intensity: 0.74 } } }, { id: 'attach-heavy-rotor-back-left', anchorId: 'anchor-heavy-rotor-bl', moduleId: 'detail_engine_block', portId: 'mount', shape: { primitive: 'cylinder', size: { x: 0.15, y: 0.16, segments: 18 }, profile: { kind: 'hardSurface', intensity: 0.74 } } }, { id: 'attach-heavy-rotor-back-right', anchorId: 'anchor-heavy-rotor-br', moduleId: 'detail_engine_block', portId: 'mount', shape: { primitive: 'cylinder', size: { x: 0.15, y: 0.16, segments: 18 }, profile: { kind: 'hardSurface', intensity: 0.74 } } }, { id: 'attach-heavy-rotor-intake-front-left', anchorId: 'anchor-heavy-rotor-fl', moduleId: 'detail_engine_intake', portId: 'mount', offset: { x: 0, y: 0.028, z: 0 }, scale: { x: 1.04, y: 0.96, z: 1.04 } }, { id: 'attach-heavy-rotor-intake-front-right', anchorId: 'anchor-heavy-rotor-fr', moduleId: 'detail_engine_intake', portId: 'mount', offset: { x: 0, y: 0.028, z: 0 }, scale: { x: 1.04, y: 0.96, z: 1.04 } }, { id: 'attach-heavy-rotor-intake-back-left', anchorId: 'anchor-heavy-rotor-bl', moduleId: 'detail_engine_intake', portId: 'mount', offset: { x: 0, y: 0.028, z: 0 }, scale: { x: 1.04, y: 0.96, z: 1.04 } }, { id: 'attach-heavy-rotor-intake-back-right', anchorId: 'anchor-heavy-rotor-br', moduleId: 'detail_engine_intake', portId: 'mount', offset: { x: 0, y: 0.028, z: 0 }, scale: { x: 1.04, y: 0.96, z: 1.04 } }, { id: 'attach-heavy-gun-l-hardpoint', anchorId: 'anchor-heavy-gun-l', moduleId: 'detail_weapon_hardpoint', portId: 'mount', scale: { x: 1.1, y: 1.02, z: 1.02 } }, { id: 'attach-heavy-gun-l-body', anchorId: 'anchor-heavy-gun-l', moduleId: 'detail_weapon_receiver', portId: 'mount', offset: { x: 0, y: 0.01, z: 0.08 }, scale: { x: 0.94, y: 0.88, z: 1.04 } }, { id: 'attach-heavy-gun-l-barrel', anchorId: 'anchor-heavy-gun-l', moduleId: 'detail_weapon_barrel', portId: 'mount', offset: { x: 0, y: 0, z: 0.28 }, scale: { x: 1.04, y: 0.98, z: 1.18 } }, { id: 'attach-heavy-gun-l-shroud', anchorId: 'anchor-heavy-gun-l', moduleId: 'detail_weapon_shroud', portId: 'mount', offset: { x: 0, y: 0.01, z: 0.16 }, scale: { x: 0.9, y: 0.86, z: 1.02 } }, { id: 'attach-heavy-gun-l-muzzle', anchorId: 'anchor-heavy-gun-l', moduleId: 'detail_weapon_muzzle', portId: 'mount', offset: { x: 0, y: 0, z: 0.5 }, scale: { x: 0.96, y: 0.92, z: 0.92 } }, { id: 'attach-heavy-gun-l-feed', anchorId: 'anchor-heavy-gun-l', moduleId: 'detail_weapon_feed', portId: 'mount', offset: { x: -0.05, y: -0.02, z: 0.04 }, scale: { x: 0.84, y: 0.8, z: 0.8 } }, { id: 'attach-heavy-gun-r-hardpoint', anchorId: 'anchor-heavy-gun-r', moduleId: 'detail_weapon_hardpoint', portId: 'mount', scale: { x: 1.1, y: 1.02, z: 1.02 } }, { id: 'attach-heavy-gun-r-body', anchorId: 'anchor-heavy-gun-r', moduleId: 'detail_weapon_receiver', portId: 'mount', offset: { x: 0, y: 0.01, z: 0.08 }, scale: { x: 0.94, y: 0.88, z: 1.04 } }, { id: 'attach-heavy-gun-r-barrel', anchorId: 'anchor-heavy-gun-r', moduleId: 'detail_weapon_barrel', portId: 'mount', offset: { x: 0, y: 0, z: 0.28 }, scale: { x: 1.04, y: 0.98, z: 1.18 } }, { id: 'attach-heavy-gun-r-shroud', anchorId: 'anchor-heavy-gun-r', moduleId: 'detail_weapon_shroud', portId: 'mount', offset: { x: 0, y: 0.01, z: 0.16 }, scale: { x: 0.9, y: 0.86, z: 1.02 } }, { id: 'attach-heavy-gun-r-muzzle', anchorId: 'anchor-heavy-gun-r', moduleId: 'detail_weapon_muzzle', portId: 'mount', offset: { x: 0, y: 0, z: 0.5 }, scale: { x: 0.96, y: 0.92, z: 0.92 } }, { id: 'attach-heavy-gun-r-feed', anchorId: 'anchor-heavy-gun-r', moduleId: 'detail_weapon_feed', portId: 'mount', offset: { x: 0.05, y: -0.02, z: 0.04 }, scale: { x: 0.84, y: 0.8, z: 0.8 } } ] }; } if (name === 'drone-scout-recon') { return { version: 1, nodes: [ { id: 'node-core', position: { x: 0, y: 0.9, z: 0.02 }, tags: ['drone', 'core'] }, { id: 'node-sensor', position: { x: 0, y: 0.96, z: 0.34 }, tags: ['sensor'] }, { id: 'node-tail', position: { x: 0, y: 0.92, z: -0.28 }, tags: ['tail'] }, { id: 'node-rotor-fl', position: { x: -0.42, y: 1.0, z: 0.22 }, tags: ['rotor', 'left', 'front'] }, { id: 'node-rotor-fr', position: { x: 0.42, y: 1.0, z: 0.22 }, tags: ['rotor', 'right', 'front'] }, { id: 'node-rotor-bl', position: { x: -0.44, y: 0.98, z: -0.22 }, tags: ['rotor', 'left', 'back'] }, { id: 'node-rotor-br', position: { x: 0.44, y: 0.98, z: -0.22 }, tags: ['rotor', 'right', 'back'] }, { id: 'node-ventral', position: { x: 0, y: 0.7, z: 0.02 }, tags: ['ventral'] } ], edges: [ { id: 'edge-sensor', a: 'node-core', b: 'node-sensor', radius: 0.04, curve: 'catmull', controlOffset: { x: 0, y: 0.02, z: 0.06 } }, { id: 'edge-tail', a: 'node-core', b: 'node-tail', radius: 0.04, curve: 'catmull', controlOffset: { x: 0, y: 0.01, z: -0.04 } }, { id: 'edge-arm-fl', a: 'node-core', b: 'node-rotor-fl', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-arm-fr', a: 'node-core', b: 'node-rotor-fr', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-arm-bl', a: 'node-core', b: 'node-rotor-bl', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-arm-br', a: 'node-core', b: 'node-rotor-br', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-ventral', a: 'node-core', b: 'node-ventral', radius: 0.03, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ], anchors: [ anchorForNode('anchor-scout-core', 'node-core', ['core'], ['core', 'detail']), anchorForNode('anchor-scout-sensor', 'node-sensor', ['sensor'], ['sensor', 'detail']), anchorForNode('anchor-scout-tail', 'node-tail', ['tail'], ['detail']), anchorForNode('anchor-scout-rotor-fl', 'node-rotor-fl', ['rotor', 'front', 'left'], ['engine', 'detail']), anchorForNode('anchor-scout-rotor-fr', 'node-rotor-fr', ['rotor', 'front', 'right'], ['engine', 'detail']), anchorForNode('anchor-scout-rotor-bl', 'node-rotor-bl', ['rotor', 'back', 'left'], ['engine', 'detail']), anchorForNode('anchor-scout-rotor-br', 'node-rotor-br', ['rotor', 'back', 'right'], ['engine', 'detail']), anchorForNode('anchor-scout-ventral', 'node-ventral', ['ventral'], ['sensor', 'detail']) ], generators: [], attachments: [ { id: 'attach-scout-hull', anchorId: 'anchor-scout-core', moduleId: 'detail_backpack_module', portId: 'mount', shape: { primitive: 'box', size: { x: 0.42, y: 0.18, z: 0.52 }, taper: { xTop: 0.88, xBottom: 1.04, zTop: 0.92, zBottom: 1.02 }, chamfer: { edge: 0.12, corner: 0.16 }, profile: { kind: 'hardSurface', intensity: 0.72 } } }, { id: 'attach-scout-sensor-head', anchorId: 'anchor-scout-sensor', moduleId: 'detail_sensor_head', portId: 'mount', offset: { x: 0, y: 0.01, z: 0.02 }, scale: { x: 0.92, y: 0.92, z: 0.92 } }, { id: 'attach-scout-sensor-eye', anchorId: 'anchor-scout-sensor', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0, y: 0.006, z: 0.06 }, scale: { x: 1.2, y: 1.2, z: 1.2 } }, { id: 'attach-scout-tail-fin', anchorId: 'anchor-scout-tail', moduleId: 'detail_tool_arm', portId: 'mount', rotation: { x: 90, y: 0, z: 0 }, shape: { primitive: 'box', size: { x: 0.08, y: 0.07, z: 0.2 }, chamfer: { edge: 0.06, corner: 0.08 }, profile: { kind: 'plate', intensity: 0.58 } } }, { id: 'attach-scout-rotor-front-left', anchorId: 'anchor-scout-rotor-fl', moduleId: 'detail_engine_block', portId: 'mount', scale: { x: 0.84, y: 0.84, z: 0.84 } }, { id: 'attach-scout-rotor-front-right', anchorId: 'anchor-scout-rotor-fr', moduleId: 'detail_engine_block', portId: 'mount', scale: { x: 0.84, y: 0.84, z: 0.84 } }, { id: 'attach-scout-rotor-back-left', anchorId: 'anchor-scout-rotor-bl', moduleId: 'detail_engine_block', portId: 'mount', scale: { x: 0.84, y: 0.84, z: 0.84 } }, { id: 'attach-scout-rotor-back-right', anchorId: 'anchor-scout-rotor-br', moduleId: 'detail_engine_block', portId: 'mount', scale: { x: 0.84, y: 0.84, z: 0.84 } }, { id: 'attach-scout-ventral-eye', anchorId: 'anchor-scout-ventral', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0, y: -0.02, z: 0.04 }, scale: { x: 0.84, y: 0.84, z: 0.84 } }, { id: 'attach-scout-top-array', anchorId: 'anchor-scout-core', moduleId: 'detail_sensor_array', portId: 'mount', offset: { x: 0, y: 0.06, z: -0.04 }, scale: { x: 0.82, y: 0.86, z: 0.82 } } ] }; } if (name === 'flip-target') { const sharedStandardId: FlipTargetSharedVariant['id'] = 'flip-target'; const sharedBoxSize = (nodeId: string) => { const [x, y, z] = getFlipTargetSharedNode(sharedStandardId, nodeId).size ?? [0, 0, 0]; return { x, y, z }; }; const sharedCylinderSize = (nodeId: string, segments = 16) => { const [radius = 0, , length = 0] = getFlipTargetSharedNode(sharedStandardId, nodeId).size ?? [0, 0, 0]; return { x: radius, y: length, segments }; }; const sharedRotation = (nodeId: string) => flipTargetNodeRotationToDegrees(getFlipTargetSharedNode(sharedStandardId, nodeId)); return { version: 1, nodes: [ { id: 'node-base-center', position: { x: 0, y: 0, z: 0 }, tags: ['base'] }, { id: 'node-base-front', position: { x: 0, y: 0, z: 0.24 }, tags: ['base'] }, { id: 'node-base-back', position: { x: 0, y: 0, z: -0.24 }, tags: ['base'] }, { id: 'node-mast-bottom', position: { x: 0, y: 0.16, z: -0.02 }, tags: ['support'] }, { id: 'node-pivot', position: { x: 0, y: 1.02, z: -0.02 }, tags: ['pivot'] }, { id: 'node-yoke-l', position: { x: -0.16, y: 1.02, z: 0.02 }, tags: ['pivot', 'support'] }, { id: 'node-yoke-r', position: { x: 0.16, y: 1.02, z: 0.02 }, tags: ['pivot', 'support'] }, { id: 'node-plate-top-l', position: { x: -0.22, y: 1.28, z: 0.08 }, tags: ['target'] }, { id: 'node-plate-top-r', position: { x: 0.22, y: 1.28, z: 0.08 }, tags: ['target'] }, { id: 'node-plate-bottom-l', position: { x: -0.22, y: 0.72, z: 0.08 }, tags: ['target'] }, { id: 'node-plate-bottom-r', position: { x: 0.22, y: 0.72, z: 0.08 }, tags: ['target'] }, { id: 'node-plate-center', position: { x: 0, y: 1, z: 0.09 }, tags: ['target', 'center'] }, { id: 'node-counterweight', position: { x: 0, y: 0.84, z: -0.36 }, tags: ['counterweight'] }, { id: 'node-actuator-base', position: { x: 0.2, y: 0.28, z: -0.16 }, tags: ['service', 'actuator'] }, { id: 'node-actuator-link', position: { x: 0.14, y: 0.76, z: -0.24 }, tags: ['service', 'actuator'] }, { id: 'node-sensor-top', position: { x: 0, y: 1.36, z: 0.1 }, tags: ['sensor'] } ], edges: [ { id: 'edge-base-spine', a: 'node-base-back', b: 'node-base-front', radius: 0.08, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-mast', a: 'node-mast-bottom', b: 'node-pivot', radius: 0.05, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-yoke-l', a: 'node-pivot', b: 'node-yoke-l', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-yoke-r', a: 'node-pivot', b: 'node-yoke-r', radius: 0.04, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-frame-top', a: 'node-plate-top-l', b: 'node-plate-top-r', radius: 0.028, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-frame-bottom', a: 'node-plate-bottom-l', b: 'node-plate-bottom-r', radius: 0.032, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-frame-left', a: 'node-plate-bottom-l', b: 'node-plate-top-l', radius: 0.028, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-frame-right', a: 'node-plate-bottom-r', b: 'node-plate-top-r', radius: 0.028, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-pivot-link', a: 'node-pivot', b: 'node-plate-center', radius: 0.035, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-hinge-brace-l', a: 'node-yoke-l', b: 'node-plate-top-l', radius: 0.024, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-hinge-brace-r', a: 'node-yoke-r', b: 'node-plate-top-r', radius: 0.024, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-counterweight-arm', a: 'node-pivot', b: 'node-counterweight', radius: 0.042, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-actuator-body', a: 'node-actuator-base', b: 'node-actuator-link', radius: 0.028, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-reset-link', a: 'node-actuator-link', b: 'node-counterweight', radius: 0.022, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-sensor-post', a: 'node-plate-center', b: 'node-sensor-top', radius: 0.018, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ], anchors: [ anchorForNode('anchor-base', 'node-base-center', ['base', 'foundation'], ['core', 'detail']), anchorForEdge('anchor-mast-mid', 'edge-mast', 0.52, ['support', 'mast'], ['core', 'detail']), { ...anchorForNode('anchor-pivot', 'node-pivot', ['pivot', 'hinge'], ['detail', 'connector']), exportSocket: true }, anchorForNode('anchor-yoke-l', 'node-yoke-l', ['pivot', 'left'], ['detail']), anchorForNode('anchor-yoke-r', 'node-yoke-r', ['pivot', 'right'], ['detail']), anchorForNode('anchor-face-center', 'node-plate-center', ['target', 'face', 'primary'], ['armor_plate', 'sensor', 'detail']), anchorForEdge('anchor-face-top', 'edge-frame-top', 0.5, ['target', 'face', 'top'], ['detail', 'sensor']), anchorForEdge('anchor-face-bottom', 'edge-frame-bottom', 0.5, ['target', 'face', 'bottom'], ['detail']), anchorForNode('anchor-counterweight', 'node-counterweight', ['counterweight', 'reset'], ['detail']), anchorForNode('anchor-actuator-base', 'node-actuator-base', ['service', 'actuator'], ['detail', 'connector']), anchorForEdge('anchor-actuator-arm', 'edge-actuator-body', 0.58, ['service', 'actuator', 'rod'], ['detail', 'connector']), { ...anchorForNode('anchor-sensor-top', 'node-sensor-top', ['sensor', 'top'], ['sensor', 'detail']), exportSocket: true }, anchorForAttachment( 'anchor-base-top', 'attach-base-plinth', 'top', ['base', 'service'], ['detail', 'sensor'], { exportSocket: true } ), anchorForAttachment( 'anchor-face-front', 'attach-target-face', 'front', ['target', 'hit_face', 'front'], ['sensor', 'detail'], { exportSocket: true } ), anchorForAttachment( 'anchor-face-back', 'attach-target-face', 'back', ['target', 'service', 'back'], ['connector', 'detail'], { exportSocket: true } ), anchorForAttachment( 'anchor-face-left', 'attach-target-face', 'left', ['target', 'marker', 'left'], ['detail', 'sensor'], { exportSocket: true } ), anchorForAttachment( 'anchor-face-right', 'attach-target-face', 'right', ['target', 'marker', 'right'], ['detail', 'sensor'], { exportSocket: true } ) ], generators: [], attachments: [ { id: 'attach-base-plinth', anchorId: 'anchor-base', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: 0.05, z: 0 }, shape: { primitive: 'box', size: sharedBoxSize('base-plinth'), chamfer: { edge: 0.08, corner: 0.12 }, profile: { kind: 'block', intensity: 0.78 } } }, { id: 'attach-base-strut-front', anchorId: 'anchor-base', moduleId: 'detail_tool_arm', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.18 }, rotation: { x: 90, y: 0, z: 0 }, shape: { primitive: 'cylinder', size: sharedCylinderSize('base-strut-front', 14) } }, { id: 'attach-base-strut-back', anchorId: 'anchor-base', moduleId: 'detail_tool_arm', portId: 'mount', offset: { x: 0, y: 0.02, z: -0.18 }, rotation: { x: 90, y: 0, z: 0 }, shape: { primitive: 'cylinder', size: sharedCylinderSize('base-strut-back', 14) } }, { id: 'attach-service-intake', anchorId: 'anchor-base-top', moduleId: 'detail_engine_intake', portId: 'mount', offset: { x: 0, y: 0.02, z: -0.06 }, scale: { x: 1.18, y: 0.94, z: 1 } }, { id: 'attach-service-louver', anchorId: 'anchor-base-top', moduleId: 'detail_engine_louver', portId: 'mount', offset: { x: 0, y: 0.025, z: 0.1 }, rotation: { x: 0, y: 180, z: 0 }, scale: { x: 1.1, y: 1, z: 1 } }, { id: 'attach-mast-core', anchorId: 'anchor-mast-mid', moduleId: 'core_spine_block', portId: 'mount', shape: { primitive: 'capsule', size: { x: 0.07, y: 0.78, segments: 16, rings: 12 }, profile: { kind: 'hardSurface', intensity: 0.52 } } }, { id: 'attach-pivot-hub', anchorId: 'anchor-pivot', moduleId: 'core_shoulder_joint', portId: 'mount', shape: { primitive: 'sphere', size: { x: 0.11, y: 0.11 } } }, { id: 'attach-yoke-left', anchorId: 'anchor-yoke-l', moduleId: 'detail_tool_arm', portId: 'mount', offset: { x: -0.01, y: 0, z: 0.02 }, shape: { primitive: 'capsule', size: { x: 0.038, y: 0.22, segments: 14, rings: 10 }, profile: { kind: 'hardSurface', intensity: 0.55 } } }, { id: 'attach-yoke-right', anchorId: 'anchor-yoke-r', moduleId: 'detail_tool_arm', portId: 'mount', offset: { x: 0.01, y: 0, z: 0.02 }, shape: { primitive: 'capsule', size: { x: 0.038, y: 0.22, segments: 14, rings: 10 }, profile: { kind: 'hardSurface', intensity: 0.55 } } }, { id: 'attach-target-face', anchorId: 'anchor-face-center', moduleId: 'detail_torso_plate', portId: 'mount', offset: { x: 0, y: 0, z: 0.01 }, shape: { primitive: 'box', size: sharedBoxSize('target-face'), taper: { xTop: 0.98, xBottom: 0.88, zTop: 0.96, zBottom: 1.04 }, chamfer: { edge: 0.1, corner: 0.14 }, profile: { kind: 'plate', intensity: 0.9 } }, sculpt: { enabled: true, primitive: 'roundedBox', size: { x: 0.48, y: 0.58, z: 0.072 }, roundness: 0.24, bulge: { enabled: true, radius: 0.035, smooth: 0.22, offset: { x: 0, y: 0.02, z: 0 } }, cut: { enabled: false, radius: 0.02, offset: { x: 0, y: 0, z: 0 } } } }, { id: 'attach-face-rib-top', anchorId: 'anchor-face-top', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: 0.005, z: 0.012 }, shape: { primitive: 'box', size: sharedBoxSize('face-rib-top'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.66 } } }, { id: 'attach-face-rib-bottom', anchorId: 'anchor-face-bottom', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: -0.005, z: 0.014 }, shape: { primitive: 'box', size: sharedBoxSize('face-rib-bottom'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.68 } } }, { id: 'attach-face-cheek-left', anchorId: 'anchor-face-left', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: -0.028, y: 0.01, z: 0.016 }, rotation: sharedRotation('face-cheek-left'), shape: { primitive: 'box', size: sharedBoxSize('face-cheek-left'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'plate', intensity: 0.74 } } }, { id: 'attach-face-cheek-right', anchorId: 'anchor-face-right', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0.028, y: 0.01, z: 0.016 }, rotation: sharedRotation('face-cheek-right'), shape: { primitive: 'box', size: sharedBoxSize('face-cheek-right'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'plate', intensity: 0.74 } } }, { id: 'attach-target-bull', anchorId: 'anchor-face-front', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.02 }, scale: { x: 1.2, y: 1.2, z: 1.2 } }, { id: 'attach-target-sensor', anchorId: 'anchor-sensor-top', moduleId: 'detail_sensor_head', portId: 'mount', offset: { x: 0, y: 0.02, z: 0.02 }, scale: { x: 0.88, y: 0.88, z: 0.88 } }, { id: 'attach-face-backbox', anchorId: 'anchor-face-back', moduleId: 'detail_backpack_module', portId: 'mount', offset: { x: 0, y: 0.04, z: -0.05 }, scale: { x: 0.92, y: 0.94, z: 0.9 } }, { id: 'attach-face-marker-left', anchorId: 'anchor-face-left', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: -0.01, y: 0.1, z: 0.012 }, scale: { x: 0.52, y: 0.52, z: 0.52 } }, { id: 'attach-face-marker-right', anchorId: 'anchor-face-right', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0.01, y: 0.1, z: 0.012 }, scale: { x: 0.52, y: 0.52, z: 0.52 } }, { id: 'attach-face-lock-top', anchorId: 'anchor-face-top', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: -0.014, z: 0.02 }, shape: { primitive: 'box', size: sharedBoxSize('face-lock-top'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.62 } } }, { id: 'attach-face-lock-bottom', anchorId: 'anchor-face-bottom', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: 0.015, z: 0.022 }, shape: { primitive: 'box', size: sharedBoxSize('face-lock-bottom'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.64 } } }, { id: 'attach-sensor-shroud', anchorId: 'anchor-sensor-top', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: -0.012, z: -0.006 }, shape: { primitive: 'box', size: sharedBoxSize('sensor-shroud'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.52 } } }, { id: 'attach-counterweight', anchorId: 'anchor-counterweight', moduleId: 'detail_weapon_counterweight', portId: 'mount', shape: { primitive: 'box', size: sharedBoxSize('counterweight-main'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'block', intensity: 0.66 } } }, { id: 'attach-counterweight-cap', anchorId: 'anchor-counterweight', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: 0.03, z: -0.02 }, scale: { x: 0.56, y: 0.46, z: 0.58 } }, { id: 'attach-counterweight-drum', anchorId: 'anchor-counterweight', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: -0.01, z: 0.01 }, rotation: { x: 90, y: 0, z: 0 }, shape: { primitive: 'cylinder', size: sharedCylinderSize('counterweight-drum'), profile: { kind: 'hardSurface', intensity: 0.58 } } }, { id: 'attach-actuator-body', anchorId: 'anchor-actuator-base', moduleId: 'detail_engine_block', portId: 'mount', rotation: { x: 0, y: 30, z: 90 }, shape: { primitive: 'cylinder', size: { x: 0.052, y: 0.22, segments: 16 }, profile: { kind: 'hardSurface', intensity: 0.48 } } }, { id: 'attach-actuator-rod', anchorId: 'anchor-actuator-arm', moduleId: 'detail_tool_arm', portId: 'mount', shape: { primitive: 'cylinder', size: { x: 0.026, y: 0.28, segments: 14 } } }, { id: 'attach-actuator-sleeve', anchorId: 'anchor-actuator-arm', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: -0.03, z: 0.01 }, rotation: { x: 0, y: 0, z: 90 }, shape: { primitive: 'cylinder', size: { x: 0.034, y: 0.16, segments: 14 }, profile: { kind: 'hardSurface', intensity: 0.44 } } } ], metadata: { runtimeTarget: createFlipTargetRuntimeMetadata('flip-target') } }; } if (name === 'flip-target-heavy') { const sharedHeavyId: FlipTargetSharedVariant['id'] = 'flip-target-heavy'; const sharedHeavyBoxSize = (nodeId: string) => { const [x, y, z] = getFlipTargetSharedNode(sharedHeavyId, nodeId).size ?? [0, 0, 0]; return { x, y, z }; }; const sharedHeavyCylinderSize = (nodeId: string, segments = 16) => { const [radius = 0, , length = 0] = getFlipTargetSharedNode(sharedHeavyId, nodeId).size ?? [0, 0, 0]; return { x: radius, y: length, segments }; }; const sharedHeavyRotation = (nodeId: string) => flipTargetNodeRotationToDegrees(getFlipTargetSharedNode(sharedHeavyId, nodeId)); const base = buildPresetRecipe('flip-target'); if (!base) return null; const setNodeX = (nodeId: string, x: number) => { const node = base.nodes.find((entry) => entry.id === nodeId); if (!node) return; node.position = { ...node.position, x }; }; setNodeX('node-yoke-l', getFlipTargetSharedNode(sharedHeavyId, 'yoke-left').position[0]); setNodeX('node-yoke-r', getFlipTargetSharedNode(sharedHeavyId, 'yoke-right').position[0]); setNodeX('node-plate-top-l', getFlipTargetSharedSocket(sharedHeavyId, 'face-left').position[0]); setNodeX('node-plate-top-r', getFlipTargetSharedSocket(sharedHeavyId, 'face-right').position[0]); setNodeX('node-plate-bottom-l', getFlipTargetSharedSocket(sharedHeavyId, 'face-left').position[0]); setNodeX('node-plate-bottom-r', getFlipTargetSharedSocket(sharedHeavyId, 'face-right').position[0]); base.nodes.push( { id: 'node-counterweight-left', position: { x: getFlipTargetSharedNode(sharedHeavyId, 'counterweight-left').position[0], y: 0.8, z: getFlipTargetSharedNode(sharedHeavyId, 'counterweight-left').position[2] }, tags: ['counterweight'] }, { id: 'node-counterweight-right', position: { x: getFlipTargetSharedNode(sharedHeavyId, 'counterweight-right').position[0], y: 0.8, z: getFlipTargetSharedNode(sharedHeavyId, 'counterweight-right').position[2] }, tags: ['counterweight'] } ); base.edges.push( { id: 'edge-counterweight-arm-left', a: 'node-pivot', b: 'node-counterweight-left', radius: 0.034, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } }, { id: 'edge-counterweight-arm-right', a: 'node-pivot', b: 'node-counterweight-right', radius: 0.034, curve: 'line', controlOffset: { x: 0, y: 0, z: 0 } } ); base.anchors.push( anchorForNode('anchor-counterweight-left', 'node-counterweight-left', ['counterweight', 'reset', 'left'], ['detail']), anchorForNode('anchor-counterweight-right', 'node-counterweight-right', ['counterweight', 'reset', 'right'], ['detail']) ); const targetFace = base.attachments.find((entry) => entry.id === 'attach-target-face'); if (targetFace) { targetFace.shape = mergeAttachmentShape(targetFace.shape, { size: sharedHeavyBoxSize('target-face'), taper: { xTop: 1.0, xBottom: 0.9, zTop: 1.0, zBottom: 1.1 } }); if (targetFace.sculpt) { targetFace.sculpt.size = { x: 0.68, y: 0.7, z: 0.095 }; } } base.attachments = [ ...base.attachments.filter((entry) => ![ 'attach-counterweight', 'attach-face-cheek-left', 'attach-face-cheek-right' ].includes(entry.id) ), { id: 'attach-counterweight-left', anchorId: 'anchor-counterweight-left', moduleId: 'detail_weapon_counterweight', portId: 'mount', shape: { primitive: 'box', size: sharedHeavyBoxSize('counterweight-left'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'block', intensity: 0.72 } } }, { id: 'attach-counterweight-right', anchorId: 'anchor-counterweight-right', moduleId: 'detail_weapon_counterweight', portId: 'mount', shape: { primitive: 'box', size: sharedHeavyBoxSize('counterweight-right'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'block', intensity: 0.72 } } }, { id: 'attach-armored-cheek-left', anchorId: 'anchor-face-left', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: -0.045, y: 0.015, z: 0.022 }, rotation: sharedHeavyRotation('armored-cheek-left'), shape: { primitive: 'box', size: sharedHeavyBoxSize('armored-cheek-left'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'plate', intensity: 0.82 } } }, { id: 'attach-armored-cheek-right', anchorId: 'anchor-face-right', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0.045, y: 0.015, z: 0.022 }, rotation: sharedHeavyRotation('armored-cheek-right'), shape: { primitive: 'box', size: sharedHeavyBoxSize('armored-cheek-right'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'plate', intensity: 0.82 } } }, { id: 'attach-face-visor-top', anchorId: 'anchor-face-top', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: 0.01, z: 0.02 }, shape: { primitive: 'box', size: sharedHeavyBoxSize('face-visor-top'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'plate', intensity: 0.82 } } }, { id: 'attach-face-skirt-bottom', anchorId: 'anchor-face-bottom', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: -0.012, z: 0.024 }, shape: { primitive: 'box', size: sharedHeavyBoxSize('face-skirt-bottom'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'plate', intensity: 0.82 } } }, { id: 'attach-visor-cheek-link-left', anchorId: 'anchor-face-left', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0.005, y: 0.24, z: 0.01 }, rotation: sharedHeavyRotation('visor-cheek-link-left'), shape: { primitive: 'box', size: sharedHeavyBoxSize('visor-cheek-link-left'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.72 } } }, { id: 'attach-visor-cheek-link-right', anchorId: 'anchor-face-right', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: -0.005, y: 0.24, z: 0.01 }, rotation: sharedHeavyRotation('visor-cheek-link-right'), shape: { primitive: 'box', size: sharedHeavyBoxSize('visor-cheek-link-right'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.72 } } }, { id: 'attach-skirt-lock-left', anchorId: 'anchor-face-bottom', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: -0.2, y: 0.015, z: 0.022 }, shape: { primitive: 'box', size: sharedHeavyBoxSize('skirt-lock-left'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.74 } } }, { id: 'attach-skirt-lock-right', anchorId: 'anchor-face-bottom', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0.2, y: 0.015, z: 0.022 }, shape: { primitive: 'box', size: sharedHeavyBoxSize('skirt-lock-right'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.74 } } }, { id: 'attach-counterweight-bridge', anchorId: 'anchor-face-back', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: -0.2, z: -0.16 }, shape: { primitive: 'box', size: sharedHeavyBoxSize('counterweight-bridge'), chamfer: { edge: 0.08, corner: 0.1 }, profile: { kind: 'hardSurface', intensity: 0.72 } } }, { id: 'attach-rear-damper', anchorId: 'anchor-actuator-arm', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0, y: -0.06, z: 0.02 }, rotation: { x: 0, y: 0, z: 90 }, shape: { primitive: 'cylinder', size: sharedHeavyCylinderSize('rear-damper'), profile: { kind: 'hardSurface', intensity: 0.54 } } } ]; base.metadata = { runtimeTarget: createFlipTargetRuntimeMetadata('flip-target-heavy') }; return base; } if (name === 'flip-target-scout') { const sharedScoutId: FlipTargetSharedVariant['id'] = 'flip-target-scout'; const sharedScoutBoxSize = (nodeId: string) => { const [x, y, z] = getFlipTargetSharedNode(sharedScoutId, nodeId).size ?? [0, 0, 0]; return { x, y, z }; }; const sharedScoutSphereSize = (nodeId: string, segments = 16, rings = 12) => { const [radius = 0] = getFlipTargetSharedNode(sharedScoutId, nodeId).size ?? [0, 0, 0]; return { x: radius, y: radius, segments, rings }; }; const sharedScoutCylinderSize = (nodeId: string, segments = 16) => { const [radius = 0, , length = 0] = getFlipTargetSharedNode(sharedScoutId, nodeId).size ?? [0, 0, 0]; return { x: radius, y: length, segments }; }; const sharedScoutRotation = (nodeId: string) => flipTargetNodeRotationToDegrees(getFlipTargetSharedNode(sharedScoutId, nodeId)); const base = buildPresetRecipe('flip-target'); if (!base) return null; const targetFace = base.attachments.find((entry) => entry.id === 'attach-target-face'); if (targetFace) { targetFace.shape = mergeAttachmentShape(targetFace.shape, { size: sharedScoutBoxSize('target-face'), taper: { xTop: 0.96, xBottom: 0.84, zTop: 0.96, zBottom: 1.02 } }); if (targetFace.sculpt) { targetFace.sculpt.size = { x: 0.34, y: 0.66, z: 0.065 }; } } base.attachments = [ ...base.attachments, { id: 'attach-sensor-mast', anchorId: 'anchor-sensor-top', moduleId: 'detail_tool_arm', portId: 'mount', offset: { x: 0, y: -0.07, z: -0.01 }, rotation: { x: 90, y: 0, z: 0 }, shape: { primitive: 'cylinder', size: sharedScoutCylinderSize('sensor-mast'), profile: { kind: 'hardSurface', intensity: 0.48 } } }, { id: 'attach-beacon-arm-left', anchorId: 'anchor-face-left', moduleId: 'detail_tool_arm', portId: 'mount', offset: { x: 0.005, y: 0.2, z: 0.004 }, rotation: sharedScoutRotation('beacon-arm-left'), shape: { primitive: 'box', size: sharedScoutBoxSize('beacon-arm-left'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.58 } } }, { id: 'attach-beacon-arm-right', anchorId: 'anchor-face-right', moduleId: 'detail_tool_arm', portId: 'mount', offset: { x: -0.005, y: 0.2, z: 0.004 }, rotation: sharedScoutRotation('beacon-arm-right'), shape: { primitive: 'box', size: sharedScoutBoxSize('beacon-arm-right'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.58 } } }, { id: 'attach-beacon-pod-left', anchorId: 'anchor-face-left', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: -0.02, y: 0.27, z: 0.01 }, shape: { primitive: 'sphere', size: sharedScoutSphereSize('beacon-pod-left'), profile: { kind: 'plate', intensity: 0.7 } } }, { id: 'attach-beacon-pod-right', anchorId: 'anchor-face-right', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0.02, y: 0.27, z: 0.01 }, shape: { primitive: 'sphere', size: sharedScoutSphereSize('beacon-pod-right'), profile: { kind: 'plate', intensity: 0.7 } } }, { id: 'attach-counter-fin', anchorId: 'anchor-face-back', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: -0.1, z: -0.12 }, rotation: sharedScoutRotation('counter-fin'), shape: { primitive: 'box', size: sharedScoutBoxSize('counter-fin'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'plate', intensity: 0.62 } } } ]; base.metadata = { runtimeTarget: createFlipTargetRuntimeMetadata('flip-target-scout') }; return base; } if (name === 'flip-target-breacher') { const sharedBreacherId: FlipTargetSharedVariant['id'] = 'flip-target-breacher'; const sharedBreacherBoxSize = (nodeId: string) => { const [x, y, z] = getFlipTargetSharedNode(sharedBreacherId, nodeId).size ?? [0, 0, 0]; return { x, y, z }; }; const sharedBreacherSphereSize = (nodeId: string, segments = 16, rings = 12) => { const [radius = 0] = getFlipTargetSharedNode(sharedBreacherId, nodeId).size ?? [0, 0, 0]; return { x: radius, y: radius, segments, rings }; }; const sharedBreacherRotation = (nodeId: string) => flipTargetNodeRotationToDegrees(getFlipTargetSharedNode(sharedBreacherId, nodeId)); const base = buildPresetRecipe('flip-target-heavy'); if (!base) return null; const targetFace = base.attachments.find((entry) => entry.id === 'attach-target-face'); if (targetFace) { targetFace.shape = mergeAttachmentShape(targetFace.shape, { size: sharedBreacherBoxSize('target-face'), taper: { xTop: 1.02, xBottom: 0.94, zTop: 1.0, zBottom: 1.14 } }); if (targetFace.sculpt) { targetFace.sculpt.size = { x: 0.78, y: 0.78, z: 0.105 }; } } base.attachments = [ ...base.attachments, { id: 'attach-breach-bar-top', anchorId: 'anchor-face-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: 0.15, z: 0.018 }, shape: { primitive: 'box', size: sharedBreacherBoxSize('breach-bar-top'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'plate', intensity: 0.84 } } }, { id: 'attach-breach-bar-mid', anchorId: 'anchor-face-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: 0, z: 0.018 }, shape: { primitive: 'box', size: sharedBreacherBoxSize('breach-bar-mid'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'plate', intensity: 0.84 } } }, { id: 'attach-breach-bar-bottom', anchorId: 'anchor-face-front', moduleId: 'detail_forearm_plate', portId: 'mount', offset: { x: 0, y: -0.15, z: 0.018 }, shape: { primitive: 'box', size: sharedBreacherBoxSize('breach-bar-bottom'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'plate', intensity: 0.84 } } }, { id: 'attach-lock-dog-left', anchorId: 'anchor-face-left', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: 0.12, y: -0.02, z: 0.05 }, rotation: sharedBreacherRotation('lock-dog-left'), shape: { primitive: 'box', size: sharedBreacherBoxSize('lock-dog-left'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.78 } } }, { id: 'attach-lock-dog-right', anchorId: 'anchor-face-right', moduleId: 'detail_engine_block', portId: 'mount', offset: { x: -0.12, y: -0.02, z: 0.05 }, rotation: sharedBreacherRotation('lock-dog-right'), shape: { primitive: 'box', size: sharedBreacherBoxSize('lock-dog-right'), chamfer: { edge: 0.08, corner: 0.08 }, profile: { kind: 'hardSurface', intensity: 0.78 } } }, { id: 'attach-warning-beacon', anchorId: 'anchor-sensor-top', moduleId: 'detail_scope_lens', portId: 'mount', offset: { x: 0, y: 0.08, z: 0.01 }, shape: { primitive: 'sphere', size: sharedBreacherSphereSize('warning-beacon'), profile: { kind: 'plate', intensity: 0.72 } } } ]; base.metadata = { runtimeTarget: createFlipTargetRuntimeMetadata('flip-target-breacher') }; return base; } return null; } function applyPreset(name: string): void { const doc = buildPresetRecipe(name); if (!doc) return; const shouldUseLowPresetLod = shouldUsePreviewLodForPreset(name, doc) && !useAttachmentPreviewLod; const restorePresetLod = shouldUseLowPresetLod; if (restorePresetLod) { useAttachmentPreviewLod = true; pendingPresetLodRestore = true; pendingPresetLodRestoreToken += 1; presetLodRestore = { token: pendingPresetLodRestoreToken, presetName: name }; setStatus('Loading preset in preview LOD for faster generation...'); } else { pendingPresetLodRestore = false; presetLodRestore = null; } currentBasePresetName = name; applyRecipe(doc); if (restorePresetLod) { setStatus('Preset loaded. Upgrading to full detail...'); schedulePresetLodRestore(name, pendingPresetLodRestoreToken); } focusPresetViewport(name); controls.presetSelect.value = name; characterSandboxState.leaperMotionStartedAtMs = performance.now(); if (name === 'leaper-arc') { characterSandboxState.leaperMotionPreset = 'idle'; } setMode('select'); void autoSelectPoseForCurrentArchetype({ load: true }); setStatus(`Preset loaded: ${name}.`); } function createDefaultRecipe(): RecipeDocument { return { version: 1, nodes: [ { id: 'node-1', position: { x: 0, y: 0, z: 0 }, tags: [] }, { id: 'node-2', position: { x: 0, y: 0.6, z: 0 }, tags: [] } ], edges: [ { id: 'edge-1', a: 'node-1', b: 'node-2', radius: 0.08, curve: 'catmull', controlOffset: { x: 0.1, y: 0.1, z: 0 } } ], anchors: [ { id: 'anchor-1', type: 'edge', edgeId: 'edge-1', s: 1, orientation: 'alongEdge', radialAngle: 0, tags: ['handle_tip'], accepts: ['weapon', 'sensor'] } ], generators: [], attachments: [] }; } function handleCanvasPointerDown(event: PointerEvent): void { markFirstInteractiveFrame(); state.pendingDeselect = false; const rect = controls.canvas.getBoundingClientRect(); pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(pointer, camera); raycaster.layers.mask = 0; raycaster.layers.enable(LAYER_NODE); if ( state.mode === 'select' && (state.selection.kind === 'node' || state.selection.kind === 'attachment') && transform.dragging ) { return; } if (state.surfacePickMode || state.mode === 'surface') { raycaster.layers.mask = 0; raycaster.layers.enable(LAYER_ATTACHMENT); const hits = raycastSurfaceTargets.length > 0 ? raycaster.intersectObjects(raycastSurfaceTargets, true) : []; if (hits.length > 0) { const hit = hits[0]; const normalMatrix = new THREE.Matrix3().getNormalMatrix(hit.object.matrixWorld); const normal = hit.face?.normal.clone().applyMatrix3(normalMatrix).normalize() ?? new THREE.Vector3(0, 1, 0); const parentAttachmentId = getAttachmentIdFromObject(hit.object); let localPoint: Vec3Like | undefined; let localNormal: Vec3Like | undefined; if (parentAttachmentId) { const attachment = getAttachmentById(parentAttachmentId); if (attachment) { const attachmentMatrix = hit.object.matrixWorld.clone(); const inverse = attachmentMatrix.clone().invert(); const local = hit.point.clone().applyMatrix4(inverse); const localNormalMatrix = new THREE.Matrix3().getNormalMatrix(inverse); const localN = normal.clone().applyMatrix3(localNormalMatrix).normalize(); localPoint = fromThree(local); localNormal = fromThree(localN); } } lastSurfaceHit = { point: fromThree(hit.point), normal: fromThree(normal), parentAttachmentId, localPoint, localNormal }; if (state.mode === 'surface') { addSurfaceAnchorFromHit( lastSurfaceHit.point, lastSurfaceHit.normal, lastSurfaceHit.parentAttachmentId, lastSurfaceHit.localPoint, lastSurfaceHit.localNormal ); } else { setStatus('Surface anchor point captured. Click Add Anchor to store.'); } } else { setStatus('No surface hit. Try again.'); } state.surfacePickMode = false; return; } updateRaycasterLayers(); const nodeHits = raycastNodeTargets.length > 0 ? raycaster.intersectObjects(raycastNodeTargets, true) : []; const anchorHits = state.showAnchors && raycastAnchorTargets.length > 0 ? raycaster.intersectObjects(raycastAnchorTargets, true) : []; const attachmentHits = state.showModules && raycastAttachmentTargets.length > 0 ? raycaster.intersectObjects(raycastAttachmentTargets, true) : []; const generatorHits = state.showGenerators && raycastGeneratorTargets.length > 0 ? raycaster.intersectObjects(raycastGeneratorTargets, true) : []; const scenePropHits = state.showSceneProps && raycastScenePropTargets.length > 0 ? raycaster.intersectObjects(raycastScenePropTargets, true) : []; const edgeHits = state.showEdges && raycastEdgeTargets.length > 0 ? raycaster.intersectObjects(raycastEdgeTargets, true) : []; const candidates = [ { kind: 'generator' as SelectionKind, hit: generatorHits[0] }, { kind: 'scene-prop' as SelectionKind, hit: scenePropHits[0] }, { kind: 'attachment' as SelectionKind, hit: attachmentHits[0] }, { kind: 'node' as SelectionKind, hit: nodeHits[0] }, { kind: 'anchor' as SelectionKind, hit: anchorHits[0] }, { kind: 'edge' as SelectionKind, hit: edgeHits[0] } ].filter((entry) => Boolean(entry.hit)); let hitKind: SelectionKind = null; let hitId: string | undefined; if (candidates.length > 0) { candidates.sort((a, b) => (a.hit!.distance ?? 0) - (b.hit!.distance ?? 0)); hitKind = candidates[0].kind; hitId = hitKind === 'scene-prop' ? getScenePropIdFromObject(candidates[0].hit?.object ?? null) : candidates[0].hit?.object.userData?.id as string | undefined; } const hitNodeId = nodeHits.length > 0 ? (nodeHits[0].object.userData?.id as string | undefined) : undefined; const handleHits = generatorHandleMeshes.length > 0 ? raycaster.intersectObjects(generatorHandleMeshes, true) : []; const handleHit = handleHits.length > 0 ? handleHits[0] : null; if (handleHit && event.button === 0 && state.mode === 'select') { const data = handleHit.object.userData ?? {}; const handle = data.handle as GeneratorDragState['handle'] | undefined; const generatorId = data.generatorId as string | undefined; if (handle && generatorId) { const axis = (data.axis as THREE.Vector3) ?? new THREE.Vector3(0, 1, 0); const basePoint = (data.basePoint as THREE.Vector3) ?? new THREE.Vector3(); const baseValue = typeof data.baseValue === 'number' ? data.baseValue : 0; setAttachmentPreviewLod(true); state.generatorDragState = { generatorId, handle, pointerId: event.pointerId, axis: axis.clone().normalize(), basePoint: basePoint.clone(), baseValue }; orbit.enabled = false; controls.canvas.setPointerCapture(event.pointerId); return; } } if (event.button === 2) { event.preventDefault(); if (hitKind && hitId) { if (event.shiftKey) { if (hitKind === 'node') duplicateNodeById(hitId); if (hitKind === 'edge') duplicateEdgeById(hitId); if (hitKind === 'anchor') duplicateAnchorById(hitId); if (hitKind === 'attachment') duplicateAttachmentById(hitId); } else { if (hitKind === 'node') deleteNodeById(hitId); if (hitKind === 'edge') deleteEdgeById(hitId); if (hitKind === 'anchor') deleteAnchorById(hitId); if (hitKind === 'attachment') deleteAttachmentById(hitId); if (hitKind === 'scene-prop') deleteScenePropById(hitId); } } return; } if (state.mode === 'add-edge') { if (!hitNodeId) { setStatus('Add edge mode: click a node to start.'); return; } if (!state.edgeDraftStart) { state.edgeDraftStart = hitNodeId; setSelection('node', hitNodeId); controls.nodeList.value = hitNodeId; rebuildScene(); refreshUi(); setStatus(`Edge start set to ${hitNodeId}. Select end node.`); return; } if (state.edgeDraftStart === hitNodeId) { setStatus('Pick a different node for the edge end.'); return; } addEdgeBetweenNodes(state.edgeDraftStart, hitNodeId, 'Added edge'); state.edgeDraftStart = null; return; } if (state.mode === 'add-node') { const ground = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); const hit = new THREE.Vector3(); const hasHit = raycaster.ray.intersectPlane(ground, hit); if (hasHit) { const node = addNodeAtPosition(fromThree(hit), 'Placed node'); controls.nodeX.value = node.position.x.toFixed(2); controls.nodeY.value = node.position.y.toFixed(2); controls.nodeZ.value = node.position.z.toFixed(2); } else { setStatus('Unable to place node here.'); } return; } if (hitNodeId) { selectNodeInstance(hitNodeId); refreshUi(); if (state.mode === 'select' && event.button === 0) { const node = getNodeById(hitNodeId); if (node) { const worldPos = getNodeWorldPosition(node); const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -worldPos.y); const hit = new THREE.Vector3(); if (raycaster.ray.intersectPlane(plane, hit)) { setAttachmentPreviewLod(true); state.dragState = { nodeId: hitNodeId, plane, offset: hit.clone().sub(worldPos), pointerId: event.pointerId }; orbit.enabled = false; controls.canvas.setPointerCapture(event.pointerId); } } } setStatus(`Selected node ${hitNodeId}.`); } else if (state.mode === 'select' && hitKind === 'edge' && hitId) { setSelection('edge', hitId); controls.edgeList.value = hitId; refreshTransformTarget(); refreshUi(); setStatus(`Selected edge ${hitId}.`); } else if (state.mode === 'select' && hitKind === 'anchor' && hitId) { setSelection('anchor', hitId); controls.anchorList.value = hitId; controls.attachmentAnchor.value = hitId; refreshTransformTarget(); refreshUi(); updateAnchorParentControls(getAnchorById(hitId)); setStatus(`Selected anchor ${hitId}.`); const anchor = getAnchorById(hitId); if (anchor && event.button === 0) { const frame = resolveAnchorFrame(anchor); if (frame) { if (event.altKey) { setAttachmentPreviewLod(true); state.anchorDragState = { anchorId: hitId, pointerId: event.pointerId, mode: 'normal', baseFrame: frame, baseOffset: anchor.offset ? { ...anchor.offset } : vec3(0, 0, 0) }; orbit.enabled = false; controls.canvas.setPointerCapture(event.pointerId); } else if (anchor.type === 'edge' && anchor.edgeId) { setAttachmentPreviewLod(true); state.anchorDragState = { anchorId: hitId, pointerId: event.pointerId, mode: 'edge', baseFrame: frame, baseOffset: anchor.offset ? { ...anchor.offset } : vec3(0, 0, 0) }; orbit.enabled = false; controls.canvas.setPointerCapture(event.pointerId); } } } } else if (state.mode === 'select' && hitKind === 'generator' && hitId) { setSelection('generator', hitId); controls.generatorList.value = hitId; updateGeneratorInputs(getGeneratorById(hitId)); refreshTransformTarget(); refreshUi(); setStatus(`Selected generator ${hitId}.`); } else if (state.mode === 'select' && hitKind === 'attachment' && hitId) { const instanceIndex = candidates[0].hit?.object.userData?.generatorIndex as number | undefined; selectAttachmentInstance(hitId, instanceIndex); refreshUi(); setStatus(`Selected module ${formatEditorItemLabel(hitId)}.`); } else if (state.mode === 'select' && hitKind === 'scene-prop' && hitId) { selectScenePropInstance(hitId); refreshUi(); } else { if (state.mode === 'select') { state.pendingDeselect = true; return; } setSelection(null); refreshTransformTarget(); refreshUi(); } } function processCanvasPointerMove(event: PointerEvent): void { updateEdgePreview(event); if (state.generatorDragState) { const rect = controls.canvas.getBoundingClientRect(); pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(pointer, camera); const drag = state.generatorDragState; const generator = getGeneratorById(drag.generatorId); if (!generator) return; if (generator.type === 'alongEdge') { const baseAnchor = getAnchorById(generator.baseAnchorId); if (!baseAnchor || baseAnchor.type !== 'edge' || !baseAnchor.edgeId) return; const edge = getEdgeById(baseAnchor.edgeId); if (!edge) return; const cache = getEdgeCache(edge); if (!cache) return; const result = findClosestEdgeSample(raycaster.ray, edge); if (!result) return; const params = generator.params as GeneratorParamsAlong; const startLen = Math.max(0, params.startLen ?? 0); const endLen = params.endLen > 0 ? Math.min(params.endLen, cache.totalLength) : cache.totalLength; const step = event.shiftKey ? 0 : 0.02; if (drag.handle === 'start') { const nextStart = Math.min(snapValue(result.len, step), endLen); params.startLen = nextStart; controls.generatorStart.value = nextStart.toFixed(2); } else if (drag.handle === 'end') { const nextEnd = Math.max(snapValue(result.len, step), startLen); params.endLen = nextEnd; controls.generatorEnd.value = nextEnd.toFixed(2); } else if (drag.handle === 'spacing') { const spacing = Math.max(0.01, snapValue(result.len - startLen, step)); params.spacing = spacing; params.mode = 'spacing'; controls.generatorSpacing.value = spacing.toFixed(2); controls.generatorMode.value = 'spacing'; } generator.params = params; rebuildDerivedGroups(); markPresetCustom(); return; } if (generator.type === 'radial' && drag.handle === 'radius') { const params = generator.params as GeneratorParamsRadial; const axis = drag.axis; const plane = new THREE.Plane(axis, -axis.dot(drag.basePoint)); const hit = new THREE.Vector3(); if (raycaster.ray.intersectPlane(plane, hit)) { const step = event.shiftKey ? 0 : 0.02; const radius = Math.max(0.01, snapValue(hit.clone().sub(drag.basePoint).length(), step)); params.radius = radius; generator.params = params; controls.generatorRadius.value = radius.toFixed(2); rebuildDerivedGroups(); markPresetCustom(); } return; } if (generator.type === 'mirror' && drag.handle === 'mirror') { const params = generator.params as GeneratorParamsMirror; const axis = drag.axis; const lineStart = axis.clone().multiplyScalar(-10); const lineEnd = axis.clone().multiplyScalar(10); const pointOnRay = new THREE.Vector3(); const pointOnLine = new THREE.Vector3(); raycaster.ray.distanceSqToSegment(lineStart, lineEnd, pointOnRay, pointOnLine); const step = event.shiftKey ? 0 : 0.02; const offset = snapValue(pointOnLine.dot(axis), step); params.offset = offset; generator.params = params; controls.generatorPlaneOffset.value = offset.toFixed(2); rebuildDerivedGroups(); markPresetCustom(); return; } } if (state.anchorDragState) { const rect = controls.canvas.getBoundingClientRect(); pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(pointer, camera); const anchor = getAnchorById(state.anchorDragState.anchorId); if (!anchor) return; if (state.anchorDragState.mode === 'edge' && anchor.type === 'edge' && anchor.edgeId) { const edge = getEdgeById(anchor.edgeId); if (!edge) return; const result = findClosestEdgeSample(raycaster.ray, edge); if (!result) return; anchor.s = result.s; if (anchor.len !== undefined) { anchor.len = result.len; } if (!isUserTyping()) { controls.anchorS.value = result.s.toFixed(3); if (anchor.len !== undefined) { controls.anchorLen.value = result.len.toFixed(3); } } scheduleLiveRebuild(); markPresetCustom(); return; } if (state.anchorDragState.mode === 'normal') { const frame = state.anchorDragState.baseFrame; const lineStart = frame.origin.clone(); const lineEnd = frame.origin.clone().add(frame.up.clone().normalize()); const pointOnRay = new THREE.Vector3(); const pointOnLine = new THREE.Vector3(); raycaster.ray.distanceSqToSegment(lineStart, lineEnd, pointOnRay, pointOnLine); const delta = frame.up.clone().normalize().dot(pointOnLine.sub(lineStart)); const baseOffset = state.anchorDragState.baseOffset; const nextOffset = vec3(baseOffset.x, baseOffset.y + delta, baseOffset.z); const isZero = Math.abs(nextOffset.x) < 1e-5 && Math.abs(nextOffset.y) < 1e-5 && Math.abs(nextOffset.z) < 1e-5; anchor.offset = isZero ? undefined : nextOffset; scheduleLiveRebuild(); markPresetCustom(); return; } } if (state.dragState) { const rect = controls.canvas.getBoundingClientRect(); pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(pointer, camera); const hit = new THREE.Vector3(); if (!raycaster.ray.intersectPlane(state.dragState.plane, hit)) return; const node = getNodeById(state.dragState.nodeId); const mesh = nodeMeshMap.get(state.dragState.nodeId); if (!node || !mesh) return; const next = hit.clone().sub(state.dragState.offset); mesh.position.copy(next); updateNodeFromWorld(node, mesh.position, mesh.quaternion); updateNodeModeInputs(node); scheduleLiveRebuild(); pendingUiRefreshOnPointerUp = true; markPresetCustom(); return; } const rect = controls.canvas.getBoundingClientRect(); pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(pointer, camera); const nodeHits = raycastNodeTargets.length > 0 ? raycaster.intersectObjects(raycastNodeTargets, true) : []; const anchorHits = state.showAnchors && raycastAnchorTargets.length > 0 ? raycaster.intersectObjects(raycastAnchorTargets, true) : []; const attachmentHits = state.showModules && raycastAttachmentTargets.length > 0 ? raycaster.intersectObjects(raycastAttachmentTargets, true) : []; const generatorHits = state.showGenerators && raycastGeneratorTargets.length > 0 ? raycaster.intersectObjects(raycastGeneratorTargets, true) : []; const scenePropHits = state.showSceneProps && raycastScenePropTargets.length > 0 ? raycaster.intersectObjects(raycastScenePropTargets, true) : []; const edgeHits = state.showEdges && raycastEdgeTargets.length > 0 ? raycaster.intersectObjects(raycastEdgeTargets, true) : []; const candidates = [ { kind: 'generator' as SelectionKind, hit: generatorHits[0] }, { kind: 'scene-prop' as SelectionKind, hit: scenePropHits[0] }, { kind: 'attachment' as SelectionKind, hit: attachmentHits[0] }, { kind: 'node' as SelectionKind, hit: nodeHits[0] }, { kind: 'anchor' as SelectionKind, hit: anchorHits[0] }, { kind: 'edge' as SelectionKind, hit: edgeHits[0] } ].filter((entry) => Boolean(entry.hit)); let hitKind: SelectionKind = null; let hitId: string | undefined; if (candidates.length > 0) { candidates.sort((a, b) => (a.hit!.distance ?? 0) - (b.hit!.distance ?? 0)); hitKind = candidates[0].kind; hitId = hitKind === 'scene-prop' ? getScenePropIdFromObject(candidates[0].hit?.object ?? null) : candidates[0].hit?.object.userData?.id as string | undefined; } if (hitKind && hitId) { setHover(hitKind, hitId); } else { setHover(null); } if (hitKind === 'node' && hitId) { const node = getNodeById(hitId); if (node) { const rotation = node.rotation ? ` · rot ${node.rotation.x.toFixed(0)} ${node.rotation.y.toFixed(0)} ${node.rotation.z.toFixed(0)}` : ''; const world = getNodeWorldPosition(node); const local = node.mode === 'relative' ? ` · local ${node.position.x.toFixed(2)} ${node.position.y.toFixed(2)} ${node.position.z.toFixed(2)}` : ''; updateHoverText( `Node ${hitId} · world ${world.x.toFixed(2)} ${world.y.toFixed(2)} ${world.z.toFixed(2)}${local}${rotation}` ); return; } } if (hitKind === 'anchor' && hitId) { const anchor = getAnchorById(hitId); if (anchor) { const tagList = anchor.tags.length ? ` · tags: ${anchor.tags.join(', ')}` : ''; const offset = anchor.offset ? ` · offset r${anchor.offset.x.toFixed(2)} u${anchor.offset.y.toFixed(2)} f${anchor.offset.z.toFixed(2)}` : ''; const parent = anchor.surface?.parentAttachmentId ? ` · parent ${anchor.surface.parentAttachmentId}` : anchor.type === 'attachment' && anchor.attachmentId ? ` · parent ${anchor.attachmentId}` : ''; const face = anchor.type === 'attachment' && anchor.attachmentFace ? ` · face ${anchor.attachmentFace}` : ''; updateHoverText(`Anchor ${hitId} (${anchor.type})${tagList}${offset}${parent}${face}`); return; } } if (hitKind === 'edge' && hitId) { const edge = getEdgeById(hitId); if (edge) { const cache = getEdgeCache(edge); updateHoverText(`Edge ${hitId} · len ${cache?.totalLength.toFixed(2) ?? '0.00'} · r ${edge.radius.toFixed(2)}`); return; } } if (hitKind === 'attachment' && hitId) { const attachment = getAttachmentById(hitId); const module = attachment ? getModuleById(attachment.moduleId) : undefined; if (attachment) { const label = module?.label ?? attachment.moduleId; updateHoverText(`Attachment ${hitId} · ${label} @ ${attachment.anchorId}`); return; } } if (hitKind === 'generator' && hitId) { const generator = getGeneratorById(hitId); if (generator) { updateHoverText(`Generator ${hitId} · ${generator.type} @ ${generator.baseAnchorId}`); return; } } if (hitKind === 'scene-prop' && hitId) { const prop = getScenePropById(hitId); if (prop) { updateHoverText(`Terrain/Prop ${hitId} · ${prop.kind} · scale ${prop.scale.x.toFixed(2)} ${prop.scale.y.toFixed(2)} ${prop.scale.z.toFixed(2)}`); return; } } if (state.mode === 'add-edge' && state.edgeDraftStart) { updateHoverText(`Edge: start ${state.edgeDraftStart} ? pick end node`); return; } updateHoverText('Move over a node, anchor, edge, module, or terrain prop for details.'); } function handleCanvasPointerMove(event: PointerEvent): void { markFirstInteractiveFrame(); pendingPointerMove = event; if (pointerMoveScheduled) return; pointerMoveScheduled = true; requestAnimationFrame(() => { pointerMoveScheduled = false; if (!pendingPointerMove) return; const latest = pendingPointerMove; pendingPointerMove = null; processCanvasPointerMove(latest); }); } function updateEdgePreview(event: PointerEvent): void { if (state.mode !== 'add-edge' || !state.edgeDraftStart) { edgePreviewLine.visible = false; edgeSnapMarker.visible = false; return; } const startNode = getNodeById(state.edgeDraftStart); if (!startNode) { edgePreviewLine.visible = false; edgeSnapMarker.visible = false; return; } const rect = controls.canvas.getBoundingClientRect(); pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(pointer, camera); const intersects = raycastNodeTargets.length > 0 ? raycaster.intersectObjects(raycastNodeTargets, true) : []; const hitNode = intersects.length > 0 ? (intersects[0].object as THREE.Mesh) : null; const hitNodeId = hitNode?.userData?.id as string | undefined; const start = getNodeWorldPosition(startNode); let end: THREE.Vector3 | null = null; if (hitNodeId && hitNodeId !== state.edgeDraftStart) { const target = getNodeById(hitNodeId); if (target) { end = getNodeWorldPosition(target); edgeSnapMarker.visible = true; edgeSnapMarker.position.copy(end); } } else { const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -start.y); const hit = new THREE.Vector3(); if (raycaster.ray.intersectPlane(plane, hit)) { end = hit; edgeSnapMarker.visible = false; } } if (!end) { edgePreviewLine.visible = false; edgeSnapMarker.visible = false; return; } const geom = edgePreviewLine.geometry as THREE.BufferGeometry; geom.setFromPoints([start, end]); geom.computeBoundingSphere(); edgePreviewLine.computeLineDistances(); edgePreviewLine.visible = true; } function handleCanvasPointerUp(): void { gizmoPointerActive = false; if (state.dragState) { const pointerId = state.dragState.pointerId; state.dragState = null; orbit.enabled = true; try { if (controls.canvas.hasPointerCapture(pointerId)) { controls.canvas.releasePointerCapture(pointerId); } } catch { // ignore } } if (state.anchorDragState) { const pointerId = state.anchorDragState.pointerId; state.anchorDragState = null; orbit.enabled = true; try { if (controls.canvas.hasPointerCapture(pointerId)) { controls.canvas.releasePointerCapture(pointerId); } } catch { // ignore } } if (state.generatorDragState) { const pointerId = state.generatorDragState.pointerId; state.generatorDragState = null; orbit.enabled = true; try { if (controls.canvas.hasPointerCapture(pointerId)) { controls.canvas.releasePointerCapture(pointerId); } } catch { // ignore } } setAttachmentPreviewLod(false); pendingPointerMove = null; if (pendingUiRefreshOnPointerUp) { pendingUiRefreshOnPointerUp = false; refreshUi(); } if (state.pendingDeselect && !gizmoPointerActive && !transform.dragging) { state.pendingDeselect = false; setSelection(null); rebuildScene(); refreshUi(); } if (state.mode !== 'add-edge') { edgePreviewLine.visible = false; edgeSnapMarker.visible = false; } } function handleCanvasPointerLeave(): void { gizmoPointerActive = false; if (state.dragState) { state.dragState = null; orbit.enabled = true; } if (state.anchorDragState) { state.anchorDragState = null; orbit.enabled = true; } if (state.generatorDragState) { state.generatorDragState = null; orbit.enabled = true; } setAttachmentPreviewLod(false); pendingPointerMove = null; pendingUiRefreshOnPointerUp = false; setHover(null); updateHoverText('Move over a node, anchor, edge, module, or terrain prop for details.'); edgePreviewLine.visible = false; edgeSnapMarker.visible = false; } function onTransformChange(): void { if (!transform.dragging) return; if (!state.selection.id) return; if (state.selection.kind === 'node') { const node = getNodeById(state.selection.id); const mesh = nodeMeshMap.get(state.selection.id); if (!node || !mesh) return; updateNodeFromWorld(node, mesh.position, mesh.quaternion); scheduleLiveRebuild(); pendingTransformUiRefresh = true; markPresetCustom(); return; } if (state.selection.kind === 'attachment') { const attachment = getAttachmentById(state.selection.id); const mesh = getAttachmentSelectionMesh(state.selection.id, state.attachmentInstanceIndex); if (!attachment || !mesh) return; const frames = getAttachmentFrames(attachment, state.attachmentInstanceIndex); const frame = frames[0]; if (!frame) return; mesh.updateMatrixWorld(true); const moduleEntry = getModuleById(attachment.moduleId); const anchorMatrix = frameToMatrix(frame); const local = anchorMatrix.clone().invert().multiply(mesh.matrixWorld); const mirrorX = attachment.mirrored ? -1 : 1; const normalizedLocal = mirrorX < 0 ? local.clone().multiply(new THREE.Matrix4().makeScale(-1, 1, 1)) : local; const pos = new THREE.Vector3(); const quat = new THREE.Quaternion(); const scale = new THREE.Vector3(); normalizedLocal.decompose(pos, quat, scale); const baseQuat = getAttachmentBaseQuaternion(attachment, moduleEntry); const isIdentity = baseQuat.equals(new THREE.Quaternion()); const userQuat = isIdentity ? quat : baseQuat.clone().invert().multiply(quat); const euler = new THREE.Euler().setFromQuaternion(userQuat, 'XYZ'); attachment.offset = Math.abs(pos.x) < 1e-5 && Math.abs(pos.y) < 1e-5 && Math.abs(pos.z) < 1e-5 ? undefined : vec3(pos.x, pos.y, pos.z); attachment.rotation = Math.abs(euler.x) < 1e-5 && Math.abs(euler.y) < 1e-5 && Math.abs(euler.z) < 1e-5 ? undefined : vec3(THREE.MathUtils.radToDeg(euler.x), THREE.MathUtils.radToDeg(euler.y), THREE.MathUtils.radToDeg(euler.z)); attachment.scale = Math.abs(scale.x - 1) < 1e-5 && Math.abs(scale.y - 1) < 1e-5 && Math.abs(scale.z - 1) < 1e-5 ? undefined : vec3(scale.x, scale.y, scale.z); if (state.mirrorAttachments && !isMirroringAttachments) { const mirror = findMirrorAttachment(attachment); if (mirror) { isMirroringAttachments = true; applyMirrorAttachmentTransform(attachment, mirror); isMirroringAttachments = false; } else { syncMirrorAttachment(attachment); } } pendingTransformUiRefresh = true; pendingAttachmentRebuild = true; markPresetCustom(); return; } if (state.selection.kind === 'scene-prop') { const prop = getScenePropById(state.selection.id); const object = state.selection.id ? scenePropMeshMap.get(state.selection.id) : null; if (!prop || !object) return; object.updateMatrixWorld(true); const pos = new THREE.Vector3(); const quat = new THREE.Quaternion(); const scale = new THREE.Vector3(); object.matrixWorld.decompose(pos, quat, scale); const euler = new THREE.Euler().setFromQuaternion(quat, 'XYZ'); prop.position = vec3(pos.x, pos.y, pos.z); prop.rotation = vec3( THREE.MathUtils.radToDeg(euler.x), THREE.MathUtils.radToDeg(euler.y), THREE.MathUtils.radToDeg(euler.z) ); prop.scale = vec3(scale.x, scale.y, scale.z); updateScenePropInfo(prop); pendingTransformUiRefresh = true; markRenderDirty(); } } function resize(): void { const parent = controls.canvas.parentElement; if (!parent) return; const width = Math.max(320, parent.clientWidth); const height = Math.max(320, parent.clientHeight); renderer.setPixelRatio(getEditorPixelRatio()); renderer.setSize(width, height, false); camera.aspect = width / height; camera.updateProjectionMatrix(); markRenderDirty(3); } function animate(now = performance.now()): void { requestAnimationFrame(animate); if (!pageVisible) { poseClock.getDelta(); return; } safeDetachTransform(); const dt = poseClock.getDelta(); updatePosePlayback(dt); const characterSandboxFxActive = updateCharacterSandboxFx(dt, now); updateCharacterSandboxInfo(now); let runtimeTargetPreviewActive = false; const dronePresetPreviewActive = updateDronePresetPreview(now); if (runtimeTargetTimelinePlaying) { const duration = getRuntimeTargetTimelineDurationMs(getRuntimeTargetMetadata()); runtimeTargetTimelineMs = Math.min(duration, runtimeTargetTimelineMs + dt * 1000); evaluateRuntimeTargetTimeline(runtimeTargetTimelineMs, true); runtimeTargetPreviewActive = true; if (runtimeTargetTimelineMs >= duration) { runtimeTargetTimelinePlaying = false; } } else if (controls.runtimeTargetTimeline && document.activeElement === controls.runtimeTargetTimeline) { runtimeTargetPreviewActive = true; } else { runtimeTargetPreviewActive = updateRuntimeTargetPreview(dt * 1000, now); } orbit.update(); updateLeaperTerrainPreview(); updateFootProbeOverlay(); const activeSceneMotion = poseState.playing || characterSandboxFxActive || characterSandboxState.autoFire || runtimeTargetPreviewActive || dronePresetPreviewActive || orbitInteracting || transform.dragging || isInteractionActive() || pointerMoveScheduled || pendingPointerMove !== null || pendingLiveRebuild || pendingTransformUiRefresh || pendingUiRefreshOnPointerUp; const dueIdleKeepalive = now - lastRenderTimeMs >= IDLE_RENDER_KEEPALIVE_MS; if (activeSceneMotion || renderDirtyFrames > 0 || dueIdleKeepalive) { renderer.render(scene, camera); lastRenderTimeMs = now; if (renderDirtyFrames > 0) { renderDirtyFrames -= 1; } } } controls.addNode.addEventListener('click', addNodeFromUi); controls.deleteNode.addEventListener('click', deleteNodeFromUi); controls.focusNode.addEventListener('click', focusSelection); controls.nodeList.addEventListener('change', () => { const id = controls.nodeList.value; if (!id) return; selectNodeInstance(id); refreshUi(); }); controls.nodeMode.addEventListener('change', applyNodeModeFromInputs); controls.nodeParent.addEventListener('change', applyNodeModeFromInputs); controls.nodePose.addEventListener('change', applyNodePoseFromInputs); controls.modeSelect.addEventListener('click', () => setMode('select')); controls.modeAddNode.addEventListener('click', () => setMode('add-node')); controls.modeAddEdge.addEventListener('click', () => setMode('add-edge')); controls.modeSurface.addEventListener('click', () => setMode('surface')); for (const button of sidebarCapabilityButtons) { button.addEventListener('click', () => { const capability = button.dataset.ueCapability as SidebarCapability | undefined; if (!capability) return; setActiveSidebarCapability(capability); }); } controls.toggleGrid.addEventListener('click', () => { state.showGrid = !state.showGrid; updateToolbarState(); updateVisibility(); }); controls.toggleEdges.addEventListener('click', () => { state.showEdges = !state.showEdges; updateToolbarState(); updateVisibility(); }); controls.toggleAnchors.addEventListener('click', () => { state.showAnchors = !state.showAnchors; updateToolbarState(); updateVisibility(); }); controls.toggleGenerators.addEventListener('click', () => { state.showGenerators = !state.showGenerators; updateToolbarState(); updateVisibility(); rebuildDerivedGroups(); }); controls.toggleModules.addEventListener('click', () => { state.showModules = !state.showModules; updateToolbarState(); updateVisibility(); }); controls.toggleProxy.addEventListener('click', () => { state.showProxy = !state.showProxy; updateToolbarState(); updateVisibility(); }); controls.toggleSkeleton?.addEventListener('click', () => { state.showSkeleton = !state.showSkeleton; updateToolbarState(); rebuildScene(); }); controls.toggleGizmo.addEventListener('click', () => { state.gizmoEnabled = !state.gizmoEnabled; transform.visible = state.gizmoEnabled && state.mode === 'select'; transform.enabled = state.gizmoEnabled; if (!state.gizmoEnabled) { transform.detach(); } updateToolbarState(); updateVisibility(); }); controls.gizmoTranslate.addEventListener('click', () => setGizmoMode('translate')); controls.gizmoRotate.addEventListener('click', () => setGizmoMode('rotate')); controls.gizmoScale.addEventListener('click', () => setGizmoMode('scale')); controls.addEdge.addEventListener('click', addEdgeFromUi); controls.deleteEdge.addEventListener('click', deleteEdgeFromUi); controls.focusEdge.addEventListener('click', focusSelection); controls.edgeList.addEventListener('change', () => { const id = controls.edgeList.value; if (!id) return; setSelection('edge', id); refreshTransformTarget(); refreshUi(); }); controls.addAnchor.addEventListener('click', addAnchorFromUi); controls.deleteAnchor.addEventListener('click', deleteAnchorFromUi); controls.focusAnchor.addEventListener('click', focusSelection); controls.anchorType.addEventListener('change', updateAnchorTypeUi); controls.detachAnchorParent.addEventListener('click', () => { const id = controls.anchorList.value; if (!id) return; detachSurfaceAnchorParent(id); }); controls.anchorList.addEventListener('change', () => { const id = controls.anchorList.value; if (!id) return; setSelection('anchor', id); controls.attachmentAnchor.value = id; const anchor = getAnchorById(id); updateAnchorTypeInputs(anchor); updateAnchorParentControls(anchor); updateAnchorMetaInputs(anchor); refreshTransformTarget(); refreshUi(); }); controls.anchorProfile?.addEventListener('change', applyAnchorMetaFromInputs); controls.anchorExportSocket?.addEventListener('change', applyAnchorMetaFromInputs); controls.pickSurface.addEventListener('click', () => { setMode('surface'); }); controls.addGenerator.addEventListener('click', addGeneratorFromUi); controls.deleteGenerator.addEventListener('click', deleteGeneratorFromUi); controls.showGenerators.addEventListener('change', () => { state.showGenerators = controls.showGenerators.checked; updateToolbarState(); updateVisibility(); rebuildDerivedGroups(); }); controls.showAttachmentAxes.addEventListener('change', () => { state.showAttachmentAxes = controls.showAttachmentAxes.checked; updateVisibility(); rebuildDerivedGroups(); }); controls.profileGeneration.addEventListener('change', () => { state.profileGeneration = controls.profileGeneration.checked; if (state.profileGeneration) { rebuildDerivedGroups(); } }); controls.generatorList.addEventListener('change', () => { const id = controls.generatorList.value; if (!id) return; setSelection('generator', id); updateGeneratorInputs(getGeneratorById(id)); refreshTransformTarget(); refreshUi(); }); [ controls.generatorMode, controls.generatorCount, controls.generatorSpacing, controls.generatorStart, controls.generatorEnd, controls.generatorRadius, controls.generatorAngle, controls.generatorPlane, controls.generatorPlaneOffset ].forEach((input) => { input.addEventListener('input', applyGeneratorFromInputs); }); controls.poseLoad.addEventListener('click', () => { loadPoseFromSelection(); }); controls.poseLoadYBot.addEventListener('click', () => { void loadYBotPoseAssetAndPlay(); }); controls.poseSelect.addEventListener('change', () => { updatePoseInfo('Select a pose and click Load.'); }); controls.posePlay.addEventListener('click', async () => { if (!poseState.clip || poseState.file !== controls.poseSelect.value) { await loadPoseFromSelection(); } if (!poseState.clip) return; poseState.playing = !poseState.playing; if (poseState.playing) { poseClock.start(); } updatePoseControls(); }); controls.poseStop.addEventListener('click', () => { stopPosePlayback(true); poseState.time = 0; if (poseState.action && poseState.mixer) { poseState.action.time = 0; poseState.mixer.update(0); } controls.poseTime.value = '0'; }); controls.poseWeaponHold.addEventListener('change', () => { poseState.weaponHoldEnabled = controls.poseWeaponHold.checked; updatePoseControls(); if (poseState.clip) { applyPoseToNodes(); } setStatus(`Weapon hold pose ${poseState.weaponHoldEnabled ? 'enabled' : 'disabled'}.`); }); controls.poseInfluence.addEventListener('input', () => { poseState.influence = THREE.MathUtils.clamp(parseNumber(controls.poseInfluence.value, 1), 0, 1); if (poseState.clip) { applyPoseToNodes(); } }); controls.poseUpperBodyInfluence.addEventListener('input', () => { poseState.upperBodyInfluence = THREE.MathUtils.clamp(parseNumber(controls.poseUpperBodyInfluence.value, 1), 0, 1); if (poseState.clip) { applyPoseToNodes(); } }); controls.poseWeaponHoldHandBlend.addEventListener('input', () => { poseState.weaponHoldHandBlend = THREE.MathUtils.clamp( parseNumber(controls.poseWeaponHoldHandBlend.value, 0.35), 0, 1 ); if (poseState.clip && poseState.weaponHoldEnabled) { applyPoseToNodes(); } }); controls.poseWeaponHoldShoulderBlend.addEventListener('input', () => { poseState.weaponHoldShoulderBlend = THREE.MathUtils.clamp( parseNumber(controls.poseWeaponHoldShoulderBlend.value, 0.12), 0, 1 ); if (poseState.clip && poseState.weaponHoldEnabled) { applyPoseToNodes(); } }); controls.poseWeaponHoldSupportElbowBias.addEventListener('input', () => { poseState.weaponHoldSupportElbowBias = THREE.MathUtils.clamp( parseNumber(controls.poseWeaponHoldSupportElbowBias.value, 0.28), 0, 1 ); if (poseState.clip && poseState.weaponHoldEnabled) { applyPoseToNodes(); } }); controls.poseLoop.addEventListener('change', () => { poseState.loop = controls.poseLoop.checked; applyPoseLoopSetting(); }); controls.poseRoot.addEventListener('change', () => { poseState.rootMotion = controls.poseRoot.checked; applyPoseToNodes(); }); controls.poseSpeed.addEventListener('change', () => { poseState.speed = Math.max(0.1, parseNumber(controls.poseSpeed.value, 1)); }); controls.poseTime.addEventListener('input', () => { if (!poseState.clip) return; poseState.playing = false; updatePoseControls(); const t = parseNumber(controls.poseTime.value, 0); setPoseTime(t * poseState.duration, false); }); controls.charSandboxEnable.addEventListener('change', () => { characterSandboxState.enabled = controls.charSandboxEnable.checked; if (!characterSandboxState.enabled) { characterSandboxState.autoFire = false; characterSandboxState.muzzleFlashTimer = 0; characterSandboxState.recoilKick = 0; } if (poseState.clip) { applyPoseToNodes(); } updatePoseControls(); setStatus(`Character sandbox ${characterSandboxState.enabled ? 'enabled' : 'disabled'}.`); }); controls.charAnimIdle.addEventListener('click', () => { if (isLeaperSandboxActivePreset()) { setLeaperSandboxMotionPreset('idle'); setStatus('Leaper preview set to idle.'); return; } void setCharacterSandboxPosePreset('idle'); }); controls.charAnimAim.addEventListener('click', () => { if (isLeaperSandboxActivePreset()) { setLeaperSandboxMotionPreset('walk'); setStatus('Leaper preview set to walk.'); return; } void setCharacterSandboxPosePreset('aim'); }); controls.charAnimFire.addEventListener('click', () => { if (isLeaperSandboxActivePreset()) { setLeaperSandboxMotionPreset('leap'); setStatus('Leaper preview set to leap.'); return; } void setCharacterSandboxPosePreset('fire'); triggerCharacterSandboxFireFx(performance.now()); markRenderDirty(2); }); controls.charAimYaw.addEventListener('input', () => { characterSandboxState.aimYawDeg = THREE.MathUtils.clamp(parseNumber(controls.charAimYaw.value, 0), -90, 90); if (poseState.clip) { applyPoseToNodes(); } updateCharacterSandboxInfo(performance.now(), true); }); controls.charAimPitch.addEventListener('input', () => { characterSandboxState.aimPitchDeg = THREE.MathUtils.clamp(parseNumber(controls.charAimPitch.value, 0), -60, 60); if (poseState.clip) { applyPoseToNodes(); } updateCharacterSandboxInfo(performance.now(), true); }); controls.charAimAds.addEventListener('input', () => { characterSandboxState.adsBlend = THREE.MathUtils.clamp(parseNumber(controls.charAimAds.value, 0.7), 0, 1); if (poseState.clip) { applyPoseToNodes(); } updateCharacterSandboxInfo(performance.now(), true); }); controls.charFireOnce.addEventListener('click', () => { triggerCharacterSandboxFireFx(performance.now()); if (poseState.clip && !poseState.playing) { applyPoseToNodes(); } markRenderDirty(2); }); controls.charFireLoop.addEventListener('change', () => { characterSandboxState.autoFire = controls.charFireLoop.checked; if (characterSandboxState.autoFire) { characterSandboxState.nextAutoFireAtMs = performance.now(); } updateCharacterSandboxInfo(performance.now(), true); }); controls.charFireRate.addEventListener('change', () => { characterSandboxState.fireRateHz = THREE.MathUtils.clamp(parseNumber(controls.charFireRate.value, 6), 1, 20); updateCharacterSandboxInfo(performance.now(), true); }); controls.reloadPreset.addEventListener('click', () => { forceReloadCurrentPreset(); }); controls.attachModule.addEventListener('click', attachModuleFromUi); controls.detachModule.addEventListener('click', detachModuleFromUi); controls.moduleFilter.addEventListener('input', () => refreshModuleOptions()); controls.attachmentAnchor.addEventListener('change', () => refreshModuleOptions()); controls.showIncompatible.addEventListener('change', () => refreshModuleOptions()); controls.attachmentList.addEventListener('change', () => { const id = controls.attachmentList.value; if (!id) return; selectAttachmentInstance(id); refreshUi(); }); controls.attachmentGenerator?.addEventListener('change', applyAttachmentGeneratorFromInputs); controls.attachmentGeneratorIndex?.addEventListener('input', applyAttachmentGeneratorFromInputs); controls.mirrorAttachments?.addEventListener('change', () => { state.mirrorAttachments = controls.mirrorAttachments?.checked ?? state.mirrorAttachments; }); [ controls.attachmentOffsetX, controls.attachmentOffsetY, controls.attachmentOffsetZ, controls.attachmentRotX, controls.attachmentRotY, controls.attachmentRotZ, controls.attachmentScaleX, controls.attachmentScaleY, controls.attachmentScaleZ ].forEach((input) => { input.addEventListener('input', applyAttachmentTransformFromInputs); }); [ controls.attachmentPrimitive, controls.attachmentSizeX, controls.attachmentSizeY, controls.attachmentSizeZ, controls.attachmentSizeMinor, controls.attachmentSizeSegments, controls.attachmentSizeRings, controls.attachmentTaperXTop, controls.attachmentTaperXBottom, controls.attachmentTaperZTop, controls.attachmentTaperZBottom, controls.attachmentChamferEdge, controls.attachmentChamferCorner, controls.attachmentProfileKind, controls.attachmentProfileIntensity ].forEach((input) => { if (!input) return; input.addEventListener('input', applyAttachmentShapeFromInputs); input.addEventListener('change', applyAttachmentShapeFromInputs); }); [ controls.sculptEnable, controls.sculptPrimitive, controls.sculptSizeX, controls.sculptSizeY, controls.sculptSizeZ, controls.sculptRoundness, controls.sculptBulgeEnable, controls.sculptBulgeRadius, controls.sculptBulgeSmooth, controls.sculptBulgeOffsetX, controls.sculptBulgeOffsetY, controls.sculptBulgeOffsetZ, controls.sculptCutEnable, controls.sculptCutRadius, controls.sculptCutOffsetX, controls.sculptCutOffsetY, controls.sculptCutOffsetZ ].forEach((input) => { input.addEventListener('input', applyAttachmentSculptFromInputs); }); controls.attachmentRotateYawNeg.addEventListener('click', () => nudgeAttachmentRotation('y', -15)); controls.attachmentRotateYawPos.addEventListener('click', () => nudgeAttachmentRotation('y', 15)); controls.attachmentRotatePitchNeg.addEventListener('click', () => nudgeAttachmentRotation('x', -15)); controls.attachmentRotatePitchPos.addEventListener('click', () => nudgeAttachmentRotation('x', 15)); controls.attachmentRotateRollNeg.addEventListener('click', () => nudgeAttachmentRotation('z', -15)); controls.attachmentRotateRollPos.addEventListener('click', () => nudgeAttachmentRotation('z', 15)); controls.attachmentTransformReset.addEventListener('click', resetAttachmentTransform); controls.attachmentShapeReset.addEventListener('click', resetAttachmentShapeOverride); controls.sculptReset.addEventListener('click', resetAttachmentSculpt); controls.sculptToggle.addEventListener('click', () => { const willOpen = controls.sculptPanel.hidden; controls.sculptPanel.hidden = !willOpen; controls.sculptToggle.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); if (willOpen && !sdfMesherModule) { queueSdfMesherHydration(); } }); controls.presetSelect.addEventListener('change', () => { const value = controls.presetSelect.value; if (value === 'custom') return; applyPreset(value); }); controls.weaponQaToggle.addEventListener('change', () => { state.showWeaponQa = controls.weaponQaToggle.checked; if (state.showWeaponQa && !weaponIterationModules) { queueWeaponIterationHydration(); } rebuildDerivedGroups(); refreshUi(); setStatus(`Weapon QA overlay ${state.showWeaponQa ? 'enabled' : 'disabled'}.`); }); controls.bakeProxy.addEventListener('click', bakeProxy); controls.showProxy.addEventListener('change', () => { state.showProxy = controls.showProxy.checked; updateToolbarState(); updateVisibility(); rebuildDerivedGroups(); }); controls.showSceneProps.addEventListener('change', () => { state.showSceneProps = controls.showSceneProps.checked; updateVisibility(); refreshUi(); }); controls.enableLeaperTerrainIk.addEventListener('change', () => { state.enableLeaperTerrainIk = controls.enableLeaperTerrainIk.checked; updateLeaperTerrainPreview(); refreshUi(); }); controls.leaperIkForeAftLimit.addEventListener('input', () => { state.leaperIkForeAftLimitDeg = Number(controls.leaperIkForeAftLimit.value) || LEAPER_ARC_TERRAIN_IK_DEFAULT_LIMITS.foreAftTiltLimitDeg; updateLeaperIkTiltLabels(); updateLeaperTerrainPreview(); refreshUi(); }); controls.leaperIkLateralLimit.addEventListener('input', () => { state.leaperIkLateralLimitDeg = Number(controls.leaperIkLateralLimit.value) || LEAPER_ARC_TERRAIN_IK_DEFAULT_LIMITS.lateralTiltLimitDeg; updateLeaperIkTiltLabels(); updateLeaperTerrainPreview(); refreshUi(); }); controls.showFootProbes.addEventListener('change', () => { state.showFootProbes = controls.showFootProbes.checked; updateVisibility(); refreshUi(); }); controls.showFootProbeLabels.addEventListener('change', () => { state.showFootProbeLabels = controls.showFootProbeLabels.checked; refreshUi(); }); controls.sceneTerrainPreset.addEventListener('change', () => { const preset = controls.sceneTerrainPreset.value as TerrainPatchPreset; applyTerrainPresetInputs(preset); }); controls.scenePropList.addEventListener('change', () => { const id = controls.scenePropList.value; if (!id) return; selectScenePropInstance(id); refreshUi(); }); controls.scenePropSpawnUnder.addEventListener('click', () => { spawnSceneProp(controls.scenePropKind.value as ScenePropKind, true); }); controls.scenePropSpawnCenter.addEventListener('click', () => { spawnSceneProp(controls.scenePropKind.value as ScenePropKind, false); }); controls.scenePropFocus.addEventListener('click', focusSelection); controls.scenePropDelete.addEventListener('click', () => { const id = controls.scenePropList.value; if (!id) return; deleteScenePropById(id); }); controls.scenePropClear.addEventListener('click', clearSceneProps); controls.sceneTerrainApply.addEventListener('click', applySceneTerrainToSelected); controls.exportObj.addEventListener('click', exportProxyObj); controls.exportSockets?.addEventListener('click', exportSockets); controls.exportRuntimeTarget?.addEventListener('click', exportRuntimeTargetPackage); controls.runtimeTargetActivate?.addEventListener('click', () => { runtimeTargetTimelinePlaying = false; triggerRuntimeTargetPreview('activate'); }); controls.runtimeTargetHit?.addEventListener('click', () => { runtimeTargetTimelinePlaying = false; triggerRuntimeTargetPreview('hit'); }); controls.runtimeTargetRecover?.addEventListener('click', () => { runtimeTargetTimelinePlaying = false; triggerRuntimeTargetPreview('recover'); }); controls.runtimeTargetAutoRecover?.addEventListener('change', () => { updateRuntimeTargetPreviewInfo(); }); controls.runtimeTargetTriggerEditorToggle?.addEventListener('click', () => { runtimeTargetTriggerEditorExpanded = !runtimeTargetTriggerEditorExpanded; updateRuntimeTargetTriggerEditorLayout(); }); controls.runtimeTargetTriggerList?.addEventListener('change', () => { runtimeTargetSelectedTriggerKey = controls.runtimeTargetTriggerList?.value ?? null; runtimeTargetTriggerEditorExpanded = true; updateRuntimeTargetTriggerInputs(); }); controls.runtimeTargetTriggerEvent?.addEventListener('change', applySelectedRuntimeTargetTriggerInputs); controls.runtimeTargetTriggerAmplitude?.addEventListener('input', applySelectedRuntimeTargetTriggerInputs); controls.runtimeTargetTriggerDelay?.addEventListener('input', applySelectedRuntimeTargetTriggerInputs); controls.runtimeTargetTriggerDuration?.addEventListener('input', applySelectedRuntimeTargetTriggerInputs); controls.runtimeTargetMotionProfile?.addEventListener('change', applySelectedRuntimeTargetTriggerInputs); controls.runtimeTargetTriggerFlicker?.addEventListener('change', applySelectedRuntimeTargetTriggerInputs); controls.runtimeTargetSoundCue?.addEventListener('change', applySelectedRuntimeTargetTriggerInputs); controls.runtimeTargetSoundFrequency?.addEventListener('input', applySelectedRuntimeTargetTriggerInputs); controls.runtimeTargetSequencePlay?.addEventListener('click', () => { runtimeTargetTimelinePlaying = true; runtimeTargetTimelineMs = 0; runtimeTargetTimelineLastMs = 0; evaluateRuntimeTargetTimeline(0, false); }); controls.runtimeTargetSequenceStop?.addEventListener('click', () => { runtimeTargetTimelinePlaying = false; runtimeTargetTimelineLastMs = runtimeTargetTimelineMs; updateRuntimeTargetTimelineInfo(); }); controls.runtimeTargetTimeline?.addEventListener('input', () => { runtimeTargetTimelinePlaying = false; runtimeTargetTimelineMs = parseNumber(controls.runtimeTargetTimeline?.value ?? '0', 0); evaluateRuntimeTargetTimeline(runtimeTargetTimelineMs, false); }); controls.saveJson.addEventListener('click', () => { const doc = serializeRecipe(); const blob = new Blob([JSON.stringify(doc, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'unit-editor-v2-recipe.json'; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); setStatus('Recipe JSON saved.'); }); controls.resetSkeleton.addEventListener('click', resetBaseSkeleton); controls.reset.addEventListener('click', resetRecipe); controls.loadJson.addEventListener('change', async () => { const file = controls.loadJson.files?.[0]; controls.loadJson.value = ''; if (!file) return; const text = await file.text(); try { const doc = JSON.parse(text) as RecipeDocument; applyRecipe(doc); controls.presetSelect.value = 'custom'; hasUserEditedRecipe = true; setMode('select'); setStatus('Recipe loaded.'); } catch (error) { console.error(error); setStatus('Failed to load recipe JSON.'); } }); controls.canvas.addEventListener('pointerdown', handleCanvasPointerDown); controls.canvas.addEventListener('pointermove', handleCanvasPointerMove); controls.canvas.addEventListener('pointerup', handleCanvasPointerUp); controls.canvas.addEventListener('pointerleave', handleCanvasPointerLeave); controls.canvas.addEventListener('contextmenu', (event) => event.preventDefault()); transform.addEventListener('objectChange', onTransformChange); transform.addEventListener('dragging-changed', (event) => { orbit.enabled = !event.value; }); transform.addEventListener('mouseDown', () => { gizmoPointerActive = true; state.pendingDeselect = false; pendingAttachmentRebuild = false; setAttachmentPreviewLod(true); }); transform.addEventListener('mouseUp', () => { gizmoPointerActive = false; setAttachmentPreviewLod(false); if (pendingAttachmentRebuild) { pendingAttachmentRebuild = false; rebuildScene(); } if (pendingTransformUiRefresh) { pendingTransformUiRefresh = false; refreshUi(); } }); window.addEventListener('resize', resize); window.addEventListener('pointerdown', markFirstInteractiveFrame, { capture: true, passive: true }); window.addEventListener('keydown', markFirstInteractiveFrame, { capture: true }); window.addEventListener('keydown', (event) => { markFirstInteractiveFrame(); if (isUserTyping()) return; const key = event.key.toLowerCase(); if (key === 'v') { setMode('select'); } else if (key === 'n') { setMode('add-node'); } else if (key === 'e') { setMode('add-edge'); } else if (key === 's') { setMode('surface'); } else if (key === 'g') { state.showGrid = !state.showGrid; updateToolbarState(); updateVisibility(); } else if (key === 'd') { state.showEdges = !state.showEdges; updateToolbarState(); updateVisibility(); } else if (key === 'a') { state.showAnchors = !state.showAnchors; updateToolbarState(); updateVisibility(); } else if (key === 'r') { state.showGenerators = !state.showGenerators; updateToolbarState(); updateVisibility(); rebuildDerivedGroups(); } else if (key === 'm') { state.showModules = !state.showModules; updateToolbarState(); updateVisibility(); } else if (key === 'p') { state.showProxy = !state.showProxy; updateToolbarState(); updateVisibility(); } else if (key === 'x') { state.gizmoEnabled = !state.gizmoEnabled; transform.visible = state.gizmoEnabled && state.mode === 'select'; transform.enabled = state.gizmoEnabled; updateToolbarState(); updateVisibility(); } else if (state.selection.kind === 'anchor' && (key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright')) { event.preventDefault(); const normalStep = 0.05; const step = event.shiftKey ? 0.1 : 0.02; if (event.shiftKey && (key === 'arrowup' || key === 'arrowdown')) { nudgeAnchorOffset(0, key === 'arrowup' ? normalStep : -normalStep, 0); } else if (key === 'arrowup') { nudgeAnchorOffset(0, 0, step); } else if (key === 'arrowdown') { nudgeAnchorOffset(0, 0, -step); } else if (key === 'arrowleft') { nudgeAnchorOffset(-step, 0, 0); } else if (key === 'arrowright') { nudgeAnchorOffset(step, 0, 0); } } else if (key === 'escape') { setMode('select'); state.edgeDraftStart = null; state.surfacePickMode = false; setStatus('Select mode. Ready.'); } }); type InitialBootRecipeResult = { basePreset: string; isCustom: boolean; recipe: RecipeDocument; status: string; }; function resolveInitialBootRecipe(): InitialBootRecipeResult { let basePreset = 'basic'; try { const storedBasePreset = window.localStorage.getItem(STORAGE_BASE_PRESET_KEY); if (storedBasePreset) { basePreset = storedBasePreset; } } catch { // ignore storage read failures } try { const stored = window.localStorage.getItem(STORAGE_KEY); if (stored) { const doc = JSON.parse(stored) as RecipeDocument; const reconciled = reconcileStandaloneWeaponCustomRecipe(doc, basePreset); return { basePreset, isCustom: true, recipe: reconciled.doc, status: reconciled.reconciled ? `Loaded saved custom recipe from base preset ${basePreset} and refreshed its standalone weapon parts to the latest preset version.` : `Loaded saved custom recipe from base preset ${basePreset}. Click Reload Preset to see latest preset edits.` }; } } catch { // ignore parse failures and fall back to default } return { basePreset: 'basic', isCustom: false, recipe: createDefaultRecipe(), status: 'Ready.' }; } currentBasePresetName = 'basic'; controls.presetSelect.value = 'basic'; updatePoseControls(); resize(); updateToolbarState(); updateVisibility(); animate(); requestAnimationFrame(() => { scheduleIdleTask(() => { const initialBootRecipe = resolveInitialBootRecipe(); currentBasePresetName = initialBootRecipe.basePreset; controls.presetSelect.value = initialBootRecipe.isCustom ? 'custom' : initialBootRecipe.basePreset; applyRecipe(initialBootRecipe.recipe, { deferSceneRebuild: true, deferUiRefresh: true }); updateToolbarState(); setStatus('Bootstrapping scene...'); scheduleIdleTask(() => { rebuildScene({ deferDerivedRebuild: true, deferDerivedUntilInteractive: true, chunkNodeBuild: true, onComplete: () => { bootHydrationInProgress = false; refreshUi({ immediate: true }); updateRecipePreview(true); setStatus(initialBootRecipe.status); markRenderDirty(4); } }); }); scheduleIdleTask(() => { void loadPoseManifest({ autoLoadClip: false }); }); }); });