From edc7a6004d83f4b60d93388d78984f1126e0e4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B0=D0=B5=D1=80=20=D0=9C=D0=B8=D1=85=D0=B0=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D1=87?= Date: Wed, 26 Aug 2026 11:27:32 +0300 Subject: [PATCH] fix: refine avatar accessory occlusion --- packages/avatar-core/src/geometry.ts | 92 +++++++++++++++++-- src/features/avatar/__tests__/avatars-test.ts | 6 +- src/features/avatar/avatars.ts | 2 +- .../avatar/components/ExpressionWorkspace.tsx | 2 + .../export/standaloneEngine.generated.ts | 2 +- .../__tests__/rendered-scene-test.ts | 35 +++++-- src/features/rendering/avatarAppearance.ts | 10 ++ .../rendering/components/AvatarCanvas.tsx | 2 + src/features/rendering/renderedScene.ts | 8 ++ .../studio/components/AvatarDrawer.tsx | 2 + src/features/studio/useStudioController.ts | 7 +- 11 files changed, 137 insertions(+), 31 deletions(-) diff --git a/packages/avatar-core/src/geometry.ts b/packages/avatar-core/src/geometry.ts index 3b4ae41..99d6073 100644 --- a/packages/avatar-core/src/geometry.ts +++ b/packages/avatar-core/src/geometry.ts @@ -1382,7 +1382,7 @@ const headPath = (pose: AvatarPose, surface: SurfaceConfig) => { return path(convexHull(projectedSamples)) } -const accessoryPath = (pose: AvatarPose, node: BodyNode) => { +const accessoryGeometry = (pose: AvatarPose, node: BodyNode) => { const key = surfaceCacheKey(node.surface) let localSamples = accessorySamplesCache.get(key) if (!localSamples) { @@ -1415,11 +1415,13 @@ const accessoryPath = (pose: AvatarPose, node: BodyNode) => { (node.surface.type === 'cube' || node.surface.type === 'diamond') && node.surface.roundness <= 0 ) { - return path(hull) + return { hull, path: path(hull) } } - return smoothClosedPath(densifyClosedPoints(hull)) + return { hull, path: smoothClosedPath(densifyClosedPoints(hull)) } } +type Point2 = readonly [number, number] + const ACCESSORY_FRONT_CROSSING_RATIO = 0.1 const accessoryCameraDepthRadius = (pose: AvatarPose, node: BodyNode) => { @@ -1444,18 +1446,83 @@ const accessoryCameraDepthRadius = (pose: AvatarPose, node: BodyNode) => { ) } -const accessoryLayers = (pose: AvatarPose, nodes: BodyNode[]) => { +const polygonSignedArea = (points: readonly Point2[]) => + points.reduce((total, point, index) => { + const next = points[(index + 1) % points.length] + return total + point[0] * next[1] - next[0] * point[1] + }, 0) / 2 + +const convexPolygonIntersection = (subject: Point2[], clip: Point2[]) => { + let output = subject + const orientation = Math.sign(polygonSignedArea(clip)) || 1 + const inside = (point: Point2, start: Point2, end: Point2) => + orientation * + ((end[0] - start[0]) * (point[1] - start[1]) - (end[1] - start[1]) * (point[0] - start[0])) >= + 0 + const crossing = (from: Point2, to: Point2, start: Point2, end: Point2): Point2 => { + const deltaX = to[0] - from[0] + const deltaY = to[1] - from[1] + const edgeX = end[0] - start[0] + const edgeY = end[1] - start[1] + const denominator = deltaX * edgeY - deltaY * edgeX + const amount = + denominator === 0 + ? 0 + : ((start[0] - from[0]) * edgeY - (start[1] - from[1]) * edgeX) / denominator + return [from[0] + amount * deltaX, from[1] + amount * deltaY] + } + + clip.forEach((start, index) => { + const end = clip[(index + 1) % clip.length] + const input = output + output = [] + input.forEach((current, currentIndex) => { + const previous = input[(currentIndex + input.length - 1) % input.length] + const currentInside = inside(current, start, end) + const previousInside = inside(previous, start, end) + if (currentInside) { + if (!previousInside) output.push(crossing(previous, current, start, end)) + output.push(current) + } else if (previousInside) { + output.push(crossing(previous, current, start, end)) + } + }) + }) + return output +} + +const AUTO_ACCESSORY_EYE_OCCLUSION_RATIO = 0.08 + +const eyeOcclusionRatio = (accessory: Point2[], eye: Point2[]) => { + const eyeArea = Math.abs(polygonSignedArea(eye)) + if (eyeArea === 0) return 0 + return Math.abs(polygonSignedArea(convexPolygonIntersection(eye, accessory))) / eyeArea +} + +const accessoryLayers = (pose: AvatarPose, nodes: BodyNode[], visibleEyes: Point2[][]) => { const layers = nodes .map(node => { const depth = rotateWithQuaternion(pose.orientation, node.position)[2] + const geometry = accessoryGeometry(pose, node) + const stronglyOccludesEye = + node.layer === 'auto' && + visibleEyes.some( + eye => + eyeOcclusionRatio( + geometry.hull.map(point => [point[0], point[1]] as Point2), + eye + ) >= AUTO_ACCESSORY_EYE_OCCLUSION_RATIO + ) return { id: node.id, - path: accessoryPath(pose, node), + path: geometry.path, depth, front: node.layer === 'front' || - (node.layer !== 'back' && - depth > accessoryCameraDepthRadius(pose, node) * ACCESSORY_FRONT_CROSSING_RATIO), + (node.layer === 'auto' + ? !stronglyOccludesEye + : node.layer !== 'back' && + depth > accessoryCameraDepthRadius(pose, node) * ACCESSORY_FRONT_CROSSING_RATIO), } }) .sort((left, right) => left.depth - right.depth) @@ -1477,7 +1544,12 @@ export const renderAvatar = ( const rightSamples = eyePoints(pose, surface, 1, blink, options.eyeOffset) const left = leftSamples.map(sample => sample.point) const right = rightSamples.map(sample => sample.point) - const accessories = accessoryLayers(pose, options.bodyNodes ?? []) + const leftVisible = leftSamples.reduce((total, sample) => total + sample.normal[2], 0) > 0 + const rightVisible = rightSamples.reduce((total, sample) => total + sample.normal[2], 0) > 0 + const visibleEyes = [leftVisible ? left : null, rightVisible ? right : null] + .filter((eye): eye is Point3[] => eye !== null) + .map(eye => eye.map(point => [point[0], point[1]] as Point2)) + const accessories = accessoryLayers(pose, options.bodyNodes ?? [], visibleEyes) const compositePaths = compositeBackPaths(pose, surface) return { backPaths: [...compositePaths, ...accessories.backPaths], @@ -1487,8 +1559,8 @@ export const renderAvatar = ( headPath: headPath(pose, surface), leftPath: path(left), rightPath: path(right), - leftVisible: leftSamples.reduce((total, sample) => total + sample.normal[2], 0) > 0, - rightVisible: rightSamples.reduce((total, sample) => total + sample.normal[2], 0) > 0, + leftVisible, + rightVisible, wirePaths: options.includeWire === false ? [] : wirePaths(pose, surface), } } diff --git a/src/features/avatar/__tests__/avatars-test.ts b/src/features/avatar/__tests__/avatars-test.ts index ce09a39..f8bfc5a 100644 --- a/src/features/avatar/__tests__/avatars-test.ts +++ b/src/features/avatar/__tests__/avatars-test.ts @@ -85,10 +85,10 @@ describe('OneWorks animal construction', () => { library.avatars.find(avatar => avatar.id === id)?.body.nodes.map(node => node.layer) expect(layers('oneworks-cat')).toBeUndefined() - expect(layers('oneworks-dog')).toEqual(['front', 'front']) + expect(layers('oneworks-dog')).toEqual(['auto', 'auto']) expect(layers('oneworks-bear')).toEqual(['back', 'back']) expect(layers('oneworks-rabbit')).toEqual(['back', 'back']) - expect(layers('oneworks-bun')).toEqual(['front']) + expect(layers('oneworks-bun')).toEqual(['back']) }) it('keeps the unfinished piglet hidden and removes a persisted copy', () => { @@ -153,7 +153,7 @@ describe('OneWorks animal construction', () => { avatar => avatar.id === 'oneworks-dog' )! - expect(dog.body.nodes.map(node => node.layer)).toEqual(['front', 'front']) + expect(dog.body.nodes.map(node => node.layer)).toEqual(['auto', 'auto']) expect(dog.body.nodes.map(node => node.position)).toEqual([ [-82, -65, 0], [82, -65, 0], diff --git a/src/features/avatar/avatars.ts b/src/features/avatar/avatars.ts index cc12557..443f7b7 100644 --- a/src/features/avatar/avatars.ts +++ b/src/features/avatar/avatars.ts @@ -746,7 +746,7 @@ const oneWorksBundledAvatars: StudioAvatar[] = [ nodes: [ oneWorksNode('bun-crown', 'Rounded bun crown', 'cone', [0, -46, -14], 0.5, 0.23, { scaleZ: 0.5, - layer: 'front', + layer: 'back', surface: { roundness: 0.92, topScale: 0.82 }, }), ], diff --git a/src/features/avatar/components/ExpressionWorkspace.tsx b/src/features/avatar/components/ExpressionWorkspace.tsx index 45c5700..6529fbe 100644 --- a/src/features/avatar/components/ExpressionWorkspace.tsx +++ b/src/features/avatar/components/ExpressionWorkspace.tsx @@ -32,6 +32,7 @@ import { avatarBodyOutlineWidth, resolveAvatarNodeColor, useAvatarBodyColorOverride, + useRenderedSceneNodeOrder, } from '@/features/rendering/avatarAppearance' import { LivePixelAvatarCanvas, @@ -199,6 +200,7 @@ export function LiveExpressionPreview({ expressionBodyColor?: string id: string }) { + useRenderedSceneNodeOrder(scene) const bodyColorIsOverridden = useAvatarBodyColorOverride(colors.body, baseBodyColor) if (renderStyle.type === 'pixel') { diff --git a/src/features/export/standaloneEngine.generated.ts b/src/features/export/standaloneEngine.generated.ts index 590a779..494e0a9 100644 --- a/src/features/export/standaloneEngine.generated.ts +++ b/src/features/export/standaloneEngine.generated.ts @@ -1,2 +1,2 @@ // Generated by scripts/generate-standalone-engine.mjs. Do not edit. -export const standaloneEngineSource = "var AvatarProceduralEngine=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=e=>e*e*(3-2*e),n=e=>{let t=Math.sin(e*127.1+311.7)*43758.5453;return(t-Math.floor(t))*2-1},r=e=>e.headX*.71+e.headY*1.13+e.headZ*1.37,i=17.29,a=(e,r,i,a)=>{let o=e/a,s=Math.floor(o),c=t(o-s),l=n(s*3+r+i);return l+(n((s+1)*3+r+i)-l)*c},o=(e,r,i)=>{let a=1100;if(e<=0)return 0;let o=Math.floor(e/a),s=(e-o*a)/140,c=t(Math.min(s,1)),l=o===0?0:n((o-1)*2+r+i);return l+(n(o*2+r+i)-l)*c},s=e=>e.eyeMotion!==`none`||e.bodyMotion!==`none`,c=(e,t,n=1)=>{let i=r(e);if(e.bodyMotion===`slowDrift`)return{x:a(t,3,i,2900)*1.45*n,y:a(t,4,i,3700)*1.1*n};if(e.bodyMotion===`shake`){let e=t/1e3;return{x:(Math.sin(e*31)+Math.sin(e*53)*.45)*1.35*n,y:(Math.sin(e*37)+Math.sin(e*61)*.4)*1.1*n}}return{x:0,y:0}},l=(e,t,n=1)=>{if(e.eyeMotion===`microSaccades`)return{x:o(t,0,i)*1.5*n,y:o(t,1,i)*.9*n};if(e.eyeMotion===`shake`){let e=t/1e3;return{x:(Math.sin(e*47)+Math.sin(e*71)*.45)*1.2*n,y:(Math.sin(e*59)+Math.sin(e*83)*.4)*.8*n}}return{x:0,y:0}},u=(e,t,n=1)=>{let i={...e},o=r(e);if(e.bodyMotion===`slowDrift`)i.headX+=a(t,0,o,2600)*.8*n,i.headY+=a(t,1,o,3300)*1.15*n,i.headZ+=a(t,2,o,4100)*.45*n;else if(e.bodyMotion===`shake`){let e=t/1e3;i.headX+=(Math.sin(e*31)+Math.sin(e*53)*.45)*1.15*n,i.headY+=(Math.sin(e*37)+Math.sin(e*61)*.4)*1.35*n,i.headZ+=Math.sin(e*43)*.7*n}return i},d=(e,t,n=1)=>{let r=u(e,t,n),i=l(e,t,n);return r.positionXLeft+=i.x,r.positionXRight+=i.x,r.positionYLeft+=i.y,r.positionYRight+=i.y,r},f=(e,t)=>Math.sign(e)*Math.abs(e)**t,p=(e,t,n)=>e+(t-e)*n,m={rounded:.5,square:.3,teardrop:.78},h=(e,t,n,r)=>{let i=Math.max(Math.cos(n),0)**r;return[e.width/2*i*f(Math.sin(t),r),e.height/2*f(Math.sin(n),r),e.depth/2*i*f(Math.cos(t),r)]},g=(e,t,n)=>{let r=m.teardrop,i=f(Math.sin(n),r),a=Math.max(0,Math.min(1,(i+1)/2)),o=Math.max(Math.cos(n),0)**r,s=p(.46,1.24,a),c=p(.7,1.12,a);return[e.width/2*s*o*f(Math.sin(t),r),e.height/2*i,e.depth/2*c*o*f(Math.cos(t),r)]},_=e=>p(.34,.76,x(e.roundness)/2),v=(e,t,n)=>{let r=_(e),i=f(Math.sin(n),r),a=Math.max(0,Math.min(1,(i+1)/2)),o=Math.max(Math.cos(n),0)**r,s=p(e.topScale??.82,1.08,a),c=p(.92,1.04,a);return[e.width/2*s*o*f(Math.sin(t),r),e.height/2*i,e.depth/2*c*o*f(Math.cos(t),r)]},y=(e,t,n,r,i,a,o)=>{let s=f(Math.cos(t),a);return[n/2*s*f(Math.sin(e),o),r/2*f(Math.sin(t),a),i/2*s*f(Math.cos(e),o)]},b=(e,t,n)=>{let r=e.width/2,i=e.depth/2,a=Math.min(r,e.height/2),o=Math.max(0,(e.height-a*2)/2),s=o*2+Math.PI*a,c=(n+Math.PI/2)/Math.PI*s,l=r,u=0;if(cMath.max(0,Math.min(2,e??0)),S=e=>1+x(e.roundness)/2,ee=.04,C=e=>e.roundness<=0?1/0:2/(ee+x(e.roundness)/2*.96),w=(e,t,n,r)=>{let i=Math.cos(n)*Math.sin(t),a=Math.sin(n),o=Math.cos(n)*Math.cos(t),s=Number.isFinite(r)?(Math.abs(i)**r+Math.abs(a)**r+Math.abs(o)**r)**(1/r)||1:Math.max(Math.abs(i),Math.abs(a),Math.abs(o))||1;return[e.width/2*(i/s),e.height/2*(a/s),e.depth/2*(o/s)]},te=(e,t,n)=>w(e,t,n,S(e)),ne=(e,t,n)=>w(e,t,n,C(e)),re=.24,ie=.2,ae=.22,oe=e=>x(e.morphRoundness)/2,se=(e,t,n)=>{let r=oe(e),i=Math.max(0,Math.min(1,t)),a=Math.sin(i*Math.PI),o=(1-Math.cos(i*Math.PI))/2;return{radiusScale:n.radiusScale+(a-n.radiusScale)*r,verticalProgress:n.verticalProgress+(o-n.verticalProgress)*r}},T=(e,t,n,r,i)=>{let a=1-i;return a**3*e+3*a*a*i*t+3*a*i*i*n+i**3*r},ce=e=>({tipFraction:(e.tipRoundness??0)*re,baseFraction:(e.baseRoundness??0)*ie}),E=(e,t)=>{let n=Math.max(0,Math.min(1,t)),r=e.roundness*ae;if(r<=0)return{radiusScale:1,verticalProgress:(Math.sin((n-.5)*Math.PI)+1)/2};if(n1-r){let e=(n-(1-r))/r*(Math.PI/2);return{radiusScale:1-r+r*Math.cos(e),verticalProgress:1-r/2+r*Math.sin(e)/2}}let i=(n-r)/(1-r*2);return{radiusScale:1,verticalProgress:r/2+i*(1-r)}},le=(e,t)=>se(e,t,E(e,t)),D=(e,t,n)=>{let r=Math.max(0,Math.min(1,t)),i=0,a=1;for(let t=0;t<14;t+=1){let t=(i+a)/2;n(e,t).verticalProgress{let n=Math.max(0,Math.min(1,t)),{tipFraction:r,baseFraction:i}=ce(e);if(i>0&&n0&&n>1-r){let e=(n-(1-r))/r;return{radiusScale:T(r,r/2,r/4,0,e),verticalProgress:T(1-r,1-r/2,1,1,e)}}return{radiusScale:1-n,verticalProgress:n}},de=(e,t)=>se(e,t,ue(e,t)),fe=(e,t)=>{let n=Math.max(0,Math.min(1,t));return{radiusScale:p(.46,1,p(n,n*n*(3-2*n),x(e.roundness)/2*.55)),verticalProgress:n}},pe=(e,t)=>{let n=Math.max(0,Math.min(1,t));return{radiusScale:n**p(1,.56,x(e.roundness)/2),verticalProgress:n}},O=e=>{let t=e.height*.36,n=e.height-t;return{coneApexY:-e.height/2,coneBaseY:-e.height/2+t,bodyHeight:n,bodyCenterY:e.height/2-n/2,bodyWidth:e.width*.54,bodyDepth:e.depth*.62}},k=(e,t,n)=>{let{width:r,height:i,depth:a}=e;switch(e.type){case`sphere`:case`mickey`:return y(t,n,r,i,a,1,1);case`ellipse`:return h(e,t,n,1);case`square`:return h(e,t,n,m.square);case`rounded`:return h(e,t,n,m.rounded);case`cube`:return ne(e,t,n);case`cylinder`:{let o=le(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),-i/2+i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}case`cursor`:{let r=O(e),i=(n+Math.PI/2)/Math.PI,a=E({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},i);return[r.bodyWidth/2*a.radiusScale*Math.sin(t),r.bodyCenterY-r.bodyHeight/2+r.bodyHeight*a.verticalProgress,r.bodyDepth/2*a.radiusScale*Math.cos(t)]}case`diamond`:return te(e,t,n);case`teardrop`:return g(e,t,n);case`trapezoid`:return v(e,t,n);case`capsule`:case`ghost`:case`apple-bite`:return b(e,t,n);case`cone`:{let o=de(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),i/2-i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}case`frustum`:{let o=fe(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),-i/2+i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}case`half-cone`:{let o=pe(e,(n+Math.PI/2)/Math.PI),s=(e.cutAngle??0)*Math.PI/180+t/2;return[r/2*o.radiusScale*Math.sin(s),-i/2+i*o.verticalProgress,a/2*o.radiusScale*Math.cos(s)]}}},me=(e,t)=>[e[0]-t[0],e[1]-t[1],e[2]-t[2]],A=([e,t,n])=>{let r=Math.hypot(e,t,n)||1;return[e/r,t/r,n/r]},he=(e,t,n)=>{let r=e.type===`cone`?-1:1;return A([r*(t[1]*n[2]-t[2]*n[1]),r*(t[2]*n[0]-t[0]*n[2]),r*(t[0]*n[1]-t[1]*n[0])])},ge=(e,t,n)=>{let r=5e-4;if(e.type===`cone`&&n>=Math.PI/2-r)return[0,-1,0];let i=k(e,t-r,n),a=k(e,t+r,n),o=k(e,t,Math.max(-Math.PI/2,n-r)),s=k(e,t,Math.min(Math.PI/2,n+r));return he(e,me(a,i),me(s,o))},j=(e,t)=>Math.sign(e)*Math.abs(e)**t,M=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;return A([j(t[0]/r,n-1)/r,j(t[1]/i,n-1)/i,j(t[2]/a,n-1)/a])},_e=(e,t)=>M(e,t,S(e)),ve=(e,t)=>{let n=C(e);if(Number.isFinite(n))return M(e,t,n);let r=[t[0]/(e.width/2||1),t[1]/(e.height/2||1),t[2]/(e.depth/2||1)],i=r.reduce((e,t,n)=>Math.abs(t)>Math.abs(r[e])?n:e,0);return[i===0?Math.sign(r[0]):0,i===1?Math.sign(r[1]):0,i===2?Math.sign(r[2]):0]},N=(e,t,n)=>M(e,t,2/n),P=(e,t,n,r,i)=>{let a=e.width/2||1,o=e.height/2||1,s=e.depth/2||1;if(!Number.isFinite(r)){let r=[Math.max(-a,Math.min(a,t)),Math.max(-o,Math.min(o,n)),s];return{point:r,normal:i(e,r)}}let c=Math.max(-1,Math.min(1,n/o)),l=Math.max(0,1-Math.abs(c)**r)**(1/r),u=Math.max(-a*l,Math.min(a*l,t)),d=u/a,f=Math.max(0,1-Math.abs(d)**r-Math.abs(c)**r)**(1/r),p=[u,c*o,s*f];return{point:p,normal:i(e,p)}},ye=(e,t,n,r,i,a=0)=>{let o=t-a,s=Math.max(0,1-(e/(n||1))**2-(o/(r||1))**2),c=i*Math.sqrt(s);return{point:[e,t,c],normal:A([e/(n*n||1),o/(r*r||1),c/(i*i||1)])}},be=(e,t,n,r)=>{let i=e.width/2||1,a=e.height/2||1,o=e.depth/2||1,s=r===`teardrop`?m.teardrop:_(e),c=Math.max(-1,Math.min(1,n/a)),l=f(c,1/s),u=Math.asin(Math.max(-1,Math.min(1,l))),d=Math.max(0,Math.min(1,(c+1)/2)),h=Math.max(Math.cos(u),0)**s,g=r===`teardrop`?p(.46,1.24,d):p(e.topScale??.82,1.08,d),v=r===`teardrop`?p(.7,1.12,d):p(.92,1.04,d),y=i*g*h,b=o*v*h,x=Math.max(-y,Math.min(y,t)),S=y>0?x/y:0,ee=f(Math.max(-1,Math.min(1,S)),1/s),C=Math.asin(Math.max(-1,Math.min(1,ee))),w=b*f(Math.max(Math.cos(C),0),s);return{point:[x,c*a,w],normal:ge(e,C,u)}},F=(e,t,n,r,i)=>{let a=e.width/2||1,o=e.depth/2||1,s=Math.max(0,Math.min(1,.5+i*(n/e.height))),c=D(e,s,r),l=a*c,u=o*c,d=Math.max(-l,Math.min(l,t)),f=l>0?Math.max(0,1-(d/l)**2):0,p=u*Math.sqrt(f),m=1e-4,h=Math.max(0,s-m),g=Math.min(1,s+m),_=D(e,h,r),v=(D(e,g,r)-_)/(g-h||1),y=Math.max(Math.sqrt(f),1e-4),b=-(o/a)*d/(l*y||1),x=i*o*v/(e.height*y||1);return{point:[d,n,p],normal:A([-b,-x,1])}},xe=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;switch(e.type){case`sphere`:case`mickey`:case`ellipse`:return ye(t,n,r,i,a);case`square`:return P(e,t,n,2/m.square,(e,t)=>N(e,t,m.square));case`rounded`:return P(e,t,n,2/m.rounded,(e,t)=>N(e,t,m.rounded));case`cube`:return P(e,t,n,C(e),ve);case`capsule`:case`ghost`:case`apple-bite`:{let e=Math.min(r,i),o=Math.max(0,i-e);return ye(t,n,r,e,a,n<-o?-o:n>o?o:n)}case`cylinder`:return F(e,t,n,le,1);case`cursor`:{let r=O(e),i=F({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},t,n-r.bodyCenterY,E,1);return{point:[i.point[0],i.point[1]+r.bodyCenterY,i.point[2]],normal:i.normal}}case`cone`:return F(e,t,n,de,-1);case`frustum`:return F(e,t,n,fe,1);case`half-cone`:return F(e,t,n,pe,1);case`teardrop`:return be(e,t,n,`teardrop`);case`trapezoid`:return be(e,t,n,`trapezoid`);case`diamond`:return P(e,t,n,S(e),_e)}},Se=(e,t,n)=>{let r=k(e,t,n);if(e.type===`sphere`||e.type===`mickey`||e.type===`ellipse`){let t=e.width/2||1,n=e.height/2||1,i=e.depth/2||1;return{point:r,normal:A([r[0]/(t*t),r[1]/(n*n),r[2]/(i*i)])}}return e.type===`cylinder`&&e.roundness<=0&&(e.morphRoundness??0)<=0?{point:r,normal:A([Math.sin(t)/(e.width/2||1),0,Math.cos(t)/(e.depth/2||1)])}:e.type===`diamond`?{point:r,normal:_e(e,r)}:e.type===`cube`?{point:r,normal:ve(e,r)}:e.type===`square`?{point:r,normal:N(e,r,m.square)}:e.type===`rounded`?{point:r,normal:N(e,r,m.rounded)}:{point:r,normal:ge(e,t,n)}},I=620,Ce=14,we=[`headX`,`headY`,`headZ`,`widthLeft`,`widthRight`,`heightLeft`,`heightRight`,`curvatureLeft`,`curvatureRight`,`spacing`,`positionXLeft`,`positionXRight`,`positionYLeft`,`positionYRight`,`leftAngle`,`rightAngle`,`perspective`],Te=(e,t,n)=>Math.max(t,Math.min(n,e)),L=e=>e*Math.PI/180,Ee=([e,t,n,r])=>{let i=Math.hypot(e,t,n,r)||1;return[e/i,t/i,n/i,r/i]},De=([e,t,n,r],[i,a,o,s])=>Ee([e*i-t*a-n*o-r*s,e*a+t*i+n*s-r*o,e*o-t*s+n*i+r*a,e*s+t*o-n*a+r*i]),R=([e,t,n],r)=>{let i=r/2,a=Math.sin(i);return Ee([Math.cos(i),e*a,t*a,n*a])},z=(e,t,n)=>{let r=R([1,0,0],e),i=R([0,1,0],t);return De(De(R([0,0,1],n),r),i)},B=([e,t,n,r],[i,a,o])=>{let s=2*(n*o-r*a),c=2*(r*i-t*o),l=2*(t*a-n*i);return[i+e*s+(n*l-r*c),a+e*c+(r*s-t*l),o+e*l+(t*c-n*s)]},Oe=(e,t)=>{let n=e/2,r=t/2,i=Math.min(r,n),a=[],o=(e,t)=>{let n=Math.max(2,Math.ceil(Math.hypot(t[0]-e[0],t[1]-e[1])/1.5));for(let r=0;r{for(let r=0;r{let n=I-e[2]*t,r=Math.abs(n)<1e-4?I/1e-4:I/n;return[e[0]*r,e[1]*r,e[2]]},H=(e,t=!0)=>e.length?`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.slice(1).map(e=>`L${e[0].toFixed(2)} ${e[1].toFixed(2)}`).join(``)}${t?`Z`:``}`:``,ke=e=>({expression:e,orientation:z(L(e.headX),L(e.headY),L(e.headZ))}),Ae=24,je=25,Me=73,U=144,Ne=33,Pe=73,W=new Map,Fe=new Map,Ie=new Map,G=e=>JSON.stringify([e.type,e.width,e.height,e.depth,e.roundness,e.morphRoundness,e.tipRoundness,e.baseRoundness,e.topScale,e.cutAngle]),K=(e,t,n)=>(e.size>=Ae&&e.delete(e.keys().next().value),e.set(t,n),n),Le=(e,t,n)=>Se(e,t,n),Re=(e,t)=>({point:V(B(e.orientation,t.point),e.expression.perspective),normal:B(e.orientation,t.normal)}),ze=(e,t)=>{let n=e/120,r=t/120;return[120*Math.cos(r)*Math.sin(n),120*Math.sin(r)]},Be=(e,t,n,r)=>{let[i,a]=ze(n,r);return Re(e,xe(t,i,a))},Ve=(e,t,n,r,i={x:0,y:0})=>{let a=e.expression,o=n<0?`Left`:`Right`,s=a[`width${o}`],c=5+(a[`height${o}`]-5)*r,l=a[`curvature${o}`],u=Math.min(s/2,c/2),d=Math.max(s/2-u,0),f=Math.max(9,s*.38)*l*r,p=d>0?.5*d**2/(2*Math.max(u,2**-52)):0,m=Math.min(f,p),h=n*a.spacing/2+a[`positionX${o}`]+i.x,g=a[`positionY${o}`]+i.y,_=L(n<0?a.leftAngle:a.rightAngle);return Oe(s,c).map(([n,r])=>{let i=d?Te(n,-d,d):0,a=d?i/d:0,o=-m*(1-a**2),s=d?2*m*i/d**2:0,c=Math.hypot(1,s),l=1/c,u=s/c,f=n-i,p=i+l*f-u*r,v=o+u*f+l*r,y=p*Math.cos(_)-v*Math.sin(_),b=p*Math.sin(_)+v*Math.cos(_);return Be(e,t,h+y,g+b)})},He=e=>{let t=[],n=[];return e.forEach(({point:e,normal:r})=>{r[2]>0?n.push(e):n.length&&(t.push(n),n=[])}),n.length&&t.push(n),t.filter(e=>e.length>1).map(e=>H(e,!1)).join(``)},Ue=(e,t)=>{let n=G(t),r=Ie.get(n);if(!r){let e=[-60,-30,0,30,60].map(e=>Array.from({length:73},(n,r)=>Le(t,L(-180+r*5),L(e)))),i=Array.from({length:12},(e,t)=>-150+t*30).map(e=>Array.from({length:37},(n,r)=>Le(t,L(e),L(-90+r*5))));r=K(Ie,n,[...e,...i])}return r.map(t=>He(t.map(t=>Re(e,t))))},q=e=>{let t=[...e].sort((e,t)=>e[0]-t[0]||e[1]-t[1]),n=(e,t,n)=>(t[0]-e[0])*(n[1]-e[1])-(t[1]-e[1])*(n[0]-e[0]),r=e=>{let t=[];return e.forEach(e=>{for(;t.length>=2&&n(t.at(-2),t.at(-1),e)<=0;)t.pop();t.push(e)}),t};return[...r(t).slice(0,-1),...r(t.reverse()).slice(0,-1)]},J=e=>{if(e.length<3)return H(e);let t=t=>e[(t+e.length)%e.length];return`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.map((e,n)=>{let r=t(n-1),i=t(n+1),a=t(n+2),o=[e[0]+(i[0]-r[0])/6,e[1]+(i[1]-r[1])/6,e[2]],s=[i[0]-(a[0]-e[0])/6,i[1]-(a[1]-e[1])/6,i[2]];return`C${o[0].toFixed(2)} ${o[1].toFixed(2)} ${s[0].toFixed(2)} ${s[1].toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``)}Z`},Y=(e,t=7)=>e.flatMap((n,r)=>{let i=e[(r+1)%e.length],a=Math.max(1,Math.ceil(Math.hypot(i[0]-n[0],i[1]-n[1])/t));return Array.from({length:a},(e,t)=>{let r=t/a;return[n[0]+(i[0]-n[0])*r,n[1]+(i[1]-n[1])*r,n[2]+(i[2]-n[2])*r]})}),We=e=>e.length?e.length===1?`${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}`:e.slice(0,-1).map((t,n)=>{let r=e[Math.max(0,n-1)],i=e[n+1],a=e[Math.min(e.length-1,n+2)],o=t[0]+(i[0]-r[0])/6,s=t[1]+(i[1]-r[1])/6,c=i[0]-(a[0]-t[0])/6,l=i[1]-(a[1]-t[1])/6;return`C${o.toFixed(2)} ${s.toFixed(2)} ${c.toFixed(2)} ${l.toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``):``,X=(e,t)=>V(B(e.orientation,t),e.expression.perspective),Z=(e,t,n)=>Array.from({length:145},(r,i)=>{let a=i/U*Math.PI*2;return[e/2*Math.sin(a),n,t/2*Math.cos(a)]}),Q=(e,t)=>{let n=G(t),r=W.get(n);return r||(r=Array.from({length:Ne},(e,n)=>{let r=-Math.PI/2+n/32*Math.PI;return Array.from({length:Pe},(e,n)=>k(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),K(W,n,r)),J(Y(q(r.map(t=>X(e,t)))))},Ge=(e,t)=>{if(t.roundness>0||(t.morphRoundness??0)>0)return Q(e,t);let n=t.height/2;return J(Y(q([...Z(t.width,t.depth,-n),...Z(t.width,t.depth,n)].map(t=>X(e,t)))))},Ke=(e,t)=>{let n=O(t),r=n.bodyHeight/2;return J(Y(q([...Z(n.bodyWidth,n.bodyDepth,n.bodyCenterY-r),...Z(n.bodyWidth,n.bodyDepth,n.bodyCenterY+r)].map(t=>X(e,t)))))},qe=(e,t)=>{let n=O(t),r=X(e,[0,n.coneApexY,0]);return J(Y(q([...Z(t.width,t.depth,n.coneBaseY).map(t=>X(e,t)),r])))},Je=(e,t)=>{if((t.morphRoundness??0)>0||(t.tipRoundness??0)>0||(t.baseRoundness??0)>0)return Q(e,t);let n=X(e,[0,-t.height/2,0]),r=q([...Z(t.width,t.depth,t.height/2).map(t=>X(e,t)),n]),i=r.findIndex(e=>Math.hypot(e[0]-n[0],e[1]-n[1])<.01);if(i<0)return J(r);let a=[...r.slice(i),...r.slice(0,i)].slice(1);return a.length<2?H(r):`M${n[0].toFixed(2)} ${n[1].toFixed(2)}L${a[0][0].toFixed(2)} ${a[0][1].toFixed(2)}${We(a)}L${n[0].toFixed(2)} ${n[1].toFixed(2)}Z`},Ye=(e,t)=>{if(t.roundness>0)return Q(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return H(q([-1,1].flatMap(e=>[-1,1].flatMap(t=>[-1,1].map(a=>[e*n,t*r,a*i]))).map(t=>X(e,t))))},Xe=(e,t)=>{if(t.roundness>0)return Q(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return H(q([[-n,0,0],[n,0,0],[0,-r,0],[0,r,0],[0,0,-i],[0,0,i]].map(t=>X(e,t))))},Ze=(e,t,n,r,i)=>{let a=n+i,o=Math.hypot(n-i,r*2),s=(a+o)/2,c=(a-o)/2;return s<=0||c<=0?null:{centerX:e,centerY:t,majorRadius:Math.sqrt(s),minorRadius:Math.sqrt(c),rotation:Math.atan2(r*2,n-i)/2}},Qe=({centerX:e,centerY:t,majorRadius:n,minorRadius:r,rotation:i})=>{let a=i*180/Math.PI,o=Math.cos(i)*n,s=Math.sin(i)*n,c=e+o,l=t+s,u=e-o,d=t-s;return`M${c.toFixed(2)} ${l.toFixed(2)}A${n.toFixed(2)} ${r.toFixed(2)} ${a.toFixed(2)} 0 1 ${u.toFixed(2)} ${d.toFixed(2)}A${n.toFixed(2)} ${r.toFixed(2)} ${a.toFixed(2)} 0 1 ${c.toFixed(2)} ${l.toFixed(2)}Z`},$=(e,t,n=[0,0,0])=>{let r=[B(e.orientation,[1,0,0]),B(e.orientation,[0,1,0]),B(e.orientation,[0,0,1])],i=B(e.orientation,n);if(Math.abs(e.expression.perspective)<1e-4){let e=r.reduce((e,n,r)=>e+n[0]*n[0]*t[r]*t[r],0),n=r.reduce((e,n,r)=>e+n[0]*n[1]*t[r]*t[r],0),a=r.reduce((e,n,r)=>e+n[1]*n[1]*t[r]*t[r],0);return Ze(i[0],i[1],e,n,a)}let a=t.map(e=>1/(e*e)),o=Array.from({length:3},(e,t)=>Array.from({length:3},(e,n)=>r.reduce((e,r,i)=>e+r[t]*a[i]*r[n],0))),s=I/e.expression.perspective,c=[-i[0],-i[1],s-i[2]],l=[o[0][0]*c[0]+o[0][1]*c[1]+o[0][2]*c[2],o[1][0]*c[0]+o[1][1]*c[1]+o[1][2]*c[2],o[2][0]*c[0]+o[2][1]*c[1]+o[2][2]*c[2]],u=c[0]*l[0]+c[1]*l[1]+c[2]*l[2]-1,d=[l[0],l[1],-s*l[2]],f=[[o[0][0],o[0][1],-s*o[0][2]],[o[1][0],o[1][1],-s*o[1][2]],[-s*o[2][0],-s*o[2][1],s*s*o[2][2]]],p=Array.from({length:3},(e,t)=>Array.from({length:3},(e,n)=>d[t]*d[n]-u*f[t][n])),m=p[0][0]*p[1][1]-p[0][1]*p[0][1];if(Math.abs(m)<1e-12)return null;let h=-(p[1][1]*p[0][2]-p[0][1]*p[1][2])/m,g=(p[0][1]*p[0][2]-p[0][0]*p[1][2])/m,_=-(p[2][2]+p[0][2]*h+p[1][2]*g);if(Math.abs(_)<1e-12)return null;let v=p[0][0]/_,y=p[0][1]/_,b=p[1][1]/_,x=v*b-y*y;return x<=0?null:Ze(h,g,b/x,-y/x,v/x)},$e=(e,t)=>{let n=$(e,[t.width/2,t.height/2,t.depth/2]),r=t.width===t.height&&t.height===t.depth;if(n&&r){let e=(n.majorRadius+n.minorRadius)/2;return Qe({centerX:0,centerY:0,majorRadius:e,minorRadius:e,rotation:0})}return n?Qe(n):null},et=(e,t)=>{if(t.type!==`mickey`)return[];let n=Math.min(t.width,t.height)*.23,r=Math.min(n,t.depth*.29),i=t.width*.37,a=-t.height*.39,o=-t.depth*.12,s=[n,n,r];return[-1,1].map(t=>$(e,s,[t*i,a,o])).filter(e=>e!==null).map(Qe)},tt=(e,t)=>t.type===`mickey`?et(e,t):t.type===`cursor`?[qe(e,t)]:[],nt=e=>Array.from({length:U},(t,n)=>{let r=n/U*Math.PI*2,i=Math.cos(r)*e.majorRadius,a=Math.sin(r)*e.minorRadius;return[e.centerX+i*Math.cos(e.rotation)-a*Math.sin(e.rotation),e.centerY+i*Math.sin(e.rotation)+a*Math.cos(e.rotation),0]}),rt=e=>{if(e.length<3)return H(e);let t=e.map((t,n)=>{let r=e[(n+1)%e.length];return Math.hypot(r[0]-t[0],r[1]-t[1])}),n=[...t].sort((e,t)=>e-t),r=n[Math.floor(n.length/2)]||1,i=Math.max(8,r*3.5),a=t.map(e=>e>i);return`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.map((t,n)=>{let r=(n+1)%e.length,i=e[r];if(a[n])return`L${i[0].toFixed(2)} ${i[1].toFixed(2)}`;let o=a[(n-1+e.length)%e.length]?t:e[(n-1+e.length)%e.length],s=a[r]?i:e[(n+2)%e.length],c=t[0]+(i[0]-o[0])/6,l=t[1]+(i[1]-o[1])/6,u=i[0]-(s[0]-t[0])/6,d=i[1]-(s[1]-t[1])/6;return`C${c.toFixed(2)} ${l.toFixed(2)} ${u.toFixed(2)} ${d.toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``)}Z`},it=(e,t)=>{let n=t.width/2,r=Math.min(n,t.height/2),i=t.depth/2,a=Math.max(0,(t.height-r*2)/2),o=[n,r,i],s=$(e,o,[0,a,0]),c=$(e,o,[0,-a,0]);return!s||!c?null:rt(q([...nt(s),...nt(c)]))},at=(e,t)=>{let n=t.width/2,r=t.height/2,i=t.width/6,a=t.width*.018,o=-r*.08,s=r*.5,c=r*.4,l=r,u=(t,n)=>X(e,[t,n,0]),d=([e,t])=>`${e.toFixed(2)} ${t.toFixed(2)}`,f=(e,t)=>`M${d(u(e,t))}`,p=(e,t)=>`L${d(u(e,t))}`,m=(e,t,n,r,i,a)=>`C${d(u(e,t))} ${d(u(n,r))} ${d(u(i,a))}`;return[f(-n,o),m(-n,-r*.62,-n*.56,-r,0,-r),m(n*.56,-r,n,-r*.62,n,o),p(n,s),m(n,l*.82,n*.82,l,n*.66,l),m(n*.49,l,i+a,l*.82,i+a,s),p(i+a,c),m(i+a,c-6,i-a,c-6,i-a,c),p(i-a,s),m(i-a,l*.82,n*.17,l,0,l),m(-n*.17,l,-i+a,l*.82,-i+a,s),p(-i+a,c),m(-i+a,c-6,-i-a,c-6,-i-a,c),p(-i-a,s),m(-i-a,l*.82,-n*.49,l,-n*.66,l),m(-n*.82,l,-n,l*.82,-n,s),p(-n,o),`Z`].join(``)},ot=(e,t)=>{let n=t.width/2,r=t.height/2,i=(t,n)=>X(e,[t,n,0]),a=([e,t])=>`${e.toFixed(2)} ${t.toFixed(2)}`,o=(e,t)=>`M${a(i(e,t))}`,s=(e,t)=>`L${a(i(e,t))}`,c=(e,t,n,r,o,s)=>`C${a(i(e,t))} ${a(i(n,r))} ${a(i(o,s))}`;return[o(-n*.46,-r),c(-n*.08,-r,n*.15,-r*.78,n*.12,-r*.54),c(n*.4,-r*.72,n*.74,-r*.75,n,-r*.56),c(n*.8,-r*.43,n*.68,-r*.24,n*.68,-r*.03),c(n*.68,r*.2,n*.8,r*.4,n,r*.5),c(n*.82,r*.83,n*.54,r,n*.26,r),c(n*.08,r,-n*.02,r*.91,-n*.16,r*.91),c(-n*.31,r*.91,-n*.4,r,-n*.57,r),c(-n*.89,r,-n,r*.67,-n,r*.3),c(-n,-r*.16,-n*.8,-r*.58,-n*.49,-r*.7),c(-n*.28,-r*.78,-n*.03,-r*.69,n*.08,-r*.55),s(-n*.1,-r*.66),c(-n*.12,-r*.82,-n*.22,-r*.88,-n*.35,-r*.84),c(-n*.41,-r*.84,-n*.45,-r*.93,-n*.46,-r),`Z`].join(``)},st=(e,t)=>{if(t.type===`sphere`||t.type===`mickey`){let n=$e(e,t);if(n)return n}if(t.type===`capsule`){let n=it(e,t);if(n)return n}if(t.type===`cylinder`)return Ge(e,t);if(t.type===`cursor`)return Ke(e,t);if(t.type===`cone`)return Je(e,t);if(t.type===`cube`)return Ye(e,t);if(t.type===`diamond`)return Xe(e,t);if(t.type===`ghost`)return at(e,t);if(t.type===`apple-bite`)return ot(e,t);let n=G(t),r=W.get(n);return r||(r=Array.from({length:je},(e,n)=>{let r=-Math.PI/2+n/24*Math.PI;return Array.from({length:Me},(e,n)=>k(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),K(W,n,r)),H(q(r.map(t=>V(B(e.orientation,t),e.expression.perspective))))},ct=(e,t)=>{let n=G(t.surface),r=Fe.get(n);r||(r=Array.from({length:17},(e,n)=>{let r=-Math.PI/2+n/16*Math.PI;return Array.from({length:49},(e,n)=>{let i=-Math.PI+n/48*Math.PI*2;return k(t.surface,i,r)})}).flat(),K(Fe,n,r));let i=z(L(t.rotation[0]),L(t.rotation[1]),L(t.rotation[2])),a=q(r.map(n=>{let r=B(i,n),a=[r[0]+t.position[0],r[1]+t.position[1],r[2]+t.position[2]];return V(B(e.orientation,a),e.expression.perspective)}));return(t.surface.type===`cube`||t.surface.type===`diamond`)&&t.surface.roundness<=0?H(a):J(Y(a))},lt=.1,ut=(e,t)=>{let n=z(L(t.rotation[0]),L(t.rotation[1]),L(t.rotation[2])),r=[[1,0,0],[0,1,0],[0,0,1]].map(t=>B(e.orientation,B(n,t))[2]);return Math.hypot(r[0]*(t.surface.width/2),r[1]*(t.surface.height/2),r[2]*(t.surface.depth/2))},dt=(e,t)=>{let n=t.map(t=>{let n=B(e.orientation,t.position)[2];return{id:t.id,path:ct(e,t),depth:n,front:t.layer===`front`||t.layer!==`back`&&n>ut(e,t)*lt}}).sort((e,t)=>e.depth-t.depth);return{backPaths:n.filter(e=>!e.front).map(e=>e.path),frontPaths:n.filter(e=>e.front).map(e=>e.path),backNodeIds:n.filter(e=>!e.front).map(e=>e.id),frontNodeIds:n.filter(e=>e.front).map(e=>e.id)}};return e.ambientBodyOffset=c,e.ambientEyeOffset=l,e.applyAmbientBodyMotion=u,e.applyAmbientMotion=d,e.expressionFields=we,e.hasAmbientMotion=s,e.poseFromExpression=ke,e.renderAvatar=(e,t,n=1,r={})=>{let i=Ve(e,t,-1,n,r.eyeOffset),a=Ve(e,t,1,n,r.eyeOffset),o=i.map(e=>e.point),s=a.map(e=>e.point),c=dt(e,r.bodyNodes??[]),l=tt(e,t);return{backPaths:[...l,...c.backPaths],frontPaths:c.frontPaths,backNodeIds:[...l.map(()=>null),...c.backNodeIds],frontNodeIds:c.frontNodeIds,headPath:st(e,t),leftPath:H(o),rightPath:H(s),leftVisible:i.reduce((e,t)=>e+t.normal[2],0)>0,rightVisible:a.reduce((e,t)=>e+t.normal[2],0)>0,wirePaths:r.includeWire===!1?[]:Ue(e,t)}},e})({});" +export const standaloneEngineSource = "var AvatarProceduralEngine=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=e=>e*e*(3-2*e),n=e=>{let t=Math.sin(e*127.1+311.7)*43758.5453;return(t-Math.floor(t))*2-1},r=e=>e.headX*.71+e.headY*1.13+e.headZ*1.37,i=17.29,a=(e,r,i,a)=>{let o=e/a,s=Math.floor(o),c=t(o-s),l=n(s*3+r+i);return l+(n((s+1)*3+r+i)-l)*c},o=(e,r,i)=>{let a=1100;if(e<=0)return 0;let o=Math.floor(e/a),s=(e-o*a)/140,c=t(Math.min(s,1)),l=o===0?0:n((o-1)*2+r+i);return l+(n(o*2+r+i)-l)*c},s=e=>e.eyeMotion!==`none`||e.bodyMotion!==`none`,c=(e,t,n=1)=>{let i=r(e);if(e.bodyMotion===`slowDrift`)return{x:a(t,3,i,2900)*1.45*n,y:a(t,4,i,3700)*1.1*n};if(e.bodyMotion===`shake`){let e=t/1e3;return{x:(Math.sin(e*31)+Math.sin(e*53)*.45)*1.35*n,y:(Math.sin(e*37)+Math.sin(e*61)*.4)*1.1*n}}return{x:0,y:0}},l=(e,t,n=1)=>{if(e.eyeMotion===`microSaccades`)return{x:o(t,0,i)*1.5*n,y:o(t,1,i)*.9*n};if(e.eyeMotion===`shake`){let e=t/1e3;return{x:(Math.sin(e*47)+Math.sin(e*71)*.45)*1.2*n,y:(Math.sin(e*59)+Math.sin(e*83)*.4)*.8*n}}return{x:0,y:0}},u=(e,t,n=1)=>{let i={...e},o=r(e);if(e.bodyMotion===`slowDrift`)i.headX+=a(t,0,o,2600)*.8*n,i.headY+=a(t,1,o,3300)*1.15*n,i.headZ+=a(t,2,o,4100)*.45*n;else if(e.bodyMotion===`shake`){let e=t/1e3;i.headX+=(Math.sin(e*31)+Math.sin(e*53)*.45)*1.15*n,i.headY+=(Math.sin(e*37)+Math.sin(e*61)*.4)*1.35*n,i.headZ+=Math.sin(e*43)*.7*n}return i},d=(e,t,n=1)=>{let r=u(e,t,n),i=l(e,t,n);return r.positionXLeft+=i.x,r.positionXRight+=i.x,r.positionYLeft+=i.y,r.positionYRight+=i.y,r},f=(e,t)=>Math.sign(e)*Math.abs(e)**t,p=(e,t,n)=>e+(t-e)*n,m={rounded:.5,square:.3,teardrop:.78},h=(e,t,n,r)=>{let i=Math.max(Math.cos(n),0)**r;return[e.width/2*i*f(Math.sin(t),r),e.height/2*f(Math.sin(n),r),e.depth/2*i*f(Math.cos(t),r)]},g=(e,t,n)=>{let r=m.teardrop,i=f(Math.sin(n),r),a=Math.max(0,Math.min(1,(i+1)/2)),o=Math.max(Math.cos(n),0)**r,s=p(.46,1.24,a),c=p(.7,1.12,a);return[e.width/2*s*o*f(Math.sin(t),r),e.height/2*i,e.depth/2*c*o*f(Math.cos(t),r)]},_=e=>p(.34,.76,x(e.roundness)/2),v=(e,t,n)=>{let r=_(e),i=f(Math.sin(n),r),a=Math.max(0,Math.min(1,(i+1)/2)),o=Math.max(Math.cos(n),0)**r,s=p(e.topScale??.82,1.08,a),c=p(.92,1.04,a);return[e.width/2*s*o*f(Math.sin(t),r),e.height/2*i,e.depth/2*c*o*f(Math.cos(t),r)]},y=(e,t,n,r,i,a,o)=>{let s=f(Math.cos(t),a);return[n/2*s*f(Math.sin(e),o),r/2*f(Math.sin(t),a),i/2*s*f(Math.cos(e),o)]},b=(e,t,n)=>{let r=e.width/2,i=e.depth/2,a=Math.min(r,e.height/2),o=Math.max(0,(e.height-a*2)/2),s=o*2+Math.PI*a,c=(n+Math.PI/2)/Math.PI*s,l=r,u=0;if(cMath.max(0,Math.min(2,e??0)),S=e=>1+x(e.roundness)/2,ee=.04,C=e=>e.roundness<=0?1/0:2/(ee+x(e.roundness)/2*.96),w=(e,t,n,r)=>{let i=Math.cos(n)*Math.sin(t),a=Math.sin(n),o=Math.cos(n)*Math.cos(t),s=Number.isFinite(r)?(Math.abs(i)**r+Math.abs(a)**r+Math.abs(o)**r)**(1/r)||1:Math.max(Math.abs(i),Math.abs(a),Math.abs(o))||1;return[e.width/2*(i/s),e.height/2*(a/s),e.depth/2*(o/s)]},te=(e,t,n)=>w(e,t,n,S(e)),ne=(e,t,n)=>w(e,t,n,C(e)),re=.24,ie=.2,ae=.22,oe=e=>x(e.morphRoundness)/2,se=(e,t,n)=>{let r=oe(e),i=Math.max(0,Math.min(1,t)),a=Math.sin(i*Math.PI),o=(1-Math.cos(i*Math.PI))/2;return{radiusScale:n.radiusScale+(a-n.radiusScale)*r,verticalProgress:n.verticalProgress+(o-n.verticalProgress)*r}},T=(e,t,n,r,i)=>{let a=1-i;return a**3*e+3*a*a*i*t+3*a*i*i*n+i**3*r},ce=e=>({tipFraction:(e.tipRoundness??0)*re,baseFraction:(e.baseRoundness??0)*ie}),E=(e,t)=>{let n=Math.max(0,Math.min(1,t)),r=e.roundness*ae;if(r<=0)return{radiusScale:1,verticalProgress:(Math.sin((n-.5)*Math.PI)+1)/2};if(n1-r){let e=(n-(1-r))/r*(Math.PI/2);return{radiusScale:1-r+r*Math.cos(e),verticalProgress:1-r/2+r*Math.sin(e)/2}}let i=(n-r)/(1-r*2);return{radiusScale:1,verticalProgress:r/2+i*(1-r)}},le=(e,t)=>se(e,t,E(e,t)),D=(e,t,n)=>{let r=Math.max(0,Math.min(1,t)),i=0,a=1;for(let t=0;t<14;t+=1){let t=(i+a)/2;n(e,t).verticalProgress{let n=Math.max(0,Math.min(1,t)),{tipFraction:r,baseFraction:i}=ce(e);if(i>0&&n0&&n>1-r){let e=(n-(1-r))/r;return{radiusScale:T(r,r/2,r/4,0,e),verticalProgress:T(1-r,1-r/2,1,1,e)}}return{radiusScale:1-n,verticalProgress:n}},de=(e,t)=>se(e,t,ue(e,t)),fe=(e,t)=>{let n=Math.max(0,Math.min(1,t));return{radiusScale:p(.46,1,p(n,n*n*(3-2*n),x(e.roundness)/2*.55)),verticalProgress:n}},pe=(e,t)=>{let n=Math.max(0,Math.min(1,t));return{radiusScale:n**p(1,.56,x(e.roundness)/2),verticalProgress:n}},O=e=>{let t=e.height*.36,n=e.height-t;return{coneApexY:-e.height/2,coneBaseY:-e.height/2+t,bodyHeight:n,bodyCenterY:e.height/2-n/2,bodyWidth:e.width*.54,bodyDepth:e.depth*.62}},k=(e,t,n)=>{let{width:r,height:i,depth:a}=e;switch(e.type){case`sphere`:case`mickey`:return y(t,n,r,i,a,1,1);case`ellipse`:return h(e,t,n,1);case`square`:return h(e,t,n,m.square);case`rounded`:return h(e,t,n,m.rounded);case`cube`:return ne(e,t,n);case`cylinder`:{let o=le(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),-i/2+i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}case`cursor`:{let r=O(e),i=(n+Math.PI/2)/Math.PI,a=E({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},i);return[r.bodyWidth/2*a.radiusScale*Math.sin(t),r.bodyCenterY-r.bodyHeight/2+r.bodyHeight*a.verticalProgress,r.bodyDepth/2*a.radiusScale*Math.cos(t)]}case`diamond`:return te(e,t,n);case`teardrop`:return g(e,t,n);case`trapezoid`:return v(e,t,n);case`capsule`:case`ghost`:case`apple-bite`:return b(e,t,n);case`cone`:{let o=de(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),i/2-i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}case`frustum`:{let o=fe(e,(n+Math.PI/2)/Math.PI);return[r/2*o.radiusScale*Math.sin(t),-i/2+i*o.verticalProgress,a/2*o.radiusScale*Math.cos(t)]}case`half-cone`:{let o=pe(e,(n+Math.PI/2)/Math.PI),s=(e.cutAngle??0)*Math.PI/180+t/2;return[r/2*o.radiusScale*Math.sin(s),-i/2+i*o.verticalProgress,a/2*o.radiusScale*Math.cos(s)]}}},me=(e,t)=>[e[0]-t[0],e[1]-t[1],e[2]-t[2]],A=([e,t,n])=>{let r=Math.hypot(e,t,n)||1;return[e/r,t/r,n/r]},he=(e,t,n)=>{let r=e.type===`cone`?-1:1;return A([r*(t[1]*n[2]-t[2]*n[1]),r*(t[2]*n[0]-t[0]*n[2]),r*(t[0]*n[1]-t[1]*n[0])])},ge=(e,t,n)=>{let r=5e-4;if(e.type===`cone`&&n>=Math.PI/2-r)return[0,-1,0];let i=k(e,t-r,n),a=k(e,t+r,n),o=k(e,t,Math.max(-Math.PI/2,n-r)),s=k(e,t,Math.min(Math.PI/2,n+r));return he(e,me(a,i),me(s,o))},j=(e,t)=>Math.sign(e)*Math.abs(e)**t,M=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;return A([j(t[0]/r,n-1)/r,j(t[1]/i,n-1)/i,j(t[2]/a,n-1)/a])},_e=(e,t)=>M(e,t,S(e)),ve=(e,t)=>{let n=C(e);if(Number.isFinite(n))return M(e,t,n);let r=[t[0]/(e.width/2||1),t[1]/(e.height/2||1),t[2]/(e.depth/2||1)],i=r.reduce((e,t,n)=>Math.abs(t)>Math.abs(r[e])?n:e,0);return[i===0?Math.sign(r[0]):0,i===1?Math.sign(r[1]):0,i===2?Math.sign(r[2]):0]},N=(e,t,n)=>M(e,t,2/n),P=(e,t,n,r,i)=>{let a=e.width/2||1,o=e.height/2||1,s=e.depth/2||1;if(!Number.isFinite(r)){let r=[Math.max(-a,Math.min(a,t)),Math.max(-o,Math.min(o,n)),s];return{point:r,normal:i(e,r)}}let c=Math.max(-1,Math.min(1,n/o)),l=Math.max(0,1-Math.abs(c)**r)**(1/r),u=Math.max(-a*l,Math.min(a*l,t)),d=u/a,f=Math.max(0,1-Math.abs(d)**r-Math.abs(c)**r)**(1/r),p=[u,c*o,s*f];return{point:p,normal:i(e,p)}},ye=(e,t,n,r,i,a=0)=>{let o=t-a,s=Math.max(0,1-(e/(n||1))**2-(o/(r||1))**2),c=i*Math.sqrt(s);return{point:[e,t,c],normal:A([e/(n*n||1),o/(r*r||1),c/(i*i||1)])}},be=(e,t,n,r)=>{let i=e.width/2||1,a=e.height/2||1,o=e.depth/2||1,s=r===`teardrop`?m.teardrop:_(e),c=Math.max(-1,Math.min(1,n/a)),l=f(c,1/s),u=Math.asin(Math.max(-1,Math.min(1,l))),d=Math.max(0,Math.min(1,(c+1)/2)),h=Math.max(Math.cos(u),0)**s,g=r===`teardrop`?p(.46,1.24,d):p(e.topScale??.82,1.08,d),v=r===`teardrop`?p(.7,1.12,d):p(.92,1.04,d),y=i*g*h,b=o*v*h,x=Math.max(-y,Math.min(y,t)),S=y>0?x/y:0,ee=f(Math.max(-1,Math.min(1,S)),1/s),C=Math.asin(Math.max(-1,Math.min(1,ee))),w=b*f(Math.max(Math.cos(C),0),s);return{point:[x,c*a,w],normal:ge(e,C,u)}},F=(e,t,n,r,i)=>{let a=e.width/2||1,o=e.depth/2||1,s=Math.max(0,Math.min(1,.5+i*(n/e.height))),c=D(e,s,r),l=a*c,u=o*c,d=Math.max(-l,Math.min(l,t)),f=l>0?Math.max(0,1-(d/l)**2):0,p=u*Math.sqrt(f),m=1e-4,h=Math.max(0,s-m),g=Math.min(1,s+m),_=D(e,h,r),v=(D(e,g,r)-_)/(g-h||1),y=Math.max(Math.sqrt(f),1e-4),b=-(o/a)*d/(l*y||1),x=i*o*v/(e.height*y||1);return{point:[d,n,p],normal:A([-b,-x,1])}},xe=(e,t,n)=>{let r=e.width/2||1,i=e.height/2||1,a=e.depth/2||1;switch(e.type){case`sphere`:case`mickey`:case`ellipse`:return ye(t,n,r,i,a);case`square`:return P(e,t,n,2/m.square,(e,t)=>N(e,t,m.square));case`rounded`:return P(e,t,n,2/m.rounded,(e,t)=>N(e,t,m.rounded));case`cube`:return P(e,t,n,C(e),ve);case`capsule`:case`ghost`:case`apple-bite`:{let e=Math.min(r,i),o=Math.max(0,i-e);return ye(t,n,r,e,a,n<-o?-o:n>o?o:n)}case`cylinder`:return F(e,t,n,le,1);case`cursor`:{let r=O(e),i=F({...e,width:r.bodyWidth,height:r.bodyHeight,depth:r.bodyDepth},t,n-r.bodyCenterY,E,1);return{point:[i.point[0],i.point[1]+r.bodyCenterY,i.point[2]],normal:i.normal}}case`cone`:return F(e,t,n,de,-1);case`frustum`:return F(e,t,n,fe,1);case`half-cone`:return F(e,t,n,pe,1);case`teardrop`:return be(e,t,n,`teardrop`);case`trapezoid`:return be(e,t,n,`trapezoid`);case`diamond`:return P(e,t,n,S(e),_e)}},Se=(e,t,n)=>{let r=k(e,t,n);if(e.type===`sphere`||e.type===`mickey`||e.type===`ellipse`){let t=e.width/2||1,n=e.height/2||1,i=e.depth/2||1;return{point:r,normal:A([r[0]/(t*t),r[1]/(n*n),r[2]/(i*i)])}}return e.type===`cylinder`&&e.roundness<=0&&(e.morphRoundness??0)<=0?{point:r,normal:A([Math.sin(t)/(e.width/2||1),0,Math.cos(t)/(e.depth/2||1)])}:e.type===`diamond`?{point:r,normal:_e(e,r)}:e.type===`cube`?{point:r,normal:ve(e,r)}:e.type===`square`?{point:r,normal:N(e,r,m.square)}:e.type===`rounded`?{point:r,normal:N(e,r,m.rounded)}:{point:r,normal:ge(e,t,n)}},I=620,Ce=14,we=[`headX`,`headY`,`headZ`,`widthLeft`,`widthRight`,`heightLeft`,`heightRight`,`curvatureLeft`,`curvatureRight`,`spacing`,`positionXLeft`,`positionXRight`,`positionYLeft`,`positionYRight`,`leftAngle`,`rightAngle`,`perspective`],Te=(e,t,n)=>Math.max(t,Math.min(n,e)),L=e=>e*Math.PI/180,Ee=([e,t,n,r])=>{let i=Math.hypot(e,t,n,r)||1;return[e/i,t/i,n/i,r/i]},De=([e,t,n,r],[i,a,o,s])=>Ee([e*i-t*a-n*o-r*s,e*a+t*i+n*s-r*o,e*o-t*s+n*i+r*a,e*s+t*o-n*a+r*i]),R=([e,t,n],r)=>{let i=r/2,a=Math.sin(i);return Ee([Math.cos(i),e*a,t*a,n*a])},z=(e,t,n)=>{let r=R([1,0,0],e),i=R([0,1,0],t);return De(De(R([0,0,1],n),r),i)},B=([e,t,n,r],[i,a,o])=>{let s=2*(n*o-r*a),c=2*(r*i-t*o),l=2*(t*a-n*i);return[i+e*s+(n*l-r*c),a+e*c+(r*s-t*l),o+e*l+(t*c-n*s)]},Oe=(e,t)=>{let n=e/2,r=t/2,i=Math.min(r,n),a=[],o=(e,t)=>{let n=Math.max(2,Math.ceil(Math.hypot(t[0]-e[0],t[1]-e[1])/1.5));for(let r=0;r{for(let r=0;r{let n=I-e[2]*t,r=Math.abs(n)<1e-4?I/1e-4:I/n;return[e[0]*r,e[1]*r,e[2]]},H=(e,t=!0)=>e.length?`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.slice(1).map(e=>`L${e[0].toFixed(2)} ${e[1].toFixed(2)}`).join(``)}${t?`Z`:``}`:``,ke=e=>({expression:e,orientation:z(L(e.headX),L(e.headY),L(e.headZ))}),Ae=24,je=25,Me=73,U=144,Ne=33,Pe=73,W=new Map,Fe=new Map,Ie=new Map,G=e=>JSON.stringify([e.type,e.width,e.height,e.depth,e.roundness,e.morphRoundness,e.tipRoundness,e.baseRoundness,e.topScale,e.cutAngle]),K=(e,t,n)=>(e.size>=Ae&&e.delete(e.keys().next().value),e.set(t,n),n),Le=(e,t,n)=>Se(e,t,n),Re=(e,t)=>({point:V(B(e.orientation,t.point),e.expression.perspective),normal:B(e.orientation,t.normal)}),ze=(e,t)=>{let n=e/120,r=t/120;return[120*Math.cos(r)*Math.sin(n),120*Math.sin(r)]},Be=(e,t,n,r)=>{let[i,a]=ze(n,r);return Re(e,xe(t,i,a))},Ve=(e,t,n,r,i={x:0,y:0})=>{let a=e.expression,o=n<0?`Left`:`Right`,s=a[`width${o}`],c=5+(a[`height${o}`]-5)*r,l=a[`curvature${o}`],u=Math.min(s/2,c/2),d=Math.max(s/2-u,0),f=Math.max(9,s*.38)*l*r,p=d>0?.5*d**2/(2*Math.max(u,2**-52)):0,m=Math.min(f,p),h=n*a.spacing/2+a[`positionX${o}`]+i.x,g=a[`positionY${o}`]+i.y,_=L(n<0?a.leftAngle:a.rightAngle);return Oe(s,c).map(([n,r])=>{let i=d?Te(n,-d,d):0,a=d?i/d:0,o=-m*(1-a**2),s=d?2*m*i/d**2:0,c=Math.hypot(1,s),l=1/c,u=s/c,f=n-i,p=i+l*f-u*r,v=o+u*f+l*r,y=p*Math.cos(_)-v*Math.sin(_),b=p*Math.sin(_)+v*Math.cos(_);return Be(e,t,h+y,g+b)})},He=e=>{let t=[],n=[];return e.forEach(({point:e,normal:r})=>{r[2]>0?n.push(e):n.length&&(t.push(n),n=[])}),n.length&&t.push(n),t.filter(e=>e.length>1).map(e=>H(e,!1)).join(``)},Ue=(e,t)=>{let n=G(t),r=Ie.get(n);if(!r){let e=[-60,-30,0,30,60].map(e=>Array.from({length:73},(n,r)=>Le(t,L(-180+r*5),L(e)))),i=Array.from({length:12},(e,t)=>-150+t*30).map(e=>Array.from({length:37},(n,r)=>Le(t,L(e),L(-90+r*5))));r=K(Ie,n,[...e,...i])}return r.map(t=>He(t.map(t=>Re(e,t))))},q=e=>{let t=[...e].sort((e,t)=>e[0]-t[0]||e[1]-t[1]),n=(e,t,n)=>(t[0]-e[0])*(n[1]-e[1])-(t[1]-e[1])*(n[0]-e[0]),r=e=>{let t=[];return e.forEach(e=>{for(;t.length>=2&&n(t.at(-2),t.at(-1),e)<=0;)t.pop();t.push(e)}),t};return[...r(t).slice(0,-1),...r(t.reverse()).slice(0,-1)]},J=e=>{if(e.length<3)return H(e);let t=t=>e[(t+e.length)%e.length];return`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.map((e,n)=>{let r=t(n-1),i=t(n+1),a=t(n+2),o=[e[0]+(i[0]-r[0])/6,e[1]+(i[1]-r[1])/6,e[2]],s=[i[0]-(a[0]-e[0])/6,i[1]-(a[1]-e[1])/6,i[2]];return`C${o[0].toFixed(2)} ${o[1].toFixed(2)} ${s[0].toFixed(2)} ${s[1].toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``)}Z`},Y=(e,t=7)=>e.flatMap((n,r)=>{let i=e[(r+1)%e.length],a=Math.max(1,Math.ceil(Math.hypot(i[0]-n[0],i[1]-n[1])/t));return Array.from({length:a},(e,t)=>{let r=t/a;return[n[0]+(i[0]-n[0])*r,n[1]+(i[1]-n[1])*r,n[2]+(i[2]-n[2])*r]})}),We=e=>e.length?e.length===1?`${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}`:e.slice(0,-1).map((t,n)=>{let r=e[Math.max(0,n-1)],i=e[n+1],a=e[Math.min(e.length-1,n+2)],o=t[0]+(i[0]-r[0])/6,s=t[1]+(i[1]-r[1])/6,c=i[0]-(a[0]-t[0])/6,l=i[1]-(a[1]-t[1])/6;return`C${o.toFixed(2)} ${s.toFixed(2)} ${c.toFixed(2)} ${l.toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``):``,X=(e,t)=>V(B(e.orientation,t),e.expression.perspective),Z=(e,t,n)=>Array.from({length:145},(r,i)=>{let a=i/U*Math.PI*2;return[e/2*Math.sin(a),n,t/2*Math.cos(a)]}),Q=(e,t)=>{let n=G(t),r=W.get(n);return r||(r=Array.from({length:Ne},(e,n)=>{let r=-Math.PI/2+n/32*Math.PI;return Array.from({length:Pe},(e,n)=>k(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),K(W,n,r)),J(Y(q(r.map(t=>X(e,t)))))},Ge=(e,t)=>{if(t.roundness>0||(t.morphRoundness??0)>0)return Q(e,t);let n=t.height/2;return J(Y(q([...Z(t.width,t.depth,-n),...Z(t.width,t.depth,n)].map(t=>X(e,t)))))},Ke=(e,t)=>{let n=O(t),r=n.bodyHeight/2;return J(Y(q([...Z(n.bodyWidth,n.bodyDepth,n.bodyCenterY-r),...Z(n.bodyWidth,n.bodyDepth,n.bodyCenterY+r)].map(t=>X(e,t)))))},qe=(e,t)=>{let n=O(t),r=X(e,[0,n.coneApexY,0]);return J(Y(q([...Z(t.width,t.depth,n.coneBaseY).map(t=>X(e,t)),r])))},Je=(e,t)=>{if((t.morphRoundness??0)>0||(t.tipRoundness??0)>0||(t.baseRoundness??0)>0)return Q(e,t);let n=X(e,[0,-t.height/2,0]),r=q([...Z(t.width,t.depth,t.height/2).map(t=>X(e,t)),n]),i=r.findIndex(e=>Math.hypot(e[0]-n[0],e[1]-n[1])<.01);if(i<0)return J(r);let a=[...r.slice(i),...r.slice(0,i)].slice(1);return a.length<2?H(r):`M${n[0].toFixed(2)} ${n[1].toFixed(2)}L${a[0][0].toFixed(2)} ${a[0][1].toFixed(2)}${We(a)}L${n[0].toFixed(2)} ${n[1].toFixed(2)}Z`},Ye=(e,t)=>{if(t.roundness>0)return Q(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return H(q([-1,1].flatMap(e=>[-1,1].flatMap(t=>[-1,1].map(a=>[e*n,t*r,a*i]))).map(t=>X(e,t))))},Xe=(e,t)=>{if(t.roundness>0)return Q(e,t);let n=t.width/2,r=t.height/2,i=t.depth/2;return H(q([[-n,0,0],[n,0,0],[0,-r,0],[0,r,0],[0,0,-i],[0,0,i]].map(t=>X(e,t))))},Ze=(e,t,n,r,i)=>{let a=n+i,o=Math.hypot(n-i,r*2),s=(a+o)/2,c=(a-o)/2;return s<=0||c<=0?null:{centerX:e,centerY:t,majorRadius:Math.sqrt(s),minorRadius:Math.sqrt(c),rotation:Math.atan2(r*2,n-i)/2}},Qe=({centerX:e,centerY:t,majorRadius:n,minorRadius:r,rotation:i})=>{let a=i*180/Math.PI,o=Math.cos(i)*n,s=Math.sin(i)*n,c=e+o,l=t+s,u=e-o,d=t-s;return`M${c.toFixed(2)} ${l.toFixed(2)}A${n.toFixed(2)} ${r.toFixed(2)} ${a.toFixed(2)} 0 1 ${u.toFixed(2)} ${d.toFixed(2)}A${n.toFixed(2)} ${r.toFixed(2)} ${a.toFixed(2)} 0 1 ${c.toFixed(2)} ${l.toFixed(2)}Z`},$=(e,t,n=[0,0,0])=>{let r=[B(e.orientation,[1,0,0]),B(e.orientation,[0,1,0]),B(e.orientation,[0,0,1])],i=B(e.orientation,n);if(Math.abs(e.expression.perspective)<1e-4){let e=r.reduce((e,n,r)=>e+n[0]*n[0]*t[r]*t[r],0),n=r.reduce((e,n,r)=>e+n[0]*n[1]*t[r]*t[r],0),a=r.reduce((e,n,r)=>e+n[1]*n[1]*t[r]*t[r],0);return Ze(i[0],i[1],e,n,a)}let a=t.map(e=>1/(e*e)),o=Array.from({length:3},(e,t)=>Array.from({length:3},(e,n)=>r.reduce((e,r,i)=>e+r[t]*a[i]*r[n],0))),s=I/e.expression.perspective,c=[-i[0],-i[1],s-i[2]],l=[o[0][0]*c[0]+o[0][1]*c[1]+o[0][2]*c[2],o[1][0]*c[0]+o[1][1]*c[1]+o[1][2]*c[2],o[2][0]*c[0]+o[2][1]*c[1]+o[2][2]*c[2]],u=c[0]*l[0]+c[1]*l[1]+c[2]*l[2]-1,d=[l[0],l[1],-s*l[2]],f=[[o[0][0],o[0][1],-s*o[0][2]],[o[1][0],o[1][1],-s*o[1][2]],[-s*o[2][0],-s*o[2][1],s*s*o[2][2]]],p=Array.from({length:3},(e,t)=>Array.from({length:3},(e,n)=>d[t]*d[n]-u*f[t][n])),m=p[0][0]*p[1][1]-p[0][1]*p[0][1];if(Math.abs(m)<1e-12)return null;let h=-(p[1][1]*p[0][2]-p[0][1]*p[1][2])/m,g=(p[0][1]*p[0][2]-p[0][0]*p[1][2])/m,_=-(p[2][2]+p[0][2]*h+p[1][2]*g);if(Math.abs(_)<1e-12)return null;let v=p[0][0]/_,y=p[0][1]/_,b=p[1][1]/_,x=v*b-y*y;return x<=0?null:Ze(h,g,b/x,-y/x,v/x)},$e=(e,t)=>{let n=$(e,[t.width/2,t.height/2,t.depth/2]),r=t.width===t.height&&t.height===t.depth;if(n&&r){let e=(n.majorRadius+n.minorRadius)/2;return Qe({centerX:0,centerY:0,majorRadius:e,minorRadius:e,rotation:0})}return n?Qe(n):null},et=(e,t)=>{if(t.type!==`mickey`)return[];let n=Math.min(t.width,t.height)*.23,r=Math.min(n,t.depth*.29),i=t.width*.37,a=-t.height*.39,o=-t.depth*.12,s=[n,n,r];return[-1,1].map(t=>$(e,s,[t*i,a,o])).filter(e=>e!==null).map(Qe)},tt=(e,t)=>t.type===`mickey`?et(e,t):t.type===`cursor`?[qe(e,t)]:[],nt=e=>Array.from({length:U},(t,n)=>{let r=n/U*Math.PI*2,i=Math.cos(r)*e.majorRadius,a=Math.sin(r)*e.minorRadius;return[e.centerX+i*Math.cos(e.rotation)-a*Math.sin(e.rotation),e.centerY+i*Math.sin(e.rotation)+a*Math.cos(e.rotation),0]}),rt=e=>{if(e.length<3)return H(e);let t=e.map((t,n)=>{let r=e[(n+1)%e.length];return Math.hypot(r[0]-t[0],r[1]-t[1])}),n=[...t].sort((e,t)=>e-t),r=n[Math.floor(n.length/2)]||1,i=Math.max(8,r*3.5),a=t.map(e=>e>i);return`M${e[0][0].toFixed(2)} ${e[0][1].toFixed(2)}${e.map((t,n)=>{let r=(n+1)%e.length,i=e[r];if(a[n])return`L${i[0].toFixed(2)} ${i[1].toFixed(2)}`;let o=a[(n-1+e.length)%e.length]?t:e[(n-1+e.length)%e.length],s=a[r]?i:e[(n+2)%e.length],c=t[0]+(i[0]-o[0])/6,l=t[1]+(i[1]-o[1])/6,u=i[0]-(s[0]-t[0])/6,d=i[1]-(s[1]-t[1])/6;return`C${c.toFixed(2)} ${l.toFixed(2)} ${u.toFixed(2)} ${d.toFixed(2)} ${i[0].toFixed(2)} ${i[1].toFixed(2)}`}).join(``)}Z`},it=(e,t)=>{let n=t.width/2,r=Math.min(n,t.height/2),i=t.depth/2,a=Math.max(0,(t.height-r*2)/2),o=[n,r,i],s=$(e,o,[0,a,0]),c=$(e,o,[0,-a,0]);return!s||!c?null:rt(q([...nt(s),...nt(c)]))},at=(e,t)=>{let n=t.width/2,r=t.height/2,i=t.width/6,a=t.width*.018,o=-r*.08,s=r*.5,c=r*.4,l=r,u=(t,n)=>X(e,[t,n,0]),d=([e,t])=>`${e.toFixed(2)} ${t.toFixed(2)}`,f=(e,t)=>`M${d(u(e,t))}`,p=(e,t)=>`L${d(u(e,t))}`,m=(e,t,n,r,i,a)=>`C${d(u(e,t))} ${d(u(n,r))} ${d(u(i,a))}`;return[f(-n,o),m(-n,-r*.62,-n*.56,-r,0,-r),m(n*.56,-r,n,-r*.62,n,o),p(n,s),m(n,l*.82,n*.82,l,n*.66,l),m(n*.49,l,i+a,l*.82,i+a,s),p(i+a,c),m(i+a,c-6,i-a,c-6,i-a,c),p(i-a,s),m(i-a,l*.82,n*.17,l,0,l),m(-n*.17,l,-i+a,l*.82,-i+a,s),p(-i+a,c),m(-i+a,c-6,-i-a,c-6,-i-a,c),p(-i-a,s),m(-i-a,l*.82,-n*.49,l,-n*.66,l),m(-n*.82,l,-n,l*.82,-n,s),p(-n,o),`Z`].join(``)},ot=(e,t)=>{let n=t.width/2,r=t.height/2,i=(t,n)=>X(e,[t,n,0]),a=([e,t])=>`${e.toFixed(2)} ${t.toFixed(2)}`,o=(e,t)=>`M${a(i(e,t))}`,s=(e,t)=>`L${a(i(e,t))}`,c=(e,t,n,r,o,s)=>`C${a(i(e,t))} ${a(i(n,r))} ${a(i(o,s))}`;return[o(-n*.46,-r),c(-n*.08,-r,n*.15,-r*.78,n*.12,-r*.54),c(n*.4,-r*.72,n*.74,-r*.75,n,-r*.56),c(n*.8,-r*.43,n*.68,-r*.24,n*.68,-r*.03),c(n*.68,r*.2,n*.8,r*.4,n,r*.5),c(n*.82,r*.83,n*.54,r,n*.26,r),c(n*.08,r,-n*.02,r*.91,-n*.16,r*.91),c(-n*.31,r*.91,-n*.4,r,-n*.57,r),c(-n*.89,r,-n,r*.67,-n,r*.3),c(-n,-r*.16,-n*.8,-r*.58,-n*.49,-r*.7),c(-n*.28,-r*.78,-n*.03,-r*.69,n*.08,-r*.55),s(-n*.1,-r*.66),c(-n*.12,-r*.82,-n*.22,-r*.88,-n*.35,-r*.84),c(-n*.41,-r*.84,-n*.45,-r*.93,-n*.46,-r),`Z`].join(``)},st=(e,t)=>{if(t.type===`sphere`||t.type===`mickey`){let n=$e(e,t);if(n)return n}if(t.type===`capsule`){let n=it(e,t);if(n)return n}if(t.type===`cylinder`)return Ge(e,t);if(t.type===`cursor`)return Ke(e,t);if(t.type===`cone`)return Je(e,t);if(t.type===`cube`)return Ye(e,t);if(t.type===`diamond`)return Xe(e,t);if(t.type===`ghost`)return at(e,t);if(t.type===`apple-bite`)return ot(e,t);let n=G(t),r=W.get(n);return r||(r=Array.from({length:je},(e,n)=>{let r=-Math.PI/2+n/24*Math.PI;return Array.from({length:Me},(e,n)=>k(t,-Math.PI+n/72*Math.PI*2,r))}).flat(),K(W,n,r)),H(q(r.map(t=>V(B(e.orientation,t),e.expression.perspective))))},ct=(e,t)=>{let n=G(t.surface),r=Fe.get(n);r||(r=Array.from({length:17},(e,n)=>{let r=-Math.PI/2+n/16*Math.PI;return Array.from({length:49},(e,n)=>{let i=-Math.PI+n/48*Math.PI*2;return k(t.surface,i,r)})}).flat(),K(Fe,n,r));let i=z(L(t.rotation[0]),L(t.rotation[1]),L(t.rotation[2])),a=q(r.map(n=>{let r=B(i,n),a=[r[0]+t.position[0],r[1]+t.position[1],r[2]+t.position[2]];return V(B(e.orientation,a),e.expression.perspective)}));return(t.surface.type===`cube`||t.surface.type===`diamond`)&&t.surface.roundness<=0?{hull:a,path:H(a)}:{hull:a,path:J(Y(a))}},lt=.1,ut=(e,t)=>{let n=z(L(t.rotation[0]),L(t.rotation[1]),L(t.rotation[2])),r=[[1,0,0],[0,1,0],[0,0,1]].map(t=>B(e.orientation,B(n,t))[2]);return Math.hypot(r[0]*(t.surface.width/2),r[1]*(t.surface.height/2),r[2]*(t.surface.depth/2))},dt=e=>e.reduce((t,n,r)=>{let i=e[(r+1)%e.length];return t+n[0]*i[1]-i[0]*n[1]},0)/2,ft=(e,t)=>{let n=e,r=Math.sign(dt(t))||1,i=(e,t,n)=>r*((n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0]))>=0,a=(e,t,n,r)=>{let i=t[0]-e[0],a=t[1]-e[1],o=r[0]-n[0],s=r[1]-n[1],c=i*s-a*o,l=c===0?0:((n[0]-e[0])*s-(n[1]-e[1])*o)/c;return[e[0]+l*i,e[1]+l*a]};return t.forEach((e,r)=>{let o=t[(r+1)%t.length],s=n;n=[],s.forEach((t,r)=>{let c=s[(r+s.length-1)%s.length],l=i(t,e,o),u=i(c,e,o);l?(u||n.push(a(c,t,e,o)),n.push(t)):u&&n.push(a(c,t,e,o))})}),n},pt=.08,mt=(e,t)=>{let n=Math.abs(dt(t));return n===0?0:Math.abs(dt(ft(t,e)))/n},ht=(e,t,n)=>{let r=t.map(t=>{let r=B(e.orientation,t.position)[2],i=ct(e,t),a=t.layer===`auto`&&n.some(e=>mt(i.hull.map(e=>[e[0],e[1]]),e)>=pt);return{id:t.id,path:i.path,depth:r,front:t.layer===`front`||(t.layer===`auto`?!a:t.layer!==`back`&&r>ut(e,t)*lt)}}).sort((e,t)=>e.depth-t.depth);return{backPaths:r.filter(e=>!e.front).map(e=>e.path),frontPaths:r.filter(e=>e.front).map(e=>e.path),backNodeIds:r.filter(e=>!e.front).map(e=>e.id),frontNodeIds:r.filter(e=>e.front).map(e=>e.id)}};return e.ambientBodyOffset=c,e.ambientEyeOffset=l,e.applyAmbientBodyMotion=u,e.applyAmbientMotion=d,e.expressionFields=we,e.hasAmbientMotion=s,e.poseFromExpression=ke,e.renderAvatar=(e,t,n=1,r={})=>{let i=Ve(e,t,-1,n,r.eyeOffset),a=Ve(e,t,1,n,r.eyeOffset),o=i.map(e=>e.point),s=a.map(e=>e.point),c=i.reduce((e,t)=>e+t.normal[2],0)>0,l=a.reduce((e,t)=>e+t.normal[2],0)>0,u=[c?o:null,l?s:null].filter(e=>e!==null).map(e=>e.map(e=>[e[0],e[1]])),d=ht(e,r.bodyNodes??[],u),f=tt(e,t);return{backPaths:[...f,...d.backPaths],frontPaths:d.frontPaths,backNodeIds:[...f.map(()=>null),...d.backNodeIds],frontNodeIds:d.frontNodeIds,headPath:st(e,t),leftPath:H(o),rightPath:H(s),leftVisible:c,rightVisible:l,wirePaths:r.includeWire===!1?[]:Ue(e,t)}},e})({});" diff --git a/src/features/rendering/__tests__/rendered-scene-test.ts b/src/features/rendering/__tests__/rendered-scene-test.ts index 7b755da..a9a4aeb 100644 --- a/src/features/rendering/__tests__/rendered-scene-test.ts +++ b/src/features/rendering/__tests__/rendered-scene-test.ts @@ -39,6 +39,7 @@ describe('rendered avatar scene', () => { bodyNodes: [node], }) const scene = createRenderedScene(first) + const initialNodeOrderVersion = scene.nodeOrderVersion.get() const rotated = renderAvatar( poseFromExpression({ ...defaultExpression, headY: 35 }), surfacePresets.sphere, @@ -51,6 +52,11 @@ describe('rendered avatar scene', () => { expect(findBodyNodePath(scene, 'primary')).toBe(scene.headPath) expect(findBodyNodePath(scene, node.id)).not.toBeNull() expect(scene.headPath.get()).toBe(rotated.headPath) + expect(scene.nodeOrderVersion.get()).toBeGreaterThan(initialNodeOrderVersion) + + const stableNodeOrderVersion = scene.nodeOrderVersion.get() + paintRenderedScene(scene, rotated) + expect(scene.nodeOrderVersion.get()).toBe(stableNodeOrderVersion) }) it('updates animated colors without replacing their motion values', () => { @@ -98,13 +104,11 @@ describe('rendered avatar scene', () => { expect(forcedFront.frontNodeIds).toContain(avatar.body.nodes[0].id) }) - it('moves dog ears between front and back with head depth', () => { + it('moves a dog ear behind the head only when it strongly overlaps an eye', () => { const studio = loadStudioDocument({ getItem: () => null }) const avatar = studio.library.avatars.find(item => item.id === 'oneworks-dog')! const detachedEars: string[] = [] - let splitDepthExpressions = 0 - studio.expressions.forEach(expression => { const geometry = renderAvatar( poseFromExpression(expression as Expression), @@ -115,12 +119,6 @@ describe('rendered avatar scene', () => { const head = pathBounds(geometry.headPath) const earIds = ['dog-ear-left', 'dog-ear-right'] as const - if ( - geometry.frontNodeIds.some(id => earIds.includes(id as (typeof earIds)[number])) && - geometry.backNodeIds.some(id => earIds.includes(id as (typeof earIds)[number])) - ) { - splitDepthExpressions += 1 - } earIds.forEach(nodeId => { const frontIndex = geometry.frontNodeIds.indexOf(nodeId) const backIndex = geometry.backNodeIds.indexOf(nodeId) @@ -137,8 +135,25 @@ describe('rendered avatar scene', () => { }) }) - expect(splitDepthExpressions).toBeGreaterThan(0) expect(detachedEars, 'detached dog ears').toEqual([]) + + const expressionById = new Map( + studio.expressions.map(expression => [expression.id, expression]) + ) + const geometryFor = (sequenceId: string) => { + const sequence = studio.sequences.find(item => item.id === sequenceId)! + const expression = expressionById.get(sequence.steps[0].expressionId) as Expression + return renderAvatar(poseFromExpression(expression), avatar.body.primary as SurfaceConfig, 1, { + bodyNodes: avatar.body.nodes as BodyNode[], + }) + } + + ;['idle', 'shy', 'proud'].forEach(sequenceId => { + expect(geometryFor(sequenceId).backNodeIds, sequenceId).toContain('dog-ear-right') + }) + expect(geometryFor('angry').frontNodeIds).toEqual( + expect.arrayContaining(['dog-ear-left', 'dog-ear-right']) + ) }) it('keeps bear ears attached behind the head across expressions', () => { diff --git a/src/features/rendering/avatarAppearance.ts b/src/features/rendering/avatarAppearance.ts index 53ff537..6d89bd9 100644 --- a/src/features/rendering/avatarAppearance.ts +++ b/src/features/rendering/avatarAppearance.ts @@ -1,6 +1,8 @@ import { useMotionValueEvent, type MotionValue } from 'motion/react' import { useEffect, useState } from 'react' +import type { RenderedScene } from './renderedScene' + export const avatarBodyOutlineWidth = () => 12 const colorsMatch = (left: string, right: string) => @@ -26,6 +28,14 @@ export const useAvatarBodyColorOverride = ( return overridden } +export const useRenderedSceneNodeOrder = (scene: RenderedScene) => { + const [, setVersion] = useState(0) + + useMotionValueEvent(scene.nodeOrderVersion, 'change', () => { + setVersion(version => version + 1) + }) +} + export const resolveAvatarNodeColor = ({ nodeColor, bodyColor, diff --git a/src/features/rendering/components/AvatarCanvas.tsx b/src/features/rendering/components/AvatarCanvas.tsx index a2872b7..8ebb5fe 100644 --- a/src/features/rendering/components/AvatarCanvas.tsx +++ b/src/features/rendering/components/AvatarCanvas.tsx @@ -44,6 +44,7 @@ import { } from '@/features/avatar/manipulationSession' import { type SurfaceConfig } from '@/features/avatar/surfaces' import { type CanvasPreviewTarget } from '@/features/rendering/canvasPreview' +import { useRenderedSceneNodeOrder } from '@/features/rendering/avatarAppearance' import { LivePixelAvatarCanvas } from '@/features/rendering/components/PixelAvatarCanvas' import { type RenderedRotationGizmo } from '@/features/rendering/renderedRotationGizmo' import { @@ -550,6 +551,7 @@ export function AvatarCanvas({ onManipulationStart: () => Expression }) { const { t } = useStudioLanguage() + useRenderedSceneNodeOrder(scene) const { wirePaths, backPaths, diff --git a/src/features/rendering/renderedScene.ts b/src/features/rendering/renderedScene.ts index 39faa5c..5f7b03c 100644 --- a/src/features/rendering/renderedScene.ts +++ b/src/features/rendering/renderedScene.ts @@ -10,6 +10,7 @@ export type RenderedScene = { frontPaths: MotionValue[] backNodeIds: { current: (string | null)[] } frontNodeIds: { current: (string | null)[] } + nodeOrderVersion: MotionValue leftPath: MotionValue rightPath: MotionValue leftOpacity: MotionValue @@ -36,6 +37,7 @@ export const createRenderedScene = (geometry: AvatarGeometry): RenderedScene => ), backNodeIds: { current: geometry.backNodeIds }, frontNodeIds: { current: geometry.frontNodeIds }, + nodeOrderVersion: motionValue(0), leftPath: motionValue(geometry.leftPath), rightPath: motionValue(geometry.rightPath), leftOpacity: motionValue(geometry.leftVisible ? 1 : 0), @@ -62,8 +64,14 @@ export const paintRenderedOffset = (scene: RenderedScene, offset: { x: number; y export const paintRenderedScene = (scene: RenderedScene, geometry: AvatarGeometry) => { scene.headPath.set(geometry.headPath) + const nodeOrderChanged = + scene.backNodeIds.current.length !== geometry.backNodeIds.length || + scene.frontNodeIds.current.length !== geometry.frontNodeIds.length || + scene.backNodeIds.current.some((id, index) => id !== geometry.backNodeIds[index]) || + scene.frontNodeIds.current.some((id, index) => id !== geometry.frontNodeIds[index]) scene.backNodeIds.current = geometry.backNodeIds scene.frontNodeIds.current = geometry.frontNodeIds + if (nodeOrderChanged) scene.nodeOrderVersion.set(scene.nodeOrderVersion.get() + 1) scene.backPaths.forEach((path, index) => path.set(geometry.backPaths[index] ?? '')) scene.frontPaths.forEach((path, index) => path.set(geometry.frontPaths[index] ?? '')) scene.leftPath.set(geometry.leftPath) diff --git a/src/features/studio/components/AvatarDrawer.tsx b/src/features/studio/components/AvatarDrawer.tsx index 9ab34e4..019e3c6 100644 --- a/src/features/studio/components/AvatarDrawer.tsx +++ b/src/features/studio/components/AvatarDrawer.tsx @@ -15,6 +15,7 @@ import { defaultExpression } from '@/features/avatar/presets' import { avatarBodyOutlineWidth, useAvatarBodyColorOverride, + useRenderedSceneNodeOrder, } from '@/features/rendering/avatarAppearance' import { LivePixelAvatarCanvas } from '@/features/rendering/components/PixelAvatarCanvas' import type { RenderedColors, RenderedScene } from '@/features/rendering/renderedScene' @@ -31,6 +32,7 @@ function LiveAvatarPreview({ scene: RenderedScene expressionBodyColor?: string }) { + useRenderedSceneNodeOrder(scene) const outlineWidth = avatarBodyOutlineWidth() const filled = avatar.renderStyle.type === 'vector' && avatar.renderStyle.filled === true const fixedSkinColors = diff --git a/src/features/studio/useStudioController.ts b/src/features/studio/useStudioController.ts index 27e92e3..8bd014b 100644 --- a/src/features/studio/useStudioController.ts +++ b/src/features/studio/useStudioController.ts @@ -1573,12 +1573,7 @@ export function useStudioController() { activeSequence && playbackVisual.position !== null ? expressionById.get(activeSequence.steps[playbackVisual.position]?.expressionId ?? '') : null - const activeSequenceUsesBodyColor = - activeSequence?.steps.some(step => expressionById.get(step.expressionId)?.bodyColor) ?? false - const renderedExpressionBodyColor = - playbackExpression?.bodyColor ?? - expression.bodyColor ?? - (activeSequenceUsesBodyColor ? renderedColors.body.get() : undefined) + const renderedExpressionBodyColor = playbackExpression?.bodyColor ?? expression.bodyColor const semanticKeyIssueMessage = (issue: SemanticKeyIssueCode | 'duplicate_semantic_key') => t( issue === 'missing_semantic_key'