{
  "sourceKind": "separate production-editor working copy; not a public GitHub commit",
  "reviewedAt": "2026-10-22",
  "excerpts": [
    {
      "file": "app/studio/text-runs.ts",
      "start": 4,
      "end": 9,
      "code": "export type CharacterStyle={openType?:OpenType;fontSize?:number;fontFamily?:string;color?:string;bold?:boolean;italic?:boolean;underline?:boolean;strike?:boolean;tracking?:number;baseline?:number};\nexport type TextRun={start:number;end:number;style:CharacterStyle};\nexport function validateTextRuns(runs:TextRun[],text:string){if(!Array.isArray(runs)||runs.length>2048)throw Error('Use up to 2048 text style ranges.');let end=0;for(const r of runs){if(!Number.isInteger(r.start)||!Number.isInteger(r.end)||r.start<end||r.end<=r.start||r.end>text.length||!r.style)throw Error('Invalid text style range.');end=r.end;const s=r.style;if(s.openType)validateOpenType(s.openType);if(s.fontFamily!==undefined&&(typeof s.fontFamily!=='string'||s.fontFamily.length>200)||s.color!==undefined&&!/^#[0-9a-f]{6}$/i.test(s.color))throw Error('Invalid character style.');for(const [k,min,max] of [['fontSize',1,1000],['tracking',-1000,10000],['baseline',-1000,1000]] as const)if(s[k]!==undefined&&(!Number.isFinite(s[k])||s[k]!<min||s[k]!>max))throw Error('Invalid character metric.');for(const k of ['bold','italic','underline','strike'] as const)if(s[k]!==undefined&&typeof s[k]!=='boolean')throw Error('Invalid character style.');}}\nexport function characterStyle(l:Layer,index:number):Required<CharacterStyle>{const t=l.typography,r=l.textRuns?.find(r=>index>=r.start&&index<r.end)?.style;return {openType:t?.openType??{},fontSize:l.fontSize,fontFamily:l.fontFamily??'Arial',color:l.color,bold:t?.bold??(l.textStyle?.fontWeight??600)>=600,italic:t?.italic??false,underline:t?.underline??false,strike:t?.strike??false,tracking:t?.tracking??0,baseline:t?.baseline??0,...r};}\nexport function applyTextStyle(text:string,runs:TextRun[]|undefined,start:number,end:number,style:CharacterStyle|null):TextRun[]{start=Math.max(0,Math.min(text.length,start));end=Math.max(start,Math.min(text.length,end));if(start===end)return runs??[];const boundaries=[...new Set([0,text.length,start,end,...(runs??[]).flatMap(r=>[r.start,r.end])])].sort((a,b)=>a-b),out:TextRun[]=[];for(let i=1;i<boundaries.length;i++){const a=boundaries[i-1],b=boundaries[i],old=runs?.find(r=>r.start<=a&&r.end>=b)?.style,inside=a>=start&&b<=end,patch=inside?(style?{...old,...style}:undefined):old;if(!patch||!Object.keys(patch).length)continue;const last=out.at(-1);if(last?.end===a&&JSON.stringify(last.style)===JSON.stringify(patch))last.end=b;else out.push({start:a,end:b,style:{...patch}});}validateTextRuns(out,text);return out;}\nexport function editTextRuns(before:string,after:string,runs:TextRun[]|undefined){if(!runs?.length)return undefined;let start=0;while(start<before.length&&start<after.length&&before[start]===after[start])start++;let tail=0;while(tail<before.length-start&&tail<after.length-start&&before[before.length-1-tail]===after[after.length-1-tail])tail++;const oldEnd=before.length-tail,newEnd=after.length-tail,delta=newEnd-oldEnd,out:TextRun[]=[];for(const r of runs){const a=r.start<=start?r.start:r.start>=oldEnd?r.start+delta:start,b=r.end<=start?r.end:r.end>=oldEnd?r.end+delta:newEnd;if(b>a&&a<after.length)out.push({start:a,end:Math.min(b,after.length),style:r.style});}let result:TextRun[]=[];for(const r of out)result=applyTextStyle(after,result,r.start,r.end,r.style);return result.length?result:undefined;}",
      "ref": "working-copy-2026-10-22",
      "sha256": "60199cfc855f0b08512bc7b0bd2e3978ca8e54361e3accd845fb72fa4abf4edd"
    },
    {
      "file": "app/studio/text-path.ts",
      "start": 3,
      "end": 9,
      "code": "export type TextPath={path:PathShape;offset:number;end?:number;flip:boolean};\nexport function validateTextPath(t:TextPath){if(!t||!Number.isFinite(t.offset)||Math.abs(t.offset)>100000||typeof t.flip!=='boolean')throw Error('Invalid text path.');if(t.end!==undefined&&(!Number.isFinite(t.end)||t.end<0||t.end>100000))throw Error('Invalid text path end.');validatePaths([{id:'type',name:'Type',paths:[t.path]}]);if(t.path.knots.length<2)throw Error('Text needs at least two path anchors.');}\n/** Adaptive subdivision preserves arc length on curved paths, rather than spacing by parameter t. */\nexport function flattenTextPath(path:PathShape){const points:Point[]=[],knots=path.knots;let length=0;const append=(p:Point)=>{if(points.length)length+=Math.hypot(p.x-points.at(-1)!.x,p.y-points.at(-1)!.y);points.push(p);distances.push(length);},distances:number[]=[];const pt=(a:number[],i:number)=>({x:a[i],y:a[i+1]}),mid=(a:Point,b:Point)=>({x:(a.x+b.x)/2,y:(a.y+b.y)/2});\n const subdivide=(a:Point,b:Point,c:Point,d:Point,depth:number)=>{const chord=Math.hypot(d.x-a.x,d.y-a.y),polygon=Math.hypot(b.x-a.x,b.y-a.y)+Math.hypot(c.x-b.x,c.y-b.y)+Math.hypot(d.x-c.x,d.y-c.y);if(depth>=12||polygon-chord<.025){append(d);return;}const ab=mid(a,b),bc=mid(b,c),cd=mid(c,d),abc=mid(ab,bc),bcd=mid(bc,cd),center=mid(abc,bcd);subdivide(a,ab,abc,center,depth+1);subdivide(center,bcd,cd,d,depth+1);};\n if(knots.length){append(pt(knots[0].points,2));for(let i=1;i<knots.length+(path.open?0:1);i++){const a=knots[(i-1)%knots.length].points,b=knots[i%knots.length].points;subdivide(pt(a,2),pt(a,4),pt(b,0),pt(b,2),0);}}\n const at=(distance:number)=>{if(!length||distance<0||distance>length)return;let lo=1,hi=distances.length-1;while(lo<hi){const m=(lo+hi)>>1;if(distances[m]<distance)lo=m+1;else hi=m;}const a=points[lo-1],b=points[lo],span=distances[lo]-distances[lo-1],t=span?(distance-distances[lo-1])/span:0;return {x:a.x+(b.x-a.x)*t,y:a.y+(b.y-a.y)*t,angle:Math.atan2(b.y-a.y,b.x-a.x)};};return {length,at};",
      "ref": "working-copy-2026-10-22",
      "sha256": "93519a1b76af83351dcb1e24eb9a3d79fb364aa470db57fc3b9120d20373beb5"
    },
    {
      "file": "app/studio/text-warp.ts",
      "start": 2,
      "end": 4,
      "code": "export const warpNames={arc:'Arc',arcLower:'Arc Lower',arcUpper:'Arc Upper',arch:'Arch',bulge:'Bulge',shellLower:'Shell Lower',shellUpper:'Shell Upper',flag:'Flag',wave:'Wave',fish:'Fish',rise:'Rise',fisheye:'Fisheye',inflate:'Inflate',squeeze:'Squeeze',twist:'Twist'};\nexport type TextWarp={style:keyof typeof warpNames;bend:number;horizontal:number;vertical:number;orientation:'horizontal'|'vertical'};\nexport function validateTextWarp(w:TextWarp){if(!w||!Object.hasOwn(warpNames,w.style)||!['horizontal','vertical'].includes(w.orientation)||[w.bend,w.horizontal,w.vertical].some(n=>!Number.isFinite(n)||n< -100||n>100))throw Error('Invalid text warp.');}",
      "ref": "working-copy-2026-10-22",
      "sha256": "9996422b9f8be4817644f7f184eddc5db3453fcf165cf8b8ab718f760e5b3373"
    },
    {
      "file": "app/studio/opentype.ts",
      "start": 4,
      "end": 9,
      "code": "export type OpenType={kerning?:boolean;ligatures?:boolean;discretionaryLigatures?:boolean;fractions?:boolean;ordinals?:boolean;alternates?:boolean;swash?:boolean;oldStyle?:boolean;tabular?:boolean;caps?:'normal'|'small-caps'|'all-small-caps'|'uppercase';position?:'normal'|'superscript'|'subscript'};\nexport const openTypeFlags=['kerning','ligatures','discretionaryLigatures','fractions','ordinals','alternates','swash','oldStyle','tabular'] as const;\nexport function validateOpenType(t:OpenType){if(!t||typeof t!=='object')throw Error('Invalid OpenType options.');for(const k of openTypeFlags)if(t[k]!==undefined&&typeof t[k]!=='boolean')throw Error('Invalid OpenType option.');if(t.caps!==undefined&&!['normal','small-caps','all-small-caps','uppercase'].includes(t.caps)||t.position!==undefined&&!['normal','superscript','subscript'].includes(t.position))throw Error('Invalid character variant.');}\nexport function openTypeStyle(t:OpenType={}){const pairs=[['kern',t.kerning??true],['liga',t.ligatures??true],['clig',t.ligatures??true],['dlig',!!t.discretionaryLigatures],['frac',!!t.fractions],['ordn',!!t.ordinals],['salt',!!t.alternates],['swsh',!!t.swash],['onum',!!t.oldStyle],['tnum',!!t.tabular],['smcp',t.caps==='small-caps'||t.caps==='all-small-caps'],['c2sc',t.caps==='all-small-caps']];return `font-feature-settings:${pairs.map(([k,v])=>`'${k}' ${v?1:0}`).join(',')};font-kerning:${t.kerning===false?'none':'normal'};text-transform:${t.caps==='uppercase'?'uppercase':'none'};`;}\nexport function openTypeToPsd(t:OpenType={}){return {autoKerning:t.kerning!==false,ligatures:t.ligatures!==false,dLigatures:!!t.discretionaryLigatures,fontCaps:t.caps==='uppercase'?2:t.caps==='small-caps'||t.caps==='all-small-caps'?1:0,fontBaseline:t.position==='superscript'?1:t.position==='subscript'?2:0};}\nexport function openTypeFromPsd(s:any):OpenType|undefined{if(!s)return;const t:OpenType={};if(s.autoKerning===false)t.kerning=false;if(s.ligatures===false)t.ligatures=false;if(s.dLigatures)t.discretionaryLigatures=true;if(s.fontCaps===2)t.caps='uppercase';else if(s.fontCaps===1)t.caps='small-caps';if(s.fontBaseline===1)t.position='superscript';else if(s.fontBaseline===2)t.position='subscript';return Object.keys(t).length?t:undefined;}",
      "ref": "working-copy-2026-10-22",
      "sha256": "3c200f1216cd90680065e0204cf4afa5366f390d46f36623c03aa85d1711a189"
    }
  ],
  "productionUI": {
    "date": "2026-10-22",
    "passed": true,
    "route": "Photoshot UI text editing and project reopen",
    "text": "SOFT\nSTUDY",
    "fontSize": 80,
    "color": "#bfff50",
    "runs": [
      {
        "start": 0,
        "end": 4,
        "style": {
          "bold": true
        }
      }
    ],
    "typography": {
      "tracking": 50,
      "leading": 0,
      "align": "left",
      "vertical": false,
      "wrap": false,
      "bold": false,
      "italic": false,
      "underline": false,
      "strike": false,
      "scaleX": 100,
      "scaleY": 100,
      "baseline": 0,
      "spaceAfter": 0,
      "openType": {
        "discretionaryLigatures": true
      }
    },
    "warp": {
      "bend": 15,
      "horizontal": 0,
      "vertical": 0,
      "orientation": "horizontal",
      "style": "arc"
    },
    "entireTextLayerExactMatch": true,
    "freshAdobeTest": false,
    "freshTextPathTest": false
  },
  "historicalReports": [
    {
      "file": "opentype-native-results.json",
      "sha256": "aac83b5fbc0ee9255c9a55d1ec806235add9bc69895cee44b10c3fa02b627103",
      "results": {
        "passed": true,
        "checks": [
          {
            "psb": false,
            "rangeFlagsPreserved": true,
            "nativeTextEdited": true
          },
          {
            "psb": true,
            "rangeFlagsPreserved": true,
            "nativeTextEdited": true
          }
        ],
        "scope": "Actual Photoshop open/resave preserves native OpenType ranges; native TextItem edit remains editable. Replacing all text through the Adobe DOM resets ranges, as expected."
      },
      "freshRun": false
    },
    {
      "file": "opentype-render-results.json",
      "sha256": "5d445aae7e9d7dd359c75dc022e0972cb7df22a641e544852435c4752ad622fe",
      "results": {
        "passed": true,
        "checks": [
          {
            "option": {
              "ligatures": false,
              "kerning": false
            },
            "ink": 5776,
            "changed": true
          },
          {
            "option": {
              "caps": "uppercase"
            },
            "ink": 7099,
            "changed": true
          },
          {
            "option": {
              "position": "superscript"
            },
            "ink": 2420,
            "changed": true
          },
          {
            "option": {
              "caps": "small-caps"
            },
            "ink": 5651,
            "changed": true
          },
          {
            "option": {
              "fractions": true
            },
            "ink": 4879,
            "changed": true
          },
          {
            "option": {
              "oldStyle": true
            },
            "ink": 5464,
            "changed": true
          }
        ],
        "nativeChecks": [
          {
            "psb": false,
            "runs": 3
          },
          {
            "psb": true,
            "runs": 3
          }
        ],
        "ink": 5786,
        "pathInk": 5360,
        "boundedPathInk": 2388,
        "runsInk": 6607
      },
      "freshRun": false
    },
    {
      "file": "opentype-ui-results.json",
      "sha256": "90b2e64f161195942cdc8871de17bc32615fe2eeabe5e621deeb0eb7f789607f",
      "results": {
        "passed": true,
        "textSelectionRange": true,
        "mixedCharacterControls": true,
        "undoRedo": true,
        "textInsertionRemap": true,
        "recovery": true,
        "clearOverrides": true,
        "errors": []
      },
      "freshRun": false
    }
  ]
}