diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..b34ffd838 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,16 @@ +**/node_modules/* +**/out/* +**/dist/* +**/dist-cjs/* +**/dist-esm/* +**/.tsbuild* +**/.next/* +*.md +**/_archive/* +**/*.css.map +**/*.js.map +**/*.d.ts +**/*.test.ts +**/api/* +!**/pages/api/* +**/*.json \ No newline at end of file diff --git a/.eslintplugin.js b/.eslintplugin.js new file mode 100644 index 000000000..465004415 --- /dev/null +++ b/.eslintplugin.js @@ -0,0 +1,2 @@ +require('ts-node/register') +module.exports = require('./scripts/lib/eslint-plugin.ts') diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..f137cec71 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,86 @@ +module.exports = { + extends: [ + 'prettier', + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@next/next/core-web-vitals', + ], + plugins: ['@typescript-eslint', 'no-only-tests', 'import', 'local', '@next/next', 'react-hooks'], + settings: { + next: { + rootDir: ['apps/*/', 'packages/*/'], + }, + }, + ignorePatterns: ['**/*.js', '**/vscode-script-utils/*'], + rules: { + '@next/next/no-html-link-for-pages': 'off', + 'react/jsx-key': 'off', + 'no-non-null-assertion': 'off', + 'no-fallthrough': 'off', + '@typescript-eslint/no-fallthrough': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + 'react/display-name': 'off', + '@next/next/no-img-element': 'off', + '@typescript-eslint/no-extra-semi': 'off', + 'no-mixed-spaces-and-tabs': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'no-throw-literal': 'error', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + 'import/no-extraneous-dependencies': 'error', + 'import/no-internal-modules': ['error', { forbid: ['@tldraw/*/**'] }], + '@typescript-eslint/consistent-type-exports': [ + 'error', + { fixMixedExportsWithInlineTypeSpecifier: true }, + ], + 'local/no-export-star': 'error', + }, + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, + overrides: [ + { + // enable the rule specifically for TypeScript files + files: ['*.ts', '*.tsx'], + rules: { + '@typescript-eslint/explicit-module-boundary-types': [0], + 'no-console': ['error', { allow: ['warn', 'error'] }], + 'no-only-tests/no-only-tests': ['error', { fix: true }], + }, + }, + { + files: ['apps/fixup/**/*', 'scripts/**/*'], + rules: { + 'no-console': 'off', + }, + }, + { + files: ['e2e/**/*'], + rules: { + '@typescript-eslint/no-empty-function': 'off', + }, + }, + { + files: 'scripts/**/*', + rules: { + 'import/no-extraneous-dependencies': 'off', + }, + }, + { + files: ['apps/examples/**/*'], + rules: { + 'import/no-internal-modules': 'off', + }, + }, + ], +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..993be3bd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-cjs +dist-esm +.tsbuild* +.lazy +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# turborepo +.turbo + +coverage + +**/*.env +**/*.tsbuildinfo + +**/*.css.map +**/*.js.map +apps/examples/www/index.js +apps/examples/www/index.css +nohup.out + +packages/*/package +packages/*/*.tgz + +tsconfig.build.json +.vercel + +api-json +api-md + +packages/tldraw/editor.css +packages/tldraw/ui.css + +packages/assets/embed-icons +packages/assets/fonts +packages/assets/icons +packages/assets/translations +apps/examples/www/embed-icons +apps/examples/www/fonts +apps/examples/www/icons +apps/examples/www/translations + +# yarn v2 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +packages/*/api +apps/examples/www/index.css +apps/examples/www/index.js +.tsbuild diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..8cbfb33ff --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +**/node_modules/* +**/out/* +**/dist/* +**/dist-cjs/* +**/dist-esm/* +**/api/* +!**/pages/api/* +**/.tsbuild* +**/.next/* +*.mdx +**/_archive/* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..7fd1df8c6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "trailingComma": "es5", + "singleQuote": true, + "semi": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": true, + "plugins": ["prettier-plugin-organize-imports"] +} diff --git a/.yarn/patches/@microsoft-api-extractor-npm-7.34.1-af268a32f8.patch b/.yarn/patches/@microsoft-api-extractor-npm-7.34.1-af268a32f8.patch new file mode 100644 index 000000000..5aad79fa4 --- /dev/null +++ b/.yarn/patches/@microsoft-api-extractor-npm-7.34.1-af268a32f8.patch @@ -0,0 +1,12 @@ +diff --git a/lib/api/ExtractorConfig.js b/lib/api/ExtractorConfig.js +index a37db0d564a5662df78055ded63069a4b2706bb1..158d4b121fa0c7cf15c3a52570218a2fe67fc567 100644 +--- a/lib/api/ExtractorConfig.js ++++ b/lib/api/ExtractorConfig.js +@@ -669,5 +669,5 @@ ExtractorConfig.FILENAME = 'api-extractor.json'; + */ + ExtractorConfig._tsdocBaseFilePath = path.resolve(__dirname, '../../extends/tsdoc-base.json'); + ExtractorConfig._defaultConfig = node_core_library_1.JsonFile.load(path.join(__dirname, '../schemas/api-extractor-defaults.json')); +-ExtractorConfig._declarationFileExtensionRegExp = /\.d\.ts$/i; ++ExtractorConfig._declarationFileExtensionRegExp = /\.d\.(m|c)?ts$/i; + //# sourceMappingURL=ExtractorConfig.js.map +\ No newline at end of file diff --git a/.yarn/releases/yarn-3.5.0.cjs b/.yarn/releases/yarn-3.5.0.cjs new file mode 100755 index 000000000..093e64a9f --- /dev/null +++ b/.yarn/releases/yarn-3.5.0.cjs @@ -0,0 +1,873 @@ +#!/usr/bin/env node +/* eslint-disable */ +//prettier-ignore +(()=>{var Qge=Object.create;var AS=Object.defineProperty;var bge=Object.getOwnPropertyDescriptor;var Sge=Object.getOwnPropertyNames;var vge=Object.getPrototypeOf,xge=Object.prototype.hasOwnProperty;var J=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+r+'" is not supported')});var Pge=(r,e)=>()=>(r&&(e=r(r=0)),e);var w=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),ut=(r,e)=>{for(var t in e)AS(r,t,{get:e[t],enumerable:!0})},Dge=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Sge(e))!xge.call(r,n)&&n!==t&&AS(r,n,{get:()=>e[n],enumerable:!(i=bge(e,n))||i.enumerable});return r};var Pe=(r,e,t)=>(t=r!=null?Qge(vge(r)):{},Dge(e||!r||!r.__esModule?AS(t,"default",{value:r,enumerable:!0}):t,r));var QK=w((GXe,BK)=>{BK.exports=wK;wK.sync=Zge;var IK=J("fs");function Xge(r,e){var t=e.pathExt!==void 0?e.pathExt:process.env.PATHEXT;if(!t||(t=t.split(";"),t.indexOf("")!==-1))return!0;for(var i=0;i{xK.exports=SK;SK.sync=_ge;var bK=J("fs");function SK(r,e,t){bK.stat(r,function(i,n){t(i,i?!1:vK(n,e))})}function _ge(r,e){return vK(bK.statSync(r),e)}function vK(r,e){return r.isFile()&&$ge(r,e)}function $ge(r,e){var t=r.mode,i=r.uid,n=r.gid,s=e.uid!==void 0?e.uid:process.getuid&&process.getuid(),o=e.gid!==void 0?e.gid:process.getgid&&process.getgid(),a=parseInt("100",8),l=parseInt("010",8),c=parseInt("001",8),u=a|l,g=t&c||t&l&&n===o||t&a&&i===s||t&u&&s===0;return g}});var kK=w((qXe,DK)=>{var jXe=J("fs"),sI;process.platform==="win32"||global.TESTING_WINDOWS?sI=QK():sI=PK();DK.exports=SS;SS.sync=efe;function SS(r,e,t){if(typeof e=="function"&&(t=e,e={}),!t){if(typeof Promise!="function")throw new TypeError("callback not provided");return new Promise(function(i,n){SS(r,e||{},function(s,o){s?n(s):i(o)})})}sI(r,e||{},function(i,n){i&&(i.code==="EACCES"||e&&e.ignoreErrors)&&(i=null,n=!1),t(i,n)})}function efe(r,e){try{return sI.sync(r,e||{})}catch(t){if(e&&e.ignoreErrors||t.code==="EACCES")return!1;throw t}}});var MK=w((JXe,OK)=>{var vg=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys",RK=J("path"),tfe=vg?";":":",FK=kK(),NK=r=>Object.assign(new Error(`not found: ${r}`),{code:"ENOENT"}),LK=(r,e)=>{let t=e.colon||tfe,i=r.match(/\//)||vg&&r.match(/\\/)?[""]:[...vg?[process.cwd()]:[],...(e.path||process.env.PATH||"").split(t)],n=vg?e.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",s=vg?n.split(t):[""];return vg&&r.indexOf(".")!==-1&&s[0]!==""&&s.unshift(""),{pathEnv:i,pathExt:s,pathExtExe:n}},TK=(r,e,t)=>{typeof e=="function"&&(t=e,e={}),e||(e={});let{pathEnv:i,pathExt:n,pathExtExe:s}=LK(r,e),o=[],a=c=>new Promise((u,g)=>{if(c===i.length)return e.all&&o.length?u(o):g(NK(r));let f=i[c],h=/^".*"$/.test(f)?f.slice(1,-1):f,p=RK.join(h,r),C=!h&&/^\.[\\\/]/.test(r)?r.slice(0,2)+p:p;u(l(C,c,0))}),l=(c,u,g)=>new Promise((f,h)=>{if(g===n.length)return f(a(u+1));let p=n[g];FK(c+p,{pathExt:s},(C,y)=>{if(!C&&y)if(e.all)o.push(c+p);else return f(c+p);return f(l(c,u,g+1))})});return t?a(0).then(c=>t(null,c),t):a(0)},rfe=(r,e)=>{e=e||{};let{pathEnv:t,pathExt:i,pathExtExe:n}=LK(r,e),s=[];for(let o=0;o{"use strict";var KK=(r={})=>{let e=r.env||process.env;return(r.platform||process.platform)!=="win32"?"PATH":Object.keys(e).reverse().find(i=>i.toUpperCase()==="PATH")||"Path"};vS.exports=KK;vS.exports.default=KK});var jK=w((zXe,YK)=>{"use strict";var HK=J("path"),ife=MK(),nfe=UK();function GK(r,e){let t=r.options.env||process.env,i=process.cwd(),n=r.options.cwd!=null,s=n&&process.chdir!==void 0&&!process.chdir.disabled;if(s)try{process.chdir(r.options.cwd)}catch{}let o;try{o=ife.sync(r.command,{path:t[nfe({env:t})],pathExt:e?HK.delimiter:void 0})}catch{}finally{s&&process.chdir(i)}return o&&(o=HK.resolve(n?r.options.cwd:"",o)),o}function sfe(r){return GK(r)||GK(r,!0)}YK.exports=sfe});var qK=w((VXe,PS)=>{"use strict";var xS=/([()\][%!^"`<>&|;, *?])/g;function ofe(r){return r=r.replace(xS,"^$1"),r}function afe(r,e){return r=`${r}`,r=r.replace(/(\\*)"/g,'$1$1\\"'),r=r.replace(/(\\*)$/,"$1$1"),r=`"${r}"`,r=r.replace(xS,"^$1"),e&&(r=r.replace(xS,"^$1")),r}PS.exports.command=ofe;PS.exports.argument=afe});var WK=w((XXe,JK)=>{"use strict";JK.exports=/^#!(.*)/});var VK=w((ZXe,zK)=>{"use strict";var Afe=WK();zK.exports=(r="")=>{let e=r.match(Afe);if(!e)return null;let[t,i]=e[0].replace(/#! ?/,"").split(" "),n=t.split("/").pop();return n==="env"?i:i?`${n} ${i}`:n}});var ZK=w((_Xe,XK)=>{"use strict";var DS=J("fs"),lfe=VK();function cfe(r){let t=Buffer.alloc(150),i;try{i=DS.openSync(r,"r"),DS.readSync(i,t,0,150,0),DS.closeSync(i)}catch{}return lfe(t.toString())}XK.exports=cfe});var tU=w(($Xe,eU)=>{"use strict";var ufe=J("path"),_K=jK(),$K=qK(),gfe=ZK(),ffe=process.platform==="win32",hfe=/\.(?:com|exe)$/i,pfe=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function dfe(r){r.file=_K(r);let e=r.file&&gfe(r.file);return e?(r.args.unshift(r.file),r.command=e,_K(r)):r.file}function Cfe(r){if(!ffe)return r;let e=dfe(r),t=!hfe.test(e);if(r.options.forceShell||t){let i=pfe.test(e);r.command=ufe.normalize(r.command),r.command=$K.command(r.command),r.args=r.args.map(s=>$K.argument(s,i));let n=[r.command].concat(r.args).join(" ");r.args=["/d","/s","/c",`"${n}"`],r.command=process.env.comspec||"cmd.exe",r.options.windowsVerbatimArguments=!0}return r}function mfe(r,e,t){e&&!Array.isArray(e)&&(t=e,e=null),e=e?e.slice(0):[],t=Object.assign({},t);let i={command:r,args:e,options:t,file:void 0,original:{command:r,args:e}};return t.shell?i:Cfe(i)}eU.exports=mfe});var nU=w((eZe,iU)=>{"use strict";var kS=process.platform==="win32";function RS(r,e){return Object.assign(new Error(`${e} ${r.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${e} ${r.command}`,path:r.command,spawnargs:r.args})}function Efe(r,e){if(!kS)return;let t=r.emit;r.emit=function(i,n){if(i==="exit"){let s=rU(n,e,"spawn");if(s)return t.call(r,"error",s)}return t.apply(r,arguments)}}function rU(r,e){return kS&&r===1&&!e.file?RS(e.original,"spawn"):null}function Ife(r,e){return kS&&r===1&&!e.file?RS(e.original,"spawnSync"):null}iU.exports={hookChildProcess:Efe,verifyENOENT:rU,verifyENOENTSync:Ife,notFoundError:RS}});var LS=w((tZe,xg)=>{"use strict";var sU=J("child_process"),FS=tU(),NS=nU();function oU(r,e,t){let i=FS(r,e,t),n=sU.spawn(i.command,i.args,i.options);return NS.hookChildProcess(n,i),n}function yfe(r,e,t){let i=FS(r,e,t),n=sU.spawnSync(i.command,i.args,i.options);return n.error=n.error||NS.verifyENOENTSync(n.status,i),n}xg.exports=oU;xg.exports.spawn=oU;xg.exports.sync=yfe;xg.exports._parse=FS;xg.exports._enoent=NS});var AU=w((rZe,aU)=>{"use strict";function wfe(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function Wl(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Wl)}wfe(Wl,Error);Wl.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;g>",ie=me(">>",!1),de=">&",_e=me(">&",!1),Pt=">",It=me(">",!1),Or="<<<",ii=me("<<<",!1),gi="<&",hr=me("<&",!1),fi="<",ni=me("<",!1),Os=function(m){return{type:"argument",segments:[].concat(...m)}},pr=function(m){return m},Ii="$'",es=me("$'",!1),ua="'",pA=me("'",!1),ag=function(m){return[{type:"text",text:m}]},ts='""',dA=me('""',!1),ga=function(){return{type:"text",text:""}},yp='"',CA=me('"',!1),mA=function(m){return m},wr=function(m){return{type:"arithmetic",arithmetic:m,quoted:!0}},kl=function(m){return{type:"shell",shell:m,quoted:!0}},Ag=function(m){return{type:"variable",...m,quoted:!0}},Io=function(m){return{type:"text",text:m}},lg=function(m){return{type:"arithmetic",arithmetic:m,quoted:!1}},wp=function(m){return{type:"shell",shell:m,quoted:!1}},Bp=function(m){return{type:"variable",...m,quoted:!1}},vr=function(m){return{type:"glob",pattern:m}},se=/^[^']/,yo=Je(["'"],!0,!1),kn=function(m){return m.join("")},cg=/^[^$"]/,Qt=Je(["$",'"'],!0,!1),Rl=`\\ +`,Rn=me(`\\ +`,!1),rs=function(){return""},is="\\",gt=me("\\",!1),wo=/^[\\$"`]/,At=Je(["\\","$",'"',"`"],!1,!1),an=function(m){return m},S="\\a",Tt=me("\\a",!1),ug=function(){return"a"},Fl="\\b",Qp=me("\\b",!1),bp=function(){return"\b"},Sp=/^[Ee]/,vp=Je(["E","e"],!1,!1),xp=function(){return"\x1B"},G="\\f",yt=me("\\f",!1),EA=function(){return"\f"},Ji="\\n",Nl=me("\\n",!1),Xe=function(){return` +`},fa="\\r",gg=me("\\r",!1),FE=function(){return"\r"},Pp="\\t",NE=me("\\t",!1),ar=function(){return" "},Fn="\\v",Ll=me("\\v",!1),Dp=function(){return"\v"},Ms=/^[\\'"?]/,ha=Je(["\\","'",'"',"?"],!1,!1),An=function(m){return String.fromCharCode(parseInt(m,16))},Te="\\x",fg=me("\\x",!1),Tl="\\u",Ks=me("\\u",!1),Ol="\\U",IA=me("\\U",!1),hg=function(m){return String.fromCodePoint(parseInt(m,16))},pg=/^[0-7]/,pa=Je([["0","7"]],!1,!1),da=/^[0-9a-fA-f]/,rt=Je([["0","9"],["a","f"],["A","f"]],!1,!1),Bo=nt(),yA="-",Ml=me("-",!1),Us="+",Kl=me("+",!1),LE=".",kp=me(".",!1),dg=function(m,b,N){return{type:"number",value:(m==="-"?-1:1)*parseFloat(b.join("")+"."+N.join(""))}},Rp=function(m,b){return{type:"number",value:(m==="-"?-1:1)*parseInt(b.join(""))}},TE=function(m){return{type:"variable",...m}},Ul=function(m){return{type:"variable",name:m}},OE=function(m){return m},Cg="*",wA=me("*",!1),Rr="/",ME=me("/",!1),Hs=function(m,b,N){return{type:b==="*"?"multiplication":"division",right:N}},Gs=function(m,b){return b.reduce((N,U)=>({left:N,...U}),m)},mg=function(m,b,N){return{type:b==="+"?"addition":"subtraction",right:N}},BA="$((",R=me("$((",!1),q="))",Ce=me("))",!1),Ke=function(m){return m},Re="$(",ze=me("$(",!1),dt=function(m){return m},Ft="${",Nn=me("${",!1),qb=":-",S1=me(":-",!1),v1=function(m,b){return{name:m,defaultValue:b}},Jb=":-}",x1=me(":-}",!1),P1=function(m){return{name:m,defaultValue:[]}},Wb=":+",D1=me(":+",!1),k1=function(m,b){return{name:m,alternativeValue:b}},zb=":+}",R1=me(":+}",!1),F1=function(m){return{name:m,alternativeValue:[]}},Vb=function(m){return{name:m}},N1="$",L1=me("$",!1),T1=function(m){return e.isGlobPattern(m)},O1=function(m){return m},Xb=/^[a-zA-Z0-9_]/,Zb=Je([["a","z"],["A","Z"],["0","9"],"_"],!1,!1),_b=function(){return T()},$b=/^[$@*?#a-zA-Z0-9_\-]/,eS=Je(["$","@","*","?","#",["a","z"],["A","Z"],["0","9"],"_","-"],!1,!1),M1=/^[(){}<>$|&; \t"']/,Eg=Je(["(",")","{","}","<",">","$","|","&",";"," "," ",'"',"'"],!1,!1),tS=/^[<>&; \t"']/,rS=Je(["<",">","&",";"," "," ",'"',"'"],!1,!1),KE=/^[ \t]/,UE=Je([" "," "],!1,!1),Q=0,Me=0,QA=[{line:1,column:1}],d=0,E=[],I=0,k;if("startRule"in e){if(!(e.startRule in i))throw new Error(`Can't start parsing from rule "`+e.startRule+'".');n=i[e.startRule]}function T(){return r.substring(Me,Q)}function Z(){return Et(Me,Q)}function te(m,b){throw b=b!==void 0?b:Et(Me,Q),Ri([lt(m)],r.substring(Me,Q),b)}function Be(m,b){throw b=b!==void 0?b:Et(Me,Q),Ln(m,b)}function me(m,b){return{type:"literal",text:m,ignoreCase:b}}function Je(m,b,N){return{type:"class",parts:m,inverted:b,ignoreCase:N}}function nt(){return{type:"any"}}function wt(){return{type:"end"}}function lt(m){return{type:"other",description:m}}function it(m){var b=QA[m],N;if(b)return b;for(N=m-1;!QA[N];)N--;for(b=QA[N],b={line:b.line,column:b.column};Nd&&(d=Q,E=[]),E.push(m))}function Ln(m,b){return new Wl(m,null,null,b)}function Ri(m,b,N){return new Wl(Wl.buildMessage(m,b),m,b,N)}function bA(){var m,b;return m=Q,b=Mr(),b===t&&(b=null),b!==t&&(Me=m,b=s(b)),m=b,m}function Mr(){var m,b,N,U,ce;if(m=Q,b=Kr(),b!==t){for(N=[],U=He();U!==t;)N.push(U),U=He();N!==t?(U=Ca(),U!==t?(ce=ns(),ce===t&&(ce=null),ce!==t?(Me=m,b=o(b,U,ce),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t)}else Q=m,m=t;if(m===t)if(m=Q,b=Kr(),b!==t){for(N=[],U=He();U!==t;)N.push(U),U=He();N!==t?(U=Ca(),U===t&&(U=null),U!==t?(Me=m,b=a(b,U),m=b):(Q=m,m=t)):(Q=m,m=t)}else Q=m,m=t;return m}function ns(){var m,b,N,U,ce;for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t)if(N=Mr(),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();U!==t?(Me=m,b=l(N),m=b):(Q=m,m=t)}else Q=m,m=t;else Q=m,m=t;return m}function Ca(){var m;return r.charCodeAt(Q)===59?(m=c,Q++):(m=t,I===0&&Qe(u)),m===t&&(r.charCodeAt(Q)===38?(m=g,Q++):(m=t,I===0&&Qe(f))),m}function Kr(){var m,b,N;return m=Q,b=K1(),b!==t?(N=age(),N===t&&(N=null),N!==t?(Me=m,b=h(b,N),m=b):(Q=m,m=t)):(Q=m,m=t),m}function age(){var m,b,N,U,ce,Se,ht;for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t)if(N=Age(),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t)if(ce=Kr(),ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();Se!==t?(Me=m,b=p(N,ce),m=b):(Q=m,m=t)}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t;else Q=m,m=t;return m}function Age(){var m;return r.substr(Q,2)===C?(m=C,Q+=2):(m=t,I===0&&Qe(y)),m===t&&(r.substr(Q,2)===B?(m=B,Q+=2):(m=t,I===0&&Qe(v))),m}function K1(){var m,b,N;return m=Q,b=uge(),b!==t?(N=lge(),N===t&&(N=null),N!==t?(Me=m,b=D(b,N),m=b):(Q=m,m=t)):(Q=m,m=t),m}function lge(){var m,b,N,U,ce,Se,ht;for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t)if(N=cge(),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t)if(ce=K1(),ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();Se!==t?(Me=m,b=L(N,ce),m=b):(Q=m,m=t)}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t;else Q=m,m=t;return m}function cge(){var m;return r.substr(Q,2)===H?(m=H,Q+=2):(m=t,I===0&&Qe(j)),m===t&&(r.charCodeAt(Q)===124?(m=$,Q++):(m=t,I===0&&Qe(V))),m}function HE(){var m,b,N,U,ce,Se;if(m=Q,b=Z1(),b!==t)if(r.charCodeAt(Q)===61?(N=W,Q++):(N=t,I===0&&Qe(_)),N!==t)if(U=G1(),U!==t){for(ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();ce!==t?(Me=m,b=A(b,U),m=b):(Q=m,m=t)}else Q=m,m=t;else Q=m,m=t;else Q=m,m=t;if(m===t)if(m=Q,b=Z1(),b!==t)if(r.charCodeAt(Q)===61?(N=W,Q++):(N=t,I===0&&Qe(_)),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();U!==t?(Me=m,b=ae(b),m=b):(Q=m,m=t)}else Q=m,m=t;else Q=m,m=t;return m}function uge(){var m,b,N,U,ce,Se,ht,Bt,Jr,hi,ss;for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t)if(r.charCodeAt(Q)===40?(N=ge,Q++):(N=t,I===0&&Qe(re)),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t)if(ce=Mr(),ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();if(Se!==t)if(r.charCodeAt(Q)===41?(ht=O,Q++):(ht=t,I===0&&Qe(F)),ht!==t){for(Bt=[],Jr=He();Jr!==t;)Bt.push(Jr),Jr=He();if(Bt!==t){for(Jr=[],hi=Fp();hi!==t;)Jr.push(hi),hi=Fp();if(Jr!==t){for(hi=[],ss=He();ss!==t;)hi.push(ss),ss=He();hi!==t?(Me=m,b=ue(ce,Jr),m=b):(Q=m,m=t)}else Q=m,m=t}else Q=m,m=t}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t;else Q=m,m=t;if(m===t){for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t)if(r.charCodeAt(Q)===123?(N=he,Q++):(N=t,I===0&&Qe(ke)),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t)if(ce=Mr(),ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();if(Se!==t)if(r.charCodeAt(Q)===125?(ht=Fe,Q++):(ht=t,I===0&&Qe(Ne)),ht!==t){for(Bt=[],Jr=He();Jr!==t;)Bt.push(Jr),Jr=He();if(Bt!==t){for(Jr=[],hi=Fp();hi!==t;)Jr.push(hi),hi=Fp();if(Jr!==t){for(hi=[],ss=He();ss!==t;)hi.push(ss),ss=He();hi!==t?(Me=m,b=oe(ce,Jr),m=b):(Q=m,m=t)}else Q=m,m=t}else Q=m,m=t}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t;else Q=m,m=t;if(m===t){for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t){for(N=[],U=HE();U!==t;)N.push(U),U=HE();if(N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t){if(ce=[],Se=H1(),Se!==t)for(;Se!==t;)ce.push(Se),Se=H1();else ce=t;if(ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();Se!==t?(Me=m,b=le(N,ce),m=b):(Q=m,m=t)}else Q=m,m=t}else Q=m,m=t}else Q=m,m=t}else Q=m,m=t;if(m===t){for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t){if(N=[],U=HE(),U!==t)for(;U!==t;)N.push(U),U=HE();else N=t;if(N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();U!==t?(Me=m,b=we(N),m=b):(Q=m,m=t)}else Q=m,m=t}else Q=m,m=t}}}return m}function U1(){var m,b,N,U,ce;for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t){if(N=[],U=GE(),U!==t)for(;U!==t;)N.push(U),U=GE();else N=t;if(N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();U!==t?(Me=m,b=fe(N),m=b):(Q=m,m=t)}else Q=m,m=t}else Q=m,m=t;return m}function H1(){var m,b,N;for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();if(b!==t?(N=Fp(),N!==t?(Me=m,b=Ae(N),m=b):(Q=m,m=t)):(Q=m,m=t),m===t){for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();b!==t?(N=GE(),N!==t?(Me=m,b=Ae(N),m=b):(Q=m,m=t)):(Q=m,m=t)}return m}function Fp(){var m,b,N,U,ce;for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();return b!==t?(qe.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(ne)),N===t&&(N=null),N!==t?(U=gge(),U!==t?(ce=GE(),ce!==t?(Me=m,b=Y(N,U,ce),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m}function gge(){var m;return r.substr(Q,2)===pe?(m=pe,Q+=2):(m=t,I===0&&Qe(ie)),m===t&&(r.substr(Q,2)===de?(m=de,Q+=2):(m=t,I===0&&Qe(_e)),m===t&&(r.charCodeAt(Q)===62?(m=Pt,Q++):(m=t,I===0&&Qe(It)),m===t&&(r.substr(Q,3)===Or?(m=Or,Q+=3):(m=t,I===0&&Qe(ii)),m===t&&(r.substr(Q,2)===gi?(m=gi,Q+=2):(m=t,I===0&&Qe(hr)),m===t&&(r.charCodeAt(Q)===60?(m=fi,Q++):(m=t,I===0&&Qe(ni))))))),m}function GE(){var m,b,N;for(m=Q,b=[],N=He();N!==t;)b.push(N),N=He();return b!==t?(N=G1(),N!==t?(Me=m,b=Ae(N),m=b):(Q=m,m=t)):(Q=m,m=t),m}function G1(){var m,b,N;if(m=Q,b=[],N=Y1(),N!==t)for(;N!==t;)b.push(N),N=Y1();else b=t;return b!==t&&(Me=m,b=Os(b)),m=b,m}function Y1(){var m,b;return m=Q,b=fge(),b!==t&&(Me=m,b=pr(b)),m=b,m===t&&(m=Q,b=hge(),b!==t&&(Me=m,b=pr(b)),m=b,m===t&&(m=Q,b=pge(),b!==t&&(Me=m,b=pr(b)),m=b,m===t&&(m=Q,b=dge(),b!==t&&(Me=m,b=pr(b)),m=b))),m}function fge(){var m,b,N,U;return m=Q,r.substr(Q,2)===Ii?(b=Ii,Q+=2):(b=t,I===0&&Qe(es)),b!==t?(N=Ege(),N!==t?(r.charCodeAt(Q)===39?(U=ua,Q++):(U=t,I===0&&Qe(pA)),U!==t?(Me=m,b=ag(N),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m}function hge(){var m,b,N,U;return m=Q,r.charCodeAt(Q)===39?(b=ua,Q++):(b=t,I===0&&Qe(pA)),b!==t?(N=Cge(),N!==t?(r.charCodeAt(Q)===39?(U=ua,Q++):(U=t,I===0&&Qe(pA)),U!==t?(Me=m,b=ag(N),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m}function pge(){var m,b,N,U;if(m=Q,r.substr(Q,2)===ts?(b=ts,Q+=2):(b=t,I===0&&Qe(dA)),b!==t&&(Me=m,b=ga()),m=b,m===t)if(m=Q,r.charCodeAt(Q)===34?(b=yp,Q++):(b=t,I===0&&Qe(CA)),b!==t){for(N=[],U=j1();U!==t;)N.push(U),U=j1();N!==t?(r.charCodeAt(Q)===34?(U=yp,Q++):(U=t,I===0&&Qe(CA)),U!==t?(Me=m,b=mA(N),m=b):(Q=m,m=t)):(Q=m,m=t)}else Q=m,m=t;return m}function dge(){var m,b,N;if(m=Q,b=[],N=q1(),N!==t)for(;N!==t;)b.push(N),N=q1();else b=t;return b!==t&&(Me=m,b=mA(b)),m=b,m}function j1(){var m,b;return m=Q,b=V1(),b!==t&&(Me=m,b=wr(b)),m=b,m===t&&(m=Q,b=X1(),b!==t&&(Me=m,b=kl(b)),m=b,m===t&&(m=Q,b=oS(),b!==t&&(Me=m,b=Ag(b)),m=b,m===t&&(m=Q,b=mge(),b!==t&&(Me=m,b=Io(b)),m=b))),m}function q1(){var m,b;return m=Q,b=V1(),b!==t&&(Me=m,b=lg(b)),m=b,m===t&&(m=Q,b=X1(),b!==t&&(Me=m,b=wp(b)),m=b,m===t&&(m=Q,b=oS(),b!==t&&(Me=m,b=Bp(b)),m=b,m===t&&(m=Q,b=wge(),b!==t&&(Me=m,b=vr(b)),m=b,m===t&&(m=Q,b=yge(),b!==t&&(Me=m,b=Io(b)),m=b)))),m}function Cge(){var m,b,N;for(m=Q,b=[],se.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(yo));N!==t;)b.push(N),se.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(yo));return b!==t&&(Me=m,b=kn(b)),m=b,m}function mge(){var m,b,N;if(m=Q,b=[],N=J1(),N===t&&(cg.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(Qt))),N!==t)for(;N!==t;)b.push(N),N=J1(),N===t&&(cg.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(Qt)));else b=t;return b!==t&&(Me=m,b=kn(b)),m=b,m}function J1(){var m,b,N;return m=Q,r.substr(Q,2)===Rl?(b=Rl,Q+=2):(b=t,I===0&&Qe(Rn)),b!==t&&(Me=m,b=rs()),m=b,m===t&&(m=Q,r.charCodeAt(Q)===92?(b=is,Q++):(b=t,I===0&&Qe(gt)),b!==t?(wo.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(At)),N!==t?(Me=m,b=an(N),m=b):(Q=m,m=t)):(Q=m,m=t)),m}function Ege(){var m,b,N;for(m=Q,b=[],N=W1(),N===t&&(se.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(yo)));N!==t;)b.push(N),N=W1(),N===t&&(se.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(yo)));return b!==t&&(Me=m,b=kn(b)),m=b,m}function W1(){var m,b,N;return m=Q,r.substr(Q,2)===S?(b=S,Q+=2):(b=t,I===0&&Qe(Tt)),b!==t&&(Me=m,b=ug()),m=b,m===t&&(m=Q,r.substr(Q,2)===Fl?(b=Fl,Q+=2):(b=t,I===0&&Qe(Qp)),b!==t&&(Me=m,b=bp()),m=b,m===t&&(m=Q,r.charCodeAt(Q)===92?(b=is,Q++):(b=t,I===0&&Qe(gt)),b!==t?(Sp.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(vp)),N!==t?(Me=m,b=xp(),m=b):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.substr(Q,2)===G?(b=G,Q+=2):(b=t,I===0&&Qe(yt)),b!==t&&(Me=m,b=EA()),m=b,m===t&&(m=Q,r.substr(Q,2)===Ji?(b=Ji,Q+=2):(b=t,I===0&&Qe(Nl)),b!==t&&(Me=m,b=Xe()),m=b,m===t&&(m=Q,r.substr(Q,2)===fa?(b=fa,Q+=2):(b=t,I===0&&Qe(gg)),b!==t&&(Me=m,b=FE()),m=b,m===t&&(m=Q,r.substr(Q,2)===Pp?(b=Pp,Q+=2):(b=t,I===0&&Qe(NE)),b!==t&&(Me=m,b=ar()),m=b,m===t&&(m=Q,r.substr(Q,2)===Fn?(b=Fn,Q+=2):(b=t,I===0&&Qe(Ll)),b!==t&&(Me=m,b=Dp()),m=b,m===t&&(m=Q,r.charCodeAt(Q)===92?(b=is,Q++):(b=t,I===0&&Qe(gt)),b!==t?(Ms.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(ha)),N!==t?(Me=m,b=an(N),m=b):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Ige()))))))))),m}function Ige(){var m,b,N,U,ce,Se,ht,Bt,Jr,hi,ss,aS;return m=Q,r.charCodeAt(Q)===92?(b=is,Q++):(b=t,I===0&&Qe(gt)),b!==t?(N=iS(),N!==t?(Me=m,b=An(N),m=b):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.substr(Q,2)===Te?(b=Te,Q+=2):(b=t,I===0&&Qe(fg)),b!==t?(N=Q,U=Q,ce=iS(),ce!==t?(Se=Tn(),Se!==t?(ce=[ce,Se],U=ce):(Q=U,U=t)):(Q=U,U=t),U===t&&(U=iS()),U!==t?N=r.substring(N,Q):N=U,N!==t?(Me=m,b=An(N),m=b):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.substr(Q,2)===Tl?(b=Tl,Q+=2):(b=t,I===0&&Qe(Ks)),b!==t?(N=Q,U=Q,ce=Tn(),ce!==t?(Se=Tn(),Se!==t?(ht=Tn(),ht!==t?(Bt=Tn(),Bt!==t?(ce=[ce,Se,ht,Bt],U=ce):(Q=U,U=t)):(Q=U,U=t)):(Q=U,U=t)):(Q=U,U=t),U!==t?N=r.substring(N,Q):N=U,N!==t?(Me=m,b=An(N),m=b):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.substr(Q,2)===Ol?(b=Ol,Q+=2):(b=t,I===0&&Qe(IA)),b!==t?(N=Q,U=Q,ce=Tn(),ce!==t?(Se=Tn(),Se!==t?(ht=Tn(),ht!==t?(Bt=Tn(),Bt!==t?(Jr=Tn(),Jr!==t?(hi=Tn(),hi!==t?(ss=Tn(),ss!==t?(aS=Tn(),aS!==t?(ce=[ce,Se,ht,Bt,Jr,hi,ss,aS],U=ce):(Q=U,U=t)):(Q=U,U=t)):(Q=U,U=t)):(Q=U,U=t)):(Q=U,U=t)):(Q=U,U=t)):(Q=U,U=t)):(Q=U,U=t),U!==t?N=r.substring(N,Q):N=U,N!==t?(Me=m,b=hg(N),m=b):(Q=m,m=t)):(Q=m,m=t)))),m}function iS(){var m;return pg.test(r.charAt(Q))?(m=r.charAt(Q),Q++):(m=t,I===0&&Qe(pa)),m}function Tn(){var m;return da.test(r.charAt(Q))?(m=r.charAt(Q),Q++):(m=t,I===0&&Qe(rt)),m}function yge(){var m,b,N,U,ce;if(m=Q,b=[],N=Q,r.charCodeAt(Q)===92?(U=is,Q++):(U=t,I===0&&Qe(gt)),U!==t?(r.length>Q?(ce=r.charAt(Q),Q++):(ce=t,I===0&&Qe(Bo)),ce!==t?(Me=N,U=an(ce),N=U):(Q=N,N=t)):(Q=N,N=t),N===t&&(N=Q,U=Q,I++,ce=_1(),I--,ce===t?U=void 0:(Q=U,U=t),U!==t?(r.length>Q?(ce=r.charAt(Q),Q++):(ce=t,I===0&&Qe(Bo)),ce!==t?(Me=N,U=an(ce),N=U):(Q=N,N=t)):(Q=N,N=t)),N!==t)for(;N!==t;)b.push(N),N=Q,r.charCodeAt(Q)===92?(U=is,Q++):(U=t,I===0&&Qe(gt)),U!==t?(r.length>Q?(ce=r.charAt(Q),Q++):(ce=t,I===0&&Qe(Bo)),ce!==t?(Me=N,U=an(ce),N=U):(Q=N,N=t)):(Q=N,N=t),N===t&&(N=Q,U=Q,I++,ce=_1(),I--,ce===t?U=void 0:(Q=U,U=t),U!==t?(r.length>Q?(ce=r.charAt(Q),Q++):(ce=t,I===0&&Qe(Bo)),ce!==t?(Me=N,U=an(ce),N=U):(Q=N,N=t)):(Q=N,N=t));else b=t;return b!==t&&(Me=m,b=kn(b)),m=b,m}function nS(){var m,b,N,U,ce,Se;if(m=Q,r.charCodeAt(Q)===45?(b=yA,Q++):(b=t,I===0&&Qe(Ml)),b===t&&(r.charCodeAt(Q)===43?(b=Us,Q++):(b=t,I===0&&Qe(Kl))),b===t&&(b=null),b!==t){if(N=[],qe.test(r.charAt(Q))?(U=r.charAt(Q),Q++):(U=t,I===0&&Qe(ne)),U!==t)for(;U!==t;)N.push(U),qe.test(r.charAt(Q))?(U=r.charAt(Q),Q++):(U=t,I===0&&Qe(ne));else N=t;if(N!==t)if(r.charCodeAt(Q)===46?(U=LE,Q++):(U=t,I===0&&Qe(kp)),U!==t){if(ce=[],qe.test(r.charAt(Q))?(Se=r.charAt(Q),Q++):(Se=t,I===0&&Qe(ne)),Se!==t)for(;Se!==t;)ce.push(Se),qe.test(r.charAt(Q))?(Se=r.charAt(Q),Q++):(Se=t,I===0&&Qe(ne));else ce=t;ce!==t?(Me=m,b=dg(b,N,ce),m=b):(Q=m,m=t)}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t;if(m===t){if(m=Q,r.charCodeAt(Q)===45?(b=yA,Q++):(b=t,I===0&&Qe(Ml)),b===t&&(r.charCodeAt(Q)===43?(b=Us,Q++):(b=t,I===0&&Qe(Kl))),b===t&&(b=null),b!==t){if(N=[],qe.test(r.charAt(Q))?(U=r.charAt(Q),Q++):(U=t,I===0&&Qe(ne)),U!==t)for(;U!==t;)N.push(U),qe.test(r.charAt(Q))?(U=r.charAt(Q),Q++):(U=t,I===0&&Qe(ne));else N=t;N!==t?(Me=m,b=Rp(b,N),m=b):(Q=m,m=t)}else Q=m,m=t;if(m===t&&(m=Q,b=oS(),b!==t&&(Me=m,b=TE(b)),m=b,m===t&&(m=Q,b=Hl(),b!==t&&(Me=m,b=Ul(b)),m=b,m===t)))if(m=Q,r.charCodeAt(Q)===40?(b=ge,Q++):(b=t,I===0&&Qe(re)),b!==t){for(N=[],U=He();U!==t;)N.push(U),U=He();if(N!==t)if(U=z1(),U!==t){for(ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();ce!==t?(r.charCodeAt(Q)===41?(Se=O,Q++):(Se=t,I===0&&Qe(F)),Se!==t?(Me=m,b=OE(U),m=b):(Q=m,m=t)):(Q=m,m=t)}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t}return m}function sS(){var m,b,N,U,ce,Se,ht,Bt;if(m=Q,b=nS(),b!==t){for(N=[],U=Q,ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();if(ce!==t)if(r.charCodeAt(Q)===42?(Se=Cg,Q++):(Se=t,I===0&&Qe(wA)),Se===t&&(r.charCodeAt(Q)===47?(Se=Rr,Q++):(Se=t,I===0&&Qe(ME))),Se!==t){for(ht=[],Bt=He();Bt!==t;)ht.push(Bt),Bt=He();ht!==t?(Bt=nS(),Bt!==t?(Me=U,ce=Hs(b,Se,Bt),U=ce):(Q=U,U=t)):(Q=U,U=t)}else Q=U,U=t;else Q=U,U=t;for(;U!==t;){for(N.push(U),U=Q,ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();if(ce!==t)if(r.charCodeAt(Q)===42?(Se=Cg,Q++):(Se=t,I===0&&Qe(wA)),Se===t&&(r.charCodeAt(Q)===47?(Se=Rr,Q++):(Se=t,I===0&&Qe(ME))),Se!==t){for(ht=[],Bt=He();Bt!==t;)ht.push(Bt),Bt=He();ht!==t?(Bt=nS(),Bt!==t?(Me=U,ce=Hs(b,Se,Bt),U=ce):(Q=U,U=t)):(Q=U,U=t)}else Q=U,U=t;else Q=U,U=t}N!==t?(Me=m,b=Gs(b,N),m=b):(Q=m,m=t)}else Q=m,m=t;return m}function z1(){var m,b,N,U,ce,Se,ht,Bt;if(m=Q,b=sS(),b!==t){for(N=[],U=Q,ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();if(ce!==t)if(r.charCodeAt(Q)===43?(Se=Us,Q++):(Se=t,I===0&&Qe(Kl)),Se===t&&(r.charCodeAt(Q)===45?(Se=yA,Q++):(Se=t,I===0&&Qe(Ml))),Se!==t){for(ht=[],Bt=He();Bt!==t;)ht.push(Bt),Bt=He();ht!==t?(Bt=sS(),Bt!==t?(Me=U,ce=mg(b,Se,Bt),U=ce):(Q=U,U=t)):(Q=U,U=t)}else Q=U,U=t;else Q=U,U=t;for(;U!==t;){for(N.push(U),U=Q,ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();if(ce!==t)if(r.charCodeAt(Q)===43?(Se=Us,Q++):(Se=t,I===0&&Qe(Kl)),Se===t&&(r.charCodeAt(Q)===45?(Se=yA,Q++):(Se=t,I===0&&Qe(Ml))),Se!==t){for(ht=[],Bt=He();Bt!==t;)ht.push(Bt),Bt=He();ht!==t?(Bt=sS(),Bt!==t?(Me=U,ce=mg(b,Se,Bt),U=ce):(Q=U,U=t)):(Q=U,U=t)}else Q=U,U=t;else Q=U,U=t}N!==t?(Me=m,b=Gs(b,N),m=b):(Q=m,m=t)}else Q=m,m=t;return m}function V1(){var m,b,N,U,ce,Se;if(m=Q,r.substr(Q,3)===BA?(b=BA,Q+=3):(b=t,I===0&&Qe(R)),b!==t){for(N=[],U=He();U!==t;)N.push(U),U=He();if(N!==t)if(U=z1(),U!==t){for(ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();ce!==t?(r.substr(Q,2)===q?(Se=q,Q+=2):(Se=t,I===0&&Qe(Ce)),Se!==t?(Me=m,b=Ke(U),m=b):(Q=m,m=t)):(Q=m,m=t)}else Q=m,m=t;else Q=m,m=t}else Q=m,m=t;return m}function X1(){var m,b,N,U;return m=Q,r.substr(Q,2)===Re?(b=Re,Q+=2):(b=t,I===0&&Qe(ze)),b!==t?(N=Mr(),N!==t?(r.charCodeAt(Q)===41?(U=O,Q++):(U=t,I===0&&Qe(F)),U!==t?(Me=m,b=dt(N),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m}function oS(){var m,b,N,U,ce,Se;return m=Q,r.substr(Q,2)===Ft?(b=Ft,Q+=2):(b=t,I===0&&Qe(Nn)),b!==t?(N=Hl(),N!==t?(r.substr(Q,2)===qb?(U=qb,Q+=2):(U=t,I===0&&Qe(S1)),U!==t?(ce=U1(),ce!==t?(r.charCodeAt(Q)===125?(Se=Fe,Q++):(Se=t,I===0&&Qe(Ne)),Se!==t?(Me=m,b=v1(N,ce),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.substr(Q,2)===Ft?(b=Ft,Q+=2):(b=t,I===0&&Qe(Nn)),b!==t?(N=Hl(),N!==t?(r.substr(Q,3)===Jb?(U=Jb,Q+=3):(U=t,I===0&&Qe(x1)),U!==t?(Me=m,b=P1(N),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.substr(Q,2)===Ft?(b=Ft,Q+=2):(b=t,I===0&&Qe(Nn)),b!==t?(N=Hl(),N!==t?(r.substr(Q,2)===Wb?(U=Wb,Q+=2):(U=t,I===0&&Qe(D1)),U!==t?(ce=U1(),ce!==t?(r.charCodeAt(Q)===125?(Se=Fe,Q++):(Se=t,I===0&&Qe(Ne)),Se!==t?(Me=m,b=k1(N,ce),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.substr(Q,2)===Ft?(b=Ft,Q+=2):(b=t,I===0&&Qe(Nn)),b!==t?(N=Hl(),N!==t?(r.substr(Q,3)===zb?(U=zb,Q+=3):(U=t,I===0&&Qe(R1)),U!==t?(Me=m,b=F1(N),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.substr(Q,2)===Ft?(b=Ft,Q+=2):(b=t,I===0&&Qe(Nn)),b!==t?(N=Hl(),N!==t?(r.charCodeAt(Q)===125?(U=Fe,Q++):(U=t,I===0&&Qe(Ne)),U!==t?(Me=m,b=Vb(N),m=b):(Q=m,m=t)):(Q=m,m=t)):(Q=m,m=t),m===t&&(m=Q,r.charCodeAt(Q)===36?(b=N1,Q++):(b=t,I===0&&Qe(L1)),b!==t?(N=Hl(),N!==t?(Me=m,b=Vb(N),m=b):(Q=m,m=t)):(Q=m,m=t)))))),m}function wge(){var m,b,N;return m=Q,b=Bge(),b!==t?(Me=Q,N=T1(b),N?N=void 0:N=t,N!==t?(Me=m,b=O1(b),m=b):(Q=m,m=t)):(Q=m,m=t),m}function Bge(){var m,b,N,U,ce;if(m=Q,b=[],N=Q,U=Q,I++,ce=$1(),I--,ce===t?U=void 0:(Q=U,U=t),U!==t?(r.length>Q?(ce=r.charAt(Q),Q++):(ce=t,I===0&&Qe(Bo)),ce!==t?(Me=N,U=an(ce),N=U):(Q=N,N=t)):(Q=N,N=t),N!==t)for(;N!==t;)b.push(N),N=Q,U=Q,I++,ce=$1(),I--,ce===t?U=void 0:(Q=U,U=t),U!==t?(r.length>Q?(ce=r.charAt(Q),Q++):(ce=t,I===0&&Qe(Bo)),ce!==t?(Me=N,U=an(ce),N=U):(Q=N,N=t)):(Q=N,N=t);else b=t;return b!==t&&(Me=m,b=kn(b)),m=b,m}function Z1(){var m,b,N;if(m=Q,b=[],Xb.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(Zb)),N!==t)for(;N!==t;)b.push(N),Xb.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(Zb));else b=t;return b!==t&&(Me=m,b=_b()),m=b,m}function Hl(){var m,b,N;if(m=Q,b=[],$b.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(eS)),N!==t)for(;N!==t;)b.push(N),$b.test(r.charAt(Q))?(N=r.charAt(Q),Q++):(N=t,I===0&&Qe(eS));else b=t;return b!==t&&(Me=m,b=_b()),m=b,m}function _1(){var m;return M1.test(r.charAt(Q))?(m=r.charAt(Q),Q++):(m=t,I===0&&Qe(Eg)),m}function $1(){var m;return tS.test(r.charAt(Q))?(m=r.charAt(Q),Q++):(m=t,I===0&&Qe(rS)),m}function He(){var m,b;if(m=[],KE.test(r.charAt(Q))?(b=r.charAt(Q),Q++):(b=t,I===0&&Qe(UE)),b!==t)for(;b!==t;)m.push(b),KE.test(r.charAt(Q))?(b=r.charAt(Q),Q++):(b=t,I===0&&Qe(UE));else m=t;return m}if(k=n(),k!==t&&Q===r.length)return k;throw k!==t&&Q{"use strict";function Qfe(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function Vl(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Vl)}Qfe(Vl,Error);Vl.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;gH&&(H=v,j=[]),j.push(ne))}function Ne(ne,Y){return new Vl(ne,null,null,Y)}function oe(ne,Y,pe){return new Vl(Vl.buildMessage(ne,Y),ne,Y,pe)}function le(){var ne,Y,pe,ie;return ne=v,Y=we(),Y!==t?(r.charCodeAt(v)===47?(pe=s,v++):(pe=t,$===0&&Fe(o)),pe!==t?(ie=we(),ie!==t?(D=ne,Y=a(Y,ie),ne=Y):(v=ne,ne=t)):(v=ne,ne=t)):(v=ne,ne=t),ne===t&&(ne=v,Y=we(),Y!==t&&(D=ne,Y=l(Y)),ne=Y),ne}function we(){var ne,Y,pe,ie;return ne=v,Y=fe(),Y!==t?(r.charCodeAt(v)===64?(pe=c,v++):(pe=t,$===0&&Fe(u)),pe!==t?(ie=qe(),ie!==t?(D=ne,Y=g(Y,ie),ne=Y):(v=ne,ne=t)):(v=ne,ne=t)):(v=ne,ne=t),ne===t&&(ne=v,Y=fe(),Y!==t&&(D=ne,Y=f(Y)),ne=Y),ne}function fe(){var ne,Y,pe,ie,de;return ne=v,r.charCodeAt(v)===64?(Y=c,v++):(Y=t,$===0&&Fe(u)),Y!==t?(pe=Ae(),pe!==t?(r.charCodeAt(v)===47?(ie=s,v++):(ie=t,$===0&&Fe(o)),ie!==t?(de=Ae(),de!==t?(D=ne,Y=h(),ne=Y):(v=ne,ne=t)):(v=ne,ne=t)):(v=ne,ne=t)):(v=ne,ne=t),ne===t&&(ne=v,Y=Ae(),Y!==t&&(D=ne,Y=h()),ne=Y),ne}function Ae(){var ne,Y,pe;if(ne=v,Y=[],p.test(r.charAt(v))?(pe=r.charAt(v),v++):(pe=t,$===0&&Fe(C)),pe!==t)for(;pe!==t;)Y.push(pe),p.test(r.charAt(v))?(pe=r.charAt(v),v++):(pe=t,$===0&&Fe(C));else Y=t;return Y!==t&&(D=ne,Y=h()),ne=Y,ne}function qe(){var ne,Y,pe;if(ne=v,Y=[],y.test(r.charAt(v))?(pe=r.charAt(v),v++):(pe=t,$===0&&Fe(B)),pe!==t)for(;pe!==t;)Y.push(pe),y.test(r.charAt(v))?(pe=r.charAt(v),v++):(pe=t,$===0&&Fe(B));else Y=t;return Y!==t&&(D=ne,Y=h()),ne=Y,ne}if(V=n(),V!==t&&v===r.length)return V;throw V!==t&&v{"use strict";function fU(r){return typeof r>"u"||r===null}function Sfe(r){return typeof r=="object"&&r!==null}function vfe(r){return Array.isArray(r)?r:fU(r)?[]:[r]}function xfe(r,e){var t,i,n,s;if(e)for(s=Object.keys(e),t=0,i=s.length;t{"use strict";function Wp(r,e){Error.call(this),this.name="YAMLException",this.reason=r,this.mark=e,this.message=(this.reason||"(unknown reason)")+(this.mark?" "+this.mark.toString():""),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack||""}Wp.prototype=Object.create(Error.prototype);Wp.prototype.constructor=Wp;Wp.prototype.toString=function(e){var t=this.name+": ";return t+=this.reason||"(unknown reason)",!e&&this.mark&&(t+=" "+this.mark.toString()),t};hU.exports=Wp});var CU=w((IZe,dU)=>{"use strict";var pU=Zl();function HS(r,e,t,i,n){this.name=r,this.buffer=e,this.position=t,this.line=i,this.column=n}HS.prototype.getSnippet=function(e,t){var i,n,s,o,a;if(!this.buffer)return null;for(e=e||4,t=t||75,i="",n=this.position;n>0&&`\0\r +\x85\u2028\u2029`.indexOf(this.buffer.charAt(n-1))===-1;)if(n-=1,this.position-n>t/2-1){i=" ... ",n+=5;break}for(s="",o=this.position;ot/2-1){s=" ... ",o-=5;break}return a=this.buffer.slice(n,o),pU.repeat(" ",e)+i+a+s+` +`+pU.repeat(" ",e+this.position-n+i.length)+"^"};HS.prototype.toString=function(e){var t,i="";return this.name&&(i+='in "'+this.name+'" '),i+="at line "+(this.line+1)+", column "+(this.column+1),e||(t=this.getSnippet(),t&&(i+=`: +`+t)),i};dU.exports=HS});var si=w((yZe,EU)=>{"use strict";var mU=kg(),kfe=["kind","resolve","construct","instanceOf","predicate","represent","defaultStyle","styleAliases"],Rfe=["scalar","sequence","mapping"];function Ffe(r){var e={};return r!==null&&Object.keys(r).forEach(function(t){r[t].forEach(function(i){e[String(i)]=t})}),e}function Nfe(r,e){if(e=e||{},Object.keys(e).forEach(function(t){if(kfe.indexOf(t)===-1)throw new mU('Unknown option "'+t+'" is met in definition of "'+r+'" YAML type.')}),this.tag=r,this.kind=e.kind||null,this.resolve=e.resolve||function(){return!0},this.construct=e.construct||function(t){return t},this.instanceOf=e.instanceOf||null,this.predicate=e.predicate||null,this.represent=e.represent||null,this.defaultStyle=e.defaultStyle||null,this.styleAliases=Ffe(e.styleAliases||null),Rfe.indexOf(this.kind)===-1)throw new mU('Unknown kind "'+this.kind+'" is specified for "'+r+'" YAML type.')}EU.exports=Nfe});var _l=w((wZe,yU)=>{"use strict";var IU=Zl(),gI=kg(),Lfe=si();function GS(r,e,t){var i=[];return r.include.forEach(function(n){t=GS(n,e,t)}),r[e].forEach(function(n){t.forEach(function(s,o){s.tag===n.tag&&s.kind===n.kind&&i.push(o)}),t.push(n)}),t.filter(function(n,s){return i.indexOf(s)===-1})}function Tfe(){var r={scalar:{},sequence:{},mapping:{},fallback:{}},e,t;function i(n){r[n.kind][n.tag]=r.fallback[n.tag]=n}for(e=0,t=arguments.length;e{"use strict";var Ofe=si();wU.exports=new Ofe("tag:yaml.org,2002:str",{kind:"scalar",construct:function(r){return r!==null?r:""}})});var bU=w((QZe,QU)=>{"use strict";var Mfe=si();QU.exports=new Mfe("tag:yaml.org,2002:seq",{kind:"sequence",construct:function(r){return r!==null?r:[]}})});var vU=w((bZe,SU)=>{"use strict";var Kfe=si();SU.exports=new Kfe("tag:yaml.org,2002:map",{kind:"mapping",construct:function(r){return r!==null?r:{}}})});var fI=w((SZe,xU)=>{"use strict";var Ufe=_l();xU.exports=new Ufe({explicit:[BU(),bU(),vU()]})});var DU=w((vZe,PU)=>{"use strict";var Hfe=si();function Gfe(r){if(r===null)return!0;var e=r.length;return e===1&&r==="~"||e===4&&(r==="null"||r==="Null"||r==="NULL")}function Yfe(){return null}function jfe(r){return r===null}PU.exports=new Hfe("tag:yaml.org,2002:null",{kind:"scalar",resolve:Gfe,construct:Yfe,predicate:jfe,represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"}},defaultStyle:"lowercase"})});var RU=w((xZe,kU)=>{"use strict";var qfe=si();function Jfe(r){if(r===null)return!1;var e=r.length;return e===4&&(r==="true"||r==="True"||r==="TRUE")||e===5&&(r==="false"||r==="False"||r==="FALSE")}function Wfe(r){return r==="true"||r==="True"||r==="TRUE"}function zfe(r){return Object.prototype.toString.call(r)==="[object Boolean]"}kU.exports=new qfe("tag:yaml.org,2002:bool",{kind:"scalar",resolve:Jfe,construct:Wfe,predicate:zfe,represent:{lowercase:function(r){return r?"true":"false"},uppercase:function(r){return r?"TRUE":"FALSE"},camelcase:function(r){return r?"True":"False"}},defaultStyle:"lowercase"})});var NU=w((PZe,FU)=>{"use strict";var Vfe=Zl(),Xfe=si();function Zfe(r){return 48<=r&&r<=57||65<=r&&r<=70||97<=r&&r<=102}function _fe(r){return 48<=r&&r<=55}function $fe(r){return 48<=r&&r<=57}function ehe(r){if(r===null)return!1;var e=r.length,t=0,i=!1,n;if(!e)return!1;if(n=r[t],(n==="-"||n==="+")&&(n=r[++t]),n==="0"){if(t+1===e)return!0;if(n=r[++t],n==="b"){for(t++;t=0?"0b"+r.toString(2):"-0b"+r.toString(2).slice(1)},octal:function(r){return r>=0?"0"+r.toString(8):"-0"+r.toString(8).slice(1)},decimal:function(r){return r.toString(10)},hexadecimal:function(r){return r>=0?"0x"+r.toString(16).toUpperCase():"-0x"+r.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}})});var OU=w((DZe,TU)=>{"use strict";var LU=Zl(),ihe=si(),nhe=new RegExp("^(?:[-+]?(?:0|[1-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");function she(r){return!(r===null||!nhe.test(r)||r[r.length-1]==="_")}function ohe(r){var e,t,i,n;return e=r.replace(/_/g,"").toLowerCase(),t=e[0]==="-"?-1:1,n=[],"+-".indexOf(e[0])>=0&&(e=e.slice(1)),e===".inf"?t===1?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:e===".nan"?NaN:e.indexOf(":")>=0?(e.split(":").forEach(function(s){n.unshift(parseFloat(s,10))}),e=0,i=1,n.forEach(function(s){e+=s*i,i*=60}),t*e):t*parseFloat(e,10)}var ahe=/^[-+]?[0-9]+e/;function Ahe(r,e){var t;if(isNaN(r))switch(e){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===r)switch(e){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===r)switch(e){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(LU.isNegativeZero(r))return"-0.0";return t=r.toString(10),ahe.test(t)?t.replace("e",".e"):t}function lhe(r){return Object.prototype.toString.call(r)==="[object Number]"&&(r%1!==0||LU.isNegativeZero(r))}TU.exports=new ihe("tag:yaml.org,2002:float",{kind:"scalar",resolve:she,construct:ohe,predicate:lhe,represent:Ahe,defaultStyle:"lowercase"})});var YS=w((kZe,MU)=>{"use strict";var che=_l();MU.exports=new che({include:[fI()],implicit:[DU(),RU(),NU(),OU()]})});var jS=w((RZe,KU)=>{"use strict";var uhe=_l();KU.exports=new uhe({include:[YS()]})});var YU=w((FZe,GU)=>{"use strict";var ghe=si(),UU=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),HU=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");function fhe(r){return r===null?!1:UU.exec(r)!==null||HU.exec(r)!==null}function hhe(r){var e,t,i,n,s,o,a,l=0,c=null,u,g,f;if(e=UU.exec(r),e===null&&(e=HU.exec(r)),e===null)throw new Error("Date resolve error");if(t=+e[1],i=+e[2]-1,n=+e[3],!e[4])return new Date(Date.UTC(t,i,n));if(s=+e[4],o=+e[5],a=+e[6],e[7]){for(l=e[7].slice(0,3);l.length<3;)l+="0";l=+l}return e[9]&&(u=+e[10],g=+(e[11]||0),c=(u*60+g)*6e4,e[9]==="-"&&(c=-c)),f=new Date(Date.UTC(t,i,n,s,o,a,l)),c&&f.setTime(f.getTime()-c),f}function phe(r){return r.toISOString()}GU.exports=new ghe("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:fhe,construct:hhe,instanceOf:Date,represent:phe})});var qU=w((NZe,jU)=>{"use strict";var dhe=si();function Che(r){return r==="<<"||r===null}jU.exports=new dhe("tag:yaml.org,2002:merge",{kind:"scalar",resolve:Che})});var zU=w((LZe,WU)=>{"use strict";var $l;try{JU=J,$l=JU("buffer").Buffer}catch{}var JU,mhe=si(),qS=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= +\r`;function Ehe(r){if(r===null)return!1;var e,t,i=0,n=r.length,s=qS;for(t=0;t64)){if(e<0)return!1;i+=6}return i%8===0}function Ihe(r){var e,t,i=r.replace(/[\r\n=]/g,""),n=i.length,s=qS,o=0,a=[];for(e=0;e>16&255),a.push(o>>8&255),a.push(o&255)),o=o<<6|s.indexOf(i.charAt(e));return t=n%4*6,t===0?(a.push(o>>16&255),a.push(o>>8&255),a.push(o&255)):t===18?(a.push(o>>10&255),a.push(o>>2&255)):t===12&&a.push(o>>4&255),$l?$l.from?$l.from(a):new $l(a):a}function yhe(r){var e="",t=0,i,n,s=r.length,o=qS;for(i=0;i>18&63],e+=o[t>>12&63],e+=o[t>>6&63],e+=o[t&63]),t=(t<<8)+r[i];return n=s%3,n===0?(e+=o[t>>18&63],e+=o[t>>12&63],e+=o[t>>6&63],e+=o[t&63]):n===2?(e+=o[t>>10&63],e+=o[t>>4&63],e+=o[t<<2&63],e+=o[64]):n===1&&(e+=o[t>>2&63],e+=o[t<<4&63],e+=o[64],e+=o[64]),e}function whe(r){return $l&&$l.isBuffer(r)}WU.exports=new mhe("tag:yaml.org,2002:binary",{kind:"scalar",resolve:Ehe,construct:Ihe,predicate:whe,represent:yhe})});var XU=w((TZe,VU)=>{"use strict";var Bhe=si(),Qhe=Object.prototype.hasOwnProperty,bhe=Object.prototype.toString;function She(r){if(r===null)return!0;var e=[],t,i,n,s,o,a=r;for(t=0,i=a.length;t{"use strict";var xhe=si(),Phe=Object.prototype.toString;function Dhe(r){if(r===null)return!0;var e,t,i,n,s,o=r;for(s=new Array(o.length),e=0,t=o.length;e{"use strict";var Rhe=si(),Fhe=Object.prototype.hasOwnProperty;function Nhe(r){if(r===null)return!0;var e,t=r;for(e in t)if(Fhe.call(t,e)&&t[e]!==null)return!1;return!0}function Lhe(r){return r!==null?r:{}}$U.exports=new Rhe("tag:yaml.org,2002:set",{kind:"mapping",resolve:Nhe,construct:Lhe})});var Fg=w((KZe,t2)=>{"use strict";var The=_l();t2.exports=new The({include:[jS()],implicit:[YU(),qU()],explicit:[zU(),XU(),_U(),e2()]})});var i2=w((UZe,r2)=>{"use strict";var Ohe=si();function Mhe(){return!0}function Khe(){}function Uhe(){return""}function Hhe(r){return typeof r>"u"}r2.exports=new Ohe("tag:yaml.org,2002:js/undefined",{kind:"scalar",resolve:Mhe,construct:Khe,predicate:Hhe,represent:Uhe})});var s2=w((HZe,n2)=>{"use strict";var Ghe=si();function Yhe(r){if(r===null||r.length===0)return!1;var e=r,t=/\/([gim]*)$/.exec(r),i="";return!(e[0]==="/"&&(t&&(i=t[1]),i.length>3||e[e.length-i.length-1]!=="/"))}function jhe(r){var e=r,t=/\/([gim]*)$/.exec(r),i="";return e[0]==="/"&&(t&&(i=t[1]),e=e.slice(1,e.length-i.length-1)),new RegExp(e,i)}function qhe(r){var e="/"+r.source+"/";return r.global&&(e+="g"),r.multiline&&(e+="m"),r.ignoreCase&&(e+="i"),e}function Jhe(r){return Object.prototype.toString.call(r)==="[object RegExp]"}n2.exports=new Ghe("tag:yaml.org,2002:js/regexp",{kind:"scalar",resolve:Yhe,construct:jhe,predicate:Jhe,represent:qhe})});var A2=w((GZe,a2)=>{"use strict";var hI;try{o2=J,hI=o2("esprima")}catch{typeof window<"u"&&(hI=window.esprima)}var o2,Whe=si();function zhe(r){if(r===null)return!1;try{var e="("+r+")",t=hI.parse(e,{range:!0});return!(t.type!=="Program"||t.body.length!==1||t.body[0].type!=="ExpressionStatement"||t.body[0].expression.type!=="ArrowFunctionExpression"&&t.body[0].expression.type!=="FunctionExpression")}catch{return!1}}function Vhe(r){var e="("+r+")",t=hI.parse(e,{range:!0}),i=[],n;if(t.type!=="Program"||t.body.length!==1||t.body[0].type!=="ExpressionStatement"||t.body[0].expression.type!=="ArrowFunctionExpression"&&t.body[0].expression.type!=="FunctionExpression")throw new Error("Failed to resolve function");return t.body[0].expression.params.forEach(function(s){i.push(s.name)}),n=t.body[0].expression.body.range,t.body[0].expression.body.type==="BlockStatement"?new Function(i,e.slice(n[0]+1,n[1]-1)):new Function(i,"return "+e.slice(n[0],n[1]))}function Xhe(r){return r.toString()}function Zhe(r){return Object.prototype.toString.call(r)==="[object Function]"}a2.exports=new Whe("tag:yaml.org,2002:js/function",{kind:"scalar",resolve:zhe,construct:Vhe,predicate:Zhe,represent:Xhe})});var zp=w((YZe,c2)=>{"use strict";var l2=_l();c2.exports=l2.DEFAULT=new l2({include:[Fg()],explicit:[i2(),s2(),A2()]})});var P2=w((jZe,Vp)=>{"use strict";var ya=Zl(),C2=kg(),_he=CU(),m2=Fg(),$he=zp(),DA=Object.prototype.hasOwnProperty,pI=1,E2=2,I2=3,dI=4,JS=1,epe=2,u2=3,tpe=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,rpe=/[\x85\u2028\u2029]/,ipe=/[,\[\]\{\}]/,y2=/^(?:!|!!|![a-z\-]+!)$/i,w2=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;function g2(r){return Object.prototype.toString.call(r)}function vo(r){return r===10||r===13}function tc(r){return r===9||r===32}function un(r){return r===9||r===32||r===10||r===13}function Ng(r){return r===44||r===91||r===93||r===123||r===125}function npe(r){var e;return 48<=r&&r<=57?r-48:(e=r|32,97<=e&&e<=102?e-97+10:-1)}function spe(r){return r===120?2:r===117?4:r===85?8:0}function ope(r){return 48<=r&&r<=57?r-48:-1}function f2(r){return r===48?"\0":r===97?"\x07":r===98?"\b":r===116||r===9?" ":r===110?` +`:r===118?"\v":r===102?"\f":r===114?"\r":r===101?"\x1B":r===32?" ":r===34?'"':r===47?"/":r===92?"\\":r===78?"\x85":r===95?"\xA0":r===76?"\u2028":r===80?"\u2029":""}function ape(r){return r<=65535?String.fromCharCode(r):String.fromCharCode((r-65536>>10)+55296,(r-65536&1023)+56320)}var B2=new Array(256),Q2=new Array(256);for(ec=0;ec<256;ec++)B2[ec]=f2(ec)?1:0,Q2[ec]=f2(ec);var ec;function Ape(r,e){this.input=r,this.filename=e.filename||null,this.schema=e.schema||$he,this.onWarning=e.onWarning||null,this.legacy=e.legacy||!1,this.json=e.json||!1,this.listener=e.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=r.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.documents=[]}function b2(r,e){return new C2(e,new _he(r.filename,r.input,r.position,r.line,r.position-r.lineStart))}function ft(r,e){throw b2(r,e)}function CI(r,e){r.onWarning&&r.onWarning.call(null,b2(r,e))}var h2={YAML:function(e,t,i){var n,s,o;e.version!==null&&ft(e,"duplication of %YAML directive"),i.length!==1&&ft(e,"YAML directive accepts exactly one argument"),n=/^([0-9]+)\.([0-9]+)$/.exec(i[0]),n===null&&ft(e,"ill-formed argument of the YAML directive"),s=parseInt(n[1],10),o=parseInt(n[2],10),s!==1&&ft(e,"unacceptable YAML version of the document"),e.version=i[0],e.checkLineBreaks=o<2,o!==1&&o!==2&&CI(e,"unsupported YAML version of the document")},TAG:function(e,t,i){var n,s;i.length!==2&&ft(e,"TAG directive accepts exactly two arguments"),n=i[0],s=i[1],y2.test(n)||ft(e,"ill-formed tag handle (first argument) of the TAG directive"),DA.call(e.tagMap,n)&&ft(e,'there is a previously declared suffix for "'+n+'" tag handle'),w2.test(s)||ft(e,"ill-formed tag prefix (second argument) of the TAG directive"),e.tagMap[n]=s}};function PA(r,e,t,i){var n,s,o,a;if(e1&&(r.result+=ya.repeat(` +`,e-1))}function lpe(r,e,t){var i,n,s,o,a,l,c,u,g=r.kind,f=r.result,h;if(h=r.input.charCodeAt(r.position),un(h)||Ng(h)||h===35||h===38||h===42||h===33||h===124||h===62||h===39||h===34||h===37||h===64||h===96||(h===63||h===45)&&(n=r.input.charCodeAt(r.position+1),un(n)||t&&Ng(n)))return!1;for(r.kind="scalar",r.result="",s=o=r.position,a=!1;h!==0;){if(h===58){if(n=r.input.charCodeAt(r.position+1),un(n)||t&&Ng(n))break}else if(h===35){if(i=r.input.charCodeAt(r.position-1),un(i))break}else{if(r.position===r.lineStart&&mI(r)||t&&Ng(h))break;if(vo(h))if(l=r.line,c=r.lineStart,u=r.lineIndent,zr(r,!1,-1),r.lineIndent>=e){a=!0,h=r.input.charCodeAt(r.position);continue}else{r.position=o,r.line=l,r.lineStart=c,r.lineIndent=u;break}}a&&(PA(r,s,o,!1),zS(r,r.line-l),s=o=r.position,a=!1),tc(h)||(o=r.position+1),h=r.input.charCodeAt(++r.position)}return PA(r,s,o,!1),r.result?!0:(r.kind=g,r.result=f,!1)}function cpe(r,e){var t,i,n;if(t=r.input.charCodeAt(r.position),t!==39)return!1;for(r.kind="scalar",r.result="",r.position++,i=n=r.position;(t=r.input.charCodeAt(r.position))!==0;)if(t===39)if(PA(r,i,r.position,!0),t=r.input.charCodeAt(++r.position),t===39)i=r.position,r.position++,n=r.position;else return!0;else vo(t)?(PA(r,i,n,!0),zS(r,zr(r,!1,e)),i=n=r.position):r.position===r.lineStart&&mI(r)?ft(r,"unexpected end of the document within a single quoted scalar"):(r.position++,n=r.position);ft(r,"unexpected end of the stream within a single quoted scalar")}function upe(r,e){var t,i,n,s,o,a;if(a=r.input.charCodeAt(r.position),a!==34)return!1;for(r.kind="scalar",r.result="",r.position++,t=i=r.position;(a=r.input.charCodeAt(r.position))!==0;){if(a===34)return PA(r,t,r.position,!0),r.position++,!0;if(a===92){if(PA(r,t,r.position,!0),a=r.input.charCodeAt(++r.position),vo(a))zr(r,!1,e);else if(a<256&&B2[a])r.result+=Q2[a],r.position++;else if((o=spe(a))>0){for(n=o,s=0;n>0;n--)a=r.input.charCodeAt(++r.position),(o=npe(a))>=0?s=(s<<4)+o:ft(r,"expected hexadecimal character");r.result+=ape(s),r.position++}else ft(r,"unknown escape sequence");t=i=r.position}else vo(a)?(PA(r,t,i,!0),zS(r,zr(r,!1,e)),t=i=r.position):r.position===r.lineStart&&mI(r)?ft(r,"unexpected end of the document within a double quoted scalar"):(r.position++,i=r.position)}ft(r,"unexpected end of the stream within a double quoted scalar")}function gpe(r,e){var t=!0,i,n=r.tag,s,o=r.anchor,a,l,c,u,g,f={},h,p,C,y;if(y=r.input.charCodeAt(r.position),y===91)l=93,g=!1,s=[];else if(y===123)l=125,g=!0,s={};else return!1;for(r.anchor!==null&&(r.anchorMap[r.anchor]=s),y=r.input.charCodeAt(++r.position);y!==0;){if(zr(r,!0,e),y=r.input.charCodeAt(r.position),y===l)return r.position++,r.tag=n,r.anchor=o,r.kind=g?"mapping":"sequence",r.result=s,!0;t||ft(r,"missed comma between flow collection entries"),p=h=C=null,c=u=!1,y===63&&(a=r.input.charCodeAt(r.position+1),un(a)&&(c=u=!0,r.position++,zr(r,!0,e))),i=r.line,Tg(r,e,pI,!1,!0),p=r.tag,h=r.result,zr(r,!0,e),y=r.input.charCodeAt(r.position),(u||r.line===i)&&y===58&&(c=!0,y=r.input.charCodeAt(++r.position),zr(r,!0,e),Tg(r,e,pI,!1,!0),C=r.result),g?Lg(r,s,f,p,h,C):c?s.push(Lg(r,null,f,p,h,C)):s.push(h),zr(r,!0,e),y=r.input.charCodeAt(r.position),y===44?(t=!0,y=r.input.charCodeAt(++r.position)):t=!1}ft(r,"unexpected end of the stream within a flow collection")}function fpe(r,e){var t,i,n=JS,s=!1,o=!1,a=e,l=0,c=!1,u,g;if(g=r.input.charCodeAt(r.position),g===124)i=!1;else if(g===62)i=!0;else return!1;for(r.kind="scalar",r.result="";g!==0;)if(g=r.input.charCodeAt(++r.position),g===43||g===45)JS===n?n=g===43?u2:epe:ft(r,"repeat of a chomping mode identifier");else if((u=ope(g))>=0)u===0?ft(r,"bad explicit indentation width of a block scalar; it cannot be less than one"):o?ft(r,"repeat of an indentation width identifier"):(a=e+u-1,o=!0);else break;if(tc(g)){do g=r.input.charCodeAt(++r.position);while(tc(g));if(g===35)do g=r.input.charCodeAt(++r.position);while(!vo(g)&&g!==0)}for(;g!==0;){for(WS(r),r.lineIndent=0,g=r.input.charCodeAt(r.position);(!o||r.lineIndenta&&(a=r.lineIndent),vo(g)){l++;continue}if(r.lineIndente)&&l!==0)ft(r,"bad indentation of a sequence entry");else if(r.lineIndente)&&(Tg(r,e,dI,!0,n)&&(p?f=r.result:h=r.result),p||(Lg(r,c,u,g,f,h,s,o),g=f=h=null),zr(r,!0,-1),y=r.input.charCodeAt(r.position)),r.lineIndent>e&&y!==0)ft(r,"bad indentation of a mapping entry");else if(r.lineIndente?l=1:r.lineIndent===e?l=0:r.lineIndente?l=1:r.lineIndent===e?l=0:r.lineIndent tag; it should be "scalar", not "'+r.kind+'"'),g=0,f=r.implicitTypes.length;g tag; it should be "'+h.kind+'", not "'+r.kind+'"'),h.resolve(r.result)?(r.result=h.construct(r.result),r.anchor!==null&&(r.anchorMap[r.anchor]=r.result)):ft(r,"cannot resolve a node with !<"+r.tag+"> explicit tag")):ft(r,"unknown tag !<"+r.tag+">");return r.listener!==null&&r.listener("close",r),r.tag!==null||r.anchor!==null||u}function mpe(r){var e=r.position,t,i,n,s=!1,o;for(r.version=null,r.checkLineBreaks=r.legacy,r.tagMap={},r.anchorMap={};(o=r.input.charCodeAt(r.position))!==0&&(zr(r,!0,-1),o=r.input.charCodeAt(r.position),!(r.lineIndent>0||o!==37));){for(s=!0,o=r.input.charCodeAt(++r.position),t=r.position;o!==0&&!un(o);)o=r.input.charCodeAt(++r.position);for(i=r.input.slice(t,r.position),n=[],i.length<1&&ft(r,"directive name must not be less than one character in length");o!==0;){for(;tc(o);)o=r.input.charCodeAt(++r.position);if(o===35){do o=r.input.charCodeAt(++r.position);while(o!==0&&!vo(o));break}if(vo(o))break;for(t=r.position;o!==0&&!un(o);)o=r.input.charCodeAt(++r.position);n.push(r.input.slice(t,r.position))}o!==0&&WS(r),DA.call(h2,i)?h2[i](r,i,n):CI(r,'unknown document directive "'+i+'"')}if(zr(r,!0,-1),r.lineIndent===0&&r.input.charCodeAt(r.position)===45&&r.input.charCodeAt(r.position+1)===45&&r.input.charCodeAt(r.position+2)===45?(r.position+=3,zr(r,!0,-1)):s&&ft(r,"directives end mark is expected"),Tg(r,r.lineIndent-1,dI,!1,!0),zr(r,!0,-1),r.checkLineBreaks&&rpe.test(r.input.slice(e,r.position))&&CI(r,"non-ASCII line breaks are interpreted as content"),r.documents.push(r.result),r.position===r.lineStart&&mI(r)){r.input.charCodeAt(r.position)===46&&(r.position+=3,zr(r,!0,-1));return}if(r.position"u"&&(t=e,e=null);var i=S2(r,t);if(typeof e!="function")return i;for(var n=0,s=i.length;n"u"&&(t=e,e=null),v2(r,e,ya.extend({schema:m2},t))}function Ipe(r,e){return x2(r,ya.extend({schema:m2},e))}Vp.exports.loadAll=v2;Vp.exports.load=x2;Vp.exports.safeLoadAll=Epe;Vp.exports.safeLoad=Ipe});var _2=w((qZe,_S)=>{"use strict";var Zp=Zl(),_p=kg(),ype=zp(),wpe=Fg(),O2=Object.prototype.toString,M2=Object.prototype.hasOwnProperty,Bpe=9,Xp=10,Qpe=13,bpe=32,Spe=33,vpe=34,K2=35,xpe=37,Ppe=38,Dpe=39,kpe=42,U2=44,Rpe=45,H2=58,Fpe=61,Npe=62,Lpe=63,Tpe=64,G2=91,Y2=93,Ope=96,j2=123,Mpe=124,q2=125,Ni={};Ni[0]="\\0";Ni[7]="\\a";Ni[8]="\\b";Ni[9]="\\t";Ni[10]="\\n";Ni[11]="\\v";Ni[12]="\\f";Ni[13]="\\r";Ni[27]="\\e";Ni[34]='\\"';Ni[92]="\\\\";Ni[133]="\\N";Ni[160]="\\_";Ni[8232]="\\L";Ni[8233]="\\P";var Kpe=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"];function Upe(r,e){var t,i,n,s,o,a,l;if(e===null)return{};for(t={},i=Object.keys(e),n=0,s=i.length;n0?r.charCodeAt(s-1):null,f=f&&R2(o,a)}else{for(s=0;si&&r[g+1]!==" ",g=s);else if(!Og(o))return EI;a=s>0?r.charCodeAt(s-1):null,f=f&&R2(o,a)}c=c||u&&s-g-1>i&&r[g+1]!==" "}return!l&&!c?f&&!n(r)?W2:z2:t>9&&J2(r)?EI:c?X2:V2}function Jpe(r,e,t,i){r.dump=function(){if(e.length===0)return"''";if(!r.noCompatMode&&Kpe.indexOf(e)!==-1)return"'"+e+"'";var n=r.indent*Math.max(1,t),s=r.lineWidth===-1?-1:Math.max(Math.min(r.lineWidth,40),r.lineWidth-n),o=i||r.flowLevel>-1&&t>=r.flowLevel;function a(l){return Gpe(r,l)}switch(qpe(e,o,r.indent,s,a)){case W2:return e;case z2:return"'"+e.replace(/'/g,"''")+"'";case V2:return"|"+F2(e,r.indent)+N2(k2(e,n));case X2:return">"+F2(e,r.indent)+N2(k2(Wpe(e,s),n));case EI:return'"'+zpe(e,s)+'"';default:throw new _p("impossible error: invalid scalar style")}}()}function F2(r,e){var t=J2(r)?String(e):"",i=r[r.length-1]===` +`,n=i&&(r[r.length-2]===` +`||r===` +`),s=n?"+":i?"":"-";return t+s+` +`}function N2(r){return r[r.length-1]===` +`?r.slice(0,-1):r}function Wpe(r,e){for(var t=/(\n+)([^\n]*)/g,i=function(){var c=r.indexOf(` +`);return c=c!==-1?c:r.length,t.lastIndex=c,L2(r.slice(0,c),e)}(),n=r[0]===` +`||r[0]===" ",s,o;o=t.exec(r);){var a=o[1],l=o[2];s=l[0]===" ",i+=a+(!n&&!s&&l!==""?` +`:"")+L2(l,e),n=s}return i}function L2(r,e){if(r===""||r[0]===" ")return r;for(var t=/ [^ ]/g,i,n=0,s,o=0,a=0,l="";i=t.exec(r);)a=i.index,a-n>e&&(s=o>n?o:a,l+=` +`+r.slice(n,s),n=s+1),o=a;return l+=` +`,r.length-n>e&&o>n?l+=r.slice(n,o)+` +`+r.slice(o+1):l+=r.slice(n),l.slice(1)}function zpe(r){for(var e="",t,i,n,s=0;s=55296&&t<=56319&&(i=r.charCodeAt(s+1),i>=56320&&i<=57343)){e+=D2((t-55296)*1024+i-56320+65536),s++;continue}n=Ni[t],e+=!n&&Og(t)?r[s]:n||D2(t)}return e}function Vpe(r,e,t){var i="",n=r.tag,s,o;for(s=0,o=t.length;s1024&&(u+="? "),u+=r.dump+(r.condenseFlow?'"':"")+":"+(r.condenseFlow?"":" "),rc(r,e,c,!1,!1)&&(u+=r.dump,i+=u));r.tag=n,r.dump="{"+i+"}"}function _pe(r,e,t,i){var n="",s=r.tag,o=Object.keys(t),a,l,c,u,g,f;if(r.sortKeys===!0)o.sort();else if(typeof r.sortKeys=="function")o.sort(r.sortKeys);else if(r.sortKeys)throw new _p("sortKeys must be a boolean or a function");for(a=0,l=o.length;a1024,g&&(r.dump&&Xp===r.dump.charCodeAt(0)?f+="?":f+="? "),f+=r.dump,g&&(f+=VS(r,e)),rc(r,e+1,u,!0,g)&&(r.dump&&Xp===r.dump.charCodeAt(0)?f+=":":f+=": ",f+=r.dump,n+=f));r.tag=s,r.dump=n||"{}"}function T2(r,e,t){var i,n,s,o,a,l;for(n=t?r.explicitTypes:r.implicitTypes,s=0,o=n.length;s tag resolver accepts not "'+l+'" style');r.dump=i}return!0}return!1}function rc(r,e,t,i,n,s){r.tag=null,r.dump=t,T2(r,t,!1)||T2(r,t,!0);var o=O2.call(r.dump);i&&(i=r.flowLevel<0||r.flowLevel>e);var a=o==="[object Object]"||o==="[object Array]",l,c;if(a&&(l=r.duplicates.indexOf(t),c=l!==-1),(r.tag!==null&&r.tag!=="?"||c||r.indent!==2&&e>0)&&(n=!1),c&&r.usedDuplicates[l])r.dump="*ref_"+l;else{if(a&&c&&!r.usedDuplicates[l]&&(r.usedDuplicates[l]=!0),o==="[object Object]")i&&Object.keys(r.dump).length!==0?(_pe(r,e,r.dump,n),c&&(r.dump="&ref_"+l+r.dump)):(Zpe(r,e,r.dump),c&&(r.dump="&ref_"+l+" "+r.dump));else if(o==="[object Array]"){var u=r.noArrayIndent&&e>0?e-1:e;i&&r.dump.length!==0?(Xpe(r,u,r.dump,n),c&&(r.dump="&ref_"+l+r.dump)):(Vpe(r,u,r.dump),c&&(r.dump="&ref_"+l+" "+r.dump))}else if(o==="[object String]")r.tag!=="?"&&Jpe(r,r.dump,e,s);else{if(r.skipInvalid)return!1;throw new _p("unacceptable kind of an object to dump "+o)}r.tag!==null&&r.tag!=="?"&&(r.dump="!<"+r.tag+"> "+r.dump)}return!0}function $pe(r,e){var t=[],i=[],n,s;for(XS(r,t,i),n=0,s=i.length;n{"use strict";var II=P2(),$2=_2();function yI(r){return function(){throw new Error("Function "+r+" is deprecated and cannot be used.")}}Fr.exports.Type=si();Fr.exports.Schema=_l();Fr.exports.FAILSAFE_SCHEMA=fI();Fr.exports.JSON_SCHEMA=YS();Fr.exports.CORE_SCHEMA=jS();Fr.exports.DEFAULT_SAFE_SCHEMA=Fg();Fr.exports.DEFAULT_FULL_SCHEMA=zp();Fr.exports.load=II.load;Fr.exports.loadAll=II.loadAll;Fr.exports.safeLoad=II.safeLoad;Fr.exports.safeLoadAll=II.safeLoadAll;Fr.exports.dump=$2.dump;Fr.exports.safeDump=$2.safeDump;Fr.exports.YAMLException=kg();Fr.exports.MINIMAL_SCHEMA=fI();Fr.exports.SAFE_SCHEMA=Fg();Fr.exports.DEFAULT_SCHEMA=zp();Fr.exports.scan=yI("scan");Fr.exports.parse=yI("parse");Fr.exports.compose=yI("compose");Fr.exports.addConstructor=yI("addConstructor")});var rH=w((WZe,tH)=>{"use strict";var tde=eH();tH.exports=tde});var nH=w((zZe,iH)=>{"use strict";function rde(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function ic(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,ic)}rde(ic,Error);ic.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;g({[Ke]:Ce})))},H=function(R){return R},j=function(R){return R},$=Ms("correct indentation"),V=" ",W=ar(" ",!1),_=function(R){return R.length===BA*mg},A=function(R){return R.length===(BA+1)*mg},ae=function(){return BA++,!0},ge=function(){return BA--,!0},re=function(){return gg()},O=Ms("pseudostring"),F=/^[^\r\n\t ?:,\][{}#&*!|>'"%@`\-]/,ue=Fn(["\r",` +`," "," ","?",":",",","]","[","{","}","#","&","*","!","|",">","'",'"',"%","@","`","-"],!0,!1),he=/^[^\r\n\t ,\][{}:#"']/,ke=Fn(["\r",` +`," "," ",",","]","[","{","}",":","#",'"',"'"],!0,!1),Fe=function(){return gg().replace(/^ *| *$/g,"")},Ne="--",oe=ar("--",!1),le=/^[a-zA-Z\/0-9]/,we=Fn([["a","z"],["A","Z"],"/",["0","9"]],!1,!1),fe=/^[^\r\n\t :,]/,Ae=Fn(["\r",` +`," "," ",":",","],!0,!1),qe="null",ne=ar("null",!1),Y=function(){return null},pe="true",ie=ar("true",!1),de=function(){return!0},_e="false",Pt=ar("false",!1),It=function(){return!1},Or=Ms("string"),ii='"',gi=ar('"',!1),hr=function(){return""},fi=function(R){return R},ni=function(R){return R.join("")},Os=/^[^"\\\0-\x1F\x7F]/,pr=Fn(['"',"\\",["\0",""],"\x7F"],!0,!1),Ii='\\"',es=ar('\\"',!1),ua=function(){return'"'},pA="\\\\",ag=ar("\\\\",!1),ts=function(){return"\\"},dA="\\/",ga=ar("\\/",!1),yp=function(){return"/"},CA="\\b",mA=ar("\\b",!1),wr=function(){return"\b"},kl="\\f",Ag=ar("\\f",!1),Io=function(){return"\f"},lg="\\n",wp=ar("\\n",!1),Bp=function(){return` +`},vr="\\r",se=ar("\\r",!1),yo=function(){return"\r"},kn="\\t",cg=ar("\\t",!1),Qt=function(){return" "},Rl="\\u",Rn=ar("\\u",!1),rs=function(R,q,Ce,Ke){return String.fromCharCode(parseInt(`0x${R}${q}${Ce}${Ke}`))},is=/^[0-9a-fA-F]/,gt=Fn([["0","9"],["a","f"],["A","F"]],!1,!1),wo=Ms("blank space"),At=/^[ \t]/,an=Fn([" "," "],!1,!1),S=Ms("white space"),Tt=/^[ \t\n\r]/,ug=Fn([" "," ",` +`,"\r"],!1,!1),Fl=`\r +`,Qp=ar(`\r +`,!1),bp=` +`,Sp=ar(` +`,!1),vp="\r",xp=ar("\r",!1),G=0,yt=0,EA=[{line:1,column:1}],Ji=0,Nl=[],Xe=0,fa;if("startRule"in e){if(!(e.startRule in i))throw new Error(`Can't start parsing from rule "`+e.startRule+'".');n=i[e.startRule]}function gg(){return r.substring(yt,G)}function FE(){return An(yt,G)}function Pp(R,q){throw q=q!==void 0?q:An(yt,G),Tl([Ms(R)],r.substring(yt,G),q)}function NE(R,q){throw q=q!==void 0?q:An(yt,G),fg(R,q)}function ar(R,q){return{type:"literal",text:R,ignoreCase:q}}function Fn(R,q,Ce){return{type:"class",parts:R,inverted:q,ignoreCase:Ce}}function Ll(){return{type:"any"}}function Dp(){return{type:"end"}}function Ms(R){return{type:"other",description:R}}function ha(R){var q=EA[R],Ce;if(q)return q;for(Ce=R-1;!EA[Ce];)Ce--;for(q=EA[Ce],q={line:q.line,column:q.column};CeJi&&(Ji=G,Nl=[]),Nl.push(R))}function fg(R,q){return new ic(R,null,null,q)}function Tl(R,q,Ce){return new ic(ic.buildMessage(R,q),R,q,Ce)}function Ks(){var R;return R=hg(),R}function Ol(){var R,q,Ce;for(R=G,q=[],Ce=IA();Ce!==t;)q.push(Ce),Ce=IA();return q!==t&&(yt=R,q=s(q)),R=q,R}function IA(){var R,q,Ce,Ke,Re;return R=G,q=da(),q!==t?(r.charCodeAt(G)===45?(Ce=o,G++):(Ce=t,Xe===0&&Te(a)),Ce!==t?(Ke=Rr(),Ke!==t?(Re=pa(),Re!==t?(yt=R,q=l(Re),R=q):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t),R}function hg(){var R,q,Ce;for(R=G,q=[],Ce=pg();Ce!==t;)q.push(Ce),Ce=pg();return q!==t&&(yt=R,q=c(q)),R=q,R}function pg(){var R,q,Ce,Ke,Re,ze,dt,Ft,Nn;if(R=G,q=Rr(),q===t&&(q=null),q!==t){if(Ce=G,r.charCodeAt(G)===35?(Ke=u,G++):(Ke=t,Xe===0&&Te(g)),Ke!==t){if(Re=[],ze=G,dt=G,Xe++,Ft=Gs(),Xe--,Ft===t?dt=void 0:(G=dt,dt=t),dt!==t?(r.length>G?(Ft=r.charAt(G),G++):(Ft=t,Xe===0&&Te(f)),Ft!==t?(dt=[dt,Ft],ze=dt):(G=ze,ze=t)):(G=ze,ze=t),ze!==t)for(;ze!==t;)Re.push(ze),ze=G,dt=G,Xe++,Ft=Gs(),Xe--,Ft===t?dt=void 0:(G=dt,dt=t),dt!==t?(r.length>G?(Ft=r.charAt(G),G++):(Ft=t,Xe===0&&Te(f)),Ft!==t?(dt=[dt,Ft],ze=dt):(G=ze,ze=t)):(G=ze,ze=t);else Re=t;Re!==t?(Ke=[Ke,Re],Ce=Ke):(G=Ce,Ce=t)}else G=Ce,Ce=t;if(Ce===t&&(Ce=null),Ce!==t){if(Ke=[],Re=Hs(),Re!==t)for(;Re!==t;)Ke.push(Re),Re=Hs();else Ke=t;Ke!==t?(yt=R,q=h(),R=q):(G=R,R=t)}else G=R,R=t}else G=R,R=t;if(R===t&&(R=G,q=da(),q!==t?(Ce=Ml(),Ce!==t?(Ke=Rr(),Ke===t&&(Ke=null),Ke!==t?(r.charCodeAt(G)===58?(Re=p,G++):(Re=t,Xe===0&&Te(C)),Re!==t?(ze=Rr(),ze===t&&(ze=null),ze!==t?(dt=pa(),dt!==t?(yt=R,q=y(Ce,dt),R=q):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t),R===t&&(R=G,q=da(),q!==t?(Ce=Us(),Ce!==t?(Ke=Rr(),Ke===t&&(Ke=null),Ke!==t?(r.charCodeAt(G)===58?(Re=p,G++):(Re=t,Xe===0&&Te(C)),Re!==t?(ze=Rr(),ze===t&&(ze=null),ze!==t?(dt=pa(),dt!==t?(yt=R,q=y(Ce,dt),R=q):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t),R===t))){if(R=G,q=da(),q!==t)if(Ce=Us(),Ce!==t)if(Ke=Rr(),Ke!==t)if(Re=LE(),Re!==t){if(ze=[],dt=Hs(),dt!==t)for(;dt!==t;)ze.push(dt),dt=Hs();else ze=t;ze!==t?(yt=R,q=y(Ce,Re),R=q):(G=R,R=t)}else G=R,R=t;else G=R,R=t;else G=R,R=t;else G=R,R=t;if(R===t)if(R=G,q=da(),q!==t)if(Ce=Us(),Ce!==t){if(Ke=[],Re=G,ze=Rr(),ze===t&&(ze=null),ze!==t?(r.charCodeAt(G)===44?(dt=B,G++):(dt=t,Xe===0&&Te(v)),dt!==t?(Ft=Rr(),Ft===t&&(Ft=null),Ft!==t?(Nn=Us(),Nn!==t?(yt=Re,ze=D(Ce,Nn),Re=ze):(G=Re,Re=t)):(G=Re,Re=t)):(G=Re,Re=t)):(G=Re,Re=t),Re!==t)for(;Re!==t;)Ke.push(Re),Re=G,ze=Rr(),ze===t&&(ze=null),ze!==t?(r.charCodeAt(G)===44?(dt=B,G++):(dt=t,Xe===0&&Te(v)),dt!==t?(Ft=Rr(),Ft===t&&(Ft=null),Ft!==t?(Nn=Us(),Nn!==t?(yt=Re,ze=D(Ce,Nn),Re=ze):(G=Re,Re=t)):(G=Re,Re=t)):(G=Re,Re=t)):(G=Re,Re=t);else Ke=t;Ke!==t?(Re=Rr(),Re===t&&(Re=null),Re!==t?(r.charCodeAt(G)===58?(ze=p,G++):(ze=t,Xe===0&&Te(C)),ze!==t?(dt=Rr(),dt===t&&(dt=null),dt!==t?(Ft=pa(),Ft!==t?(yt=R,q=L(Ce,Ke,Ft),R=q):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)}else G=R,R=t;else G=R,R=t}return R}function pa(){var R,q,Ce,Ke,Re,ze,dt;if(R=G,q=G,Xe++,Ce=G,Ke=Gs(),Ke!==t?(Re=rt(),Re!==t?(r.charCodeAt(G)===45?(ze=o,G++):(ze=t,Xe===0&&Te(a)),ze!==t?(dt=Rr(),dt!==t?(Ke=[Ke,Re,ze,dt],Ce=Ke):(G=Ce,Ce=t)):(G=Ce,Ce=t)):(G=Ce,Ce=t)):(G=Ce,Ce=t),Xe--,Ce!==t?(G=q,q=void 0):q=t,q!==t?(Ce=Hs(),Ce!==t?(Ke=Bo(),Ke!==t?(Re=Ol(),Re!==t?(ze=yA(),ze!==t?(yt=R,q=H(Re),R=q):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t),R===t&&(R=G,q=Gs(),q!==t?(Ce=Bo(),Ce!==t?(Ke=hg(),Ke!==t?(Re=yA(),Re!==t?(yt=R,q=H(Ke),R=q):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t),R===t))if(R=G,q=Kl(),q!==t){if(Ce=[],Ke=Hs(),Ke!==t)for(;Ke!==t;)Ce.push(Ke),Ke=Hs();else Ce=t;Ce!==t?(yt=R,q=j(q),R=q):(G=R,R=t)}else G=R,R=t;return R}function da(){var R,q,Ce;for(Xe++,R=G,q=[],r.charCodeAt(G)===32?(Ce=V,G++):(Ce=t,Xe===0&&Te(W));Ce!==t;)q.push(Ce),r.charCodeAt(G)===32?(Ce=V,G++):(Ce=t,Xe===0&&Te(W));return q!==t?(yt=G,Ce=_(q),Ce?Ce=void 0:Ce=t,Ce!==t?(q=[q,Ce],R=q):(G=R,R=t)):(G=R,R=t),Xe--,R===t&&(q=t,Xe===0&&Te($)),R}function rt(){var R,q,Ce;for(R=G,q=[],r.charCodeAt(G)===32?(Ce=V,G++):(Ce=t,Xe===0&&Te(W));Ce!==t;)q.push(Ce),r.charCodeAt(G)===32?(Ce=V,G++):(Ce=t,Xe===0&&Te(W));return q!==t?(yt=G,Ce=A(q),Ce?Ce=void 0:Ce=t,Ce!==t?(q=[q,Ce],R=q):(G=R,R=t)):(G=R,R=t),R}function Bo(){var R;return yt=G,R=ae(),R?R=void 0:R=t,R}function yA(){var R;return yt=G,R=ge(),R?R=void 0:R=t,R}function Ml(){var R;return R=Ul(),R===t&&(R=kp()),R}function Us(){var R,q,Ce;if(R=Ul(),R===t){if(R=G,q=[],Ce=dg(),Ce!==t)for(;Ce!==t;)q.push(Ce),Ce=dg();else q=t;q!==t&&(yt=R,q=re()),R=q}return R}function Kl(){var R;return R=Rp(),R===t&&(R=TE(),R===t&&(R=Ul(),R===t&&(R=kp()))),R}function LE(){var R;return R=Rp(),R===t&&(R=Ul(),R===t&&(R=dg())),R}function kp(){var R,q,Ce,Ke,Re,ze;if(Xe++,R=G,F.test(r.charAt(G))?(q=r.charAt(G),G++):(q=t,Xe===0&&Te(ue)),q!==t){for(Ce=[],Ke=G,Re=Rr(),Re===t&&(Re=null),Re!==t?(he.test(r.charAt(G))?(ze=r.charAt(G),G++):(ze=t,Xe===0&&Te(ke)),ze!==t?(Re=[Re,ze],Ke=Re):(G=Ke,Ke=t)):(G=Ke,Ke=t);Ke!==t;)Ce.push(Ke),Ke=G,Re=Rr(),Re===t&&(Re=null),Re!==t?(he.test(r.charAt(G))?(ze=r.charAt(G),G++):(ze=t,Xe===0&&Te(ke)),ze!==t?(Re=[Re,ze],Ke=Re):(G=Ke,Ke=t)):(G=Ke,Ke=t);Ce!==t?(yt=R,q=Fe(),R=q):(G=R,R=t)}else G=R,R=t;return Xe--,R===t&&(q=t,Xe===0&&Te(O)),R}function dg(){var R,q,Ce,Ke,Re;if(R=G,r.substr(G,2)===Ne?(q=Ne,G+=2):(q=t,Xe===0&&Te(oe)),q===t&&(q=null),q!==t)if(le.test(r.charAt(G))?(Ce=r.charAt(G),G++):(Ce=t,Xe===0&&Te(we)),Ce!==t){for(Ke=[],fe.test(r.charAt(G))?(Re=r.charAt(G),G++):(Re=t,Xe===0&&Te(Ae));Re!==t;)Ke.push(Re),fe.test(r.charAt(G))?(Re=r.charAt(G),G++):(Re=t,Xe===0&&Te(Ae));Ke!==t?(yt=R,q=Fe(),R=q):(G=R,R=t)}else G=R,R=t;else G=R,R=t;return R}function Rp(){var R,q;return R=G,r.substr(G,4)===qe?(q=qe,G+=4):(q=t,Xe===0&&Te(ne)),q!==t&&(yt=R,q=Y()),R=q,R}function TE(){var R,q;return R=G,r.substr(G,4)===pe?(q=pe,G+=4):(q=t,Xe===0&&Te(ie)),q!==t&&(yt=R,q=de()),R=q,R===t&&(R=G,r.substr(G,5)===_e?(q=_e,G+=5):(q=t,Xe===0&&Te(Pt)),q!==t&&(yt=R,q=It()),R=q),R}function Ul(){var R,q,Ce,Ke;return Xe++,R=G,r.charCodeAt(G)===34?(q=ii,G++):(q=t,Xe===0&&Te(gi)),q!==t?(r.charCodeAt(G)===34?(Ce=ii,G++):(Ce=t,Xe===0&&Te(gi)),Ce!==t?(yt=R,q=hr(),R=q):(G=R,R=t)):(G=R,R=t),R===t&&(R=G,r.charCodeAt(G)===34?(q=ii,G++):(q=t,Xe===0&&Te(gi)),q!==t?(Ce=OE(),Ce!==t?(r.charCodeAt(G)===34?(Ke=ii,G++):(Ke=t,Xe===0&&Te(gi)),Ke!==t?(yt=R,q=fi(Ce),R=q):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)),Xe--,R===t&&(q=t,Xe===0&&Te(Or)),R}function OE(){var R,q,Ce;if(R=G,q=[],Ce=Cg(),Ce!==t)for(;Ce!==t;)q.push(Ce),Ce=Cg();else q=t;return q!==t&&(yt=R,q=ni(q)),R=q,R}function Cg(){var R,q,Ce,Ke,Re,ze;return Os.test(r.charAt(G))?(R=r.charAt(G),G++):(R=t,Xe===0&&Te(pr)),R===t&&(R=G,r.substr(G,2)===Ii?(q=Ii,G+=2):(q=t,Xe===0&&Te(es)),q!==t&&(yt=R,q=ua()),R=q,R===t&&(R=G,r.substr(G,2)===pA?(q=pA,G+=2):(q=t,Xe===0&&Te(ag)),q!==t&&(yt=R,q=ts()),R=q,R===t&&(R=G,r.substr(G,2)===dA?(q=dA,G+=2):(q=t,Xe===0&&Te(ga)),q!==t&&(yt=R,q=yp()),R=q,R===t&&(R=G,r.substr(G,2)===CA?(q=CA,G+=2):(q=t,Xe===0&&Te(mA)),q!==t&&(yt=R,q=wr()),R=q,R===t&&(R=G,r.substr(G,2)===kl?(q=kl,G+=2):(q=t,Xe===0&&Te(Ag)),q!==t&&(yt=R,q=Io()),R=q,R===t&&(R=G,r.substr(G,2)===lg?(q=lg,G+=2):(q=t,Xe===0&&Te(wp)),q!==t&&(yt=R,q=Bp()),R=q,R===t&&(R=G,r.substr(G,2)===vr?(q=vr,G+=2):(q=t,Xe===0&&Te(se)),q!==t&&(yt=R,q=yo()),R=q,R===t&&(R=G,r.substr(G,2)===kn?(q=kn,G+=2):(q=t,Xe===0&&Te(cg)),q!==t&&(yt=R,q=Qt()),R=q,R===t&&(R=G,r.substr(G,2)===Rl?(q=Rl,G+=2):(q=t,Xe===0&&Te(Rn)),q!==t?(Ce=wA(),Ce!==t?(Ke=wA(),Ke!==t?(Re=wA(),Re!==t?(ze=wA(),ze!==t?(yt=R,q=rs(Ce,Ke,Re,ze),R=q):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)):(G=R,R=t)))))))))),R}function wA(){var R;return is.test(r.charAt(G))?(R=r.charAt(G),G++):(R=t,Xe===0&&Te(gt)),R}function Rr(){var R,q;if(Xe++,R=[],At.test(r.charAt(G))?(q=r.charAt(G),G++):(q=t,Xe===0&&Te(an)),q!==t)for(;q!==t;)R.push(q),At.test(r.charAt(G))?(q=r.charAt(G),G++):(q=t,Xe===0&&Te(an));else R=t;return Xe--,R===t&&(q=t,Xe===0&&Te(wo)),R}function ME(){var R,q;if(Xe++,R=[],Tt.test(r.charAt(G))?(q=r.charAt(G),G++):(q=t,Xe===0&&Te(ug)),q!==t)for(;q!==t;)R.push(q),Tt.test(r.charAt(G))?(q=r.charAt(G),G++):(q=t,Xe===0&&Te(ug));else R=t;return Xe--,R===t&&(q=t,Xe===0&&Te(S)),R}function Hs(){var R,q,Ce,Ke,Re,ze;if(R=G,q=Gs(),q!==t){for(Ce=[],Ke=G,Re=Rr(),Re===t&&(Re=null),Re!==t?(ze=Gs(),ze!==t?(Re=[Re,ze],Ke=Re):(G=Ke,Ke=t)):(G=Ke,Ke=t);Ke!==t;)Ce.push(Ke),Ke=G,Re=Rr(),Re===t&&(Re=null),Re!==t?(ze=Gs(),ze!==t?(Re=[Re,ze],Ke=Re):(G=Ke,Ke=t)):(G=Ke,Ke=t);Ce!==t?(q=[q,Ce],R=q):(G=R,R=t)}else G=R,R=t;return R}function Gs(){var R;return r.substr(G,2)===Fl?(R=Fl,G+=2):(R=t,Xe===0&&Te(Qp)),R===t&&(r.charCodeAt(G)===10?(R=bp,G++):(R=t,Xe===0&&Te(Sp)),R===t&&(r.charCodeAt(G)===13?(R=vp,G++):(R=t,Xe===0&&Te(xp)))),R}let mg=2,BA=0;if(fa=n(),fa!==t&&G===r.length)return fa;throw fa!==t&&G{"use strict";var Ade=r=>{let e=!1,t=!1,i=!1;for(let n=0;n{if(!(typeof r=="string"||Array.isArray(r)))throw new TypeError("Expected the input to be `string | string[]`");e=Object.assign({pascalCase:!1},e);let t=n=>e.pascalCase?n.charAt(0).toUpperCase()+n.slice(1):n;return Array.isArray(r)?r=r.map(n=>n.trim()).filter(n=>n.length).join("-"):r=r.trim(),r.length===0?"":r.length===1?e.pascalCase?r.toUpperCase():r.toLowerCase():(r!==r.toLowerCase()&&(r=Ade(r)),r=r.replace(/^[_.\- ]+/,"").toLowerCase().replace(/[_.\- ]+(\w|$)/g,(n,s)=>s.toUpperCase()).replace(/\d+(\w|$)/g,n=>n.toUpperCase()),t(r))};ev.exports=lH;ev.exports.default=lH});var uH=w((e_e,lde)=>{lde.exports=[{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",pr:"SYSTEM_PULLREQUEST_PULLREQUESTID"},{name:"Appcircle",constant:"APPCIRCLE",env:"AC_APPCIRCLE"},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Codefresh",constant:"CODEFRESH",env:"CF_BUILD_ID",pr:{any:["CF_PULL_REQUEST_NUMBER","CF_PULL_REQUEST_ID"]}},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"GitHub Actions",constant:"GITHUB_ACTIONS",env:"GITHUB_ACTIONS",pr:{GITHUB_EVENT_NAME:"pull_request"}},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI",pr:"CI_MERGE_REQUEST_ID"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"LayerCI",constant:"LAYERCI",env:"LAYERCI",pr:"LAYERCI_PULL_REQUEST"},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Nevercode",constant:"NEVERCODE",env:"NEVERCODE",pr:{env:"NEVERCODE_PULL_REQUEST",ne:"false"}},{name:"Render",constant:"RENDER",env:"RENDER",pr:{IS_PULL_REQUEST:"true"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Screwdriver",constant:"SCREWDRIVER",env:"SCREWDRIVER",pr:{env:"SD_PULL_REQUEST",ne:"false"}},{name:"Shippable",constant:"SHIPPABLE",env:"SHIPPABLE",pr:{IS_PULL_REQUEST:"true"}},{name:"Solano CI",constant:"SOLANO",env:"TDDIUM",pr:"TDDIUM_PR_ID"},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}},{name:"Vercel",constant:"VERCEL",env:"NOW_BUILDER"},{name:"Visual Studio App Center",constant:"APPCENTER",env:"APPCENTER_BUILD_ID"}]});var nc=w(Mn=>{"use strict";var fH=uH(),xo=process.env;Object.defineProperty(Mn,"_vendors",{value:fH.map(function(r){return r.constant})});Mn.name=null;Mn.isPR=null;fH.forEach(function(r){let t=(Array.isArray(r.env)?r.env:[r.env]).every(function(i){return gH(i)});if(Mn[r.constant]=t,t)switch(Mn.name=r.name,typeof r.pr){case"string":Mn.isPR=!!xo[r.pr];break;case"object":"env"in r.pr?Mn.isPR=r.pr.env in xo&&xo[r.pr.env]!==r.pr.ne:"any"in r.pr?Mn.isPR=r.pr.any.some(function(i){return!!xo[i]}):Mn.isPR=gH(r.pr);break;default:Mn.isPR=null}});Mn.isCI=!!(xo.CI||xo.CONTINUOUS_INTEGRATION||xo.BUILD_NUMBER||xo.RUN_ID||Mn.name);function gH(r){return typeof r=="string"?!!xo[r]:Object.keys(r).every(function(e){return xo[e]===r[e]})}});var gn={};ut(gn,{KeyRelationship:()=>sc,applyCascade:()=>nd,base64RegExp:()=>mH,colorStringAlphaRegExp:()=>CH,colorStringRegExp:()=>dH,computeKey:()=>kA,getPrintable:()=>Vr,hasExactLength:()=>BH,hasForbiddenKeys:()=>Hde,hasKeyRelationship:()=>av,hasMaxLength:()=>Qde,hasMinLength:()=>Bde,hasMutuallyExclusiveKeys:()=>Gde,hasRequiredKeys:()=>Ude,hasUniqueItems:()=>bde,isArray:()=>pde,isAtLeast:()=>xde,isAtMost:()=>Pde,isBase64:()=>Mde,isBoolean:()=>gde,isDate:()=>hde,isDict:()=>Cde,isEnum:()=>Vi,isHexColor:()=>Ode,isISO8601:()=>Tde,isInExclusiveRange:()=>kde,isInInclusiveRange:()=>Dde,isInstanceOf:()=>Ede,isInteger:()=>Rde,isJSON:()=>Kde,isLiteral:()=>cde,isLowerCase:()=>Fde,isNegative:()=>Sde,isNullable:()=>wde,isNumber:()=>fde,isObject:()=>mde,isOneOf:()=>Ide,isOptional:()=>yde,isPositive:()=>vde,isString:()=>id,isTuple:()=>dde,isUUID4:()=>Lde,isUnknown:()=>wH,isUpperCase:()=>Nde,iso8601RegExp:()=>ov,makeCoercionFn:()=>oc,makeSetter:()=>yH,makeTrait:()=>IH,makeValidator:()=>bt,matchesRegExp:()=>sd,plural:()=>vI,pushError:()=>pt,simpleKeyRegExp:()=>pH,uuid4RegExp:()=>EH});function bt({test:r}){return IH(r)()}function Vr(r){return r===null?"null":r===void 0?"undefined":r===""?"an empty string":JSON.stringify(r)}function kA(r,e){var t,i,n;return typeof e=="number"?`${(t=r==null?void 0:r.p)!==null&&t!==void 0?t:"."}[${e}]`:pH.test(e)?`${(i=r==null?void 0:r.p)!==null&&i!==void 0?i:""}.${e}`:`${(n=r==null?void 0:r.p)!==null&&n!==void 0?n:"."}[${JSON.stringify(e)}]`}function oc(r,e){return t=>{let i=r[e];return r[e]=t,oc(r,e).bind(null,i)}}function yH(r,e){return t=>{r[e]=t}}function vI(r,e,t){return r===1?e:t}function pt({errors:r,p:e}={},t){return r==null||r.push(`${e!=null?e:"."}: ${t}`),!1}function cde(r){return bt({test:(e,t)=>e!==r?pt(t,`Expected a literal (got ${Vr(r)})`):!0})}function Vi(r){let e=Array.isArray(r)?r:Object.values(r),t=new Set(e);return bt({test:(i,n)=>t.has(i)?!0:pt(n,`Expected a valid enumeration value (got ${Vr(i)})`)})}var pH,dH,CH,mH,EH,ov,IH,wH,id,ude,gde,fde,hde,pde,dde,Cde,mde,Ede,Ide,nd,yde,wde,Bde,Qde,BH,bde,Sde,vde,xde,Pde,Dde,kde,Rde,sd,Fde,Nde,Lde,Tde,Ode,Mde,Kde,Ude,Hde,Gde,sc,Yde,av,as=Pge(()=>{pH=/^[a-zA-Z_][a-zA-Z0-9_]*$/,dH=/^#[0-9a-f]{6}$/i,CH=/^#[0-9a-f]{6}([0-9a-f]{2})?$/i,mH=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/,EH=/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/i,ov=/^(?:[1-9]\d{3}(-?)(?:(?:0[1-9]|1[0-2])\1(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])\1(?:29|30)|(?:0[13578]|1[02])(?:\1)31|00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[0-5]))|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)(?:(-?)02(?:\2)29|-?366))T(?:[01]\d|2[0-3])(:?)[0-5]\d(?:\3[0-5]\d)?(?:Z|[+-][01]\d(?:\3[0-5]\d)?)$/,IH=r=>()=>r;wH=()=>bt({test:(r,e)=>!0});id=()=>bt({test:(r,e)=>typeof r!="string"?pt(e,`Expected a string (got ${Vr(r)})`):!0});ude=new Map([["true",!0],["True",!0],["1",!0],[1,!0],["false",!1],["False",!1],["0",!1],[0,!1]]),gde=()=>bt({test:(r,e)=>{var t;if(typeof r!="boolean"){if(typeof(e==null?void 0:e.coercions)<"u"){if(typeof(e==null?void 0:e.coercion)>"u")return pt(e,"Unbound coercion result");let i=ude.get(r);if(typeof i<"u")return e.coercions.push([(t=e.p)!==null&&t!==void 0?t:".",e.coercion.bind(null,i)]),!0}return pt(e,`Expected a boolean (got ${Vr(r)})`)}return!0}}),fde=()=>bt({test:(r,e)=>{var t;if(typeof r!="number"){if(typeof(e==null?void 0:e.coercions)<"u"){if(typeof(e==null?void 0:e.coercion)>"u")return pt(e,"Unbound coercion result");let i;if(typeof r=="string"){let n;try{n=JSON.parse(r)}catch{}if(typeof n=="number")if(JSON.stringify(n)===r)i=n;else return pt(e,`Received a number that can't be safely represented by the runtime (${r})`)}if(typeof i<"u")return e.coercions.push([(t=e.p)!==null&&t!==void 0?t:".",e.coercion.bind(null,i)]),!0}return pt(e,`Expected a number (got ${Vr(r)})`)}return!0}}),hde=()=>bt({test:(r,e)=>{var t;if(!(r instanceof Date)){if(typeof(e==null?void 0:e.coercions)<"u"){if(typeof(e==null?void 0:e.coercion)>"u")return pt(e,"Unbound coercion result");let i;if(typeof r=="string"&&ov.test(r))i=new Date(r);else{let n;if(typeof r=="string"){let s;try{s=JSON.parse(r)}catch{}typeof s=="number"&&(n=s)}else typeof r=="number"&&(n=r);if(typeof n<"u")if(Number.isSafeInteger(n)||!Number.isSafeInteger(n*1e3))i=new Date(n*1e3);else return pt(e,`Received a timestamp that can't be safely represented by the runtime (${r})`)}if(typeof i<"u")return e.coercions.push([(t=e.p)!==null&&t!==void 0?t:".",e.coercion.bind(null,i)]),!0}return pt(e,`Expected a date (got ${Vr(r)})`)}return!0}}),pde=(r,{delimiter:e}={})=>bt({test:(t,i)=>{var n;if(typeof t=="string"&&typeof e<"u"&&typeof(i==null?void 0:i.coercions)<"u"){if(typeof(i==null?void 0:i.coercion)>"u")return pt(i,"Unbound coercion result");t=t.split(e),i.coercions.push([(n=i.p)!==null&&n!==void 0?n:".",i.coercion.bind(null,t)])}if(!Array.isArray(t))return pt(i,`Expected an array (got ${Vr(t)})`);let s=!0;for(let o=0,a=t.length;o{let t=BH(r.length);return bt({test:(i,n)=>{var s;if(typeof i=="string"&&typeof e<"u"&&typeof(n==null?void 0:n.coercions)<"u"){if(typeof(n==null?void 0:n.coercion)>"u")return pt(n,"Unbound coercion result");i=i.split(e),n.coercions.push([(s=n.p)!==null&&s!==void 0?s:".",n.coercion.bind(null,i)])}if(!Array.isArray(i))return pt(n,`Expected a tuple (got ${Vr(i)})`);let o=t(i,Object.assign({},n));for(let a=0,l=i.length;abt({test:(t,i)=>{if(typeof t!="object"||t===null)return pt(i,`Expected an object (got ${Vr(t)})`);let n=Object.keys(t),s=!0;for(let o=0,a=n.length;o{let t=Object.keys(r);return bt({test:(i,n)=>{if(typeof i!="object"||i===null)return pt(n,`Expected an object (got ${Vr(i)})`);let s=new Set([...t,...Object.keys(i)]),o={},a=!0;for(let l of s){if(l==="constructor"||l==="__proto__")a=pt(Object.assign(Object.assign({},n),{p:kA(n,l)}),"Unsafe property name");else{let c=Object.prototype.hasOwnProperty.call(r,l)?r[l]:void 0,u=Object.prototype.hasOwnProperty.call(i,l)?i[l]:void 0;typeof c<"u"?a=c(u,Object.assign(Object.assign({},n),{p:kA(n,l),coercion:oc(i,l)}))&&a:e===null?a=pt(Object.assign(Object.assign({},n),{p:kA(n,l)}),`Extraneous property (got ${Vr(u)})`):Object.defineProperty(o,l,{enumerable:!0,get:()=>u,set:yH(i,l)})}if(!a&&(n==null?void 0:n.errors)==null)break}return e!==null&&(a||(n==null?void 0:n.errors)!=null)&&(a=e(o,n)&&a),a}})},Ede=r=>bt({test:(e,t)=>e instanceof r?!0:pt(t,`Expected an instance of ${r.name} (got ${Vr(e)})`)}),Ide=(r,{exclusive:e=!1}={})=>bt({test:(t,i)=>{var n,s,o;let a=[],l=typeof(i==null?void 0:i.errors)<"u"?[]:void 0;for(let c=0,u=r.length;c1?pt(i,`Expected to match exactly a single predicate (matched ${a.join(", ")})`):(o=i==null?void 0:i.errors)===null||o===void 0||o.push(...l),!1}}),nd=(r,e)=>bt({test:(t,i)=>{var n,s;let o={value:t},a=typeof(i==null?void 0:i.coercions)<"u"?oc(o,"value"):void 0,l=typeof(i==null?void 0:i.coercions)<"u"?[]:void 0;if(!r(t,Object.assign(Object.assign({},i),{coercion:a,coercions:l})))return!1;let c=[];if(typeof l<"u")for(let[,u]of l)c.push(u());try{if(typeof(i==null?void 0:i.coercions)<"u"){if(o.value!==t){if(typeof(i==null?void 0:i.coercion)>"u")return pt(i,"Unbound coercion result");i.coercions.push([(n=i.p)!==null&&n!==void 0?n:".",i.coercion.bind(null,o.value)])}(s=i==null?void 0:i.coercions)===null||s===void 0||s.push(...l)}return e.every(u=>u(o.value,i))}finally{for(let u of c)u()}}}),yde=r=>bt({test:(e,t)=>typeof e>"u"?!0:r(e,t)}),wde=r=>bt({test:(e,t)=>e===null?!0:r(e,t)}),Bde=r=>bt({test:(e,t)=>e.length>=r?!0:pt(t,`Expected to have a length of at least ${r} elements (got ${e.length})`)}),Qde=r=>bt({test:(e,t)=>e.length<=r?!0:pt(t,`Expected to have a length of at most ${r} elements (got ${e.length})`)}),BH=r=>bt({test:(e,t)=>e.length!==r?pt(t,`Expected to have a length of exactly ${r} elements (got ${e.length})`):!0}),bde=({map:r}={})=>bt({test:(e,t)=>{let i=new Set,n=new Set;for(let s=0,o=e.length;sbt({test:(r,e)=>r<=0?!0:pt(e,`Expected to be negative (got ${r})`)}),vde=()=>bt({test:(r,e)=>r>=0?!0:pt(e,`Expected to be positive (got ${r})`)}),xde=r=>bt({test:(e,t)=>e>=r?!0:pt(t,`Expected to be at least ${r} (got ${e})`)}),Pde=r=>bt({test:(e,t)=>e<=r?!0:pt(t,`Expected to be at most ${r} (got ${e})`)}),Dde=(r,e)=>bt({test:(t,i)=>t>=r&&t<=e?!0:pt(i,`Expected to be in the [${r}; ${e}] range (got ${t})`)}),kde=(r,e)=>bt({test:(t,i)=>t>=r&&tbt({test:(e,t)=>e!==Math.round(e)?pt(t,`Expected to be an integer (got ${e})`):Number.isSafeInteger(e)?!0:pt(t,`Expected to be a safe integer (got ${e})`)}),sd=r=>bt({test:(e,t)=>r.test(e)?!0:pt(t,`Expected to match the pattern ${r.toString()} (got ${Vr(e)})`)}),Fde=()=>bt({test:(r,e)=>r!==r.toLowerCase()?pt(e,`Expected to be all-lowercase (got ${r})`):!0}),Nde=()=>bt({test:(r,e)=>r!==r.toUpperCase()?pt(e,`Expected to be all-uppercase (got ${r})`):!0}),Lde=()=>bt({test:(r,e)=>EH.test(r)?!0:pt(e,`Expected to be a valid UUID v4 (got ${Vr(r)})`)}),Tde=()=>bt({test:(r,e)=>ov.test(r)?!1:pt(e,`Expected to be a valid ISO 8601 date string (got ${Vr(r)})`)}),Ode=({alpha:r=!1})=>bt({test:(e,t)=>(r?dH.test(e):CH.test(e))?!0:pt(t,`Expected to be a valid hexadecimal color string (got ${Vr(e)})`)}),Mde=()=>bt({test:(r,e)=>mH.test(r)?!0:pt(e,`Expected to be a valid base 64 string (got ${Vr(r)})`)}),Kde=(r=wH())=>bt({test:(e,t)=>{let i;try{i=JSON.parse(e)}catch{return pt(t,`Expected to be a valid JSON string (got ${Vr(e)})`)}return r(i,t)}}),Ude=r=>{let e=new Set(r);return bt({test:(t,i)=>{let n=new Set(Object.keys(t)),s=[];for(let o of e)n.has(o)||s.push(o);return s.length>0?pt(i,`Missing required ${vI(s.length,"property","properties")} ${s.map(o=>`"${o}"`).join(", ")}`):!0}})},Hde=r=>{let e=new Set(r);return bt({test:(t,i)=>{let n=new Set(Object.keys(t)),s=[];for(let o of e)n.has(o)&&s.push(o);return s.length>0?pt(i,`Forbidden ${vI(s.length,"property","properties")} ${s.map(o=>`"${o}"`).join(", ")}`):!0}})},Gde=r=>{let e=new Set(r);return bt({test:(t,i)=>{let n=new Set(Object.keys(t)),s=[];for(let o of e)n.has(o)&&s.push(o);return s.length>1?pt(i,`Mutually exclusive properties ${s.map(o=>`"${o}"`).join(", ")}`):!0}})};(function(r){r.Forbids="Forbids",r.Requires="Requires"})(sc||(sc={}));Yde={[sc.Forbids]:{expect:!1,message:"forbids using"},[sc.Requires]:{expect:!0,message:"requires using"}},av=(r,e,t,{ignore:i=[]}={})=>{let n=new Set(i),s=new Set(t),o=Yde[e];return bt({test:(a,l)=>{let c=new Set(Object.keys(a));if(!c.has(r)||n.has(a[r]))return!0;let u=[];for(let g of s)(c.has(g)&&!n.has(a[g]))!==o.expect&&u.push(g);return u.length>=1?pt(l,`Property "${r}" ${o.message} ${vI(u.length,"property","properties")} ${u.map(g=>`"${g}"`).join(", ")}`):!0}})}});var UH=w((e$e,KH)=>{"use strict";KH.exports=(r,...e)=>new Promise(t=>{t(r(...e))})});var Yg=w((t$e,pv)=>{"use strict";var oCe=UH(),HH=r=>{if(r<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let e=[],t=0,i=()=>{t--,e.length>0&&e.shift()()},n=(a,l,...c)=>{t++;let u=oCe(a,...c);l(u),u.then(i,i)},s=(a,l,...c)=>{tnew Promise(c=>s(a,c,...l));return Object.defineProperties(o,{activeCount:{get:()=>t},pendingCount:{get:()=>e.length}}),o};pv.exports=HH;pv.exports.default=HH});var cd=w((i$e,GH)=>{var aCe="2.0.0",ACe=Number.MAX_SAFE_INTEGER||9007199254740991,lCe=16;GH.exports={SEMVER_SPEC_VERSION:aCe,MAX_LENGTH:256,MAX_SAFE_INTEGER:ACe,MAX_SAFE_COMPONENT_LENGTH:lCe}});var ud=w((n$e,YH)=>{var cCe=typeof process=="object"&&process.env&&process.env.NODE_DEBUG&&/\bsemver\b/i.test(process.env.NODE_DEBUG)?(...r)=>console.error("SEMVER",...r):()=>{};YH.exports=cCe});var ac=w((FA,jH)=>{var{MAX_SAFE_COMPONENT_LENGTH:dv}=cd(),uCe=ud();FA=jH.exports={};var gCe=FA.re=[],et=FA.src=[],tt=FA.t={},fCe=0,St=(r,e,t)=>{let i=fCe++;uCe(i,e),tt[r]=i,et[i]=e,gCe[i]=new RegExp(e,t?"g":void 0)};St("NUMERICIDENTIFIER","0|[1-9]\\d*");St("NUMERICIDENTIFIERLOOSE","[0-9]+");St("NONNUMERICIDENTIFIER","\\d*[a-zA-Z-][a-zA-Z0-9-]*");St("MAINVERSION",`(${et[tt.NUMERICIDENTIFIER]})\\.(${et[tt.NUMERICIDENTIFIER]})\\.(${et[tt.NUMERICIDENTIFIER]})`);St("MAINVERSIONLOOSE",`(${et[tt.NUMERICIDENTIFIERLOOSE]})\\.(${et[tt.NUMERICIDENTIFIERLOOSE]})\\.(${et[tt.NUMERICIDENTIFIERLOOSE]})`);St("PRERELEASEIDENTIFIER",`(?:${et[tt.NUMERICIDENTIFIER]}|${et[tt.NONNUMERICIDENTIFIER]})`);St("PRERELEASEIDENTIFIERLOOSE",`(?:${et[tt.NUMERICIDENTIFIERLOOSE]}|${et[tt.NONNUMERICIDENTIFIER]})`);St("PRERELEASE",`(?:-(${et[tt.PRERELEASEIDENTIFIER]}(?:\\.${et[tt.PRERELEASEIDENTIFIER]})*))`);St("PRERELEASELOOSE",`(?:-?(${et[tt.PRERELEASEIDENTIFIERLOOSE]}(?:\\.${et[tt.PRERELEASEIDENTIFIERLOOSE]})*))`);St("BUILDIDENTIFIER","[0-9A-Za-z-]+");St("BUILD",`(?:\\+(${et[tt.BUILDIDENTIFIER]}(?:\\.${et[tt.BUILDIDENTIFIER]})*))`);St("FULLPLAIN",`v?${et[tt.MAINVERSION]}${et[tt.PRERELEASE]}?${et[tt.BUILD]}?`);St("FULL",`^${et[tt.FULLPLAIN]}$`);St("LOOSEPLAIN",`[v=\\s]*${et[tt.MAINVERSIONLOOSE]}${et[tt.PRERELEASELOOSE]}?${et[tt.BUILD]}?`);St("LOOSE",`^${et[tt.LOOSEPLAIN]}$`);St("GTLT","((?:<|>)?=?)");St("XRANGEIDENTIFIERLOOSE",`${et[tt.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`);St("XRANGEIDENTIFIER",`${et[tt.NUMERICIDENTIFIER]}|x|X|\\*`);St("XRANGEPLAIN",`[v=\\s]*(${et[tt.XRANGEIDENTIFIER]})(?:\\.(${et[tt.XRANGEIDENTIFIER]})(?:\\.(${et[tt.XRANGEIDENTIFIER]})(?:${et[tt.PRERELEASE]})?${et[tt.BUILD]}?)?)?`);St("XRANGEPLAINLOOSE",`[v=\\s]*(${et[tt.XRANGEIDENTIFIERLOOSE]})(?:\\.(${et[tt.XRANGEIDENTIFIERLOOSE]})(?:\\.(${et[tt.XRANGEIDENTIFIERLOOSE]})(?:${et[tt.PRERELEASELOOSE]})?${et[tt.BUILD]}?)?)?`);St("XRANGE",`^${et[tt.GTLT]}\\s*${et[tt.XRANGEPLAIN]}$`);St("XRANGELOOSE",`^${et[tt.GTLT]}\\s*${et[tt.XRANGEPLAINLOOSE]}$`);St("COERCE",`(^|[^\\d])(\\d{1,${dv}})(?:\\.(\\d{1,${dv}}))?(?:\\.(\\d{1,${dv}}))?(?:$|[^\\d])`);St("COERCERTL",et[tt.COERCE],!0);St("LONETILDE","(?:~>?)");St("TILDETRIM",`(\\s*)${et[tt.LONETILDE]}\\s+`,!0);FA.tildeTrimReplace="$1~";St("TILDE",`^${et[tt.LONETILDE]}${et[tt.XRANGEPLAIN]}$`);St("TILDELOOSE",`^${et[tt.LONETILDE]}${et[tt.XRANGEPLAINLOOSE]}$`);St("LONECARET","(?:\\^)");St("CARETTRIM",`(\\s*)${et[tt.LONECARET]}\\s+`,!0);FA.caretTrimReplace="$1^";St("CARET",`^${et[tt.LONECARET]}${et[tt.XRANGEPLAIN]}$`);St("CARETLOOSE",`^${et[tt.LONECARET]}${et[tt.XRANGEPLAINLOOSE]}$`);St("COMPARATORLOOSE",`^${et[tt.GTLT]}\\s*(${et[tt.LOOSEPLAIN]})$|^$`);St("COMPARATOR",`^${et[tt.GTLT]}\\s*(${et[tt.FULLPLAIN]})$|^$`);St("COMPARATORTRIM",`(\\s*)${et[tt.GTLT]}\\s*(${et[tt.LOOSEPLAIN]}|${et[tt.XRANGEPLAIN]})`,!0);FA.comparatorTrimReplace="$1$2$3";St("HYPHENRANGE",`^\\s*(${et[tt.XRANGEPLAIN]})\\s+-\\s+(${et[tt.XRANGEPLAIN]})\\s*$`);St("HYPHENRANGELOOSE",`^\\s*(${et[tt.XRANGEPLAINLOOSE]})\\s+-\\s+(${et[tt.XRANGEPLAINLOOSE]})\\s*$`);St("STAR","(<|>)?=?\\s*\\*");St("GTE0","^\\s*>=\\s*0.0.0\\s*$");St("GTE0PRE","^\\s*>=\\s*0.0.0-0\\s*$")});var gd=w((s$e,qH)=>{var hCe=["includePrerelease","loose","rtl"],pCe=r=>r?typeof r!="object"?{loose:!0}:hCe.filter(e=>r[e]).reduce((e,t)=>(e[t]=!0,e),{}):{};qH.exports=pCe});var FI=w((o$e,zH)=>{var JH=/^[0-9]+$/,WH=(r,e)=>{let t=JH.test(r),i=JH.test(e);return t&&i&&(r=+r,e=+e),r===e?0:t&&!i?-1:i&&!t?1:rWH(e,r);zH.exports={compareIdentifiers:WH,rcompareIdentifiers:dCe}});var Ti=w((a$e,_H)=>{var NI=ud(),{MAX_LENGTH:VH,MAX_SAFE_INTEGER:LI}=cd(),{re:XH,t:ZH}=ac(),CCe=gd(),{compareIdentifiers:fd}=FI(),Hn=class{constructor(e,t){if(t=CCe(t),e instanceof Hn){if(e.loose===!!t.loose&&e.includePrerelease===!!t.includePrerelease)return e;e=e.version}else if(typeof e!="string")throw new TypeError(`Invalid Version: ${e}`);if(e.length>VH)throw new TypeError(`version is longer than ${VH} characters`);NI("SemVer",e,t),this.options=t,this.loose=!!t.loose,this.includePrerelease=!!t.includePrerelease;let i=e.trim().match(t.loose?XH[ZH.LOOSE]:XH[ZH.FULL]);if(!i)throw new TypeError(`Invalid Version: ${e}`);if(this.raw=e,this.major=+i[1],this.minor=+i[2],this.patch=+i[3],this.major>LI||this.major<0)throw new TypeError("Invalid major version");if(this.minor>LI||this.minor<0)throw new TypeError("Invalid minor version");if(this.patch>LI||this.patch<0)throw new TypeError("Invalid patch version");i[4]?this.prerelease=i[4].split(".").map(n=>{if(/^[0-9]+$/.test(n)){let s=+n;if(s>=0&&s=0;)typeof this.prerelease[i]=="number"&&(this.prerelease[i]++,i=-2);i===-1&&this.prerelease.push(0)}t&&(this.prerelease[0]===t?isNaN(this.prerelease[1])&&(this.prerelease=[t,0]):this.prerelease=[t,0]);break;default:throw new Error(`invalid increment argument: ${e}`)}return this.format(),this.raw=this.version,this}};_H.exports=Hn});var Ac=w((A$e,rG)=>{var{MAX_LENGTH:mCe}=cd(),{re:$H,t:eG}=ac(),tG=Ti(),ECe=gd(),ICe=(r,e)=>{if(e=ECe(e),r instanceof tG)return r;if(typeof r!="string"||r.length>mCe||!(e.loose?$H[eG.LOOSE]:$H[eG.FULL]).test(r))return null;try{return new tG(r,e)}catch{return null}};rG.exports=ICe});var nG=w((l$e,iG)=>{var yCe=Ac(),wCe=(r,e)=>{let t=yCe(r,e);return t?t.version:null};iG.exports=wCe});var oG=w((c$e,sG)=>{var BCe=Ac(),QCe=(r,e)=>{let t=BCe(r.trim().replace(/^[=v]+/,""),e);return t?t.version:null};sG.exports=QCe});var AG=w((u$e,aG)=>{var bCe=Ti(),SCe=(r,e,t,i)=>{typeof t=="string"&&(i=t,t=void 0);try{return new bCe(r,t).inc(e,i).version}catch{return null}};aG.exports=SCe});var As=w((g$e,cG)=>{var lG=Ti(),vCe=(r,e,t)=>new lG(r,t).compare(new lG(e,t));cG.exports=vCe});var TI=w((f$e,uG)=>{var xCe=As(),PCe=(r,e,t)=>xCe(r,e,t)===0;uG.exports=PCe});var hG=w((h$e,fG)=>{var gG=Ac(),DCe=TI(),kCe=(r,e)=>{if(DCe(r,e))return null;{let t=gG(r),i=gG(e),n=t.prerelease.length||i.prerelease.length,s=n?"pre":"",o=n?"prerelease":"";for(let a in t)if((a==="major"||a==="minor"||a==="patch")&&t[a]!==i[a])return s+a;return o}};fG.exports=kCe});var dG=w((p$e,pG)=>{var RCe=Ti(),FCe=(r,e)=>new RCe(r,e).major;pG.exports=FCe});var mG=w((d$e,CG)=>{var NCe=Ti(),LCe=(r,e)=>new NCe(r,e).minor;CG.exports=LCe});var IG=w((C$e,EG)=>{var TCe=Ti(),OCe=(r,e)=>new TCe(r,e).patch;EG.exports=OCe});var wG=w((m$e,yG)=>{var MCe=Ac(),KCe=(r,e)=>{let t=MCe(r,e);return t&&t.prerelease.length?t.prerelease:null};yG.exports=KCe});var QG=w((E$e,BG)=>{var UCe=As(),HCe=(r,e,t)=>UCe(e,r,t);BG.exports=HCe});var SG=w((I$e,bG)=>{var GCe=As(),YCe=(r,e)=>GCe(r,e,!0);bG.exports=YCe});var OI=w((y$e,xG)=>{var vG=Ti(),jCe=(r,e,t)=>{let i=new vG(r,t),n=new vG(e,t);return i.compare(n)||i.compareBuild(n)};xG.exports=jCe});var DG=w((w$e,PG)=>{var qCe=OI(),JCe=(r,e)=>r.sort((t,i)=>qCe(t,i,e));PG.exports=JCe});var RG=w((B$e,kG)=>{var WCe=OI(),zCe=(r,e)=>r.sort((t,i)=>WCe(i,t,e));kG.exports=zCe});var hd=w((Q$e,FG)=>{var VCe=As(),XCe=(r,e,t)=>VCe(r,e,t)>0;FG.exports=XCe});var MI=w((b$e,NG)=>{var ZCe=As(),_Ce=(r,e,t)=>ZCe(r,e,t)<0;NG.exports=_Ce});var Cv=w((S$e,LG)=>{var $Ce=As(),eme=(r,e,t)=>$Ce(r,e,t)!==0;LG.exports=eme});var KI=w((v$e,TG)=>{var tme=As(),rme=(r,e,t)=>tme(r,e,t)>=0;TG.exports=rme});var UI=w((x$e,OG)=>{var ime=As(),nme=(r,e,t)=>ime(r,e,t)<=0;OG.exports=nme});var mv=w((P$e,MG)=>{var sme=TI(),ome=Cv(),ame=hd(),Ame=KI(),lme=MI(),cme=UI(),ume=(r,e,t,i)=>{switch(e){case"===":return typeof r=="object"&&(r=r.version),typeof t=="object"&&(t=t.version),r===t;case"!==":return typeof r=="object"&&(r=r.version),typeof t=="object"&&(t=t.version),r!==t;case"":case"=":case"==":return sme(r,t,i);case"!=":return ome(r,t,i);case">":return ame(r,t,i);case">=":return Ame(r,t,i);case"<":return lme(r,t,i);case"<=":return cme(r,t,i);default:throw new TypeError(`Invalid operator: ${e}`)}};MG.exports=ume});var UG=w((D$e,KG)=>{var gme=Ti(),fme=Ac(),{re:HI,t:GI}=ac(),hme=(r,e)=>{if(r instanceof gme)return r;if(typeof r=="number"&&(r=String(r)),typeof r!="string")return null;e=e||{};let t=null;if(!e.rtl)t=r.match(HI[GI.COERCE]);else{let i;for(;(i=HI[GI.COERCERTL].exec(r))&&(!t||t.index+t[0].length!==r.length);)(!t||i.index+i[0].length!==t.index+t[0].length)&&(t=i),HI[GI.COERCERTL].lastIndex=i.index+i[1].length+i[2].length;HI[GI.COERCERTL].lastIndex=-1}return t===null?null:fme(`${t[2]}.${t[3]||"0"}.${t[4]||"0"}`,e)};KG.exports=hme});var GG=w((k$e,HG)=>{"use strict";HG.exports=function(r){r.prototype[Symbol.iterator]=function*(){for(let e=this.head;e;e=e.next)yield e.value}}});var YI=w((R$e,YG)=>{"use strict";YG.exports=Ht;Ht.Node=lc;Ht.create=Ht;function Ht(r){var e=this;if(e instanceof Ht||(e=new Ht),e.tail=null,e.head=null,e.length=0,r&&typeof r.forEach=="function")r.forEach(function(n){e.push(n)});else if(arguments.length>0)for(var t=0,i=arguments.length;t1)t=e;else if(this.head)i=this.head.next,t=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;i!==null;n++)t=r(t,i.value,n),i=i.next;return t};Ht.prototype.reduceReverse=function(r,e){var t,i=this.tail;if(arguments.length>1)t=e;else if(this.tail)i=this.tail.prev,t=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=this.length-1;i!==null;n--)t=r(t,i.value,n),i=i.prev;return t};Ht.prototype.toArray=function(){for(var r=new Array(this.length),e=0,t=this.head;t!==null;e++)r[e]=t.value,t=t.next;return r};Ht.prototype.toArrayReverse=function(){for(var r=new Array(this.length),e=0,t=this.tail;t!==null;e++)r[e]=t.value,t=t.prev;return r};Ht.prototype.slice=function(r,e){e=e||this.length,e<0&&(e+=this.length),r=r||0,r<0&&(r+=this.length);var t=new Ht;if(ethis.length&&(e=this.length);for(var i=0,n=this.head;n!==null&&ithis.length&&(e=this.length);for(var i=this.length,n=this.tail;n!==null&&i>e;i--)n=n.prev;for(;n!==null&&i>r;i--,n=n.prev)t.push(n.value);return t};Ht.prototype.splice=function(r,e,...t){r>this.length&&(r=this.length-1),r<0&&(r=this.length+r);for(var i=0,n=this.head;n!==null&&i{"use strict";var mme=YI(),cc=Symbol("max"),ba=Symbol("length"),jg=Symbol("lengthCalculator"),dd=Symbol("allowStale"),uc=Symbol("maxAge"),Qa=Symbol("dispose"),jG=Symbol("noDisposeOnSet"),di=Symbol("lruList"),Vs=Symbol("cache"),JG=Symbol("updateAgeOnGet"),Ev=()=>1,yv=class{constructor(e){if(typeof e=="number"&&(e={max:e}),e||(e={}),e.max&&(typeof e.max!="number"||e.max<0))throw new TypeError("max must be a non-negative number");let t=this[cc]=e.max||1/0,i=e.length||Ev;if(this[jg]=typeof i!="function"?Ev:i,this[dd]=e.stale||!1,e.maxAge&&typeof e.maxAge!="number")throw new TypeError("maxAge must be a number");this[uc]=e.maxAge||0,this[Qa]=e.dispose,this[jG]=e.noDisposeOnSet||!1,this[JG]=e.updateAgeOnGet||!1,this.reset()}set max(e){if(typeof e!="number"||e<0)throw new TypeError("max must be a non-negative number");this[cc]=e||1/0,pd(this)}get max(){return this[cc]}set allowStale(e){this[dd]=!!e}get allowStale(){return this[dd]}set maxAge(e){if(typeof e!="number")throw new TypeError("maxAge must be a non-negative number");this[uc]=e,pd(this)}get maxAge(){return this[uc]}set lengthCalculator(e){typeof e!="function"&&(e=Ev),e!==this[jg]&&(this[jg]=e,this[ba]=0,this[di].forEach(t=>{t.length=this[jg](t.value,t.key),this[ba]+=t.length})),pd(this)}get lengthCalculator(){return this[jg]}get length(){return this[ba]}get itemCount(){return this[di].length}rforEach(e,t){t=t||this;for(let i=this[di].tail;i!==null;){let n=i.prev;qG(this,e,i,t),i=n}}forEach(e,t){t=t||this;for(let i=this[di].head;i!==null;){let n=i.next;qG(this,e,i,t),i=n}}keys(){return this[di].toArray().map(e=>e.key)}values(){return this[di].toArray().map(e=>e.value)}reset(){this[Qa]&&this[di]&&this[di].length&&this[di].forEach(e=>this[Qa](e.key,e.value)),this[Vs]=new Map,this[di]=new mme,this[ba]=0}dump(){return this[di].map(e=>jI(this,e)?!1:{k:e.key,v:e.value,e:e.now+(e.maxAge||0)}).toArray().filter(e=>e)}dumpLru(){return this[di]}set(e,t,i){if(i=i||this[uc],i&&typeof i!="number")throw new TypeError("maxAge must be a number");let n=i?Date.now():0,s=this[jg](t,e);if(this[Vs].has(e)){if(s>this[cc])return qg(this,this[Vs].get(e)),!1;let l=this[Vs].get(e).value;return this[Qa]&&(this[jG]||this[Qa](e,l.value)),l.now=n,l.maxAge=i,l.value=t,this[ba]+=s-l.length,l.length=s,this.get(e),pd(this),!0}let o=new wv(e,t,s,n,i);return o.length>this[cc]?(this[Qa]&&this[Qa](e,t),!1):(this[ba]+=o.length,this[di].unshift(o),this[Vs].set(e,this[di].head),pd(this),!0)}has(e){if(!this[Vs].has(e))return!1;let t=this[Vs].get(e).value;return!jI(this,t)}get(e){return Iv(this,e,!0)}peek(e){return Iv(this,e,!1)}pop(){let e=this[di].tail;return e?(qg(this,e),e.value):null}del(e){qg(this,this[Vs].get(e))}load(e){this.reset();let t=Date.now();for(let i=e.length-1;i>=0;i--){let n=e[i],s=n.e||0;if(s===0)this.set(n.k,n.v);else{let o=s-t;o>0&&this.set(n.k,n.v,o)}}}prune(){this[Vs].forEach((e,t)=>Iv(this,t,!1))}},Iv=(r,e,t)=>{let i=r[Vs].get(e);if(i){let n=i.value;if(jI(r,n)){if(qg(r,i),!r[dd])return}else t&&(r[JG]&&(i.value.now=Date.now()),r[di].unshiftNode(i));return n.value}},jI=(r,e)=>{if(!e||!e.maxAge&&!r[uc])return!1;let t=Date.now()-e.now;return e.maxAge?t>e.maxAge:r[uc]&&t>r[uc]},pd=r=>{if(r[ba]>r[cc])for(let e=r[di].tail;r[ba]>r[cc]&&e!==null;){let t=e.prev;qg(r,e),e=t}},qg=(r,e)=>{if(e){let t=e.value;r[Qa]&&r[Qa](t.key,t.value),r[ba]-=t.length,r[Vs].delete(t.key),r[di].removeNode(e)}},wv=class{constructor(e,t,i,n,s){this.key=e,this.value=t,this.length=i,this.now=n,this.maxAge=s||0}},qG=(r,e,t,i)=>{let n=t.value;jI(r,n)&&(qg(r,t),r[dd]||(n=void 0)),n&&e.call(i,n.value,n.key,r)};WG.exports=yv});var ls=w((N$e,_G)=>{var gc=class{constructor(e,t){if(t=Ime(t),e instanceof gc)return e.loose===!!t.loose&&e.includePrerelease===!!t.includePrerelease?e:new gc(e.raw,t);if(e instanceof Bv)return this.raw=e.value,this.set=[[e]],this.format(),this;if(this.options=t,this.loose=!!t.loose,this.includePrerelease=!!t.includePrerelease,this.raw=e,this.set=e.split(/\s*\|\|\s*/).map(i=>this.parseRange(i.trim())).filter(i=>i.length),!this.set.length)throw new TypeError(`Invalid SemVer Range: ${e}`);if(this.set.length>1){let i=this.set[0];if(this.set=this.set.filter(n=>!XG(n[0])),this.set.length===0)this.set=[i];else if(this.set.length>1){for(let n of this.set)if(n.length===1&&bme(n[0])){this.set=[n];break}}}this.format()}format(){return this.range=this.set.map(e=>e.join(" ").trim()).join("||").trim(),this.range}toString(){return this.range}parseRange(e){e=e.trim();let i=`parseRange:${Object.keys(this.options).join(",")}:${e}`,n=VG.get(i);if(n)return n;let s=this.options.loose,o=s?Oi[Qi.HYPHENRANGELOOSE]:Oi[Qi.HYPHENRANGE];e=e.replace(o,Lme(this.options.includePrerelease)),Gr("hyphen replace",e),e=e.replace(Oi[Qi.COMPARATORTRIM],wme),Gr("comparator trim",e,Oi[Qi.COMPARATORTRIM]),e=e.replace(Oi[Qi.TILDETRIM],Bme),e=e.replace(Oi[Qi.CARETTRIM],Qme),e=e.split(/\s+/).join(" ");let a=s?Oi[Qi.COMPARATORLOOSE]:Oi[Qi.COMPARATOR],l=e.split(" ").map(f=>Sme(f,this.options)).join(" ").split(/\s+/).map(f=>Nme(f,this.options)).filter(this.options.loose?f=>!!f.match(a):()=>!0).map(f=>new Bv(f,this.options)),c=l.length,u=new Map;for(let f of l){if(XG(f))return[f];u.set(f.value,f)}u.size>1&&u.has("")&&u.delete("");let g=[...u.values()];return VG.set(i,g),g}intersects(e,t){if(!(e instanceof gc))throw new TypeError("a Range is required");return this.set.some(i=>ZG(i,t)&&e.set.some(n=>ZG(n,t)&&i.every(s=>n.every(o=>s.intersects(o,t)))))}test(e){if(!e)return!1;if(typeof e=="string")try{e=new yme(e,this.options)}catch{return!1}for(let t=0;tr.value==="<0.0.0-0",bme=r=>r.value==="",ZG=(r,e)=>{let t=!0,i=r.slice(),n=i.pop();for(;t&&i.length;)t=i.every(s=>n.intersects(s,e)),n=i.pop();return t},Sme=(r,e)=>(Gr("comp",r,e),r=Pme(r,e),Gr("caret",r),r=vme(r,e),Gr("tildes",r),r=kme(r,e),Gr("xrange",r),r=Fme(r,e),Gr("stars",r),r),Zi=r=>!r||r.toLowerCase()==="x"||r==="*",vme=(r,e)=>r.trim().split(/\s+/).map(t=>xme(t,e)).join(" "),xme=(r,e)=>{let t=e.loose?Oi[Qi.TILDELOOSE]:Oi[Qi.TILDE];return r.replace(t,(i,n,s,o,a)=>{Gr("tilde",r,i,n,s,o,a);let l;return Zi(n)?l="":Zi(s)?l=`>=${n}.0.0 <${+n+1}.0.0-0`:Zi(o)?l=`>=${n}.${s}.0 <${n}.${+s+1}.0-0`:a?(Gr("replaceTilde pr",a),l=`>=${n}.${s}.${o}-${a} <${n}.${+s+1}.0-0`):l=`>=${n}.${s}.${o} <${n}.${+s+1}.0-0`,Gr("tilde return",l),l})},Pme=(r,e)=>r.trim().split(/\s+/).map(t=>Dme(t,e)).join(" "),Dme=(r,e)=>{Gr("caret",r,e);let t=e.loose?Oi[Qi.CARETLOOSE]:Oi[Qi.CARET],i=e.includePrerelease?"-0":"";return r.replace(t,(n,s,o,a,l)=>{Gr("caret",r,n,s,o,a,l);let c;return Zi(s)?c="":Zi(o)?c=`>=${s}.0.0${i} <${+s+1}.0.0-0`:Zi(a)?s==="0"?c=`>=${s}.${o}.0${i} <${s}.${+o+1}.0-0`:c=`>=${s}.${o}.0${i} <${+s+1}.0.0-0`:l?(Gr("replaceCaret pr",l),s==="0"?o==="0"?c=`>=${s}.${o}.${a}-${l} <${s}.${o}.${+a+1}-0`:c=`>=${s}.${o}.${a}-${l} <${s}.${+o+1}.0-0`:c=`>=${s}.${o}.${a}-${l} <${+s+1}.0.0-0`):(Gr("no pr"),s==="0"?o==="0"?c=`>=${s}.${o}.${a}${i} <${s}.${o}.${+a+1}-0`:c=`>=${s}.${o}.${a}${i} <${s}.${+o+1}.0-0`:c=`>=${s}.${o}.${a} <${+s+1}.0.0-0`),Gr("caret return",c),c})},kme=(r,e)=>(Gr("replaceXRanges",r,e),r.split(/\s+/).map(t=>Rme(t,e)).join(" ")),Rme=(r,e)=>{r=r.trim();let t=e.loose?Oi[Qi.XRANGELOOSE]:Oi[Qi.XRANGE];return r.replace(t,(i,n,s,o,a,l)=>{Gr("xRange",r,i,n,s,o,a,l);let c=Zi(s),u=c||Zi(o),g=u||Zi(a),f=g;return n==="="&&f&&(n=""),l=e.includePrerelease?"-0":"",c?n===">"||n==="<"?i="<0.0.0-0":i="*":n&&f?(u&&(o=0),a=0,n===">"?(n=">=",u?(s=+s+1,o=0,a=0):(o=+o+1,a=0)):n==="<="&&(n="<",u?s=+s+1:o=+o+1),n==="<"&&(l="-0"),i=`${n+s}.${o}.${a}${l}`):u?i=`>=${s}.0.0${l} <${+s+1}.0.0-0`:g&&(i=`>=${s}.${o}.0${l} <${s}.${+o+1}.0-0`),Gr("xRange return",i),i})},Fme=(r,e)=>(Gr("replaceStars",r,e),r.trim().replace(Oi[Qi.STAR],"")),Nme=(r,e)=>(Gr("replaceGTE0",r,e),r.trim().replace(Oi[e.includePrerelease?Qi.GTE0PRE:Qi.GTE0],"")),Lme=r=>(e,t,i,n,s,o,a,l,c,u,g,f,h)=>(Zi(i)?t="":Zi(n)?t=`>=${i}.0.0${r?"-0":""}`:Zi(s)?t=`>=${i}.${n}.0${r?"-0":""}`:o?t=`>=${t}`:t=`>=${t}${r?"-0":""}`,Zi(c)?l="":Zi(u)?l=`<${+c+1}.0.0-0`:Zi(g)?l=`<${c}.${+u+1}.0-0`:f?l=`<=${c}.${u}.${g}-${f}`:r?l=`<${c}.${u}.${+g+1}-0`:l=`<=${l}`,`${t} ${l}`.trim()),Tme=(r,e,t)=>{for(let i=0;i0){let n=r[i].semver;if(n.major===e.major&&n.minor===e.minor&&n.patch===e.patch)return!0}return!1}return!0}});var Cd=w((L$e,iY)=>{var md=Symbol("SemVer ANY"),Jg=class{static get ANY(){return md}constructor(e,t){if(t=Ome(t),e instanceof Jg){if(e.loose===!!t.loose)return e;e=e.value}bv("comparator",e,t),this.options=t,this.loose=!!t.loose,this.parse(e),this.semver===md?this.value="":this.value=this.operator+this.semver.version,bv("comp",this)}parse(e){let t=this.options.loose?$G[eY.COMPARATORLOOSE]:$G[eY.COMPARATOR],i=e.match(t);if(!i)throw new TypeError(`Invalid comparator: ${e}`);this.operator=i[1]!==void 0?i[1]:"",this.operator==="="&&(this.operator=""),i[2]?this.semver=new tY(i[2],this.options.loose):this.semver=md}toString(){return this.value}test(e){if(bv("Comparator.test",e,this.options.loose),this.semver===md||e===md)return!0;if(typeof e=="string")try{e=new tY(e,this.options)}catch{return!1}return Qv(e,this.operator,this.semver,this.options)}intersects(e,t){if(!(e instanceof Jg))throw new TypeError("a Comparator is required");if((!t||typeof t!="object")&&(t={loose:!!t,includePrerelease:!1}),this.operator==="")return this.value===""?!0:new rY(e.value,t).test(this.value);if(e.operator==="")return e.value===""?!0:new rY(this.value,t).test(e.semver);let i=(this.operator===">="||this.operator===">")&&(e.operator===">="||e.operator===">"),n=(this.operator==="<="||this.operator==="<")&&(e.operator==="<="||e.operator==="<"),s=this.semver.version===e.semver.version,o=(this.operator===">="||this.operator==="<=")&&(e.operator===">="||e.operator==="<="),a=Qv(this.semver,"<",e.semver,t)&&(this.operator===">="||this.operator===">")&&(e.operator==="<="||e.operator==="<"),l=Qv(this.semver,">",e.semver,t)&&(this.operator==="<="||this.operator==="<")&&(e.operator===">="||e.operator===">");return i||n||s&&o||a||l}};iY.exports=Jg;var Ome=gd(),{re:$G,t:eY}=ac(),Qv=mv(),bv=ud(),tY=Ti(),rY=ls()});var Ed=w((T$e,nY)=>{var Mme=ls(),Kme=(r,e,t)=>{try{e=new Mme(e,t)}catch{return!1}return e.test(r)};nY.exports=Kme});var oY=w((O$e,sY)=>{var Ume=ls(),Hme=(r,e)=>new Ume(r,e).set.map(t=>t.map(i=>i.value).join(" ").trim().split(" "));sY.exports=Hme});var AY=w((M$e,aY)=>{var Gme=Ti(),Yme=ls(),jme=(r,e,t)=>{let i=null,n=null,s=null;try{s=new Yme(e,t)}catch{return null}return r.forEach(o=>{s.test(o)&&(!i||n.compare(o)===-1)&&(i=o,n=new Gme(i,t))}),i};aY.exports=jme});var cY=w((K$e,lY)=>{var qme=Ti(),Jme=ls(),Wme=(r,e,t)=>{let i=null,n=null,s=null;try{s=new Jme(e,t)}catch{return null}return r.forEach(o=>{s.test(o)&&(!i||n.compare(o)===1)&&(i=o,n=new qme(i,t))}),i};lY.exports=Wme});var fY=w((U$e,gY)=>{var Sv=Ti(),zme=ls(),uY=hd(),Vme=(r,e)=>{r=new zme(r,e);let t=new Sv("0.0.0");if(r.test(t)||(t=new Sv("0.0.0-0"),r.test(t)))return t;t=null;for(let i=0;i{let a=new Sv(o.semver.version);switch(o.operator){case">":a.prerelease.length===0?a.patch++:a.prerelease.push(0),a.raw=a.format();case"":case">=":(!s||uY(a,s))&&(s=a);break;case"<":case"<=":break;default:throw new Error(`Unexpected operation: ${o.operator}`)}}),s&&(!t||uY(t,s))&&(t=s)}return t&&r.test(t)?t:null};gY.exports=Vme});var pY=w((H$e,hY)=>{var Xme=ls(),Zme=(r,e)=>{try{return new Xme(r,e).range||"*"}catch{return null}};hY.exports=Zme});var qI=w((G$e,EY)=>{var _me=Ti(),mY=Cd(),{ANY:$me}=mY,eEe=ls(),tEe=Ed(),dY=hd(),CY=MI(),rEe=UI(),iEe=KI(),nEe=(r,e,t,i)=>{r=new _me(r,i),e=new eEe(e,i);let n,s,o,a,l;switch(t){case">":n=dY,s=rEe,o=CY,a=">",l=">=";break;case"<":n=CY,s=iEe,o=dY,a="<",l="<=";break;default:throw new TypeError('Must provide a hilo val of "<" or ">"')}if(tEe(r,e,i))return!1;for(let c=0;c{h.semver===$me&&(h=new mY(">=0.0.0")),g=g||h,f=f||h,n(h.semver,g.semver,i)?g=h:o(h.semver,f.semver,i)&&(f=h)}),g.operator===a||g.operator===l||(!f.operator||f.operator===a)&&s(r,f.semver))return!1;if(f.operator===l&&o(r,f.semver))return!1}return!0};EY.exports=nEe});var yY=w((Y$e,IY)=>{var sEe=qI(),oEe=(r,e,t)=>sEe(r,e,">",t);IY.exports=oEe});var BY=w((j$e,wY)=>{var aEe=qI(),AEe=(r,e,t)=>aEe(r,e,"<",t);wY.exports=AEe});var SY=w((q$e,bY)=>{var QY=ls(),lEe=(r,e,t)=>(r=new QY(r,t),e=new QY(e,t),r.intersects(e));bY.exports=lEe});var xY=w((J$e,vY)=>{var cEe=Ed(),uEe=As();vY.exports=(r,e,t)=>{let i=[],n=null,s=null,o=r.sort((u,g)=>uEe(u,g,t));for(let u of o)cEe(u,e,t)?(s=u,n||(n=u)):(s&&i.push([n,s]),s=null,n=null);n&&i.push([n,null]);let a=[];for(let[u,g]of i)u===g?a.push(u):!g&&u===o[0]?a.push("*"):g?u===o[0]?a.push(`<=${g}`):a.push(`${u} - ${g}`):a.push(`>=${u}`);let l=a.join(" || "),c=typeof e.raw=="string"?e.raw:String(e);return l.length{var PY=ls(),JI=Cd(),{ANY:vv}=JI,Id=Ed(),xv=As(),gEe=(r,e,t={})=>{if(r===e)return!0;r=new PY(r,t),e=new PY(e,t);let i=!1;e:for(let n of r.set){for(let s of e.set){let o=fEe(n,s,t);if(i=i||o!==null,o)continue e}if(i)return!1}return!0},fEe=(r,e,t)=>{if(r===e)return!0;if(r.length===1&&r[0].semver===vv){if(e.length===1&&e[0].semver===vv)return!0;t.includePrerelease?r=[new JI(">=0.0.0-0")]:r=[new JI(">=0.0.0")]}if(e.length===1&&e[0].semver===vv){if(t.includePrerelease)return!0;e=[new JI(">=0.0.0")]}let i=new Set,n,s;for(let h of r)h.operator===">"||h.operator===">="?n=DY(n,h,t):h.operator==="<"||h.operator==="<="?s=kY(s,h,t):i.add(h.semver);if(i.size>1)return null;let o;if(n&&s){if(o=xv(n.semver,s.semver,t),o>0)return null;if(o===0&&(n.operator!==">="||s.operator!=="<="))return null}for(let h of i){if(n&&!Id(h,String(n),t)||s&&!Id(h,String(s),t))return null;for(let p of e)if(!Id(h,String(p),t))return!1;return!0}let a,l,c,u,g=s&&!t.includePrerelease&&s.semver.prerelease.length?s.semver:!1,f=n&&!t.includePrerelease&&n.semver.prerelease.length?n.semver:!1;g&&g.prerelease.length===1&&s.operator==="<"&&g.prerelease[0]===0&&(g=!1);for(let h of e){if(u=u||h.operator===">"||h.operator===">=",c=c||h.operator==="<"||h.operator==="<=",n){if(f&&h.semver.prerelease&&h.semver.prerelease.length&&h.semver.major===f.major&&h.semver.minor===f.minor&&h.semver.patch===f.patch&&(f=!1),h.operator===">"||h.operator===">="){if(a=DY(n,h,t),a===h&&a!==n)return!1}else if(n.operator===">="&&!Id(n.semver,String(h),t))return!1}if(s){if(g&&h.semver.prerelease&&h.semver.prerelease.length&&h.semver.major===g.major&&h.semver.minor===g.minor&&h.semver.patch===g.patch&&(g=!1),h.operator==="<"||h.operator==="<="){if(l=kY(s,h,t),l===h&&l!==s)return!1}else if(s.operator==="<="&&!Id(s.semver,String(h),t))return!1}if(!h.operator&&(s||n)&&o!==0)return!1}return!(n&&c&&!s&&o!==0||s&&u&&!n&&o!==0||f||g)},DY=(r,e,t)=>{if(!r)return e;let i=xv(r.semver,e.semver,t);return i>0?r:i<0||e.operator===">"&&r.operator===">="?e:r},kY=(r,e,t)=>{if(!r)return e;let i=xv(r.semver,e.semver,t);return i<0?r:i>0||e.operator==="<"&&r.operator==="<="?e:r};RY.exports=gEe});var Xr=w((z$e,NY)=>{var Pv=ac();NY.exports={re:Pv.re,src:Pv.src,tokens:Pv.t,SEMVER_SPEC_VERSION:cd().SEMVER_SPEC_VERSION,SemVer:Ti(),compareIdentifiers:FI().compareIdentifiers,rcompareIdentifiers:FI().rcompareIdentifiers,parse:Ac(),valid:nG(),clean:oG(),inc:AG(),diff:hG(),major:dG(),minor:mG(),patch:IG(),prerelease:wG(),compare:As(),rcompare:QG(),compareLoose:SG(),compareBuild:OI(),sort:DG(),rsort:RG(),gt:hd(),lt:MI(),eq:TI(),neq:Cv(),gte:KI(),lte:UI(),cmp:mv(),coerce:UG(),Comparator:Cd(),Range:ls(),satisfies:Ed(),toComparators:oY(),maxSatisfying:AY(),minSatisfying:cY(),minVersion:fY(),validRange:pY(),outside:qI(),gtr:yY(),ltr:BY(),intersects:SY(),simplifyRange:xY(),subset:FY()}});var Dv=w(WI=>{"use strict";Object.defineProperty(WI,"__esModule",{value:!0});WI.VERSION=void 0;WI.VERSION="9.1.0"});var Gt=w((exports,module)=>{"use strict";var __spreadArray=exports&&exports.__spreadArray||function(r,e,t){if(t||arguments.length===2)for(var i=0,n=e.length,s;i{(function(r,e){typeof define=="function"&&define.amd?define([],e):typeof zI=="object"&&zI.exports?zI.exports=e():r.regexpToAst=e()})(typeof self<"u"?self:LY,function(){function r(){}r.prototype.saveState=function(){return{idx:this.idx,input:this.input,groupIdx:this.groupIdx}},r.prototype.restoreState=function(p){this.idx=p.idx,this.input=p.input,this.groupIdx=p.groupIdx},r.prototype.pattern=function(p){this.idx=0,this.input=p,this.groupIdx=0,this.consumeChar("/");var C=this.disjunction();this.consumeChar("/");for(var y={type:"Flags",loc:{begin:this.idx,end:p.length},global:!1,ignoreCase:!1,multiLine:!1,unicode:!1,sticky:!1};this.isRegExpFlag();)switch(this.popChar()){case"g":o(y,"global");break;case"i":o(y,"ignoreCase");break;case"m":o(y,"multiLine");break;case"u":o(y,"unicode");break;case"y":o(y,"sticky");break}if(this.idx!==this.input.length)throw Error("Redundant input: "+this.input.substring(this.idx));return{type:"Pattern",flags:y,value:C,loc:this.loc(0)}},r.prototype.disjunction=function(){var p=[],C=this.idx;for(p.push(this.alternative());this.peekChar()==="|";)this.consumeChar("|"),p.push(this.alternative());return{type:"Disjunction",value:p,loc:this.loc(C)}},r.prototype.alternative=function(){for(var p=[],C=this.idx;this.isTerm();)p.push(this.term());return{type:"Alternative",value:p,loc:this.loc(C)}},r.prototype.term=function(){return this.isAssertion()?this.assertion():this.atom()},r.prototype.assertion=function(){var p=this.idx;switch(this.popChar()){case"^":return{type:"StartAnchor",loc:this.loc(p)};case"$":return{type:"EndAnchor",loc:this.loc(p)};case"\\":switch(this.popChar()){case"b":return{type:"WordBoundary",loc:this.loc(p)};case"B":return{type:"NonWordBoundary",loc:this.loc(p)}}throw Error("Invalid Assertion Escape");case"(":this.consumeChar("?");var C;switch(this.popChar()){case"=":C="Lookahead";break;case"!":C="NegativeLookahead";break}a(C);var y=this.disjunction();return this.consumeChar(")"),{type:C,value:y,loc:this.loc(p)}}l()},r.prototype.quantifier=function(p){var C,y=this.idx;switch(this.popChar()){case"*":C={atLeast:0,atMost:1/0};break;case"+":C={atLeast:1,atMost:1/0};break;case"?":C={atLeast:0,atMost:1};break;case"{":var B=this.integerIncludingZero();switch(this.popChar()){case"}":C={atLeast:B,atMost:B};break;case",":var v;this.isDigit()?(v=this.integerIncludingZero(),C={atLeast:B,atMost:v}):C={atLeast:B,atMost:1/0},this.consumeChar("}");break}if(p===!0&&C===void 0)return;a(C);break}if(!(p===!0&&C===void 0))return a(C),this.peekChar(0)==="?"?(this.consumeChar("?"),C.greedy=!1):C.greedy=!0,C.type="Quantifier",C.loc=this.loc(y),C},r.prototype.atom=function(){var p,C=this.idx;switch(this.peekChar()){case".":p=this.dotAll();break;case"\\":p=this.atomEscape();break;case"[":p=this.characterClass();break;case"(":p=this.group();break}return p===void 0&&this.isPatternCharacter()&&(p=this.patternCharacter()),a(p),p.loc=this.loc(C),this.isQuantifier()&&(p.quantifier=this.quantifier()),p},r.prototype.dotAll=function(){return this.consumeChar("."),{type:"Set",complement:!0,value:[n(` +`),n("\r"),n("\u2028"),n("\u2029")]}},r.prototype.atomEscape=function(){switch(this.consumeChar("\\"),this.peekChar()){case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":return this.decimalEscapeAtom();case"d":case"D":case"s":case"S":case"w":case"W":return this.characterClassEscape();case"f":case"n":case"r":case"t":case"v":return this.controlEscapeAtom();case"c":return this.controlLetterEscapeAtom();case"0":return this.nulCharacterAtom();case"x":return this.hexEscapeSequenceAtom();case"u":return this.regExpUnicodeEscapeSequenceAtom();default:return this.identityEscapeAtom()}},r.prototype.decimalEscapeAtom=function(){var p=this.positiveInteger();return{type:"GroupBackReference",value:p}},r.prototype.characterClassEscape=function(){var p,C=!1;switch(this.popChar()){case"d":p=u;break;case"D":p=u,C=!0;break;case"s":p=f;break;case"S":p=f,C=!0;break;case"w":p=g;break;case"W":p=g,C=!0;break}return a(p),{type:"Set",value:p,complement:C}},r.prototype.controlEscapeAtom=function(){var p;switch(this.popChar()){case"f":p=n("\f");break;case"n":p=n(` +`);break;case"r":p=n("\r");break;case"t":p=n(" ");break;case"v":p=n("\v");break}return a(p),{type:"Character",value:p}},r.prototype.controlLetterEscapeAtom=function(){this.consumeChar("c");var p=this.popChar();if(/[a-zA-Z]/.test(p)===!1)throw Error("Invalid ");var C=p.toUpperCase().charCodeAt(0)-64;return{type:"Character",value:C}},r.prototype.nulCharacterAtom=function(){return this.consumeChar("0"),{type:"Character",value:n("\0")}},r.prototype.hexEscapeSequenceAtom=function(){return this.consumeChar("x"),this.parseHexDigits(2)},r.prototype.regExpUnicodeEscapeSequenceAtom=function(){return this.consumeChar("u"),this.parseHexDigits(4)},r.prototype.identityEscapeAtom=function(){var p=this.popChar();return{type:"Character",value:n(p)}},r.prototype.classPatternCharacterAtom=function(){switch(this.peekChar()){case` +`:case"\r":case"\u2028":case"\u2029":case"\\":case"]":throw Error("TBD");default:var p=this.popChar();return{type:"Character",value:n(p)}}},r.prototype.characterClass=function(){var p=[],C=!1;for(this.consumeChar("["),this.peekChar(0)==="^"&&(this.consumeChar("^"),C=!0);this.isClassAtom();){var y=this.classAtom(),B=y.type==="Character";if(B&&this.isRangeDash()){this.consumeChar("-");var v=this.classAtom(),D=v.type==="Character";if(D){if(v.value=this.input.length)throw Error("Unexpected end of input");this.idx++},r.prototype.loc=function(p){return{begin:p,end:this.idx}};var e=/[0-9a-fA-F]/,t=/[0-9]/,i=/[1-9]/;function n(p){return p.charCodeAt(0)}function s(p,C){p.length!==void 0?p.forEach(function(y){C.push(y)}):C.push(p)}function o(p,C){if(p[C]===!0)throw"duplicate flag "+C;p[C]=!0}function a(p){if(p===void 0)throw Error("Internal Error - Should never get here!")}function l(){throw Error("Internal Error - Should never get here!")}var c,u=[];for(c=n("0");c<=n("9");c++)u.push(c);var g=[n("_")].concat(u);for(c=n("a");c<=n("z");c++)g.push(c);for(c=n("A");c<=n("Z");c++)g.push(c);var f=[n(" "),n("\f"),n(` +`),n("\r"),n(" "),n("\v"),n(" "),n("\xA0"),n("\u1680"),n("\u2000"),n("\u2001"),n("\u2002"),n("\u2003"),n("\u2004"),n("\u2005"),n("\u2006"),n("\u2007"),n("\u2008"),n("\u2009"),n("\u200A"),n("\u2028"),n("\u2029"),n("\u202F"),n("\u205F"),n("\u3000"),n("\uFEFF")];function h(){}return h.prototype.visitChildren=function(p){for(var C in p){var y=p[C];p.hasOwnProperty(C)&&(y.type!==void 0?this.visit(y):Array.isArray(y)&&y.forEach(function(B){this.visit(B)},this))}},h.prototype.visit=function(p){switch(p.type){case"Pattern":this.visitPattern(p);break;case"Flags":this.visitFlags(p);break;case"Disjunction":this.visitDisjunction(p);break;case"Alternative":this.visitAlternative(p);break;case"StartAnchor":this.visitStartAnchor(p);break;case"EndAnchor":this.visitEndAnchor(p);break;case"WordBoundary":this.visitWordBoundary(p);break;case"NonWordBoundary":this.visitNonWordBoundary(p);break;case"Lookahead":this.visitLookahead(p);break;case"NegativeLookahead":this.visitNegativeLookahead(p);break;case"Character":this.visitCharacter(p);break;case"Set":this.visitSet(p);break;case"Group":this.visitGroup(p);break;case"GroupBackReference":this.visitGroupBackReference(p);break;case"Quantifier":this.visitQuantifier(p);break}this.visitChildren(p)},h.prototype.visitPattern=function(p){},h.prototype.visitFlags=function(p){},h.prototype.visitDisjunction=function(p){},h.prototype.visitAlternative=function(p){},h.prototype.visitStartAnchor=function(p){},h.prototype.visitEndAnchor=function(p){},h.prototype.visitWordBoundary=function(p){},h.prototype.visitNonWordBoundary=function(p){},h.prototype.visitLookahead=function(p){},h.prototype.visitNegativeLookahead=function(p){},h.prototype.visitCharacter=function(p){},h.prototype.visitSet=function(p){},h.prototype.visitGroup=function(p){},h.prototype.visitGroupBackReference=function(p){},h.prototype.visitQuantifier=function(p){},{RegExpParser:r,BaseRegExpVisitor:h,VERSION:"0.5.0"}})});var ZI=w(Wg=>{"use strict";Object.defineProperty(Wg,"__esModule",{value:!0});Wg.clearRegExpParserCache=Wg.getRegExpAst=void 0;var hEe=VI(),XI={},pEe=new hEe.RegExpParser;function dEe(r){var e=r.toString();if(XI.hasOwnProperty(e))return XI[e];var t=pEe.pattern(e);return XI[e]=t,t}Wg.getRegExpAst=dEe;function CEe(){XI={}}Wg.clearRegExpParserCache=CEe});var UY=w(pn=>{"use strict";var mEe=pn&&pn.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(pn,"__esModule",{value:!0});pn.canMatchCharCode=pn.firstCharOptimizedIndices=pn.getOptimizedStartCodesIndices=pn.failedOptimizationPrefixMsg=void 0;var OY=VI(),cs=Gt(),MY=ZI(),Sa=Rv(),KY="Complement Sets are not supported for first char optimization";pn.failedOptimizationPrefixMsg=`Unable to use "first char" lexer optimizations: +`;function EEe(r,e){e===void 0&&(e=!1);try{var t=(0,MY.getRegExpAst)(r),i=$I(t.value,{},t.flags.ignoreCase);return i}catch(s){if(s.message===KY)e&&(0,cs.PRINT_WARNING)(""+pn.failedOptimizationPrefixMsg+(" Unable to optimize: < "+r.toString()+` > +`)+` Complement Sets cannot be automatically optimized. + This will disable the lexer's first char optimizations. + See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#COMPLEMENT for details.`);else{var n="";e&&(n=` + This will disable the lexer's first char optimizations. + See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#REGEXP_PARSING for details.`),(0,cs.PRINT_ERROR)(pn.failedOptimizationPrefixMsg+` +`+(" Failed parsing: < "+r.toString()+` > +`)+(" Using the regexp-to-ast library version: "+OY.VERSION+` +`)+" Please open an issue at: https://github.com/bd82/regexp-to-ast/issues"+n)}}return[]}pn.getOptimizedStartCodesIndices=EEe;function $I(r,e,t){switch(r.type){case"Disjunction":for(var i=0;i=Sa.minOptimizationVal)for(var f=u.from>=Sa.minOptimizationVal?u.from:Sa.minOptimizationVal,h=u.to,p=(0,Sa.charCodeToOptimizedIndex)(f),C=(0,Sa.charCodeToOptimizedIndex)(h),y=p;y<=C;y++)e[y]=y}}});break;case"Group":$I(o.value,e,t);break;default:throw Error("Non Exhaustive Match")}var a=o.quantifier!==void 0&&o.quantifier.atLeast===0;if(o.type==="Group"&&kv(o)===!1||o.type!=="Group"&&a===!1)break}break;default:throw Error("non exhaustive match!")}return(0,cs.values)(e)}pn.firstCharOptimizedIndices=$I;function _I(r,e,t){var i=(0,Sa.charCodeToOptimizedIndex)(r);e[i]=i,t===!0&&IEe(r,e)}function IEe(r,e){var t=String.fromCharCode(r),i=t.toUpperCase();if(i!==t){var n=(0,Sa.charCodeToOptimizedIndex)(i.charCodeAt(0));e[n]=n}else{var s=t.toLowerCase();if(s!==t){var n=(0,Sa.charCodeToOptimizedIndex)(s.charCodeAt(0));e[n]=n}}}function TY(r,e){return(0,cs.find)(r.value,function(t){if(typeof t=="number")return(0,cs.contains)(e,t);var i=t;return(0,cs.find)(e,function(n){return i.from<=n&&n<=i.to})!==void 0})}function kv(r){return r.quantifier&&r.quantifier.atLeast===0?!0:r.value?(0,cs.isArray)(r.value)?(0,cs.every)(r.value,kv):kv(r.value):!1}var yEe=function(r){mEe(e,r);function e(t){var i=r.call(this)||this;return i.targetCharCodes=t,i.found=!1,i}return e.prototype.visitChildren=function(t){if(this.found!==!0){switch(t.type){case"Lookahead":this.visitLookahead(t);return;case"NegativeLookahead":this.visitNegativeLookahead(t);return}r.prototype.visitChildren.call(this,t)}},e.prototype.visitCharacter=function(t){(0,cs.contains)(this.targetCharCodes,t.value)&&(this.found=!0)},e.prototype.visitSet=function(t){t.complement?TY(t,this.targetCharCodes)===void 0&&(this.found=!0):TY(t,this.targetCharCodes)!==void 0&&(this.found=!0)},e}(OY.BaseRegExpVisitor);function wEe(r,e){if(e instanceof RegExp){var t=(0,MY.getRegExpAst)(e),i=new yEe(r);return i.visit(t),i.found}else return(0,cs.find)(e,function(n){return(0,cs.contains)(r,n.charCodeAt(0))})!==void 0}pn.canMatchCharCode=wEe});var Rv=w(Ve=>{"use strict";var HY=Ve&&Ve.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Ve,"__esModule",{value:!0});Ve.charCodeToOptimizedIndex=Ve.minOptimizationVal=Ve.buildLineBreakIssueMessage=Ve.LineTerminatorOptimizedTester=Ve.isShortPattern=Ve.isCustomPattern=Ve.cloneEmptyGroups=Ve.performWarningRuntimeChecks=Ve.performRuntimeChecks=Ve.addStickyFlag=Ve.addStartOfInput=Ve.findUnreachablePatterns=Ve.findModesThatDoNotExist=Ve.findInvalidGroupType=Ve.findDuplicatePatterns=Ve.findUnsupportedFlags=Ve.findStartOfInputAnchor=Ve.findEmptyMatchRegExps=Ve.findEndOfInputAnchor=Ve.findInvalidPatterns=Ve.findMissingPatterns=Ve.validatePatterns=Ve.analyzeTokenTypes=Ve.enableSticky=Ve.disableSticky=Ve.SUPPORT_STICKY=Ve.MODES=Ve.DEFAULT_MODE=void 0;var GY=VI(),ir=yd(),xe=Gt(),zg=UY(),YY=ZI(),Do="PATTERN";Ve.DEFAULT_MODE="defaultMode";Ve.MODES="modes";Ve.SUPPORT_STICKY=typeof new RegExp("(?:)").sticky=="boolean";function BEe(){Ve.SUPPORT_STICKY=!1}Ve.disableSticky=BEe;function QEe(){Ve.SUPPORT_STICKY=!0}Ve.enableSticky=QEe;function bEe(r,e){e=(0,xe.defaults)(e,{useSticky:Ve.SUPPORT_STICKY,debug:!1,safeMode:!1,positionTracking:"full",lineTerminatorCharacters:["\r",` +`],tracer:function(v,D){return D()}});var t=e.tracer;t("initCharCodeToOptimizedIndexMap",function(){LEe()});var i;t("Reject Lexer.NA",function(){i=(0,xe.reject)(r,function(v){return v[Do]===ir.Lexer.NA})});var n=!1,s;t("Transform Patterns",function(){n=!1,s=(0,xe.map)(i,function(v){var D=v[Do];if((0,xe.isRegExp)(D)){var L=D.source;return L.length===1&&L!=="^"&&L!=="$"&&L!=="."&&!D.ignoreCase?L:L.length===2&&L[0]==="\\"&&!(0,xe.contains)(["d","D","s","S","t","r","n","t","0","c","b","B","f","v","w","W"],L[1])?L[1]:e.useSticky?Lv(D):Nv(D)}else{if((0,xe.isFunction)(D))return n=!0,{exec:D};if((0,xe.has)(D,"exec"))return n=!0,D;if(typeof D=="string"){if(D.length===1)return D;var H=D.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&"),j=new RegExp(H);return e.useSticky?Lv(j):Nv(j)}else throw Error("non exhaustive match")}})});var o,a,l,c,u;t("misc mapping",function(){o=(0,xe.map)(i,function(v){return v.tokenTypeIdx}),a=(0,xe.map)(i,function(v){var D=v.GROUP;if(D!==ir.Lexer.SKIPPED){if((0,xe.isString)(D))return D;if((0,xe.isUndefined)(D))return!1;throw Error("non exhaustive match")}}),l=(0,xe.map)(i,function(v){var D=v.LONGER_ALT;if(D){var L=(0,xe.isArray)(D)?(0,xe.map)(D,function(H){return(0,xe.indexOf)(i,H)}):[(0,xe.indexOf)(i,D)];return L}}),c=(0,xe.map)(i,function(v){return v.PUSH_MODE}),u=(0,xe.map)(i,function(v){return(0,xe.has)(v,"POP_MODE")})});var g;t("Line Terminator Handling",function(){var v=ij(e.lineTerminatorCharacters);g=(0,xe.map)(i,function(D){return!1}),e.positionTracking!=="onlyOffset"&&(g=(0,xe.map)(i,function(D){if((0,xe.has)(D,"LINE_BREAKS"))return D.LINE_BREAKS;if(tj(D,v)===!1)return(0,zg.canMatchCharCode)(v,D.PATTERN)}))});var f,h,p,C;t("Misc Mapping #2",function(){f=(0,xe.map)(i,Ov),h=(0,xe.map)(s,ej),p=(0,xe.reduce)(i,function(v,D){var L=D.GROUP;return(0,xe.isString)(L)&&L!==ir.Lexer.SKIPPED&&(v[L]=[]),v},{}),C=(0,xe.map)(s,function(v,D){return{pattern:s[D],longerAlt:l[D],canLineTerminator:g[D],isCustom:f[D],short:h[D],group:a[D],push:c[D],pop:u[D],tokenTypeIdx:o[D],tokenType:i[D]}})});var y=!0,B=[];return e.safeMode||t("First Char Optimization",function(){B=(0,xe.reduce)(i,function(v,D,L){if(typeof D.PATTERN=="string"){var H=D.PATTERN.charCodeAt(0),j=Tv(H);Fv(v,j,C[L])}else if((0,xe.isArray)(D.START_CHARS_HINT)){var $;(0,xe.forEach)(D.START_CHARS_HINT,function(W){var _=typeof W=="string"?W.charCodeAt(0):W,A=Tv(_);$!==A&&($=A,Fv(v,A,C[L]))})}else if((0,xe.isRegExp)(D.PATTERN))if(D.PATTERN.unicode)y=!1,e.ensureOptimizations&&(0,xe.PRINT_ERROR)(""+zg.failedOptimizationPrefixMsg+(" Unable to analyze < "+D.PATTERN.toString()+` > pattern. +`)+` The regexp unicode flag is not currently supported by the regexp-to-ast library. + This will disable the lexer's first char optimizations. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNICODE_OPTIMIZE`);else{var V=(0,zg.getOptimizedStartCodesIndices)(D.PATTERN,e.ensureOptimizations);(0,xe.isEmpty)(V)&&(y=!1),(0,xe.forEach)(V,function(W){Fv(v,W,C[L])})}else e.ensureOptimizations&&(0,xe.PRINT_ERROR)(""+zg.failedOptimizationPrefixMsg+(" TokenType: <"+D.name+`> is using a custom token pattern without providing parameter. +`)+` This will disable the lexer's first char optimizations. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_OPTIMIZE`),y=!1;return v},[])}),t("ArrayPacking",function(){B=(0,xe.packArray)(B)}),{emptyGroups:p,patternIdxToConfig:C,charCodeToPatternIdxToConfig:B,hasCustom:n,canBeOptimized:y}}Ve.analyzeTokenTypes=bEe;function SEe(r,e){var t=[],i=jY(r);t=t.concat(i.errors);var n=qY(i.valid),s=n.valid;return t=t.concat(n.errors),t=t.concat(vEe(s)),t=t.concat(ZY(s)),t=t.concat(_Y(s,e)),t=t.concat($Y(s)),t}Ve.validatePatterns=SEe;function vEe(r){var e=[],t=(0,xe.filter)(r,function(i){return(0,xe.isRegExp)(i[Do])});return e=e.concat(JY(t)),e=e.concat(zY(t)),e=e.concat(VY(t)),e=e.concat(XY(t)),e=e.concat(WY(t)),e}function jY(r){var e=(0,xe.filter)(r,function(n){return!(0,xe.has)(n,Do)}),t=(0,xe.map)(e,function(n){return{message:"Token Type: ->"+n.name+"<- missing static 'PATTERN' property",type:ir.LexerDefinitionErrorType.MISSING_PATTERN,tokenTypes:[n]}}),i=(0,xe.difference)(r,e);return{errors:t,valid:i}}Ve.findMissingPatterns=jY;function qY(r){var e=(0,xe.filter)(r,function(n){var s=n[Do];return!(0,xe.isRegExp)(s)&&!(0,xe.isFunction)(s)&&!(0,xe.has)(s,"exec")&&!(0,xe.isString)(s)}),t=(0,xe.map)(e,function(n){return{message:"Token Type: ->"+n.name+"<- static 'PATTERN' can only be a RegExp, a Function matching the {CustomPatternMatcherFunc} type or an Object matching the {ICustomPattern} interface.",type:ir.LexerDefinitionErrorType.INVALID_PATTERN,tokenTypes:[n]}}),i=(0,xe.difference)(r,e);return{errors:t,valid:i}}Ve.findInvalidPatterns=qY;var xEe=/[^\\][\$]/;function JY(r){var e=function(n){HY(s,n);function s(){var o=n!==null&&n.apply(this,arguments)||this;return o.found=!1,o}return s.prototype.visitEndAnchor=function(o){this.found=!0},s}(GY.BaseRegExpVisitor),t=(0,xe.filter)(r,function(n){var s=n[Do];try{var o=(0,YY.getRegExpAst)(s),a=new e;return a.visit(o),a.found}catch{return xEe.test(s.source)}}),i=(0,xe.map)(t,function(n){return{message:`Unexpected RegExp Anchor Error: + Token Type: ->`+n.name+`<- static 'PATTERN' cannot contain end of input anchor '$' + See chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:ir.LexerDefinitionErrorType.EOI_ANCHOR_FOUND,tokenTypes:[n]}});return i}Ve.findEndOfInputAnchor=JY;function WY(r){var e=(0,xe.filter)(r,function(i){var n=i[Do];return n.test("")}),t=(0,xe.map)(e,function(i){return{message:"Token Type: ->"+i.name+"<- static 'PATTERN' must not match an empty string",type:ir.LexerDefinitionErrorType.EMPTY_MATCH_PATTERN,tokenTypes:[i]}});return t}Ve.findEmptyMatchRegExps=WY;var PEe=/[^\\[][\^]|^\^/;function zY(r){var e=function(n){HY(s,n);function s(){var o=n!==null&&n.apply(this,arguments)||this;return o.found=!1,o}return s.prototype.visitStartAnchor=function(o){this.found=!0},s}(GY.BaseRegExpVisitor),t=(0,xe.filter)(r,function(n){var s=n[Do];try{var o=(0,YY.getRegExpAst)(s),a=new e;return a.visit(o),a.found}catch{return PEe.test(s.source)}}),i=(0,xe.map)(t,function(n){return{message:`Unexpected RegExp Anchor Error: + Token Type: ->`+n.name+`<- static 'PATTERN' cannot contain start of input anchor '^' + See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:ir.LexerDefinitionErrorType.SOI_ANCHOR_FOUND,tokenTypes:[n]}});return i}Ve.findStartOfInputAnchor=zY;function VY(r){var e=(0,xe.filter)(r,function(i){var n=i[Do];return n instanceof RegExp&&(n.multiline||n.global)}),t=(0,xe.map)(e,function(i){return{message:"Token Type: ->"+i.name+"<- static 'PATTERN' may NOT contain global('g') or multiline('m')",type:ir.LexerDefinitionErrorType.UNSUPPORTED_FLAGS_FOUND,tokenTypes:[i]}});return t}Ve.findUnsupportedFlags=VY;function XY(r){var e=[],t=(0,xe.map)(r,function(s){return(0,xe.reduce)(r,function(o,a){return s.PATTERN.source===a.PATTERN.source&&!(0,xe.contains)(e,a)&&a.PATTERN!==ir.Lexer.NA&&(e.push(a),o.push(a)),o},[])});t=(0,xe.compact)(t);var i=(0,xe.filter)(t,function(s){return s.length>1}),n=(0,xe.map)(i,function(s){var o=(0,xe.map)(s,function(l){return l.name}),a=(0,xe.first)(s).PATTERN;return{message:"The same RegExp pattern ->"+a+"<-"+("has been used in all of the following Token Types: "+o.join(", ")+" <-"),type:ir.LexerDefinitionErrorType.DUPLICATE_PATTERNS_FOUND,tokenTypes:s}});return n}Ve.findDuplicatePatterns=XY;function ZY(r){var e=(0,xe.filter)(r,function(i){if(!(0,xe.has)(i,"GROUP"))return!1;var n=i.GROUP;return n!==ir.Lexer.SKIPPED&&n!==ir.Lexer.NA&&!(0,xe.isString)(n)}),t=(0,xe.map)(e,function(i){return{message:"Token Type: ->"+i.name+"<- static 'GROUP' can only be Lexer.SKIPPED/Lexer.NA/A String",type:ir.LexerDefinitionErrorType.INVALID_GROUP_TYPE_FOUND,tokenTypes:[i]}});return t}Ve.findInvalidGroupType=ZY;function _Y(r,e){var t=(0,xe.filter)(r,function(n){return n.PUSH_MODE!==void 0&&!(0,xe.contains)(e,n.PUSH_MODE)}),i=(0,xe.map)(t,function(n){var s="Token Type: ->"+n.name+"<- static 'PUSH_MODE' value cannot refer to a Lexer Mode ->"+n.PUSH_MODE+"<-which does not exist";return{message:s,type:ir.LexerDefinitionErrorType.PUSH_MODE_DOES_NOT_EXIST,tokenTypes:[n]}});return i}Ve.findModesThatDoNotExist=_Y;function $Y(r){var e=[],t=(0,xe.reduce)(r,function(i,n,s){var o=n.PATTERN;return o===ir.Lexer.NA||((0,xe.isString)(o)?i.push({str:o,idx:s,tokenType:n}):(0,xe.isRegExp)(o)&&kEe(o)&&i.push({str:o.source,idx:s,tokenType:n})),i},[]);return(0,xe.forEach)(r,function(i,n){(0,xe.forEach)(t,function(s){var o=s.str,a=s.idx,l=s.tokenType;if(n"+i.name+"<-")+`in the lexer's definition. +See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNREACHABLE`;e.push({message:c,type:ir.LexerDefinitionErrorType.UNREACHABLE_PATTERN,tokenTypes:[i,l]})}})}),e}Ve.findUnreachablePatterns=$Y;function DEe(r,e){if((0,xe.isRegExp)(e)){var t=e.exec(r);return t!==null&&t.index===0}else{if((0,xe.isFunction)(e))return e(r,0,[],{});if((0,xe.has)(e,"exec"))return e.exec(r,0,[],{});if(typeof e=="string")return e===r;throw Error("non exhaustive match")}}function kEe(r){var e=[".","\\","[","]","|","^","$","(",")","?","*","+","{"];return(0,xe.find)(e,function(t){return r.source.indexOf(t)!==-1})===void 0}function Nv(r){var e=r.ignoreCase?"i":"";return new RegExp("^(?:"+r.source+")",e)}Ve.addStartOfInput=Nv;function Lv(r){var e=r.ignoreCase?"iy":"y";return new RegExp(""+r.source,e)}Ve.addStickyFlag=Lv;function REe(r,e,t){var i=[];return(0,xe.has)(r,Ve.DEFAULT_MODE)||i.push({message:"A MultiMode Lexer cannot be initialized without a <"+Ve.DEFAULT_MODE+`> property in its definition +`,type:ir.LexerDefinitionErrorType.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE}),(0,xe.has)(r,Ve.MODES)||i.push({message:"A MultiMode Lexer cannot be initialized without a <"+Ve.MODES+`> property in its definition +`,type:ir.LexerDefinitionErrorType.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY}),(0,xe.has)(r,Ve.MODES)&&(0,xe.has)(r,Ve.DEFAULT_MODE)&&!(0,xe.has)(r.modes,r.defaultMode)&&i.push({message:"A MultiMode Lexer cannot be initialized with a "+Ve.DEFAULT_MODE+": <"+r.defaultMode+`>which does not exist +`,type:ir.LexerDefinitionErrorType.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST}),(0,xe.has)(r,Ve.MODES)&&(0,xe.forEach)(r.modes,function(n,s){(0,xe.forEach)(n,function(o,a){(0,xe.isUndefined)(o)&&i.push({message:"A Lexer cannot be initialized using an undefined Token Type. Mode:"+("<"+s+"> at index: <"+a+`> +`),type:ir.LexerDefinitionErrorType.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED})})}),i}Ve.performRuntimeChecks=REe;function FEe(r,e,t){var i=[],n=!1,s=(0,xe.compact)((0,xe.flatten)((0,xe.mapValues)(r.modes,function(l){return l}))),o=(0,xe.reject)(s,function(l){return l[Do]===ir.Lexer.NA}),a=ij(t);return e&&(0,xe.forEach)(o,function(l){var c=tj(l,a);if(c!==!1){var u=rj(l,c),g={message:u,type:c.issue,tokenType:l};i.push(g)}else(0,xe.has)(l,"LINE_BREAKS")?l.LINE_BREAKS===!0&&(n=!0):(0,zg.canMatchCharCode)(a,l.PATTERN)&&(n=!0)}),e&&!n&&i.push({message:`Warning: No LINE_BREAKS Found. + This Lexer has been defined to track line and column information, + But none of the Token Types can be identified as matching a line terminator. + See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#LINE_BREAKS + for details.`,type:ir.LexerDefinitionErrorType.NO_LINE_BREAKS_FLAGS}),i}Ve.performWarningRuntimeChecks=FEe;function NEe(r){var e={},t=(0,xe.keys)(r);return(0,xe.forEach)(t,function(i){var n=r[i];if((0,xe.isArray)(n))e[i]=[];else throw Error("non exhaustive match")}),e}Ve.cloneEmptyGroups=NEe;function Ov(r){var e=r.PATTERN;if((0,xe.isRegExp)(e))return!1;if((0,xe.isFunction)(e))return!0;if((0,xe.has)(e,"exec"))return!0;if((0,xe.isString)(e))return!1;throw Error("non exhaustive match")}Ve.isCustomPattern=Ov;function ej(r){return(0,xe.isString)(r)&&r.length===1?r.charCodeAt(0):!1}Ve.isShortPattern=ej;Ve.LineTerminatorOptimizedTester={test:function(r){for(var e=r.length,t=this.lastIndex;t Token Type +`)+(" Root cause: "+e.errMsg+`. +`)+" For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#IDENTIFY_TERMINATOR";if(e.issue===ir.LexerDefinitionErrorType.CUSTOM_LINE_BREAK)return`Warning: A Custom Token Pattern should specify the option. +`+(" The problem is in the <"+r.name+`> Token Type +`)+" For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_LINE_BREAK";throw Error("non exhaustive match")}Ve.buildLineBreakIssueMessage=rj;function ij(r){var e=(0,xe.map)(r,function(t){return(0,xe.isString)(t)&&t.length>0?t.charCodeAt(0):t});return e}function Fv(r,e,t){r[e]===void 0?r[e]=[t]:r[e].push(t)}Ve.minOptimizationVal=256;var ey=[];function Tv(r){return r255?255+~~(r/255):r}}});var Vg=w(Nt=>{"use strict";Object.defineProperty(Nt,"__esModule",{value:!0});Nt.isTokenType=Nt.hasExtendingTokensTypesMapProperty=Nt.hasExtendingTokensTypesProperty=Nt.hasCategoriesProperty=Nt.hasShortKeyProperty=Nt.singleAssignCategoriesToksMap=Nt.assignCategoriesMapProp=Nt.assignCategoriesTokensProp=Nt.assignTokenDefaultProps=Nt.expandCategories=Nt.augmentTokenTypes=Nt.tokenIdxToClass=Nt.tokenShortNameIdx=Nt.tokenStructuredMatcherNoCategories=Nt.tokenStructuredMatcher=void 0;var Zr=Gt();function TEe(r,e){var t=r.tokenTypeIdx;return t===e.tokenTypeIdx?!0:e.isParent===!0&&e.categoryMatchesMap[t]===!0}Nt.tokenStructuredMatcher=TEe;function OEe(r,e){return r.tokenTypeIdx===e.tokenTypeIdx}Nt.tokenStructuredMatcherNoCategories=OEe;Nt.tokenShortNameIdx=1;Nt.tokenIdxToClass={};function MEe(r){var e=nj(r);sj(e),aj(e),oj(e),(0,Zr.forEach)(e,function(t){t.isParent=t.categoryMatches.length>0})}Nt.augmentTokenTypes=MEe;function nj(r){for(var e=(0,Zr.cloneArr)(r),t=r,i=!0;i;){t=(0,Zr.compact)((0,Zr.flatten)((0,Zr.map)(t,function(s){return s.CATEGORIES})));var n=(0,Zr.difference)(t,e);e=e.concat(n),(0,Zr.isEmpty)(n)?i=!1:t=n}return e}Nt.expandCategories=nj;function sj(r){(0,Zr.forEach)(r,function(e){Aj(e)||(Nt.tokenIdxToClass[Nt.tokenShortNameIdx]=e,e.tokenTypeIdx=Nt.tokenShortNameIdx++),Mv(e)&&!(0,Zr.isArray)(e.CATEGORIES)&&(e.CATEGORIES=[e.CATEGORIES]),Mv(e)||(e.CATEGORIES=[]),lj(e)||(e.categoryMatches=[]),cj(e)||(e.categoryMatchesMap={})})}Nt.assignTokenDefaultProps=sj;function oj(r){(0,Zr.forEach)(r,function(e){e.categoryMatches=[],(0,Zr.forEach)(e.categoryMatchesMap,function(t,i){e.categoryMatches.push(Nt.tokenIdxToClass[i].tokenTypeIdx)})})}Nt.assignCategoriesTokensProp=oj;function aj(r){(0,Zr.forEach)(r,function(e){Kv([],e)})}Nt.assignCategoriesMapProp=aj;function Kv(r,e){(0,Zr.forEach)(r,function(t){e.categoryMatchesMap[t.tokenTypeIdx]=!0}),(0,Zr.forEach)(e.CATEGORIES,function(t){var i=r.concat(e);(0,Zr.contains)(i,t)||Kv(i,t)})}Nt.singleAssignCategoriesToksMap=Kv;function Aj(r){return(0,Zr.has)(r,"tokenTypeIdx")}Nt.hasShortKeyProperty=Aj;function Mv(r){return(0,Zr.has)(r,"CATEGORIES")}Nt.hasCategoriesProperty=Mv;function lj(r){return(0,Zr.has)(r,"categoryMatches")}Nt.hasExtendingTokensTypesProperty=lj;function cj(r){return(0,Zr.has)(r,"categoryMatchesMap")}Nt.hasExtendingTokensTypesMapProperty=cj;function KEe(r){return(0,Zr.has)(r,"tokenTypeIdx")}Nt.isTokenType=KEe});var Uv=w(ty=>{"use strict";Object.defineProperty(ty,"__esModule",{value:!0});ty.defaultLexerErrorProvider=void 0;ty.defaultLexerErrorProvider={buildUnableToPopLexerModeMessage:function(r){return"Unable to pop Lexer Mode after encountering Token ->"+r.image+"<- The Mode Stack is empty"},buildUnexpectedCharactersMessage:function(r,e,t,i,n){return"unexpected character: ->"+r.charAt(e)+"<- at offset: "+e+","+(" skipped "+t+" characters.")}}});var yd=w(fc=>{"use strict";Object.defineProperty(fc,"__esModule",{value:!0});fc.Lexer=fc.LexerDefinitionErrorType=void 0;var Xs=Rv(),nr=Gt(),UEe=Vg(),HEe=Uv(),GEe=ZI(),YEe;(function(r){r[r.MISSING_PATTERN=0]="MISSING_PATTERN",r[r.INVALID_PATTERN=1]="INVALID_PATTERN",r[r.EOI_ANCHOR_FOUND=2]="EOI_ANCHOR_FOUND",r[r.UNSUPPORTED_FLAGS_FOUND=3]="UNSUPPORTED_FLAGS_FOUND",r[r.DUPLICATE_PATTERNS_FOUND=4]="DUPLICATE_PATTERNS_FOUND",r[r.INVALID_GROUP_TYPE_FOUND=5]="INVALID_GROUP_TYPE_FOUND",r[r.PUSH_MODE_DOES_NOT_EXIST=6]="PUSH_MODE_DOES_NOT_EXIST",r[r.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE=7]="MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE",r[r.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY=8]="MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY",r[r.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST=9]="MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST",r[r.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED=10]="LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED",r[r.SOI_ANCHOR_FOUND=11]="SOI_ANCHOR_FOUND",r[r.EMPTY_MATCH_PATTERN=12]="EMPTY_MATCH_PATTERN",r[r.NO_LINE_BREAKS_FLAGS=13]="NO_LINE_BREAKS_FLAGS",r[r.UNREACHABLE_PATTERN=14]="UNREACHABLE_PATTERN",r[r.IDENTIFY_TERMINATOR=15]="IDENTIFY_TERMINATOR",r[r.CUSTOM_LINE_BREAK=16]="CUSTOM_LINE_BREAK"})(YEe=fc.LexerDefinitionErrorType||(fc.LexerDefinitionErrorType={}));var wd={deferDefinitionErrorsHandling:!1,positionTracking:"full",lineTerminatorsPattern:/\n|\r\n?/g,lineTerminatorCharacters:[` +`,"\r"],ensureOptimizations:!1,safeMode:!1,errorMessageProvider:HEe.defaultLexerErrorProvider,traceInitPerf:!1,skipValidations:!1};Object.freeze(wd);var jEe=function(){function r(e,t){var i=this;if(t===void 0&&(t=wd),this.lexerDefinition=e,this.lexerDefinitionErrors=[],this.lexerDefinitionWarning=[],this.patternIdxToConfig={},this.charCodeToPatternIdxToConfig={},this.modes=[],this.emptyGroups={},this.config=void 0,this.trackStartLines=!0,this.trackEndLines=!0,this.hasCustom=!1,this.canModeBeOptimized={},typeof t=="boolean")throw Error(`The second argument to the Lexer constructor is now an ILexerConfig Object. +a boolean 2nd argument is no longer supported`);this.config=(0,nr.merge)(wd,t);var n=this.config.traceInitPerf;n===!0?(this.traceInitMaxIdent=1/0,this.traceInitPerf=!0):typeof n=="number"&&(this.traceInitMaxIdent=n,this.traceInitPerf=!0),this.traceInitIndent=-1,this.TRACE_INIT("Lexer Constructor",function(){var s,o=!0;i.TRACE_INIT("Lexer Config handling",function(){if(i.config.lineTerminatorsPattern===wd.lineTerminatorsPattern)i.config.lineTerminatorsPattern=Xs.LineTerminatorOptimizedTester;else if(i.config.lineTerminatorCharacters===wd.lineTerminatorCharacters)throw Error(`Error: Missing property on the Lexer config. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#MISSING_LINE_TERM_CHARS`);if(t.safeMode&&t.ensureOptimizations)throw Error('"safeMode" and "ensureOptimizations" flags are mutually exclusive.');i.trackStartLines=/full|onlyStart/i.test(i.config.positionTracking),i.trackEndLines=/full/i.test(i.config.positionTracking),(0,nr.isArray)(e)?(s={modes:{}},s.modes[Xs.DEFAULT_MODE]=(0,nr.cloneArr)(e),s[Xs.DEFAULT_MODE]=Xs.DEFAULT_MODE):(o=!1,s=(0,nr.cloneObj)(e))}),i.config.skipValidations===!1&&(i.TRACE_INIT("performRuntimeChecks",function(){i.lexerDefinitionErrors=i.lexerDefinitionErrors.concat((0,Xs.performRuntimeChecks)(s,i.trackStartLines,i.config.lineTerminatorCharacters))}),i.TRACE_INIT("performWarningRuntimeChecks",function(){i.lexerDefinitionWarning=i.lexerDefinitionWarning.concat((0,Xs.performWarningRuntimeChecks)(s,i.trackStartLines,i.config.lineTerminatorCharacters))})),s.modes=s.modes?s.modes:{},(0,nr.forEach)(s.modes,function(u,g){s.modes[g]=(0,nr.reject)(u,function(f){return(0,nr.isUndefined)(f)})});var a=(0,nr.keys)(s.modes);if((0,nr.forEach)(s.modes,function(u,g){i.TRACE_INIT("Mode: <"+g+"> processing",function(){if(i.modes.push(g),i.config.skipValidations===!1&&i.TRACE_INIT("validatePatterns",function(){i.lexerDefinitionErrors=i.lexerDefinitionErrors.concat((0,Xs.validatePatterns)(u,a))}),(0,nr.isEmpty)(i.lexerDefinitionErrors)){(0,UEe.augmentTokenTypes)(u);var f;i.TRACE_INIT("analyzeTokenTypes",function(){f=(0,Xs.analyzeTokenTypes)(u,{lineTerminatorCharacters:i.config.lineTerminatorCharacters,positionTracking:t.positionTracking,ensureOptimizations:t.ensureOptimizations,safeMode:t.safeMode,tracer:i.TRACE_INIT.bind(i)})}),i.patternIdxToConfig[g]=f.patternIdxToConfig,i.charCodeToPatternIdxToConfig[g]=f.charCodeToPatternIdxToConfig,i.emptyGroups=(0,nr.merge)(i.emptyGroups,f.emptyGroups),i.hasCustom=f.hasCustom||i.hasCustom,i.canModeBeOptimized[g]=f.canBeOptimized}})}),i.defaultMode=s.defaultMode,!(0,nr.isEmpty)(i.lexerDefinitionErrors)&&!i.config.deferDefinitionErrorsHandling){var l=(0,nr.map)(i.lexerDefinitionErrors,function(u){return u.message}),c=l.join(`----------------------- +`);throw new Error(`Errors detected in definition of Lexer: +`+c)}(0,nr.forEach)(i.lexerDefinitionWarning,function(u){(0,nr.PRINT_WARNING)(u.message)}),i.TRACE_INIT("Choosing sub-methods implementations",function(){if(Xs.SUPPORT_STICKY?(i.chopInput=nr.IDENTITY,i.match=i.matchWithTest):(i.updateLastIndex=nr.NOOP,i.match=i.matchWithExec),o&&(i.handleModes=nr.NOOP),i.trackStartLines===!1&&(i.computeNewColumn=nr.IDENTITY),i.trackEndLines===!1&&(i.updateTokenEndLineColumnLocation=nr.NOOP),/full/i.test(i.config.positionTracking))i.createTokenInstance=i.createFullToken;else if(/onlyStart/i.test(i.config.positionTracking))i.createTokenInstance=i.createStartOnlyToken;else if(/onlyOffset/i.test(i.config.positionTracking))i.createTokenInstance=i.createOffsetOnlyToken;else throw Error('Invalid config option: "'+i.config.positionTracking+'"');i.hasCustom?(i.addToken=i.addTokenUsingPush,i.handlePayload=i.handlePayloadWithCustom):(i.addToken=i.addTokenUsingMemberAccess,i.handlePayload=i.handlePayloadNoCustom)}),i.TRACE_INIT("Failed Optimization Warnings",function(){var u=(0,nr.reduce)(i.canModeBeOptimized,function(g,f,h){return f===!1&&g.push(h),g},[]);if(t.ensureOptimizations&&!(0,nr.isEmpty)(u))throw Error("Lexer Modes: < "+u.join(", ")+` > cannot be optimized. + Disable the "ensureOptimizations" lexer config flag to silently ignore this and run the lexer in an un-optimized mode. + Or inspect the console log for details on how to resolve these issues.`)}),i.TRACE_INIT("clearRegExpParserCache",function(){(0,GEe.clearRegExpParserCache)()}),i.TRACE_INIT("toFastProperties",function(){(0,nr.toFastProperties)(i)})})}return r.prototype.tokenize=function(e,t){if(t===void 0&&(t=this.defaultMode),!(0,nr.isEmpty)(this.lexerDefinitionErrors)){var i=(0,nr.map)(this.lexerDefinitionErrors,function(o){return o.message}),n=i.join(`----------------------- +`);throw new Error(`Unable to Tokenize because Errors detected in definition of Lexer: +`+n)}var s=this.tokenizeInternal(e,t);return s},r.prototype.tokenizeInternal=function(e,t){var i=this,n,s,o,a,l,c,u,g,f,h,p,C,y,B,v,D,L=e,H=L.length,j=0,$=0,V=this.hasCustom?0:Math.floor(e.length/10),W=new Array(V),_=[],A=this.trackStartLines?1:void 0,ae=this.trackStartLines?1:void 0,ge=(0,Xs.cloneEmptyGroups)(this.emptyGroups),re=this.trackStartLines,O=this.config.lineTerminatorsPattern,F=0,ue=[],he=[],ke=[],Fe=[];Object.freeze(Fe);var Ne=void 0;function oe(){return ue}function le(pr){var Ii=(0,Xs.charCodeToOptimizedIndex)(pr),es=he[Ii];return es===void 0?Fe:es}var we=function(pr){if(ke.length===1&&pr.tokenType.PUSH_MODE===void 0){var Ii=i.config.errorMessageProvider.buildUnableToPopLexerModeMessage(pr);_.push({offset:pr.startOffset,line:pr.startLine!==void 0?pr.startLine:void 0,column:pr.startColumn!==void 0?pr.startColumn:void 0,length:pr.image.length,message:Ii})}else{ke.pop();var es=(0,nr.last)(ke);ue=i.patternIdxToConfig[es],he=i.charCodeToPatternIdxToConfig[es],F=ue.length;var ua=i.canModeBeOptimized[es]&&i.config.safeMode===!1;he&&ua?Ne=le:Ne=oe}};function fe(pr){ke.push(pr),he=this.charCodeToPatternIdxToConfig[pr],ue=this.patternIdxToConfig[pr],F=ue.length,F=ue.length;var Ii=this.canModeBeOptimized[pr]&&this.config.safeMode===!1;he&&Ii?Ne=le:Ne=oe}fe.call(this,t);for(var Ae;jc.length){c=a,u=g,Ae=_e;break}}}break}}if(c!==null){if(f=c.length,h=Ae.group,h!==void 0&&(p=Ae.tokenTypeIdx,C=this.createTokenInstance(c,j,p,Ae.tokenType,A,ae,f),this.handlePayload(C,u),h===!1?$=this.addToken(W,$,C):ge[h].push(C)),e=this.chopInput(e,f),j=j+f,ae=this.computeNewColumn(ae,f),re===!0&&Ae.canLineTerminator===!0){var It=0,Or=void 0,ii=void 0;O.lastIndex=0;do Or=O.test(c),Or===!0&&(ii=O.lastIndex-1,It++);while(Or===!0);It!==0&&(A=A+It,ae=f-ii,this.updateTokenEndLineColumnLocation(C,h,ii,It,A,ae,f))}this.handleModes(Ae,we,fe,C)}else{for(var gi=j,hr=A,fi=ae,ni=!1;!ni&&j <"+e+">");var n=(0,nr.timer)(t),s=n.time,o=n.value,a=s>10?console.warn:console.log;return this.traceInitIndent time: "+s+"ms"),this.traceInitIndent--,o}else return t()},r.SKIPPED="This marks a skipped Token pattern, this means each token identified by it willbe consumed and then thrown into oblivion, this can be used to for example to completely ignore whitespace.",r.NA=/NOT_APPLICABLE/,r}();fc.Lexer=jEe});var NA=w(bi=>{"use strict";Object.defineProperty(bi,"__esModule",{value:!0});bi.tokenMatcher=bi.createTokenInstance=bi.EOF=bi.createToken=bi.hasTokenLabel=bi.tokenName=bi.tokenLabel=void 0;var Zs=Gt(),qEe=yd(),Hv=Vg();function JEe(r){return Ej(r)?r.LABEL:r.name}bi.tokenLabel=JEe;function WEe(r){return r.name}bi.tokenName=WEe;function Ej(r){return(0,Zs.isString)(r.LABEL)&&r.LABEL!==""}bi.hasTokenLabel=Ej;var zEe="parent",uj="categories",gj="label",fj="group",hj="push_mode",pj="pop_mode",dj="longer_alt",Cj="line_breaks",mj="start_chars_hint";function Ij(r){return VEe(r)}bi.createToken=Ij;function VEe(r){var e=r.pattern,t={};if(t.name=r.name,(0,Zs.isUndefined)(e)||(t.PATTERN=e),(0,Zs.has)(r,zEe))throw`The parent property is no longer supported. +See: https://github.com/chevrotain/chevrotain/issues/564#issuecomment-349062346 for details.`;return(0,Zs.has)(r,uj)&&(t.CATEGORIES=r[uj]),(0,Hv.augmentTokenTypes)([t]),(0,Zs.has)(r,gj)&&(t.LABEL=r[gj]),(0,Zs.has)(r,fj)&&(t.GROUP=r[fj]),(0,Zs.has)(r,pj)&&(t.POP_MODE=r[pj]),(0,Zs.has)(r,hj)&&(t.PUSH_MODE=r[hj]),(0,Zs.has)(r,dj)&&(t.LONGER_ALT=r[dj]),(0,Zs.has)(r,Cj)&&(t.LINE_BREAKS=r[Cj]),(0,Zs.has)(r,mj)&&(t.START_CHARS_HINT=r[mj]),t}bi.EOF=Ij({name:"EOF",pattern:qEe.Lexer.NA});(0,Hv.augmentTokenTypes)([bi.EOF]);function XEe(r,e,t,i,n,s,o,a){return{image:e,startOffset:t,endOffset:i,startLine:n,endLine:s,startColumn:o,endColumn:a,tokenTypeIdx:r.tokenTypeIdx,tokenType:r}}bi.createTokenInstance=XEe;function ZEe(r,e){return(0,Hv.tokenStructuredMatcher)(r,e)}bi.tokenMatcher=ZEe});var dn=w(zt=>{"use strict";var va=zt&&zt.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(zt,"__esModule",{value:!0});zt.serializeProduction=zt.serializeGrammar=zt.Terminal=zt.Alternation=zt.RepetitionWithSeparator=zt.Repetition=zt.RepetitionMandatoryWithSeparator=zt.RepetitionMandatory=zt.Option=zt.Alternative=zt.Rule=zt.NonTerminal=zt.AbstractProduction=void 0;var Ar=Gt(),_Ee=NA(),ko=function(){function r(e){this._definition=e}return Object.defineProperty(r.prototype,"definition",{get:function(){return this._definition},set:function(e){this._definition=e},enumerable:!1,configurable:!0}),r.prototype.accept=function(e){e.visit(this),(0,Ar.forEach)(this.definition,function(t){t.accept(e)})},r}();zt.AbstractProduction=ko;var yj=function(r){va(e,r);function e(t){var i=r.call(this,[])||this;return i.idx=1,(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return Object.defineProperty(e.prototype,"definition",{get:function(){return this.referencedRule!==void 0?this.referencedRule.definition:[]},set:function(t){},enumerable:!1,configurable:!0}),e.prototype.accept=function(t){t.visit(this)},e}(ko);zt.NonTerminal=yj;var wj=function(r){va(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.orgText="",(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return e}(ko);zt.Rule=wj;var Bj=function(r){va(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.ignoreAmbiguities=!1,(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return e}(ko);zt.Alternative=Bj;var Qj=function(r){va(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return e}(ko);zt.Option=Qj;var bj=function(r){va(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return e}(ko);zt.RepetitionMandatory=bj;var Sj=function(r){va(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return e}(ko);zt.RepetitionMandatoryWithSeparator=Sj;var vj=function(r){va(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return e}(ko);zt.Repetition=vj;var xj=function(r){va(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return e}(ko);zt.RepetitionWithSeparator=xj;var Pj=function(r){va(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,i.ignoreAmbiguities=!1,i.hasPredicates=!1,(0,Ar.assign)(i,(0,Ar.pick)(t,function(n){return n!==void 0})),i}return Object.defineProperty(e.prototype,"definition",{get:function(){return this._definition},set:function(t){this._definition=t},enumerable:!1,configurable:!0}),e}(ko);zt.Alternation=Pj;var ry=function(){function r(e){this.idx=1,(0,Ar.assign)(this,(0,Ar.pick)(e,function(t){return t!==void 0}))}return r.prototype.accept=function(e){e.visit(this)},r}();zt.Terminal=ry;function $Ee(r){return(0,Ar.map)(r,Bd)}zt.serializeGrammar=$Ee;function Bd(r){function e(s){return(0,Ar.map)(s,Bd)}if(r instanceof yj){var t={type:"NonTerminal",name:r.nonTerminalName,idx:r.idx};return(0,Ar.isString)(r.label)&&(t.label=r.label),t}else{if(r instanceof Bj)return{type:"Alternative",definition:e(r.definition)};if(r instanceof Qj)return{type:"Option",idx:r.idx,definition:e(r.definition)};if(r instanceof bj)return{type:"RepetitionMandatory",idx:r.idx,definition:e(r.definition)};if(r instanceof Sj)return{type:"RepetitionMandatoryWithSeparator",idx:r.idx,separator:Bd(new ry({terminalType:r.separator})),definition:e(r.definition)};if(r instanceof xj)return{type:"RepetitionWithSeparator",idx:r.idx,separator:Bd(new ry({terminalType:r.separator})),definition:e(r.definition)};if(r instanceof vj)return{type:"Repetition",idx:r.idx,definition:e(r.definition)};if(r instanceof Pj)return{type:"Alternation",idx:r.idx,definition:e(r.definition)};if(r instanceof ry){var i={type:"Terminal",name:r.terminalType.name,label:(0,_Ee.tokenLabel)(r.terminalType),idx:r.idx};(0,Ar.isString)(r.label)&&(i.terminalLabel=r.label);var n=r.terminalType.PATTERN;return r.terminalType.PATTERN&&(i.pattern=(0,Ar.isRegExp)(n)?n.source:n),i}else{if(r instanceof wj)return{type:"Rule",name:r.name,orgText:r.orgText,definition:e(r.definition)};throw Error("non exhaustive match")}}}zt.serializeProduction=Bd});var ny=w(iy=>{"use strict";Object.defineProperty(iy,"__esModule",{value:!0});iy.RestWalker=void 0;var Gv=Gt(),Cn=dn(),eIe=function(){function r(){}return r.prototype.walk=function(e,t){var i=this;t===void 0&&(t=[]),(0,Gv.forEach)(e.definition,function(n,s){var o=(0,Gv.drop)(e.definition,s+1);if(n instanceof Cn.NonTerminal)i.walkProdRef(n,o,t);else if(n instanceof Cn.Terminal)i.walkTerminal(n,o,t);else if(n instanceof Cn.Alternative)i.walkFlat(n,o,t);else if(n instanceof Cn.Option)i.walkOption(n,o,t);else if(n instanceof Cn.RepetitionMandatory)i.walkAtLeastOne(n,o,t);else if(n instanceof Cn.RepetitionMandatoryWithSeparator)i.walkAtLeastOneSep(n,o,t);else if(n instanceof Cn.RepetitionWithSeparator)i.walkManySep(n,o,t);else if(n instanceof Cn.Repetition)i.walkMany(n,o,t);else if(n instanceof Cn.Alternation)i.walkOr(n,o,t);else throw Error("non exhaustive match")})},r.prototype.walkTerminal=function(e,t,i){},r.prototype.walkProdRef=function(e,t,i){},r.prototype.walkFlat=function(e,t,i){var n=t.concat(i);this.walk(e,n)},r.prototype.walkOption=function(e,t,i){var n=t.concat(i);this.walk(e,n)},r.prototype.walkAtLeastOne=function(e,t,i){var n=[new Cn.Option({definition:e.definition})].concat(t,i);this.walk(e,n)},r.prototype.walkAtLeastOneSep=function(e,t,i){var n=Dj(e,t,i);this.walk(e,n)},r.prototype.walkMany=function(e,t,i){var n=[new Cn.Option({definition:e.definition})].concat(t,i);this.walk(e,n)},r.prototype.walkManySep=function(e,t,i){var n=Dj(e,t,i);this.walk(e,n)},r.prototype.walkOr=function(e,t,i){var n=this,s=t.concat(i);(0,Gv.forEach)(e.definition,function(o){var a=new Cn.Alternative({definition:[o]});n.walk(a,s)})},r}();iy.RestWalker=eIe;function Dj(r,e,t){var i=[new Cn.Option({definition:[new Cn.Terminal({terminalType:r.separator})].concat(r.definition)})],n=i.concat(e,t);return n}});var Xg=w(sy=>{"use strict";Object.defineProperty(sy,"__esModule",{value:!0});sy.GAstVisitor=void 0;var Ro=dn(),tIe=function(){function r(){}return r.prototype.visit=function(e){var t=e;switch(t.constructor){case Ro.NonTerminal:return this.visitNonTerminal(t);case Ro.Alternative:return this.visitAlternative(t);case Ro.Option:return this.visitOption(t);case Ro.RepetitionMandatory:return this.visitRepetitionMandatory(t);case Ro.RepetitionMandatoryWithSeparator:return this.visitRepetitionMandatoryWithSeparator(t);case Ro.RepetitionWithSeparator:return this.visitRepetitionWithSeparator(t);case Ro.Repetition:return this.visitRepetition(t);case Ro.Alternation:return this.visitAlternation(t);case Ro.Terminal:return this.visitTerminal(t);case Ro.Rule:return this.visitRule(t);default:throw Error("non exhaustive match")}},r.prototype.visitNonTerminal=function(e){},r.prototype.visitAlternative=function(e){},r.prototype.visitOption=function(e){},r.prototype.visitRepetition=function(e){},r.prototype.visitRepetitionMandatory=function(e){},r.prototype.visitRepetitionMandatoryWithSeparator=function(e){},r.prototype.visitRepetitionWithSeparator=function(e){},r.prototype.visitAlternation=function(e){},r.prototype.visitTerminal=function(e){},r.prototype.visitRule=function(e){},r}();sy.GAstVisitor=tIe});var bd=w(Mi=>{"use strict";var rIe=Mi&&Mi.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Mi,"__esModule",{value:!0});Mi.collectMethods=Mi.DslMethodsCollectorVisitor=Mi.getProductionDslName=Mi.isBranchingProd=Mi.isOptionalProd=Mi.isSequenceProd=void 0;var Qd=Gt(),Qr=dn(),iIe=Xg();function nIe(r){return r instanceof Qr.Alternative||r instanceof Qr.Option||r instanceof Qr.Repetition||r instanceof Qr.RepetitionMandatory||r instanceof Qr.RepetitionMandatoryWithSeparator||r instanceof Qr.RepetitionWithSeparator||r instanceof Qr.Terminal||r instanceof Qr.Rule}Mi.isSequenceProd=nIe;function Yv(r,e){e===void 0&&(e=[]);var t=r instanceof Qr.Option||r instanceof Qr.Repetition||r instanceof Qr.RepetitionWithSeparator;return t?!0:r instanceof Qr.Alternation?(0,Qd.some)(r.definition,function(i){return Yv(i,e)}):r instanceof Qr.NonTerminal&&(0,Qd.contains)(e,r)?!1:r instanceof Qr.AbstractProduction?(r instanceof Qr.NonTerminal&&e.push(r),(0,Qd.every)(r.definition,function(i){return Yv(i,e)})):!1}Mi.isOptionalProd=Yv;function sIe(r){return r instanceof Qr.Alternation}Mi.isBranchingProd=sIe;function oIe(r){if(r instanceof Qr.NonTerminal)return"SUBRULE";if(r instanceof Qr.Option)return"OPTION";if(r instanceof Qr.Alternation)return"OR";if(r instanceof Qr.RepetitionMandatory)return"AT_LEAST_ONE";if(r instanceof Qr.RepetitionMandatoryWithSeparator)return"AT_LEAST_ONE_SEP";if(r instanceof Qr.RepetitionWithSeparator)return"MANY_SEP";if(r instanceof Qr.Repetition)return"MANY";if(r instanceof Qr.Terminal)return"CONSUME";throw Error("non exhaustive match")}Mi.getProductionDslName=oIe;var kj=function(r){rIe(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.separator="-",t.dslMethods={option:[],alternation:[],repetition:[],repetitionWithSeparator:[],repetitionMandatory:[],repetitionMandatoryWithSeparator:[]},t}return e.prototype.reset=function(){this.dslMethods={option:[],alternation:[],repetition:[],repetitionWithSeparator:[],repetitionMandatory:[],repetitionMandatoryWithSeparator:[]}},e.prototype.visitTerminal=function(t){var i=t.terminalType.name+this.separator+"Terminal";(0,Qd.has)(this.dslMethods,i)||(this.dslMethods[i]=[]),this.dslMethods[i].push(t)},e.prototype.visitNonTerminal=function(t){var i=t.nonTerminalName+this.separator+"Terminal";(0,Qd.has)(this.dslMethods,i)||(this.dslMethods[i]=[]),this.dslMethods[i].push(t)},e.prototype.visitOption=function(t){this.dslMethods.option.push(t)},e.prototype.visitRepetitionWithSeparator=function(t){this.dslMethods.repetitionWithSeparator.push(t)},e.prototype.visitRepetitionMandatory=function(t){this.dslMethods.repetitionMandatory.push(t)},e.prototype.visitRepetitionMandatoryWithSeparator=function(t){this.dslMethods.repetitionMandatoryWithSeparator.push(t)},e.prototype.visitRepetition=function(t){this.dslMethods.repetition.push(t)},e.prototype.visitAlternation=function(t){this.dslMethods.alternation.push(t)},e}(iIe.GAstVisitor);Mi.DslMethodsCollectorVisitor=kj;var oy=new kj;function aIe(r){oy.reset(),r.accept(oy);var e=oy.dslMethods;return oy.reset(),e}Mi.collectMethods=aIe});var qv=w(Fo=>{"use strict";Object.defineProperty(Fo,"__esModule",{value:!0});Fo.firstForTerminal=Fo.firstForBranching=Fo.firstForSequence=Fo.first=void 0;var ay=Gt(),Rj=dn(),jv=bd();function Ay(r){if(r instanceof Rj.NonTerminal)return Ay(r.referencedRule);if(r instanceof Rj.Terminal)return Lj(r);if((0,jv.isSequenceProd)(r))return Fj(r);if((0,jv.isBranchingProd)(r))return Nj(r);throw Error("non exhaustive match")}Fo.first=Ay;function Fj(r){for(var e=[],t=r.definition,i=0,n=t.length>i,s,o=!0;n&&o;)s=t[i],o=(0,jv.isOptionalProd)(s),e=e.concat(Ay(s)),i=i+1,n=t.length>i;return(0,ay.uniq)(e)}Fo.firstForSequence=Fj;function Nj(r){var e=(0,ay.map)(r.definition,function(t){return Ay(t)});return(0,ay.uniq)((0,ay.flatten)(e))}Fo.firstForBranching=Nj;function Lj(r){return[r.terminalType]}Fo.firstForTerminal=Lj});var Jv=w(ly=>{"use strict";Object.defineProperty(ly,"__esModule",{value:!0});ly.IN=void 0;ly.IN="_~IN~_"});var Uj=w(us=>{"use strict";var AIe=us&&us.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(us,"__esModule",{value:!0});us.buildInProdFollowPrefix=us.buildBetweenProdsFollowPrefix=us.computeAllProdsFollows=us.ResyncFollowsWalker=void 0;var lIe=ny(),cIe=qv(),Tj=Gt(),Oj=Jv(),uIe=dn(),Mj=function(r){AIe(e,r);function e(t){var i=r.call(this)||this;return i.topProd=t,i.follows={},i}return e.prototype.startWalking=function(){return this.walk(this.topProd),this.follows},e.prototype.walkTerminal=function(t,i,n){},e.prototype.walkProdRef=function(t,i,n){var s=Kj(t.referencedRule,t.idx)+this.topProd.name,o=i.concat(n),a=new uIe.Alternative({definition:o}),l=(0,cIe.first)(a);this.follows[s]=l},e}(lIe.RestWalker);us.ResyncFollowsWalker=Mj;function gIe(r){var e={};return(0,Tj.forEach)(r,function(t){var i=new Mj(t).startWalking();(0,Tj.assign)(e,i)}),e}us.computeAllProdsFollows=gIe;function Kj(r,e){return r.name+e+Oj.IN}us.buildBetweenProdsFollowPrefix=Kj;function fIe(r){var e=r.terminalType.name;return e+r.idx+Oj.IN}us.buildInProdFollowPrefix=fIe});var Sd=w(xa=>{"use strict";Object.defineProperty(xa,"__esModule",{value:!0});xa.defaultGrammarValidatorErrorProvider=xa.defaultGrammarResolverErrorProvider=xa.defaultParserErrorProvider=void 0;var Zg=NA(),hIe=Gt(),_s=Gt(),Wv=dn(),Hj=bd();xa.defaultParserErrorProvider={buildMismatchTokenMessage:function(r){var e=r.expected,t=r.actual,i=r.previous,n=r.ruleName,s=(0,Zg.hasTokenLabel)(e),o=s?"--> "+(0,Zg.tokenLabel)(e)+" <--":"token of type --> "+e.name+" <--",a="Expecting "+o+" but found --> '"+t.image+"' <--";return a},buildNotAllInputParsedMessage:function(r){var e=r.firstRedundant,t=r.ruleName;return"Redundant input, expecting EOF but found: "+e.image},buildNoViableAltMessage:function(r){var e=r.expectedPathsPerAlt,t=r.actual,i=r.previous,n=r.customUserDescription,s=r.ruleName,o="Expecting: ",a=(0,_s.first)(t).image,l=` +but found: '`+a+"'";if(n)return o+n+l;var c=(0,_s.reduce)(e,function(h,p){return h.concat(p)},[]),u=(0,_s.map)(c,function(h){return"["+(0,_s.map)(h,function(p){return(0,Zg.tokenLabel)(p)}).join(", ")+"]"}),g=(0,_s.map)(u,function(h,p){return" "+(p+1)+". "+h}),f=`one of these possible Token sequences: +`+g.join(` +`);return o+f+l},buildEarlyExitMessage:function(r){var e=r.expectedIterationPaths,t=r.actual,i=r.customUserDescription,n=r.ruleName,s="Expecting: ",o=(0,_s.first)(t).image,a=` +but found: '`+o+"'";if(i)return s+i+a;var l=(0,_s.map)(e,function(u){return"["+(0,_s.map)(u,function(g){return(0,Zg.tokenLabel)(g)}).join(",")+"]"}),c=`expecting at least one iteration which starts with one of these possible Token sequences:: + `+("<"+l.join(" ,")+">");return s+c+a}};Object.freeze(xa.defaultParserErrorProvider);xa.defaultGrammarResolverErrorProvider={buildRuleNotFoundError:function(r,e){var t="Invalid grammar, reference to a rule which is not defined: ->"+e.nonTerminalName+`<- +inside top level rule: ->`+r.name+"<-";return t}};xa.defaultGrammarValidatorErrorProvider={buildDuplicateFoundError:function(r,e){function t(u){return u instanceof Wv.Terminal?u.terminalType.name:u instanceof Wv.NonTerminal?u.nonTerminalName:""}var i=r.name,n=(0,_s.first)(e),s=n.idx,o=(0,Hj.getProductionDslName)(n),a=t(n),l=s>0,c="->"+o+(l?s:"")+"<- "+(a?"with argument: ->"+a+"<-":"")+` + appears more than once (`+e.length+" times) in the top level rule: ->"+i+`<-. + For further details see: https://chevrotain.io/docs/FAQ.html#NUMERICAL_SUFFIXES + `;return c=c.replace(/[ \t]+/g," "),c=c.replace(/\s\s+/g,` +`),c},buildNamespaceConflictError:function(r){var e=`Namespace conflict found in grammar. +`+("The grammar has both a Terminal(Token) and a Non-Terminal(Rule) named: <"+r.name+`>. +`)+`To resolve this make sure each Terminal and Non-Terminal names are unique +This is easy to accomplish by using the convention that Terminal names start with an uppercase letter +and Non-Terminal names start with a lower case letter.`;return e},buildAlternationPrefixAmbiguityError:function(r){var e=(0,_s.map)(r.prefixPath,function(n){return(0,Zg.tokenLabel)(n)}).join(", "),t=r.alternation.idx===0?"":r.alternation.idx,i="Ambiguous alternatives: <"+r.ambiguityIndices.join(" ,")+`> due to common lookahead prefix +`+("in inside <"+r.topLevelRule.name+`> Rule, +`)+("<"+e+`> may appears as a prefix path in all these alternatives. +`)+`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#COMMON_PREFIX +For Further details.`;return i},buildAlternationAmbiguityError:function(r){var e=(0,_s.map)(r.prefixPath,function(n){return(0,Zg.tokenLabel)(n)}).join(", "),t=r.alternation.idx===0?"":r.alternation.idx,i="Ambiguous Alternatives Detected: <"+r.ambiguityIndices.join(" ,")+"> in "+(" inside <"+r.topLevelRule.name+`> Rule, +`)+("<"+e+`> may appears as a prefix path in all these alternatives. +`);return i=i+`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES +For Further details.`,i},buildEmptyRepetitionError:function(r){var e=(0,Hj.getProductionDslName)(r.repetition);r.repetition.idx!==0&&(e+=r.repetition.idx);var t="The repetition <"+e+"> within Rule <"+r.topLevelRule.name+`> can never consume any tokens. +This could lead to an infinite loop.`;return t},buildTokenNameError:function(r){return"deprecated"},buildEmptyAlternationError:function(r){var e="Ambiguous empty alternative: <"+(r.emptyChoiceIdx+1)+">"+(" in inside <"+r.topLevelRule.name+`> Rule. +`)+"Only the last alternative may be an empty alternative.";return e},buildTooManyAlternativesError:function(r){var e=`An Alternation cannot have more than 256 alternatives: +`+(" inside <"+r.topLevelRule.name+`> Rule. + has `+(r.alternation.definition.length+1)+" alternatives.");return e},buildLeftRecursionError:function(r){var e=r.topLevelRule.name,t=hIe.map(r.leftRecursionPath,function(s){return s.name}),i=e+" --> "+t.concat([e]).join(" --> "),n=`Left Recursion found in grammar. +`+("rule: <"+e+`> can be invoked from itself (directly or indirectly) +`)+(`without consuming any Tokens. The grammar path that causes this is: + `+i+` +`)+` To fix this refactor your grammar to remove the left recursion. +see: https://en.wikipedia.org/wiki/LL_parser#Left_Factoring.`;return n},buildInvalidRuleNameError:function(r){return"deprecated"},buildDuplicateRuleNameError:function(r){var e;r.topLevelRule instanceof Wv.Rule?e=r.topLevelRule.name:e=r.topLevelRule;var t="Duplicate definition, rule: ->"+e+"<- is already defined in the grammar: ->"+r.grammarName+"<-";return t}}});var jj=w(LA=>{"use strict";var pIe=LA&&LA.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(LA,"__esModule",{value:!0});LA.GastRefResolverVisitor=LA.resolveGrammar=void 0;var dIe=Gn(),Gj=Gt(),CIe=Xg();function mIe(r,e){var t=new Yj(r,e);return t.resolveRefs(),t.errors}LA.resolveGrammar=mIe;var Yj=function(r){pIe(e,r);function e(t,i){var n=r.call(this)||this;return n.nameToTopRule=t,n.errMsgProvider=i,n.errors=[],n}return e.prototype.resolveRefs=function(){var t=this;(0,Gj.forEach)((0,Gj.values)(this.nameToTopRule),function(i){t.currTopLevel=i,i.accept(t)})},e.prototype.visitNonTerminal=function(t){var i=this.nameToTopRule[t.nonTerminalName];if(i)t.referencedRule=i;else{var n=this.errMsgProvider.buildRuleNotFoundError(this.currTopLevel,t);this.errors.push({message:n,type:dIe.ParserDefinitionErrorType.UNRESOLVED_SUBRULE_REF,ruleName:this.currTopLevel.name,unresolvedRefName:t.nonTerminalName})}},e}(CIe.GAstVisitor);LA.GastRefResolverVisitor=Yj});var xd=w(Nr=>{"use strict";var hc=Nr&&Nr.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Nr,"__esModule",{value:!0});Nr.nextPossibleTokensAfter=Nr.possiblePathsFrom=Nr.NextTerminalAfterAtLeastOneSepWalker=Nr.NextTerminalAfterAtLeastOneWalker=Nr.NextTerminalAfterManySepWalker=Nr.NextTerminalAfterManyWalker=Nr.AbstractNextTerminalAfterProductionWalker=Nr.NextAfterTokenWalker=Nr.AbstractNextPossibleTokensWalker=void 0;var qj=ny(),Kt=Gt(),EIe=qv(),kt=dn(),Jj=function(r){hc(e,r);function e(t,i){var n=r.call(this)||this;return n.topProd=t,n.path=i,n.possibleTokTypes=[],n.nextProductionName="",n.nextProductionOccurrence=0,n.found=!1,n.isAtEndOfPath=!1,n}return e.prototype.startWalking=function(){if(this.found=!1,this.path.ruleStack[0]!==this.topProd.name)throw Error("The path does not start with the walker's top Rule!");return this.ruleStack=(0,Kt.cloneArr)(this.path.ruleStack).reverse(),this.occurrenceStack=(0,Kt.cloneArr)(this.path.occurrenceStack).reverse(),this.ruleStack.pop(),this.occurrenceStack.pop(),this.updateExpectedNext(),this.walk(this.topProd),this.possibleTokTypes},e.prototype.walk=function(t,i){i===void 0&&(i=[]),this.found||r.prototype.walk.call(this,t,i)},e.prototype.walkProdRef=function(t,i,n){if(t.referencedRule.name===this.nextProductionName&&t.idx===this.nextProductionOccurrence){var s=i.concat(n);this.updateExpectedNext(),this.walk(t.referencedRule,s)}},e.prototype.updateExpectedNext=function(){(0,Kt.isEmpty)(this.ruleStack)?(this.nextProductionName="",this.nextProductionOccurrence=0,this.isAtEndOfPath=!0):(this.nextProductionName=this.ruleStack.pop(),this.nextProductionOccurrence=this.occurrenceStack.pop())},e}(qj.RestWalker);Nr.AbstractNextPossibleTokensWalker=Jj;var IIe=function(r){hc(e,r);function e(t,i){var n=r.call(this,t,i)||this;return n.path=i,n.nextTerminalName="",n.nextTerminalOccurrence=0,n.nextTerminalName=n.path.lastTok.name,n.nextTerminalOccurrence=n.path.lastTokOccurrence,n}return e.prototype.walkTerminal=function(t,i,n){if(this.isAtEndOfPath&&t.terminalType.name===this.nextTerminalName&&t.idx===this.nextTerminalOccurrence&&!this.found){var s=i.concat(n),o=new kt.Alternative({definition:s});this.possibleTokTypes=(0,EIe.first)(o),this.found=!0}},e}(Jj);Nr.NextAfterTokenWalker=IIe;var vd=function(r){hc(e,r);function e(t,i){var n=r.call(this)||this;return n.topRule=t,n.occurrence=i,n.result={token:void 0,occurrence:void 0,isEndOfRule:void 0},n}return e.prototype.startWalking=function(){return this.walk(this.topRule),this.result},e}(qj.RestWalker);Nr.AbstractNextTerminalAfterProductionWalker=vd;var yIe=function(r){hc(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.walkMany=function(t,i,n){if(t.idx===this.occurrence){var s=(0,Kt.first)(i.concat(n));this.result.isEndOfRule=s===void 0,s instanceof kt.Terminal&&(this.result.token=s.terminalType,this.result.occurrence=s.idx)}else r.prototype.walkMany.call(this,t,i,n)},e}(vd);Nr.NextTerminalAfterManyWalker=yIe;var wIe=function(r){hc(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.walkManySep=function(t,i,n){if(t.idx===this.occurrence){var s=(0,Kt.first)(i.concat(n));this.result.isEndOfRule=s===void 0,s instanceof kt.Terminal&&(this.result.token=s.terminalType,this.result.occurrence=s.idx)}else r.prototype.walkManySep.call(this,t,i,n)},e}(vd);Nr.NextTerminalAfterManySepWalker=wIe;var BIe=function(r){hc(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.walkAtLeastOne=function(t,i,n){if(t.idx===this.occurrence){var s=(0,Kt.first)(i.concat(n));this.result.isEndOfRule=s===void 0,s instanceof kt.Terminal&&(this.result.token=s.terminalType,this.result.occurrence=s.idx)}else r.prototype.walkAtLeastOne.call(this,t,i,n)},e}(vd);Nr.NextTerminalAfterAtLeastOneWalker=BIe;var QIe=function(r){hc(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.walkAtLeastOneSep=function(t,i,n){if(t.idx===this.occurrence){var s=(0,Kt.first)(i.concat(n));this.result.isEndOfRule=s===void 0,s instanceof kt.Terminal&&(this.result.token=s.terminalType,this.result.occurrence=s.idx)}else r.prototype.walkAtLeastOneSep.call(this,t,i,n)},e}(vd);Nr.NextTerminalAfterAtLeastOneSepWalker=QIe;function Wj(r,e,t){t===void 0&&(t=[]),t=(0,Kt.cloneArr)(t);var i=[],n=0;function s(c){return c.concat((0,Kt.drop)(r,n+1))}function o(c){var u=Wj(s(c),e,t);return i.concat(u)}for(;t.length=0;ge--){var re=B.definition[ge],O={idx:p,def:re.definition.concat((0,Kt.drop)(h)),ruleStack:C,occurrenceStack:y};g.push(O),g.push(o)}else if(B instanceof kt.Alternative)g.push({idx:p,def:B.definition.concat((0,Kt.drop)(h)),ruleStack:C,occurrenceStack:y});else if(B instanceof kt.Rule)g.push(SIe(B,p,C,y));else throw Error("non exhaustive match")}}return u}Nr.nextPossibleTokensAfter=bIe;function SIe(r,e,t,i){var n=(0,Kt.cloneArr)(t);n.push(r.name);var s=(0,Kt.cloneArr)(i);return s.push(1),{idx:e,def:r.definition,ruleStack:n,occurrenceStack:s}}});var Pd=w(Zt=>{"use strict";var Xj=Zt&&Zt.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Zt,"__esModule",{value:!0});Zt.areTokenCategoriesNotUsed=Zt.isStrictPrefixOfPath=Zt.containsPath=Zt.getLookaheadPathsForOptionalProd=Zt.getLookaheadPathsForOr=Zt.lookAheadSequenceFromAlternatives=Zt.buildSingleAlternativeLookaheadFunction=Zt.buildAlternativesLookAheadFunc=Zt.buildLookaheadFuncForOptionalProd=Zt.buildLookaheadFuncForOr=Zt.getProdType=Zt.PROD_TYPE=void 0;var sr=Gt(),zj=xd(),vIe=ny(),cy=Vg(),TA=dn(),xIe=Xg(),oi;(function(r){r[r.OPTION=0]="OPTION",r[r.REPETITION=1]="REPETITION",r[r.REPETITION_MANDATORY=2]="REPETITION_MANDATORY",r[r.REPETITION_MANDATORY_WITH_SEPARATOR=3]="REPETITION_MANDATORY_WITH_SEPARATOR",r[r.REPETITION_WITH_SEPARATOR=4]="REPETITION_WITH_SEPARATOR",r[r.ALTERNATION=5]="ALTERNATION"})(oi=Zt.PROD_TYPE||(Zt.PROD_TYPE={}));function PIe(r){if(r instanceof TA.Option)return oi.OPTION;if(r instanceof TA.Repetition)return oi.REPETITION;if(r instanceof TA.RepetitionMandatory)return oi.REPETITION_MANDATORY;if(r instanceof TA.RepetitionMandatoryWithSeparator)return oi.REPETITION_MANDATORY_WITH_SEPARATOR;if(r instanceof TA.RepetitionWithSeparator)return oi.REPETITION_WITH_SEPARATOR;if(r instanceof TA.Alternation)return oi.ALTERNATION;throw Error("non exhaustive match")}Zt.getProdType=PIe;function DIe(r,e,t,i,n,s){var o=_j(r,e,t),a=Xv(o)?cy.tokenStructuredMatcherNoCategories:cy.tokenStructuredMatcher;return s(o,i,a,n)}Zt.buildLookaheadFuncForOr=DIe;function kIe(r,e,t,i,n,s){var o=$j(r,e,n,t),a=Xv(o)?cy.tokenStructuredMatcherNoCategories:cy.tokenStructuredMatcher;return s(o[0],a,i)}Zt.buildLookaheadFuncForOptionalProd=kIe;function RIe(r,e,t,i){var n=r.length,s=(0,sr.every)(r,function(l){return(0,sr.every)(l,function(c){return c.length===1})});if(e)return function(l){for(var c=(0,sr.map)(l,function(D){return D.GATE}),u=0;u{"use strict";var Zv=Vt&&Vt.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Vt,"__esModule",{value:!0});Vt.checkPrefixAlternativesAmbiguities=Vt.validateSomeNonEmptyLookaheadPath=Vt.validateTooManyAlts=Vt.RepetionCollector=Vt.validateAmbiguousAlternationAlternatives=Vt.validateEmptyOrAlternative=Vt.getFirstNoneTerminal=Vt.validateNoLeftRecursion=Vt.validateRuleIsOverridden=Vt.validateRuleDoesNotAlreadyExist=Vt.OccurrenceValidationCollector=Vt.identifyProductionForDuplicates=Vt.validateGrammar=void 0;var er=Gt(),br=Gt(),No=Gn(),_v=bd(),_g=Pd(),OIe=xd(),$s=dn(),$v=Xg();function MIe(r,e,t,i,n){var s=er.map(r,function(h){return KIe(h,i)}),o=er.map(r,function(h){return ex(h,h,i)}),a=[],l=[],c=[];(0,br.every)(o,br.isEmpty)&&(a=(0,br.map)(r,function(h){return sq(h,i)}),l=(0,br.map)(r,function(h){return oq(h,e,i)}),c=lq(r,e,i));var u=GIe(r,t,i),g=(0,br.map)(r,function(h){return Aq(h,i)}),f=(0,br.map)(r,function(h){return nq(h,r,n,i)});return er.flatten(s.concat(c,o,a,l,u,g,f))}Vt.validateGrammar=MIe;function KIe(r,e){var t=new iq;r.accept(t);var i=t.allProductions,n=er.groupBy(i,tq),s=er.pick(n,function(a){return a.length>1}),o=er.map(er.values(s),function(a){var l=er.first(a),c=e.buildDuplicateFoundError(r,a),u=(0,_v.getProductionDslName)(l),g={message:c,type:No.ParserDefinitionErrorType.DUPLICATE_PRODUCTIONS,ruleName:r.name,dslName:u,occurrence:l.idx},f=rq(l);return f&&(g.parameter=f),g});return o}function tq(r){return(0,_v.getProductionDslName)(r)+"_#_"+r.idx+"_#_"+rq(r)}Vt.identifyProductionForDuplicates=tq;function rq(r){return r instanceof $s.Terminal?r.terminalType.name:r instanceof $s.NonTerminal?r.nonTerminalName:""}var iq=function(r){Zv(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.allProductions=[],t}return e.prototype.visitNonTerminal=function(t){this.allProductions.push(t)},e.prototype.visitOption=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionWithSeparator=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionMandatory=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionMandatoryWithSeparator=function(t){this.allProductions.push(t)},e.prototype.visitRepetition=function(t){this.allProductions.push(t)},e.prototype.visitAlternation=function(t){this.allProductions.push(t)},e.prototype.visitTerminal=function(t){this.allProductions.push(t)},e}($v.GAstVisitor);Vt.OccurrenceValidationCollector=iq;function nq(r,e,t,i){var n=[],s=(0,br.reduce)(e,function(a,l){return l.name===r.name?a+1:a},0);if(s>1){var o=i.buildDuplicateRuleNameError({topLevelRule:r,grammarName:t});n.push({message:o,type:No.ParserDefinitionErrorType.DUPLICATE_RULE_NAME,ruleName:r.name})}return n}Vt.validateRuleDoesNotAlreadyExist=nq;function UIe(r,e,t){var i=[],n;return er.contains(e,r)||(n="Invalid rule override, rule: ->"+r+"<- cannot be overridden in the grammar: ->"+t+"<-as it is not defined in any of the super grammars ",i.push({message:n,type:No.ParserDefinitionErrorType.INVALID_RULE_OVERRIDE,ruleName:r})),i}Vt.validateRuleIsOverridden=UIe;function ex(r,e,t,i){i===void 0&&(i=[]);var n=[],s=Dd(e.definition);if(er.isEmpty(s))return[];var o=r.name,a=er.contains(s,r);a&&n.push({message:t.buildLeftRecursionError({topLevelRule:r,leftRecursionPath:i}),type:No.ParserDefinitionErrorType.LEFT_RECURSION,ruleName:o});var l=er.difference(s,i.concat([r])),c=er.map(l,function(u){var g=er.cloneArr(i);return g.push(u),ex(r,u,t,g)});return n.concat(er.flatten(c))}Vt.validateNoLeftRecursion=ex;function Dd(r){var e=[];if(er.isEmpty(r))return e;var t=er.first(r);if(t instanceof $s.NonTerminal)e.push(t.referencedRule);else if(t instanceof $s.Alternative||t instanceof $s.Option||t instanceof $s.RepetitionMandatory||t instanceof $s.RepetitionMandatoryWithSeparator||t instanceof $s.RepetitionWithSeparator||t instanceof $s.Repetition)e=e.concat(Dd(t.definition));else if(t instanceof $s.Alternation)e=er.flatten(er.map(t.definition,function(o){return Dd(o.definition)}));else if(!(t instanceof $s.Terminal))throw Error("non exhaustive match");var i=(0,_v.isOptionalProd)(t),n=r.length>1;if(i&&n){var s=er.drop(r);return e.concat(Dd(s))}else return e}Vt.getFirstNoneTerminal=Dd;var tx=function(r){Zv(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.alternations=[],t}return e.prototype.visitAlternation=function(t){this.alternations.push(t)},e}($v.GAstVisitor);function sq(r,e){var t=new tx;r.accept(t);var i=t.alternations,n=er.reduce(i,function(s,o){var a=er.dropRight(o.definition),l=er.map(a,function(c,u){var g=(0,OIe.nextPossibleTokensAfter)([c],[],null,1);return er.isEmpty(g)?{message:e.buildEmptyAlternationError({topLevelRule:r,alternation:o,emptyChoiceIdx:u}),type:No.ParserDefinitionErrorType.NONE_LAST_EMPTY_ALT,ruleName:r.name,occurrence:o.idx,alternative:u+1}:null});return s.concat(er.compact(l))},[]);return n}Vt.validateEmptyOrAlternative=sq;function oq(r,e,t){var i=new tx;r.accept(i);var n=i.alternations;n=(0,br.reject)(n,function(o){return o.ignoreAmbiguities===!0});var s=er.reduce(n,function(o,a){var l=a.idx,c=a.maxLookahead||e,u=(0,_g.getLookaheadPathsForOr)(l,r,c,a),g=HIe(u,a,r,t),f=cq(u,a,r,t);return o.concat(g,f)},[]);return s}Vt.validateAmbiguousAlternationAlternatives=oq;var aq=function(r){Zv(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.allProductions=[],t}return e.prototype.visitRepetitionWithSeparator=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionMandatory=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionMandatoryWithSeparator=function(t){this.allProductions.push(t)},e.prototype.visitRepetition=function(t){this.allProductions.push(t)},e}($v.GAstVisitor);Vt.RepetionCollector=aq;function Aq(r,e){var t=new tx;r.accept(t);var i=t.alternations,n=er.reduce(i,function(s,o){return o.definition.length>255&&s.push({message:e.buildTooManyAlternativesError({topLevelRule:r,alternation:o}),type:No.ParserDefinitionErrorType.TOO_MANY_ALTS,ruleName:r.name,occurrence:o.idx}),s},[]);return n}Vt.validateTooManyAlts=Aq;function lq(r,e,t){var i=[];return(0,br.forEach)(r,function(n){var s=new aq;n.accept(s);var o=s.allProductions;(0,br.forEach)(o,function(a){var l=(0,_g.getProdType)(a),c=a.maxLookahead||e,u=a.idx,g=(0,_g.getLookaheadPathsForOptionalProd)(u,n,l,c),f=g[0];if((0,br.isEmpty)((0,br.flatten)(f))){var h=t.buildEmptyRepetitionError({topLevelRule:n,repetition:a});i.push({message:h,type:No.ParserDefinitionErrorType.NO_NON_EMPTY_LOOKAHEAD,ruleName:n.name})}})}),i}Vt.validateSomeNonEmptyLookaheadPath=lq;function HIe(r,e,t,i){var n=[],s=(0,br.reduce)(r,function(a,l,c){return e.definition[c].ignoreAmbiguities===!0||(0,br.forEach)(l,function(u){var g=[c];(0,br.forEach)(r,function(f,h){c!==h&&(0,_g.containsPath)(f,u)&&e.definition[h].ignoreAmbiguities!==!0&&g.push(h)}),g.length>1&&!(0,_g.containsPath)(n,u)&&(n.push(u),a.push({alts:g,path:u}))}),a},[]),o=er.map(s,function(a){var l=(0,br.map)(a.alts,function(u){return u+1}),c=i.buildAlternationAmbiguityError({topLevelRule:t,alternation:e,ambiguityIndices:l,prefixPath:a.path});return{message:c,type:No.ParserDefinitionErrorType.AMBIGUOUS_ALTS,ruleName:t.name,occurrence:e.idx,alternatives:[a.alts]}});return o}function cq(r,e,t,i){var n=[],s=(0,br.reduce)(r,function(o,a,l){var c=(0,br.map)(a,function(u){return{idx:l,path:u}});return o.concat(c)},[]);return(0,br.forEach)(s,function(o){var a=e.definition[o.idx];if(a.ignoreAmbiguities!==!0){var l=o.idx,c=o.path,u=(0,br.findAll)(s,function(f){return e.definition[f.idx].ignoreAmbiguities!==!0&&f.idx{"use strict";Object.defineProperty($g,"__esModule",{value:!0});$g.validateGrammar=$g.resolveGrammar=void 0;var ix=Gt(),YIe=jj(),jIe=rx(),uq=Sd();function qIe(r){r=(0,ix.defaults)(r,{errMsgProvider:uq.defaultGrammarResolverErrorProvider});var e={};return(0,ix.forEach)(r.rules,function(t){e[t.name]=t}),(0,YIe.resolveGrammar)(e,r.errMsgProvider)}$g.resolveGrammar=qIe;function JIe(r){return r=(0,ix.defaults)(r,{errMsgProvider:uq.defaultGrammarValidatorErrorProvider}),(0,jIe.validateGrammar)(r.rules,r.maxLookahead,r.tokenTypes,r.errMsgProvider,r.grammarName)}$g.validateGrammar=JIe});var ef=w(mn=>{"use strict";var kd=mn&&mn.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(mn,"__esModule",{value:!0});mn.EarlyExitException=mn.NotAllInputParsedException=mn.NoViableAltException=mn.MismatchedTokenException=mn.isRecognitionException=void 0;var WIe=Gt(),fq="MismatchedTokenException",hq="NoViableAltException",pq="EarlyExitException",dq="NotAllInputParsedException",Cq=[fq,hq,pq,dq];Object.freeze(Cq);function zIe(r){return(0,WIe.contains)(Cq,r.name)}mn.isRecognitionException=zIe;var uy=function(r){kd(e,r);function e(t,i){var n=this.constructor,s=r.call(this,t)||this;return s.token=i,s.resyncedTokens=[],Object.setPrototypeOf(s,n.prototype),Error.captureStackTrace&&Error.captureStackTrace(s,s.constructor),s}return e}(Error),VIe=function(r){kd(e,r);function e(t,i,n){var s=r.call(this,t,i)||this;return s.previousToken=n,s.name=fq,s}return e}(uy);mn.MismatchedTokenException=VIe;var XIe=function(r){kd(e,r);function e(t,i,n){var s=r.call(this,t,i)||this;return s.previousToken=n,s.name=hq,s}return e}(uy);mn.NoViableAltException=XIe;var ZIe=function(r){kd(e,r);function e(t,i){var n=r.call(this,t,i)||this;return n.name=dq,n}return e}(uy);mn.NotAllInputParsedException=ZIe;var _Ie=function(r){kd(e,r);function e(t,i,n){var s=r.call(this,t,i)||this;return s.previousToken=n,s.name=pq,s}return e}(uy);mn.EarlyExitException=_Ie});var sx=w(Ki=>{"use strict";Object.defineProperty(Ki,"__esModule",{value:!0});Ki.attemptInRepetitionRecovery=Ki.Recoverable=Ki.InRuleRecoveryException=Ki.IN_RULE_RECOVERY_EXCEPTION=Ki.EOF_FOLLOW_KEY=void 0;var gy=NA(),gs=Gt(),$Ie=ef(),eye=Jv(),tye=Gn();Ki.EOF_FOLLOW_KEY={};Ki.IN_RULE_RECOVERY_EXCEPTION="InRuleRecoveryException";function nx(r){this.name=Ki.IN_RULE_RECOVERY_EXCEPTION,this.message=r}Ki.InRuleRecoveryException=nx;nx.prototype=Error.prototype;var rye=function(){function r(){}return r.prototype.initRecoverable=function(e){this.firstAfterRepMap={},this.resyncFollows={},this.recoveryEnabled=(0,gs.has)(e,"recoveryEnabled")?e.recoveryEnabled:tye.DEFAULT_PARSER_CONFIG.recoveryEnabled,this.recoveryEnabled&&(this.attemptInRepetitionRecovery=mq)},r.prototype.getTokenToInsert=function(e){var t=(0,gy.createTokenInstance)(e,"",NaN,NaN,NaN,NaN,NaN,NaN);return t.isInsertedInRecovery=!0,t},r.prototype.canTokenTypeBeInsertedInRecovery=function(e){return!0},r.prototype.tryInRepetitionRecovery=function(e,t,i,n){for(var s=this,o=this.findReSyncTokenType(),a=this.exportLexerState(),l=[],c=!1,u=this.LA(1),g=this.LA(1),f=function(){var h=s.LA(0),p=s.errorMessageProvider.buildMismatchTokenMessage({expected:n,actual:u,previous:h,ruleName:s.getCurrRuleFullName()}),C=new $Ie.MismatchedTokenException(p,u,s.LA(0));C.resyncedTokens=(0,gs.dropRight)(l),s.SAVE_ERROR(C)};!c;)if(this.tokenMatcher(g,n)){f();return}else if(i.call(this)){f(),e.apply(this,t);return}else this.tokenMatcher(g,o)?c=!0:(g=this.SKIP_TOKEN(),this.addToResyncTokens(g,l));this.importLexerState(a)},r.prototype.shouldInRepetitionRecoveryBeTried=function(e,t,i){return!(i===!1||e===void 0||t===void 0||this.tokenMatcher(this.LA(1),e)||this.isBackTracking()||this.canPerformInRuleRecovery(e,this.getFollowsForInRuleRecovery(e,t)))},r.prototype.getFollowsForInRuleRecovery=function(e,t){var i=this.getCurrentGrammarPath(e,t),n=this.getNextPossibleTokenTypes(i);return n},r.prototype.tryInRuleRecovery=function(e,t){if(this.canRecoverWithSingleTokenInsertion(e,t)){var i=this.getTokenToInsert(e);return i}if(this.canRecoverWithSingleTokenDeletion(e)){var n=this.SKIP_TOKEN();return this.consumeToken(),n}throw new nx("sad sad panda")},r.prototype.canPerformInRuleRecovery=function(e,t){return this.canRecoverWithSingleTokenInsertion(e,t)||this.canRecoverWithSingleTokenDeletion(e)},r.prototype.canRecoverWithSingleTokenInsertion=function(e,t){var i=this;if(!this.canTokenTypeBeInsertedInRecovery(e)||(0,gs.isEmpty)(t))return!1;var n=this.LA(1),s=(0,gs.find)(t,function(o){return i.tokenMatcher(n,o)})!==void 0;return s},r.prototype.canRecoverWithSingleTokenDeletion=function(e){var t=this.tokenMatcher(this.LA(2),e);return t},r.prototype.isInCurrentRuleReSyncSet=function(e){var t=this.getCurrFollowKey(),i=this.getFollowSetFromFollowKey(t);return(0,gs.contains)(i,e)},r.prototype.findReSyncTokenType=function(){for(var e=this.flattenFollowSet(),t=this.LA(1),i=2;;){var n=t.tokenType;if((0,gs.contains)(e,n))return n;t=this.LA(i),i++}},r.prototype.getCurrFollowKey=function(){if(this.RULE_STACK.length===1)return Ki.EOF_FOLLOW_KEY;var e=this.getLastExplicitRuleShortName(),t=this.getLastExplicitRuleOccurrenceIndex(),i=this.getPreviousExplicitRuleShortName();return{ruleName:this.shortRuleNameToFullName(e),idxInCallingRule:t,inRule:this.shortRuleNameToFullName(i)}},r.prototype.buildFullFollowKeyStack=function(){var e=this,t=this.RULE_STACK,i=this.RULE_OCCURRENCE_STACK;return(0,gs.map)(t,function(n,s){return s===0?Ki.EOF_FOLLOW_KEY:{ruleName:e.shortRuleNameToFullName(n),idxInCallingRule:i[s],inRule:e.shortRuleNameToFullName(t[s-1])}})},r.prototype.flattenFollowSet=function(){var e=this,t=(0,gs.map)(this.buildFullFollowKeyStack(),function(i){return e.getFollowSetFromFollowKey(i)});return(0,gs.flatten)(t)},r.prototype.getFollowSetFromFollowKey=function(e){if(e===Ki.EOF_FOLLOW_KEY)return[gy.EOF];var t=e.ruleName+e.idxInCallingRule+eye.IN+e.inRule;return this.resyncFollows[t]},r.prototype.addToResyncTokens=function(e,t){return this.tokenMatcher(e,gy.EOF)||t.push(e),t},r.prototype.reSyncTo=function(e){for(var t=[],i=this.LA(1);this.tokenMatcher(i,e)===!1;)i=this.SKIP_TOKEN(),this.addToResyncTokens(i,t);return(0,gs.dropRight)(t)},r.prototype.attemptInRepetitionRecovery=function(e,t,i,n,s,o,a){},r.prototype.getCurrentGrammarPath=function(e,t){var i=this.getHumanReadableRuleStack(),n=(0,gs.cloneArr)(this.RULE_OCCURRENCE_STACK),s={ruleStack:i,occurrenceStack:n,lastTok:e,lastTokOccurrence:t};return s},r.prototype.getHumanReadableRuleStack=function(){var e=this;return(0,gs.map)(this.RULE_STACK,function(t){return e.shortRuleNameToFullName(t)})},r}();Ki.Recoverable=rye;function mq(r,e,t,i,n,s,o){var a=this.getKeyForAutomaticLookahead(i,n),l=this.firstAfterRepMap[a];if(l===void 0){var c=this.getCurrRuleFullName(),u=this.getGAstProductions()[c],g=new s(u,n);l=g.startWalking(),this.firstAfterRepMap[a]=l}var f=l.token,h=l.occurrence,p=l.isEndOfRule;this.RULE_STACK.length===1&&p&&f===void 0&&(f=gy.EOF,h=1),this.shouldInRepetitionRecoveryBeTried(f,h,o)&&this.tryInRepetitionRecovery(r,e,t,f)}Ki.attemptInRepetitionRecovery=mq});var fy=w(Jt=>{"use strict";Object.defineProperty(Jt,"__esModule",{value:!0});Jt.getKeyForAutomaticLookahead=Jt.AT_LEAST_ONE_SEP_IDX=Jt.MANY_SEP_IDX=Jt.AT_LEAST_ONE_IDX=Jt.MANY_IDX=Jt.OPTION_IDX=Jt.OR_IDX=Jt.BITS_FOR_ALT_IDX=Jt.BITS_FOR_RULE_IDX=Jt.BITS_FOR_OCCURRENCE_IDX=Jt.BITS_FOR_METHOD_TYPE=void 0;Jt.BITS_FOR_METHOD_TYPE=4;Jt.BITS_FOR_OCCURRENCE_IDX=8;Jt.BITS_FOR_RULE_IDX=12;Jt.BITS_FOR_ALT_IDX=8;Jt.OR_IDX=1<{"use strict";Object.defineProperty(hy,"__esModule",{value:!0});hy.LooksAhead=void 0;var Pa=Pd(),eo=Gt(),Eq=Gn(),Da=fy(),pc=bd(),nye=function(){function r(){}return r.prototype.initLooksAhead=function(e){this.dynamicTokensEnabled=(0,eo.has)(e,"dynamicTokensEnabled")?e.dynamicTokensEnabled:Eq.DEFAULT_PARSER_CONFIG.dynamicTokensEnabled,this.maxLookahead=(0,eo.has)(e,"maxLookahead")?e.maxLookahead:Eq.DEFAULT_PARSER_CONFIG.maxLookahead,this.lookAheadFuncsCache=(0,eo.isES2015MapSupported)()?new Map:[],(0,eo.isES2015MapSupported)()?(this.getLaFuncFromCache=this.getLaFuncFromMap,this.setLaFuncCache=this.setLaFuncCacheUsingMap):(this.getLaFuncFromCache=this.getLaFuncFromObj,this.setLaFuncCache=this.setLaFuncUsingObj)},r.prototype.preComputeLookaheadFunctions=function(e){var t=this;(0,eo.forEach)(e,function(i){t.TRACE_INIT(i.name+" Rule Lookahead",function(){var n=(0,pc.collectMethods)(i),s=n.alternation,o=n.repetition,a=n.option,l=n.repetitionMandatory,c=n.repetitionMandatoryWithSeparator,u=n.repetitionWithSeparator;(0,eo.forEach)(s,function(g){var f=g.idx===0?"":g.idx;t.TRACE_INIT(""+(0,pc.getProductionDslName)(g)+f,function(){var h=(0,Pa.buildLookaheadFuncForOr)(g.idx,i,g.maxLookahead||t.maxLookahead,g.hasPredicates,t.dynamicTokensEnabled,t.lookAheadBuilderForAlternatives),p=(0,Da.getKeyForAutomaticLookahead)(t.fullRuleNameToShort[i.name],Da.OR_IDX,g.idx);t.setLaFuncCache(p,h)})}),(0,eo.forEach)(o,function(g){t.computeLookaheadFunc(i,g.idx,Da.MANY_IDX,Pa.PROD_TYPE.REPETITION,g.maxLookahead,(0,pc.getProductionDslName)(g))}),(0,eo.forEach)(a,function(g){t.computeLookaheadFunc(i,g.idx,Da.OPTION_IDX,Pa.PROD_TYPE.OPTION,g.maxLookahead,(0,pc.getProductionDslName)(g))}),(0,eo.forEach)(l,function(g){t.computeLookaheadFunc(i,g.idx,Da.AT_LEAST_ONE_IDX,Pa.PROD_TYPE.REPETITION_MANDATORY,g.maxLookahead,(0,pc.getProductionDslName)(g))}),(0,eo.forEach)(c,function(g){t.computeLookaheadFunc(i,g.idx,Da.AT_LEAST_ONE_SEP_IDX,Pa.PROD_TYPE.REPETITION_MANDATORY_WITH_SEPARATOR,g.maxLookahead,(0,pc.getProductionDslName)(g))}),(0,eo.forEach)(u,function(g){t.computeLookaheadFunc(i,g.idx,Da.MANY_SEP_IDX,Pa.PROD_TYPE.REPETITION_WITH_SEPARATOR,g.maxLookahead,(0,pc.getProductionDslName)(g))})})})},r.prototype.computeLookaheadFunc=function(e,t,i,n,s,o){var a=this;this.TRACE_INIT(""+o+(t===0?"":t),function(){var l=(0,Pa.buildLookaheadFuncForOptionalProd)(t,e,s||a.maxLookahead,a.dynamicTokensEnabled,n,a.lookAheadBuilderForOptional),c=(0,Da.getKeyForAutomaticLookahead)(a.fullRuleNameToShort[e.name],i,t);a.setLaFuncCache(c,l)})},r.prototype.lookAheadBuilderForOptional=function(e,t,i){return(0,Pa.buildSingleAlternativeLookaheadFunction)(e,t,i)},r.prototype.lookAheadBuilderForAlternatives=function(e,t,i,n){return(0,Pa.buildAlternativesLookAheadFunc)(e,t,i,n)},r.prototype.getKeyForAutomaticLookahead=function(e,t){var i=this.getLastExplicitRuleShortName();return(0,Da.getKeyForAutomaticLookahead)(i,e,t)},r.prototype.getLaFuncFromCache=function(e){},r.prototype.getLaFuncFromMap=function(e){return this.lookAheadFuncsCache.get(e)},r.prototype.getLaFuncFromObj=function(e){return this.lookAheadFuncsCache[e]},r.prototype.setLaFuncCache=function(e,t){},r.prototype.setLaFuncCacheUsingMap=function(e,t){this.lookAheadFuncsCache.set(e,t)},r.prototype.setLaFuncUsingObj=function(e,t){this.lookAheadFuncsCache[e]=t},r}();hy.LooksAhead=nye});var yq=w(Lo=>{"use strict";Object.defineProperty(Lo,"__esModule",{value:!0});Lo.addNoneTerminalToCst=Lo.addTerminalToCst=Lo.setNodeLocationFull=Lo.setNodeLocationOnlyOffset=void 0;function sye(r,e){isNaN(r.startOffset)===!0?(r.startOffset=e.startOffset,r.endOffset=e.endOffset):r.endOffset{"use strict";Object.defineProperty(OA,"__esModule",{value:!0});OA.defineNameProp=OA.functionName=OA.classNameFromInstance=void 0;var lye=Gt();function cye(r){return Bq(r.constructor)}OA.classNameFromInstance=cye;var wq="name";function Bq(r){var e=r.name;return e||"anonymous"}OA.functionName=Bq;function uye(r,e){var t=Object.getOwnPropertyDescriptor(r,wq);return(0,lye.isUndefined)(t)||t.configurable?(Object.defineProperty(r,wq,{enumerable:!1,configurable:!0,writable:!1,value:e}),!0):!1}OA.defineNameProp=uye});var xq=w(Si=>{"use strict";Object.defineProperty(Si,"__esModule",{value:!0});Si.validateRedundantMethods=Si.validateMissingCstMethods=Si.validateVisitor=Si.CstVisitorDefinitionError=Si.createBaseVisitorConstructorWithDefaults=Si.createBaseSemanticVisitorConstructor=Si.defaultVisit=void 0;var fs=Gt(),Rd=ox();function Qq(r,e){for(var t=(0,fs.keys)(r),i=t.length,n=0;n: + `+(""+s.join(` + +`).replace(/\n/g,` + `)))}}};return t.prototype=i,t.prototype.constructor=t,t._RULE_NAMES=e,t}Si.createBaseSemanticVisitorConstructor=gye;function fye(r,e,t){var i=function(){};(0,Rd.defineNameProp)(i,r+"BaseSemanticsWithDefaults");var n=Object.create(t.prototype);return(0,fs.forEach)(e,function(s){n[s]=Qq}),i.prototype=n,i.prototype.constructor=i,i}Si.createBaseVisitorConstructorWithDefaults=fye;var ax;(function(r){r[r.REDUNDANT_METHOD=0]="REDUNDANT_METHOD",r[r.MISSING_METHOD=1]="MISSING_METHOD"})(ax=Si.CstVisitorDefinitionError||(Si.CstVisitorDefinitionError={}));function bq(r,e){var t=Sq(r,e),i=vq(r,e);return t.concat(i)}Si.validateVisitor=bq;function Sq(r,e){var t=(0,fs.map)(e,function(i){if(!(0,fs.isFunction)(r[i]))return{msg:"Missing visitor method: <"+i+"> on "+(0,Rd.functionName)(r.constructor)+" CST Visitor.",type:ax.MISSING_METHOD,methodName:i}});return(0,fs.compact)(t)}Si.validateMissingCstMethods=Sq;var hye=["constructor","visit","validateVisitor"];function vq(r,e){var t=[];for(var i in r)(0,fs.isFunction)(r[i])&&!(0,fs.contains)(hye,i)&&!(0,fs.contains)(e,i)&&t.push({msg:"Redundant visitor method: <"+i+"> on "+(0,Rd.functionName)(r.constructor)+` CST Visitor +There is no Grammar Rule corresponding to this method's name. +`,type:ax.REDUNDANT_METHOD,methodName:i});return t}Si.validateRedundantMethods=vq});var Dq=w(py=>{"use strict";Object.defineProperty(py,"__esModule",{value:!0});py.TreeBuilder=void 0;var tf=yq(),_r=Gt(),Pq=xq(),pye=Gn(),dye=function(){function r(){}return r.prototype.initTreeBuilder=function(e){if(this.CST_STACK=[],this.outputCst=e.outputCst,this.nodeLocationTracking=(0,_r.has)(e,"nodeLocationTracking")?e.nodeLocationTracking:pye.DEFAULT_PARSER_CONFIG.nodeLocationTracking,!this.outputCst)this.cstInvocationStateUpdate=_r.NOOP,this.cstFinallyStateUpdate=_r.NOOP,this.cstPostTerminal=_r.NOOP,this.cstPostNonTerminal=_r.NOOP,this.cstPostRule=_r.NOOP;else if(/full/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=tf.setNodeLocationFull,this.setNodeLocationFromNode=tf.setNodeLocationFull,this.cstPostRule=_r.NOOP,this.setInitialNodeLocation=this.setInitialNodeLocationFullRecovery):(this.setNodeLocationFromToken=_r.NOOP,this.setNodeLocationFromNode=_r.NOOP,this.cstPostRule=this.cstPostRuleFull,this.setInitialNodeLocation=this.setInitialNodeLocationFullRegular);else if(/onlyOffset/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=tf.setNodeLocationOnlyOffset,this.setNodeLocationFromNode=tf.setNodeLocationOnlyOffset,this.cstPostRule=_r.NOOP,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRecovery):(this.setNodeLocationFromToken=_r.NOOP,this.setNodeLocationFromNode=_r.NOOP,this.cstPostRule=this.cstPostRuleOnlyOffset,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRegular);else if(/none/i.test(this.nodeLocationTracking))this.setNodeLocationFromToken=_r.NOOP,this.setNodeLocationFromNode=_r.NOOP,this.cstPostRule=_r.NOOP,this.setInitialNodeLocation=_r.NOOP;else throw Error('Invalid config option: "'+e.nodeLocationTracking+'"')},r.prototype.setInitialNodeLocationOnlyOffsetRecovery=function(e){e.location={startOffset:NaN,endOffset:NaN}},r.prototype.setInitialNodeLocationOnlyOffsetRegular=function(e){e.location={startOffset:this.LA(1).startOffset,endOffset:NaN}},r.prototype.setInitialNodeLocationFullRecovery=function(e){e.location={startOffset:NaN,startLine:NaN,startColumn:NaN,endOffset:NaN,endLine:NaN,endColumn:NaN}},r.prototype.setInitialNodeLocationFullRegular=function(e){var t=this.LA(1);e.location={startOffset:t.startOffset,startLine:t.startLine,startColumn:t.startColumn,endOffset:NaN,endLine:NaN,endColumn:NaN}},r.prototype.cstInvocationStateUpdate=function(e,t){var i={name:e,children:{}};this.setInitialNodeLocation(i),this.CST_STACK.push(i)},r.prototype.cstFinallyStateUpdate=function(){this.CST_STACK.pop()},r.prototype.cstPostRuleFull=function(e){var t=this.LA(0),i=e.location;i.startOffset<=t.startOffset?(i.endOffset=t.endOffset,i.endLine=t.endLine,i.endColumn=t.endColumn):(i.startOffset=NaN,i.startLine=NaN,i.startColumn=NaN)},r.prototype.cstPostRuleOnlyOffset=function(e){var t=this.LA(0),i=e.location;i.startOffset<=t.startOffset?i.endOffset=t.endOffset:i.startOffset=NaN},r.prototype.cstPostTerminal=function(e,t){var i=this.CST_STACK[this.CST_STACK.length-1];(0,tf.addTerminalToCst)(i,t,e),this.setNodeLocationFromToken(i.location,t)},r.prototype.cstPostNonTerminal=function(e,t){var i=this.CST_STACK[this.CST_STACK.length-1];(0,tf.addNoneTerminalToCst)(i,t,e),this.setNodeLocationFromNode(i.location,e.location)},r.prototype.getBaseCstVisitorConstructor=function(){if((0,_r.isUndefined)(this.baseCstVisitorConstructor)){var e=(0,Pq.createBaseSemanticVisitorConstructor)(this.className,(0,_r.keys)(this.gastProductionsCache));return this.baseCstVisitorConstructor=e,e}return this.baseCstVisitorConstructor},r.prototype.getBaseCstVisitorConstructorWithDefaults=function(){if((0,_r.isUndefined)(this.baseCstVisitorWithDefaultsConstructor)){var e=(0,Pq.createBaseVisitorConstructorWithDefaults)(this.className,(0,_r.keys)(this.gastProductionsCache),this.getBaseCstVisitorConstructor());return this.baseCstVisitorWithDefaultsConstructor=e,e}return this.baseCstVisitorWithDefaultsConstructor},r.prototype.getLastExplicitRuleShortName=function(){var e=this.RULE_STACK;return e[e.length-1]},r.prototype.getPreviousExplicitRuleShortName=function(){var e=this.RULE_STACK;return e[e.length-2]},r.prototype.getLastExplicitRuleOccurrenceIndex=function(){var e=this.RULE_OCCURRENCE_STACK;return e[e.length-1]},r}();py.TreeBuilder=dye});var Rq=w(dy=>{"use strict";Object.defineProperty(dy,"__esModule",{value:!0});dy.LexerAdapter=void 0;var kq=Gn(),Cye=function(){function r(){}return r.prototype.initLexerAdapter=function(){this.tokVector=[],this.tokVectorLength=0,this.currIdx=-1},Object.defineProperty(r.prototype,"input",{get:function(){return this.tokVector},set:function(e){if(this.selfAnalysisDone!==!0)throw Error("Missing invocation at the end of the Parser's constructor.");this.reset(),this.tokVector=e,this.tokVectorLength=e.length},enumerable:!1,configurable:!0}),r.prototype.SKIP_TOKEN=function(){return this.currIdx<=this.tokVector.length-2?(this.consumeToken(),this.LA(1)):kq.END_OF_FILE},r.prototype.LA=function(e){var t=this.currIdx+e;return t<0||this.tokVectorLength<=t?kq.END_OF_FILE:this.tokVector[t]},r.prototype.consumeToken=function(){this.currIdx++},r.prototype.exportLexerState=function(){return this.currIdx},r.prototype.importLexerState=function(e){this.currIdx=e},r.prototype.resetLexerState=function(){this.currIdx=-1},r.prototype.moveToTerminatedState=function(){this.currIdx=this.tokVector.length-1},r.prototype.getLexerPosition=function(){return this.exportLexerState()},r}();dy.LexerAdapter=Cye});var Nq=w(Cy=>{"use strict";Object.defineProperty(Cy,"__esModule",{value:!0});Cy.RecognizerApi=void 0;var Fq=Gt(),mye=ef(),Ax=Gn(),Eye=Sd(),Iye=rx(),yye=dn(),wye=function(){function r(){}return r.prototype.ACTION=function(e){return e.call(this)},r.prototype.consume=function(e,t,i){return this.consumeInternal(t,e,i)},r.prototype.subrule=function(e,t,i){return this.subruleInternal(t,e,i)},r.prototype.option=function(e,t){return this.optionInternal(t,e)},r.prototype.or=function(e,t){return this.orInternal(t,e)},r.prototype.many=function(e,t){return this.manyInternal(e,t)},r.prototype.atLeastOne=function(e,t){return this.atLeastOneInternal(e,t)},r.prototype.CONSUME=function(e,t){return this.consumeInternal(e,0,t)},r.prototype.CONSUME1=function(e,t){return this.consumeInternal(e,1,t)},r.prototype.CONSUME2=function(e,t){return this.consumeInternal(e,2,t)},r.prototype.CONSUME3=function(e,t){return this.consumeInternal(e,3,t)},r.prototype.CONSUME4=function(e,t){return this.consumeInternal(e,4,t)},r.prototype.CONSUME5=function(e,t){return this.consumeInternal(e,5,t)},r.prototype.CONSUME6=function(e,t){return this.consumeInternal(e,6,t)},r.prototype.CONSUME7=function(e,t){return this.consumeInternal(e,7,t)},r.prototype.CONSUME8=function(e,t){return this.consumeInternal(e,8,t)},r.prototype.CONSUME9=function(e,t){return this.consumeInternal(e,9,t)},r.prototype.SUBRULE=function(e,t){return this.subruleInternal(e,0,t)},r.prototype.SUBRULE1=function(e,t){return this.subruleInternal(e,1,t)},r.prototype.SUBRULE2=function(e,t){return this.subruleInternal(e,2,t)},r.prototype.SUBRULE3=function(e,t){return this.subruleInternal(e,3,t)},r.prototype.SUBRULE4=function(e,t){return this.subruleInternal(e,4,t)},r.prototype.SUBRULE5=function(e,t){return this.subruleInternal(e,5,t)},r.prototype.SUBRULE6=function(e,t){return this.subruleInternal(e,6,t)},r.prototype.SUBRULE7=function(e,t){return this.subruleInternal(e,7,t)},r.prototype.SUBRULE8=function(e,t){return this.subruleInternal(e,8,t)},r.prototype.SUBRULE9=function(e,t){return this.subruleInternal(e,9,t)},r.prototype.OPTION=function(e){return this.optionInternal(e,0)},r.prototype.OPTION1=function(e){return this.optionInternal(e,1)},r.prototype.OPTION2=function(e){return this.optionInternal(e,2)},r.prototype.OPTION3=function(e){return this.optionInternal(e,3)},r.prototype.OPTION4=function(e){return this.optionInternal(e,4)},r.prototype.OPTION5=function(e){return this.optionInternal(e,5)},r.prototype.OPTION6=function(e){return this.optionInternal(e,6)},r.prototype.OPTION7=function(e){return this.optionInternal(e,7)},r.prototype.OPTION8=function(e){return this.optionInternal(e,8)},r.prototype.OPTION9=function(e){return this.optionInternal(e,9)},r.prototype.OR=function(e){return this.orInternal(e,0)},r.prototype.OR1=function(e){return this.orInternal(e,1)},r.prototype.OR2=function(e){return this.orInternal(e,2)},r.prototype.OR3=function(e){return this.orInternal(e,3)},r.prototype.OR4=function(e){return this.orInternal(e,4)},r.prototype.OR5=function(e){return this.orInternal(e,5)},r.prototype.OR6=function(e){return this.orInternal(e,6)},r.prototype.OR7=function(e){return this.orInternal(e,7)},r.prototype.OR8=function(e){return this.orInternal(e,8)},r.prototype.OR9=function(e){return this.orInternal(e,9)},r.prototype.MANY=function(e){this.manyInternal(0,e)},r.prototype.MANY1=function(e){this.manyInternal(1,e)},r.prototype.MANY2=function(e){this.manyInternal(2,e)},r.prototype.MANY3=function(e){this.manyInternal(3,e)},r.prototype.MANY4=function(e){this.manyInternal(4,e)},r.prototype.MANY5=function(e){this.manyInternal(5,e)},r.prototype.MANY6=function(e){this.manyInternal(6,e)},r.prototype.MANY7=function(e){this.manyInternal(7,e)},r.prototype.MANY8=function(e){this.manyInternal(8,e)},r.prototype.MANY9=function(e){this.manyInternal(9,e)},r.prototype.MANY_SEP=function(e){this.manySepFirstInternal(0,e)},r.prototype.MANY_SEP1=function(e){this.manySepFirstInternal(1,e)},r.prototype.MANY_SEP2=function(e){this.manySepFirstInternal(2,e)},r.prototype.MANY_SEP3=function(e){this.manySepFirstInternal(3,e)},r.prototype.MANY_SEP4=function(e){this.manySepFirstInternal(4,e)},r.prototype.MANY_SEP5=function(e){this.manySepFirstInternal(5,e)},r.prototype.MANY_SEP6=function(e){this.manySepFirstInternal(6,e)},r.prototype.MANY_SEP7=function(e){this.manySepFirstInternal(7,e)},r.prototype.MANY_SEP8=function(e){this.manySepFirstInternal(8,e)},r.prototype.MANY_SEP9=function(e){this.manySepFirstInternal(9,e)},r.prototype.AT_LEAST_ONE=function(e){this.atLeastOneInternal(0,e)},r.prototype.AT_LEAST_ONE1=function(e){return this.atLeastOneInternal(1,e)},r.prototype.AT_LEAST_ONE2=function(e){this.atLeastOneInternal(2,e)},r.prototype.AT_LEAST_ONE3=function(e){this.atLeastOneInternal(3,e)},r.prototype.AT_LEAST_ONE4=function(e){this.atLeastOneInternal(4,e)},r.prototype.AT_LEAST_ONE5=function(e){this.atLeastOneInternal(5,e)},r.prototype.AT_LEAST_ONE6=function(e){this.atLeastOneInternal(6,e)},r.prototype.AT_LEAST_ONE7=function(e){this.atLeastOneInternal(7,e)},r.prototype.AT_LEAST_ONE8=function(e){this.atLeastOneInternal(8,e)},r.prototype.AT_LEAST_ONE9=function(e){this.atLeastOneInternal(9,e)},r.prototype.AT_LEAST_ONE_SEP=function(e){this.atLeastOneSepFirstInternal(0,e)},r.prototype.AT_LEAST_ONE_SEP1=function(e){this.atLeastOneSepFirstInternal(1,e)},r.prototype.AT_LEAST_ONE_SEP2=function(e){this.atLeastOneSepFirstInternal(2,e)},r.prototype.AT_LEAST_ONE_SEP3=function(e){this.atLeastOneSepFirstInternal(3,e)},r.prototype.AT_LEAST_ONE_SEP4=function(e){this.atLeastOneSepFirstInternal(4,e)},r.prototype.AT_LEAST_ONE_SEP5=function(e){this.atLeastOneSepFirstInternal(5,e)},r.prototype.AT_LEAST_ONE_SEP6=function(e){this.atLeastOneSepFirstInternal(6,e)},r.prototype.AT_LEAST_ONE_SEP7=function(e){this.atLeastOneSepFirstInternal(7,e)},r.prototype.AT_LEAST_ONE_SEP8=function(e){this.atLeastOneSepFirstInternal(8,e)},r.prototype.AT_LEAST_ONE_SEP9=function(e){this.atLeastOneSepFirstInternal(9,e)},r.prototype.RULE=function(e,t,i){if(i===void 0&&(i=Ax.DEFAULT_RULE_CONFIG),(0,Fq.contains)(this.definedRulesNames,e)){var n=Eye.defaultGrammarValidatorErrorProvider.buildDuplicateRuleNameError({topLevelRule:e,grammarName:this.className}),s={message:n,type:Ax.ParserDefinitionErrorType.DUPLICATE_RULE_NAME,ruleName:e};this.definitionErrors.push(s)}this.definedRulesNames.push(e);var o=this.defineRule(e,t,i);return this[e]=o,o},r.prototype.OVERRIDE_RULE=function(e,t,i){i===void 0&&(i=Ax.DEFAULT_RULE_CONFIG);var n=[];n=n.concat((0,Iye.validateRuleIsOverridden)(e,this.definedRulesNames,this.className)),this.definitionErrors=this.definitionErrors.concat(n);var s=this.defineRule(e,t,i);return this[e]=s,s},r.prototype.BACKTRACK=function(e,t){return function(){this.isBackTrackingStack.push(1);var i=this.saveRecogState();try{return e.apply(this,t),!0}catch(n){if((0,mye.isRecognitionException)(n))return!1;throw n}finally{this.reloadRecogState(i),this.isBackTrackingStack.pop()}}},r.prototype.getGAstProductions=function(){return this.gastProductionsCache},r.prototype.getSerializedGastProductions=function(){return(0,yye.serializeGrammar)((0,Fq.values)(this.gastProductionsCache))},r}();Cy.RecognizerApi=wye});var Mq=w(Ey=>{"use strict";Object.defineProperty(Ey,"__esModule",{value:!0});Ey.RecognizerEngine=void 0;var Pr=Gt(),Yn=fy(),my=ef(),Lq=Pd(),rf=xd(),Tq=Gn(),Bye=sx(),Oq=NA(),Fd=Vg(),Qye=ox(),bye=function(){function r(){}return r.prototype.initRecognizerEngine=function(e,t){if(this.className=(0,Qye.classNameFromInstance)(this),this.shortRuleNameToFull={},this.fullRuleNameToShort={},this.ruleShortNameIdx=256,this.tokenMatcher=Fd.tokenStructuredMatcherNoCategories,this.definedRulesNames=[],this.tokensMap={},this.isBackTrackingStack=[],this.RULE_STACK=[],this.RULE_OCCURRENCE_STACK=[],this.gastProductionsCache={},(0,Pr.has)(t,"serializedGrammar"))throw Error(`The Parser's configuration can no longer contain a property. + See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_6-0-0 + For Further details.`);if((0,Pr.isArray)(e)){if((0,Pr.isEmpty)(e))throw Error(`A Token Vocabulary cannot be empty. + Note that the first argument for the parser constructor + is no longer a Token vector (since v4.0).`);if(typeof e[0].startOffset=="number")throw Error(`The Parser constructor no longer accepts a token vector as the first argument. + See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_4-0-0 + For Further details.`)}if((0,Pr.isArray)(e))this.tokensMap=(0,Pr.reduce)(e,function(o,a){return o[a.name]=a,o},{});else if((0,Pr.has)(e,"modes")&&(0,Pr.every)((0,Pr.flatten)((0,Pr.values)(e.modes)),Fd.isTokenType)){var i=(0,Pr.flatten)((0,Pr.values)(e.modes)),n=(0,Pr.uniq)(i);this.tokensMap=(0,Pr.reduce)(n,function(o,a){return o[a.name]=a,o},{})}else if((0,Pr.isObject)(e))this.tokensMap=(0,Pr.cloneObj)(e);else throw new Error(" argument must be An Array of Token constructors, A dictionary of Token constructors or an IMultiModeLexerDefinition");this.tokensMap.EOF=Oq.EOF;var s=(0,Pr.every)((0,Pr.values)(e),function(o){return(0,Pr.isEmpty)(o.categoryMatches)});this.tokenMatcher=s?Fd.tokenStructuredMatcherNoCategories:Fd.tokenStructuredMatcher,(0,Fd.augmentTokenTypes)((0,Pr.values)(this.tokensMap))},r.prototype.defineRule=function(e,t,i){if(this.selfAnalysisDone)throw Error("Grammar rule <"+e+`> may not be defined after the 'performSelfAnalysis' method has been called' +Make sure that all grammar rule definitions are done before 'performSelfAnalysis' is called.`);var n=(0,Pr.has)(i,"resyncEnabled")?i.resyncEnabled:Tq.DEFAULT_RULE_CONFIG.resyncEnabled,s=(0,Pr.has)(i,"recoveryValueFunc")?i.recoveryValueFunc:Tq.DEFAULT_RULE_CONFIG.recoveryValueFunc,o=this.ruleShortNameIdx<t},r.prototype.orInternal=function(e,t){var i=this.getKeyForAutomaticLookahead(Yn.OR_IDX,t),n=(0,Pr.isArray)(e)?e:e.DEF,s=this.getLaFuncFromCache(i),o=s.call(this,n);if(o!==void 0){var a=n[o];return a.ALT.call(this)}this.raiseNoAltException(t,e.ERR_MSG)},r.prototype.ruleFinallyStateUpdate=function(){if(this.RULE_STACK.pop(),this.RULE_OCCURRENCE_STACK.pop(),this.cstFinallyStateUpdate(),this.RULE_STACK.length===0&&this.isAtEndOfInput()===!1){var e=this.LA(1),t=this.errorMessageProvider.buildNotAllInputParsedMessage({firstRedundant:e,ruleName:this.getCurrRuleFullName()});this.SAVE_ERROR(new my.NotAllInputParsedException(t,e))}},r.prototype.subruleInternal=function(e,t,i){var n;try{var s=i!==void 0?i.ARGS:void 0;return n=e.call(this,t,s),this.cstPostNonTerminal(n,i!==void 0&&i.LABEL!==void 0?i.LABEL:e.ruleName),n}catch(o){this.subruleInternalError(o,i,e.ruleName)}},r.prototype.subruleInternalError=function(e,t,i){throw(0,my.isRecognitionException)(e)&&e.partialCstResult!==void 0&&(this.cstPostNonTerminal(e.partialCstResult,t!==void 0&&t.LABEL!==void 0?t.LABEL:i),delete e.partialCstResult),e},r.prototype.consumeInternal=function(e,t,i){var n;try{var s=this.LA(1);this.tokenMatcher(s,e)===!0?(this.consumeToken(),n=s):this.consumeInternalError(e,s,i)}catch(o){n=this.consumeInternalRecovery(e,t,o)}return this.cstPostTerminal(i!==void 0&&i.LABEL!==void 0?i.LABEL:e.name,n),n},r.prototype.consumeInternalError=function(e,t,i){var n,s=this.LA(0);throw i!==void 0&&i.ERR_MSG?n=i.ERR_MSG:n=this.errorMessageProvider.buildMismatchTokenMessage({expected:e,actual:t,previous:s,ruleName:this.getCurrRuleFullName()}),this.SAVE_ERROR(new my.MismatchedTokenException(n,t,s))},r.prototype.consumeInternalRecovery=function(e,t,i){if(this.recoveryEnabled&&i.name==="MismatchedTokenException"&&!this.isBackTracking()){var n=this.getFollowsForInRuleRecovery(e,t);try{return this.tryInRuleRecovery(e,n)}catch(s){throw s.name===Bye.IN_RULE_RECOVERY_EXCEPTION?i:s}}else throw i},r.prototype.saveRecogState=function(){var e=this.errors,t=(0,Pr.cloneArr)(this.RULE_STACK);return{errors:e,lexerState:this.exportLexerState(),RULE_STACK:t,CST_STACK:this.CST_STACK}},r.prototype.reloadRecogState=function(e){this.errors=e.errors,this.importLexerState(e.lexerState),this.RULE_STACK=e.RULE_STACK},r.prototype.ruleInvocationStateUpdate=function(e,t,i){this.RULE_OCCURRENCE_STACK.push(i),this.RULE_STACK.push(e),this.cstInvocationStateUpdate(t,e)},r.prototype.isBackTracking=function(){return this.isBackTrackingStack.length!==0},r.prototype.getCurrRuleFullName=function(){var e=this.getLastExplicitRuleShortName();return this.shortRuleNameToFull[e]},r.prototype.shortRuleNameToFullName=function(e){return this.shortRuleNameToFull[e]},r.prototype.isAtEndOfInput=function(){return this.tokenMatcher(this.LA(1),Oq.EOF)},r.prototype.reset=function(){this.resetLexerState(),this.isBackTrackingStack=[],this.errors=[],this.RULE_STACK=[],this.CST_STACK=[],this.RULE_OCCURRENCE_STACK=[]},r}();Ey.RecognizerEngine=bye});var Uq=w(Iy=>{"use strict";Object.defineProperty(Iy,"__esModule",{value:!0});Iy.ErrorHandler=void 0;var lx=ef(),cx=Gt(),Kq=Pd(),Sye=Gn(),vye=function(){function r(){}return r.prototype.initErrorHandler=function(e){this._errors=[],this.errorMessageProvider=(0,cx.has)(e,"errorMessageProvider")?e.errorMessageProvider:Sye.DEFAULT_PARSER_CONFIG.errorMessageProvider},r.prototype.SAVE_ERROR=function(e){if((0,lx.isRecognitionException)(e))return e.context={ruleStack:this.getHumanReadableRuleStack(),ruleOccurrenceStack:(0,cx.cloneArr)(this.RULE_OCCURRENCE_STACK)},this._errors.push(e),e;throw Error("Trying to save an Error which is not a RecognitionException")},Object.defineProperty(r.prototype,"errors",{get:function(){return(0,cx.cloneArr)(this._errors)},set:function(e){this._errors=e},enumerable:!1,configurable:!0}),r.prototype.raiseEarlyExitException=function(e,t,i){for(var n=this.getCurrRuleFullName(),s=this.getGAstProductions()[n],o=(0,Kq.getLookaheadPathsForOptionalProd)(e,s,t,this.maxLookahead),a=o[0],l=[],c=1;c<=this.maxLookahead;c++)l.push(this.LA(c));var u=this.errorMessageProvider.buildEarlyExitMessage({expectedIterationPaths:a,actual:l,previous:this.LA(0),customUserDescription:i,ruleName:n});throw this.SAVE_ERROR(new lx.EarlyExitException(u,this.LA(1),this.LA(0)))},r.prototype.raiseNoAltException=function(e,t){for(var i=this.getCurrRuleFullName(),n=this.getGAstProductions()[i],s=(0,Kq.getLookaheadPathsForOr)(e,n,this.maxLookahead),o=[],a=1;a<=this.maxLookahead;a++)o.push(this.LA(a));var l=this.LA(0),c=this.errorMessageProvider.buildNoViableAltMessage({expectedPathsPerAlt:s,actual:o,previous:l,customUserDescription:t,ruleName:this.getCurrRuleFullName()});throw this.SAVE_ERROR(new lx.NoViableAltException(c,this.LA(1),l))},r}();Iy.ErrorHandler=vye});var Yq=w(yy=>{"use strict";Object.defineProperty(yy,"__esModule",{value:!0});yy.ContentAssist=void 0;var Hq=xd(),Gq=Gt(),xye=function(){function r(){}return r.prototype.initContentAssist=function(){},r.prototype.computeContentAssist=function(e,t){var i=this.gastProductionsCache[e];if((0,Gq.isUndefined)(i))throw Error("Rule ->"+e+"<- does not exist in this grammar.");return(0,Hq.nextPossibleTokensAfter)([i],t,this.tokenMatcher,this.maxLookahead)},r.prototype.getNextPossibleTokenTypes=function(e){var t=(0,Gq.first)(e.ruleStack),i=this.getGAstProductions(),n=i[t],s=new Hq.NextAfterTokenWalker(n,e).startWalking();return s},r}();yy.ContentAssist=xye});var Zq=w(Qy=>{"use strict";Object.defineProperty(Qy,"__esModule",{value:!0});Qy.GastRecorder=void 0;var En=Gt(),To=dn(),Pye=yd(),Wq=Vg(),zq=NA(),Dye=Gn(),kye=fy(),By={description:"This Object indicates the Parser is during Recording Phase"};Object.freeze(By);var jq=!0,qq=Math.pow(2,kye.BITS_FOR_OCCURRENCE_IDX)-1,Vq=(0,zq.createToken)({name:"RECORDING_PHASE_TOKEN",pattern:Pye.Lexer.NA});(0,Wq.augmentTokenTypes)([Vq]);var Xq=(0,zq.createTokenInstance)(Vq,`This IToken indicates the Parser is in Recording Phase + See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,-1,-1,-1,-1,-1,-1);Object.freeze(Xq);var Rye={name:`This CSTNode indicates the Parser is in Recording Phase + See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,children:{}},Fye=function(){function r(){}return r.prototype.initGastRecorder=function(e){this.recordingProdStack=[],this.RECORDING_PHASE=!1},r.prototype.enableRecording=function(){var e=this;this.RECORDING_PHASE=!0,this.TRACE_INIT("Enable Recording",function(){for(var t=function(n){var s=n>0?n:"";e["CONSUME"+s]=function(o,a){return this.consumeInternalRecord(o,n,a)},e["SUBRULE"+s]=function(o,a){return this.subruleInternalRecord(o,n,a)},e["OPTION"+s]=function(o){return this.optionInternalRecord(o,n)},e["OR"+s]=function(o){return this.orInternalRecord(o,n)},e["MANY"+s]=function(o){this.manyInternalRecord(n,o)},e["MANY_SEP"+s]=function(o){this.manySepFirstInternalRecord(n,o)},e["AT_LEAST_ONE"+s]=function(o){this.atLeastOneInternalRecord(n,o)},e["AT_LEAST_ONE_SEP"+s]=function(o){this.atLeastOneSepFirstInternalRecord(n,o)}},i=0;i<10;i++)t(i);e.consume=function(n,s,o){return this.consumeInternalRecord(s,n,o)},e.subrule=function(n,s,o){return this.subruleInternalRecord(s,n,o)},e.option=function(n,s){return this.optionInternalRecord(s,n)},e.or=function(n,s){return this.orInternalRecord(s,n)},e.many=function(n,s){this.manyInternalRecord(n,s)},e.atLeastOne=function(n,s){this.atLeastOneInternalRecord(n,s)},e.ACTION=e.ACTION_RECORD,e.BACKTRACK=e.BACKTRACK_RECORD,e.LA=e.LA_RECORD})},r.prototype.disableRecording=function(){var e=this;this.RECORDING_PHASE=!1,this.TRACE_INIT("Deleting Recording methods",function(){for(var t=0;t<10;t++){var i=t>0?t:"";delete e["CONSUME"+i],delete e["SUBRULE"+i],delete e["OPTION"+i],delete e["OR"+i],delete e["MANY"+i],delete e["MANY_SEP"+i],delete e["AT_LEAST_ONE"+i],delete e["AT_LEAST_ONE_SEP"+i]}delete e.consume,delete e.subrule,delete e.option,delete e.or,delete e.many,delete e.atLeastOne,delete e.ACTION,delete e.BACKTRACK,delete e.LA})},r.prototype.ACTION_RECORD=function(e){},r.prototype.BACKTRACK_RECORD=function(e,t){return function(){return!0}},r.prototype.LA_RECORD=function(e){return Dye.END_OF_FILE},r.prototype.topLevelRuleRecord=function(e,t){try{var i=new To.Rule({definition:[],name:e});return i.name=e,this.recordingProdStack.push(i),t.call(this),this.recordingProdStack.pop(),i}catch(n){if(n.KNOWN_RECORDER_ERROR!==!0)try{n.message=n.message+` + This error was thrown during the "grammar recording phase" For more info see: + https://chevrotain.io/docs/guide/internals.html#grammar-recording`}catch{throw n}throw n}},r.prototype.optionInternalRecord=function(e,t){return Nd.call(this,To.Option,e,t)},r.prototype.atLeastOneInternalRecord=function(e,t){Nd.call(this,To.RepetitionMandatory,t,e)},r.prototype.atLeastOneSepFirstInternalRecord=function(e,t){Nd.call(this,To.RepetitionMandatoryWithSeparator,t,e,jq)},r.prototype.manyInternalRecord=function(e,t){Nd.call(this,To.Repetition,t,e)},r.prototype.manySepFirstInternalRecord=function(e,t){Nd.call(this,To.RepetitionWithSeparator,t,e,jq)},r.prototype.orInternalRecord=function(e,t){return Nye.call(this,e,t)},r.prototype.subruleInternalRecord=function(e,t,i){if(wy(t),!e||(0,En.has)(e,"ruleName")===!1){var n=new Error(" argument is invalid"+(" expecting a Parser method reference but got: <"+JSON.stringify(e)+">")+(` + inside top level rule: <`+this.recordingProdStack[0].name+">"));throw n.KNOWN_RECORDER_ERROR=!0,n}var s=(0,En.peek)(this.recordingProdStack),o=e.ruleName,a=new To.NonTerminal({idx:t,nonTerminalName:o,label:i==null?void 0:i.LABEL,referencedRule:void 0});return s.definition.push(a),this.outputCst?Rye:By},r.prototype.consumeInternalRecord=function(e,t,i){if(wy(t),!(0,Wq.hasShortKeyProperty)(e)){var n=new Error(" argument is invalid"+(" expecting a TokenType reference but got: <"+JSON.stringify(e)+">")+(` + inside top level rule: <`+this.recordingProdStack[0].name+">"));throw n.KNOWN_RECORDER_ERROR=!0,n}var s=(0,En.peek)(this.recordingProdStack),o=new To.Terminal({idx:t,terminalType:e,label:i==null?void 0:i.LABEL});return s.definition.push(o),Xq},r}();Qy.GastRecorder=Fye;function Nd(r,e,t,i){i===void 0&&(i=!1),wy(t);var n=(0,En.peek)(this.recordingProdStack),s=(0,En.isFunction)(e)?e:e.DEF,o=new r({definition:[],idx:t});return i&&(o.separator=e.SEP),(0,En.has)(e,"MAX_LOOKAHEAD")&&(o.maxLookahead=e.MAX_LOOKAHEAD),this.recordingProdStack.push(o),s.call(this),n.definition.push(o),this.recordingProdStack.pop(),By}function Nye(r,e){var t=this;wy(e);var i=(0,En.peek)(this.recordingProdStack),n=(0,En.isArray)(r)===!1,s=n===!1?r:r.DEF,o=new To.Alternation({definition:[],idx:e,ignoreAmbiguities:n&&r.IGNORE_AMBIGUITIES===!0});(0,En.has)(r,"MAX_LOOKAHEAD")&&(o.maxLookahead=r.MAX_LOOKAHEAD);var a=(0,En.some)(s,function(l){return(0,En.isFunction)(l.GATE)});return o.hasPredicates=a,i.definition.push(o),(0,En.forEach)(s,function(l){var c=new To.Alternative({definition:[]});o.definition.push(c),(0,En.has)(l,"IGNORE_AMBIGUITIES")?c.ignoreAmbiguities=l.IGNORE_AMBIGUITIES:(0,En.has)(l,"GATE")&&(c.ignoreAmbiguities=!0),t.recordingProdStack.push(c),l.ALT.call(t),t.recordingProdStack.pop()}),By}function Jq(r){return r===0?"":""+r}function wy(r){if(r<0||r>qq){var e=new Error("Invalid DSL Method idx value: <"+r+`> + `+("Idx value must be a none negative value smaller than "+(qq+1)));throw e.KNOWN_RECORDER_ERROR=!0,e}}});var $q=w(by=>{"use strict";Object.defineProperty(by,"__esModule",{value:!0});by.PerformanceTracer=void 0;var _q=Gt(),Lye=Gn(),Tye=function(){function r(){}return r.prototype.initPerformanceTracer=function(e){if((0,_q.has)(e,"traceInitPerf")){var t=e.traceInitPerf,i=typeof t=="number";this.traceInitMaxIdent=i?t:1/0,this.traceInitPerf=i?t>0:t}else this.traceInitMaxIdent=0,this.traceInitPerf=Lye.DEFAULT_PARSER_CONFIG.traceInitPerf;this.traceInitIndent=-1},r.prototype.TRACE_INIT=function(e,t){if(this.traceInitPerf===!0){this.traceInitIndent++;var i=new Array(this.traceInitIndent+1).join(" ");this.traceInitIndent <"+e+">");var n=(0,_q.timer)(t),s=n.time,o=n.value,a=s>10?console.warn:console.log;return this.traceInitIndent time: "+s+"ms"),this.traceInitIndent--,o}else return t()},r}();by.PerformanceTracer=Tye});var eJ=w(Sy=>{"use strict";Object.defineProperty(Sy,"__esModule",{value:!0});Sy.applyMixins=void 0;function Oye(r,e){e.forEach(function(t){var i=t.prototype;Object.getOwnPropertyNames(i).forEach(function(n){if(n!=="constructor"){var s=Object.getOwnPropertyDescriptor(i,n);s&&(s.get||s.set)?Object.defineProperty(r.prototype,n,s):r.prototype[n]=t.prototype[n]}})})}Sy.applyMixins=Oye});var Gn=w(dr=>{"use strict";var iJ=dr&&dr.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(dr,"__esModule",{value:!0});dr.EmbeddedActionsParser=dr.CstParser=dr.Parser=dr.EMPTY_ALT=dr.ParserDefinitionErrorType=dr.DEFAULT_RULE_CONFIG=dr.DEFAULT_PARSER_CONFIG=dr.END_OF_FILE=void 0;var _i=Gt(),Mye=Uj(),tJ=NA(),nJ=Sd(),rJ=gq(),Kye=sx(),Uye=Iq(),Hye=Dq(),Gye=Rq(),Yye=Nq(),jye=Mq(),qye=Uq(),Jye=Yq(),Wye=Zq(),zye=$q(),Vye=eJ();dr.END_OF_FILE=(0,tJ.createTokenInstance)(tJ.EOF,"",NaN,NaN,NaN,NaN,NaN,NaN);Object.freeze(dr.END_OF_FILE);dr.DEFAULT_PARSER_CONFIG=Object.freeze({recoveryEnabled:!1,maxLookahead:3,dynamicTokensEnabled:!1,outputCst:!0,errorMessageProvider:nJ.defaultParserErrorProvider,nodeLocationTracking:"none",traceInitPerf:!1,skipValidations:!1});dr.DEFAULT_RULE_CONFIG=Object.freeze({recoveryValueFunc:function(){},resyncEnabled:!0});var Xye;(function(r){r[r.INVALID_RULE_NAME=0]="INVALID_RULE_NAME",r[r.DUPLICATE_RULE_NAME=1]="DUPLICATE_RULE_NAME",r[r.INVALID_RULE_OVERRIDE=2]="INVALID_RULE_OVERRIDE",r[r.DUPLICATE_PRODUCTIONS=3]="DUPLICATE_PRODUCTIONS",r[r.UNRESOLVED_SUBRULE_REF=4]="UNRESOLVED_SUBRULE_REF",r[r.LEFT_RECURSION=5]="LEFT_RECURSION",r[r.NONE_LAST_EMPTY_ALT=6]="NONE_LAST_EMPTY_ALT",r[r.AMBIGUOUS_ALTS=7]="AMBIGUOUS_ALTS",r[r.CONFLICT_TOKENS_RULES_NAMESPACE=8]="CONFLICT_TOKENS_RULES_NAMESPACE",r[r.INVALID_TOKEN_NAME=9]="INVALID_TOKEN_NAME",r[r.NO_NON_EMPTY_LOOKAHEAD=10]="NO_NON_EMPTY_LOOKAHEAD",r[r.AMBIGUOUS_PREFIX_ALTS=11]="AMBIGUOUS_PREFIX_ALTS",r[r.TOO_MANY_ALTS=12]="TOO_MANY_ALTS"})(Xye=dr.ParserDefinitionErrorType||(dr.ParserDefinitionErrorType={}));function Zye(r){return r===void 0&&(r=void 0),function(){return r}}dr.EMPTY_ALT=Zye;var vy=function(){function r(e,t){this.definitionErrors=[],this.selfAnalysisDone=!1;var i=this;if(i.initErrorHandler(t),i.initLexerAdapter(),i.initLooksAhead(t),i.initRecognizerEngine(e,t),i.initRecoverable(t),i.initTreeBuilder(t),i.initContentAssist(),i.initGastRecorder(t),i.initPerformanceTracer(t),(0,_i.has)(t,"ignoredIssues"))throw new Error(`The IParserConfig property has been deprecated. + Please use the flag on the relevant DSL method instead. + See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#IGNORING_AMBIGUITIES + For further details.`);this.skipValidations=(0,_i.has)(t,"skipValidations")?t.skipValidations:dr.DEFAULT_PARSER_CONFIG.skipValidations}return r.performSelfAnalysis=function(e){throw Error("The **static** `performSelfAnalysis` method has been deprecated. \nUse the **instance** method with the same name instead.")},r.prototype.performSelfAnalysis=function(){var e=this;this.TRACE_INIT("performSelfAnalysis",function(){var t;e.selfAnalysisDone=!0;var i=e.className;e.TRACE_INIT("toFastProps",function(){(0,_i.toFastProperties)(e)}),e.TRACE_INIT("Grammar Recording",function(){try{e.enableRecording(),(0,_i.forEach)(e.definedRulesNames,function(s){var o=e[s],a=o.originalGrammarAction,l=void 0;e.TRACE_INIT(s+" Rule",function(){l=e.topLevelRuleRecord(s,a)}),e.gastProductionsCache[s]=l})}finally{e.disableRecording()}});var n=[];if(e.TRACE_INIT("Grammar Resolving",function(){n=(0,rJ.resolveGrammar)({rules:(0,_i.values)(e.gastProductionsCache)}),e.definitionErrors=e.definitionErrors.concat(n)}),e.TRACE_INIT("Grammar Validations",function(){if((0,_i.isEmpty)(n)&&e.skipValidations===!1){var s=(0,rJ.validateGrammar)({rules:(0,_i.values)(e.gastProductionsCache),maxLookahead:e.maxLookahead,tokenTypes:(0,_i.values)(e.tokensMap),errMsgProvider:nJ.defaultGrammarValidatorErrorProvider,grammarName:i});e.definitionErrors=e.definitionErrors.concat(s)}}),(0,_i.isEmpty)(e.definitionErrors)&&(e.recoveryEnabled&&e.TRACE_INIT("computeAllProdsFollows",function(){var s=(0,Mye.computeAllProdsFollows)((0,_i.values)(e.gastProductionsCache));e.resyncFollows=s}),e.TRACE_INIT("ComputeLookaheadFunctions",function(){e.preComputeLookaheadFunctions((0,_i.values)(e.gastProductionsCache))})),!r.DEFER_DEFINITION_ERRORS_HANDLING&&!(0,_i.isEmpty)(e.definitionErrors))throw t=(0,_i.map)(e.definitionErrors,function(s){return s.message}),new Error(`Parser Definition Errors detected: + `+t.join(` +------------------------------- +`))})},r.DEFER_DEFINITION_ERRORS_HANDLING=!1,r}();dr.Parser=vy;(0,Vye.applyMixins)(vy,[Kye.Recoverable,Uye.LooksAhead,Hye.TreeBuilder,Gye.LexerAdapter,jye.RecognizerEngine,Yye.RecognizerApi,qye.ErrorHandler,Jye.ContentAssist,Wye.GastRecorder,zye.PerformanceTracer]);var _ye=function(r){iJ(e,r);function e(t,i){i===void 0&&(i=dr.DEFAULT_PARSER_CONFIG);var n=this,s=(0,_i.cloneObj)(i);return s.outputCst=!0,n=r.call(this,t,s)||this,n}return e}(vy);dr.CstParser=_ye;var $ye=function(r){iJ(e,r);function e(t,i){i===void 0&&(i=dr.DEFAULT_PARSER_CONFIG);var n=this,s=(0,_i.cloneObj)(i);return s.outputCst=!1,n=r.call(this,t,s)||this,n}return e}(vy);dr.EmbeddedActionsParser=$ye});var oJ=w(xy=>{"use strict";Object.defineProperty(xy,"__esModule",{value:!0});xy.createSyntaxDiagramsCode=void 0;var sJ=Dv();function ewe(r,e){var t=e===void 0?{}:e,i=t.resourceBase,n=i===void 0?"https://unpkg.com/chevrotain@"+sJ.VERSION+"/diagrams/":i,s=t.css,o=s===void 0?"https://unpkg.com/chevrotain@"+sJ.VERSION+"/diagrams/diagrams.css":s,a=` + + + + + +`,l=` + +`,c=` + + + + +
+ + diff --git a/apps/examples/www/manifest.json b/apps/examples/www/manifest.json new file mode 100644 index 000000000..1b75c65a4 --- /dev/null +++ b/apps/examples/www/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "tldraw", + "short_name": "tldraw", + "icons": [ + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/android-chrome-maskable-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/android-chrome-maskable-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "start_url": "/", + "display": "standalone", + "orientation": "portrait" +} diff --git a/apps/examples/www/social-image.png b/apps/examples/www/social-image.png new file mode 100644 index 000000000..d2863ddaa Binary files /dev/null and b/apps/examples/www/social-image.png differ diff --git a/apps/examples/www/tldraw.svg b/apps/examples/www/tldraw.svg new file mode 100644 index 000000000..f0c9e1f94 --- /dev/null +++ b/apps/examples/www/tldraw.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/vscode/README.md b/apps/vscode/README.md new file mode 100644 index 000000000..482b69fad --- /dev/null +++ b/apps/vscode/README.md @@ -0,0 +1,87 @@ +# @tldraw/vscode + +This folder contains the source for the tldraw VS Code extension. + +## Developing + +## 1. Install dependencies + +- Run `yarn` from the root folder + +## 2. Start the editor + +In the root folder: + +- Run `yarn dev:vscode`. + +This will start the development server for the `apps/vscode/editor` project and open the `apps/vscode/extension` folder in a new VS Code window. + +In the `apps/vscode/extension` window, open the terminal and: + +- Install dependencies (`yarn`) +- Start the VS Code debugger (`Menu > Run > Start Debugging` or by pressing `F5`). This will open another VS Code window with the extension running. + +Open a `.tldr` file from the file explorer or create a new `.tldr` file from the command palette. + +## 3. Debugging +You can use standard debugging techniques like `console.log`, which will be displayed in the VS Code window with the extension running. It will display logs both from the Extension and the Editor. VS Code editor with the Extension folder will show more detailed logs from the Extension project. You can also use a debugger. + +The code is hot-reloaded, so the developer experience is quite nice. + +## Publishing + +Update the `CHANGELOG.md` with the new version number and the changes. + +To publish: + +- Install `vsce` globally +- Run `vsce login tldraw-org` and sign in. For this to work you need to create a [personal access token](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token) and you also need to be added to the `tldraw-org` organization on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage). + +In the `apps/vscode/extension` folder: +- Run `yarn package` +- Run `yarn publish` + +## Project overview + +The Visual Studio Code extension is made of two projects: + +### 1. Extension project +Extension project is under `apps/vscode/extension` and contains the code needed to run a VS Code Extension - it implements the required VS Code interfaces so that VS Code can call our extension and start running it. + +It registers the command for generating a new `.tldr` file, custom editor for `.tldr` files, and it communicates with the WebViews that run `@tldraw/editor` (more on this later on). + +VS Code Extension API offers two ways for adding [new editors](https://code.visualstudio.com/api/extension-guides/custom-editors): `CustomEditor` and `CustomTextEditor`. We are using [`CustomEditor`](https://code.visualstudio.com/api/extension-guides/custom-editors#custom-editor), even though it means we have to do a bit more work and maintain the contents of the document ourselves. This allows us to better support features like `undo`, `redo`, and `revert`, since we are in complete control of the contents of the document. + +The custom editor logic lives in `TldrawDocument`, where we handle all the required custom editor operations like reading the file from disk, saving the file, backups, reverting, etc. When a `.tldr` file is opened a new instance of a `TldrawDocument` is created and this instance then serves as the underlying document model for displaying in the VS Code editors for editing this file. You can open the same file in multiple editors, but even then only a single instance of `TldrawDocument` is created per file. + +When a users opens a file a new WebView is created by the `TldrawWebviewManager` and the file's contents are sent do it. WebViews then show our editor project, which is described below. + +### 2. Editor project +Editor project is under `apps/vscode/editor`. When a file is opened a new instance of a WebView is created and we show `@tldraw/editor` this WebView. + +The implementation is pretty straight forward, but there are some limitations of running `tldraw` inside a WebView, like `window.open` and `window.prompt` not being available, as well as some issues with embeds. We are using `useLocalSyncClient` to sync between different editor instances for cases when the same file is opened in multiple editors. + +When users interact with tldraw we listen for changes and when changes happen we serialize the document contents and send them over to `TldrawDocument`. This makes VS Code aware of the changes and allows users to use built in features like `save`, `save as`, `undo`, `redo`, and `revert`. + +### Overview of the communication between VS Code, Extension, and the Editor + +VS Code actives our extension when needed - when a user opens the first `.tldr` file or when a user runs our registered command. Then, VS Code calls into `TldrawEditorProvider` to open the custom editor, which in turn creates a `TldrawDocument` instance. We read the file contents from disk and send them to the WebView, which then shows the Editor. When the user interacts with the editor we send the changes back to the Extension, which then updates the `TldrawDocument` instance. Since the instance is always kept up to date we can correctly handle user actions like `save`, `save as`, `undo`, `redo`, and `revert`. + +![VS Code Extension](VS-Code-Extension-1.png) + + +#### References + +- [VS Code Marketplace Manager](https://marketplace.visualstudio.com/manage/) +- [Web Extensions Guide](https://code.visualstudio.com/api/extension-guides/web-extensions) + - [Test Your Web Extension](https://code.visualstudio.com/api/extension-guides/web-extensions#test-your-web-extension) + - [Web Extension Testing](https://code.visualstudio.com/api/extension-guides/web-extensions#web-extension-tests) + - An example custom editor that does work as a Web Extension + - https://marketplace.visualstudio.com/items?itemName=hediet.vscode-drawio + - https://github.com/hediet/vscode-drawio +- [VS Code Extension API/Landing Page](https://code.visualstudio.com/api) +- [Getting Started](https://code.visualstudio.com/api/get-started/your-first-extension) +- [Custom Editor API](https://code.visualstudio.com/api/extension-guides/custom-editors) +- [github.com/microsoft/vscode-extension-samples](https://github.com/microsoft/vscode-extension-samples) +- [Extensions Guide -> Webviews](https://code.visualstudio.com/api/extension-guides/webview) +- [Publishing Extensions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) diff --git a/apps/vscode/VS-Code-Extension-1.png b/apps/vscode/VS-Code-Extension-1.png new file mode 100644 index 000000000..c064562ba Binary files /dev/null and b/apps/vscode/VS-Code-Extension-1.png differ diff --git a/apps/vscode/VS-Code-Extension-1.tldr b/apps/vscode/VS-Code-Extension-1.tldr new file mode 100644 index 000000000..9d70c0639 --- /dev/null +++ b/apps/vscode/VS-Code-Extension-1.tldr @@ -0,0 +1 @@ +{"tldrawFileFormatVersion":1,"schema":{"schemaVersion":1,"storeVersion":1,"recordVersions":{"asset":{"version":0,"subTypeKey":"type","subTypeVersions":{"image":2,"video":2,"bookmark":0}},"camera":{"version":0},"document":{"version":0},"instance":{"version":5},"instance_page_state":{"version":0},"page":{"version":0},"shape":{"version":1,"subTypeKey":"type","subTypeVersions":{"draw":1,"text":0,"line":0,"arrow":1,"image":1,"video":1,"geo":2,"note":1,"group":0,"bookmark":1,"embed":1,"frame":0}},"user":{"version":0},"user_document":{"version":2},"user_presence":{"version":0}}},"records":[{"gridSize":10,"id":"document:document","typeName":"document"},{"isReadOnly":false,"isPenMode":false,"isGridMode":false,"isDarkMode":false,"isMobileMode":false,"isSnapMode":false,"lastUpdatedPageId":"page:X1OJGTODYVEbqMm4emGxU","lastUsedTabId":"instance:60mSLsiPkp717P1dhfGoX","id":"user_document:dhjuyBaIRT-nxKEk4T-Dr","userId":"user:eRaHxyvxHiyaNUnBLl13B","typeName":"user_document"},{"id":"page:X1OJGTODYVEbqMm4emGxU","name":"Page 1","index":"a1","typeName":"page"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"s","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":false,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":2560,"h":1244},"id":"instance:60mSLsiPkp717P1dhfGoX","userId":"user:eRaHxyvxHiyaNUnBLl13B","currentPageId":"page:X1OJGTODYVEbqMm4emGxU","typeName":"instance"},{"name":"New User","locale":"en","id":"user:eRaHxyvxHiyaNUnBLl13B","typeName":"user"},{"lastUsedInstanceId":"instance:60mSLsiPkp717P1dhfGoX","lastActivityTimestamp":1679993331149,"cursor":{"x":-174.1017576653468,"y":83.06549148035545,"z":0},"color":"#7B66DC","id":"user_presence:h4Ki5KkNq8ELqokhTa4Un","userId":"user:eRaHxyvxHiyaNUnBLl13B","typeName":"user_presence"},{"x":212.5069673674164,"y":-21.350623881036793,"z":0.9848720796845886,"id":"camera:Mr_u3Aotw1TjJtpna1U7s","typeName":"camera"},{"editingId":null,"selectedIds":[],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:UQjFFzCH9NUGVN30guxB1","pageId":"page:X1OJGTODYVEbqMm4emGxU","instanceId":"instance:60mSLsiPkp717P1dhfGoX","cameraId":"camera:Mr_u3Aotw1TjJtpna1U7s","typeName":"instance_page_state"},{"x":197.22265625,"y":268.8671875,"rotation":0,"isLocked":false,"id":"shape:Wtlf97-4l2hmPNiDynU74","type":"frame","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a1","props":{"opacity":"1","w":320,"h":582.37890625,"name":"VS Code"},"typeName":"shape"},{"x":826.451171875,"y":268.8671875,"rotation":0,"isLocked":false,"id":"shape:wXQpeAP_5tJh_cJZlosOu","type":"frame","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a3","props":{"opacity":"1","w":320,"h":693.5156250000001,"name":"Extension"},"typeName":"shape"},{"x":38.6845703125,"y":62.18404385288068,"rotation":0,"isLocked":false,"id":"shape:r_4-Oa6Ku0lbEXFc4JbU1","type":"frame","parentId":"shape:iKqC_CvoeAudEOmKppTEy","index":"a1","props":{"opacity":"1","w":358.73828125,"h":606.8901748971193,"name":"Editor"},"typeName":"shape"},{"x":593.35546875,"y":296.41796875,"rotation":0,"isLocked":false,"id":"shape:NW10RHwmrCFczlX_w7Frb","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a4","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:Wtlf97-4l2hmPNiDynU74","normalizedAnchor":{"x":0.9227538947053106,"y":0.04545518820763115},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:wXQpeAP_5tJh_cJZlosOu","normalizedAnchor":{"x":0.023609879961458802,"y":0.038170939251366},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"activate","font":"draw"},"typeName":"shape"},{"x":30.548217271090493,"y":51.36368312757202,"rotation":0,"isLocked":false,"id":"shape:j_3oED2NXYMk9qXK9dK2G","type":"frame","props":{"opacity":"1","w":256.08984375,"h":187.09667774571852,"name":"TldrawEditorProvider"},"parentId":"shape:wXQpeAP_5tJh_cJZlosOu","index":"a1","typeName":"shape"},{"x":610.28515625,"y":373.73046875,"rotation":0,"isLocked":false,"id":"shape:CUMcLcUdAa237XHHVikd-","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a3V","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:Wtlf97-4l2hmPNiDynU74","normalizedAnchor":{"x":0.8496790848134607,"y":0.1399171158211434},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:j_3oED2NXYMk9qXK9dK2G","normalizedAnchor":{"x":0.13925406707908122,"y":0.16099213579269095},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"openCustomDocument","font":"draw"},"typeName":"shape"},{"x":30.291015625,"y":322.2848211920109,"rotation":0,"isLocked":false,"id":"shape:LnAfU8esZED6dp884vums","type":"frame","props":{"opacity":"1","w":256.08984375,"h":77.01171694306264,"name":"TldrawDocument"},"parentId":"shape:wXQpeAP_5tJh_cJZlosOu","index":"a5","typeName":"shape"},{"x":517.63671875,"y":517.3203125,"rotation":0,"isLocked":false,"id":"shape:FTFuGX_qM_MX1Hr1HTFrx","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a7","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:Wtlf97-4l2hmPNiDynU74","normalizedAnchor":{"x":0.9983721751425151,"y":0.2488277313021776},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:j_3oED2NXYMk9qXK9dK2G","normalizedAnchor":{"x":0.5,"y":0.5},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"save, undo, redo,...","font":"draw"},"typeName":"shape"},{"x":30.291015625,"y":587.7915404796821,"rotation":0,"isLocked":false,"id":"shape:jz2HbIY3LkZ5n1lkaPUYC","type":"frame","props":{"opacity":"1","w":256.08984375,"h":75.9029911018856,"name":"WebViewMessageHandler"},"parentId":"shape:wXQpeAP_5tJh_cJZlosOu","index":"a2","typeName":"shape"},{"x":610.88671875,"y":402.46484375,"rotation":0,"isLocked":false,"id":"shape:Wx6xyRiMXYKaDKm7jBzxa","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a3l","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:Wtlf97-4l2hmPNiDynU74","normalizedAnchor":{"x":0.944094113516129,"y":0.19612515263137095},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:j_3oED2NXYMk9qXK9dK2G","normalizedAnchor":{"x":0.08964421176735406,"y":0.335951816501127},"isExact":false},"arrowheadStart":"arrow","arrowheadEnd":"none","text":"document","font":"draw"},"typeName":"shape"},{"x":1044.30078125,"y":691.4765625,"rotation":0,"isLocked":false,"id":"shape:PIimyxsDcGXkYlnk8j5eO","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a34","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:jz2HbIY3LkZ5n1lkaPUYC","normalizedAnchor":{"x":0.9039414878201315,"y":0.232335303320007},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:iKqC_CvoeAudEOmKppTEy","normalizedAnchor":{"x":0.0018109176887933151,"y":0.8729817514064436},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"webview.postMessage","font":"draw"},"typeName":"shape"},{"x":1408.2529296875,"y":268.8671875,"rotation":0,"isLocked":false,"id":"shape:iKqC_CvoeAudEOmKppTEy","type":"frame","props":{"opacity":"1","w":437.06640625,"h":693.5156250000001,"name":"WebView"},"parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a0","typeName":"shape"},{"x":1537.7890625,"y":69.1796875,"rotation":0,"isLocked":false,"id":"shape:196us7Ohaw0bL7_D7249I","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"aA","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:98Wl2N7Ffi-4tuVdZvzUR","normalizedAnchor":{"x":0.520276467505241,"y":0.7422025723472668},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:iKqC_CvoeAudEOmKppTEy","normalizedAnchor":{"x":0.2243205319557776,"y":0.011012255120370327},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"","font":"draw"},"typeName":"shape"},{"x":1257.8554205246912,"y":143.57073581104254,"rotation":0,"isLocked":false,"id":"shape:98Wl2N7Ffi-4tuVdZvzUR","type":"text","props":{"opacity":"1","color":"black","size":"s","w":477,"text":"Each file on disk can be shown in multiple WebView\ninstances when you split the editor pane","font":"draw","align":"middle","autoSize":true,"scale":1},"parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a9","typeName":"shape"},{"x":1471.7890625,"y":773.360876118845,"rotation":0,"isLocked":false,"id":"shape:SquMbZDtzYW2NCqwvR6T0","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"a38","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"point","x":-68.52211728176576,"y":129.88187744576715},"end":{"type":"binding","boundShapeId":"shape:jz2HbIY3LkZ5n1lkaPUYC","normalizedAnchor":{"x":0.858732527338509,"y":0.6137310916034874},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"vscode.postMessage","font":"draw"},"typeName":"shape"},{"x":1239.203125,"y":1044.786657368845,"rotation":0,"isLocked":false,"id":"shape:podi8CHhecANxPACUxIqT","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"aD","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:FBH7Sd7p10BLfY5Utc8a8","normalizedAnchor":{"x":0.5031195079086116,"y":0.129983922829582},"isExact":false},"end":{"type":"point","x":29.68750000000001,"y":-110.2301315011413},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"","font":"draw"},"typeName":"shape"},{"x":1089.96875,"y":1047.576034230166,"rotation":0,"isLocked":false,"id":"shape:FBH7Sd7p10BLfY5Utc8a8","type":"text","props":{"opacity":"1","color":"black","size":"s","w":355.625,"text":"Extension and WebViews communicate\nby passing VscodeMessages.","font":"draw","align":"middle","autoSize":true,"scale":1},"parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"aC","typeName":"shape"},{"x":26.377953329805223,"y":57.64541160122542,"rotation":0,"isLocked":false,"id":"shape:liVshvnaGaAli-VQbMaDR","type":"frame","props":{"opacity":"1","w":314.145018861454,"h":196.72175068587103,"name":"App"},"parentId":"shape:r_4-Oa6Ku0lbEXFc4JbU1","index":"a1","typeName":"shape"},{"x":1473.3154533298052,"y":653.4589457661774,"rotation":0,"isLocked":false,"id":"shape:zOSnHYdcFrA8-vNuzLhTf","type":"frame","props":{"opacity":"1","w":314.145018861454,"h":85.78746570644728,"name":"FileOpen"},"parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"aE","typeName":"shape"},{"x":26.377953329805223,"y":481.2297308742021,"rotation":0,"isLocked":false,"id":"shape:ior383FyEMZhkkzas0rZ8","type":"frame","props":{"opacity":"1","w":314.145018861454,"h":85.78746570644728,"name":"ChangeResponder"},"parentId":"shape:r_4-Oa6Ku0lbEXFc4JbU1","index":"a2","typeName":"shape"},{"x":155.02126475453292,"y":116.91748469855753,"rotation":0,"isLocked":false,"id":"shape:K7qQ8DJgx5G3eXAG879Sw","type":"arrow","parentId":"shape:wXQpeAP_5tJh_cJZlosOu","index":"a4","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:j_3oED2NXYMk9qXK9dK2G","normalizedAnchor":{"x":0.5,"y":0.5},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:LnAfU8esZED6dp884vums","normalizedAnchor":{"x":0.5,"y":0.5},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"create","font":"draw"},"typeName":"shape"},{"x":856.7421875,"y":707.6714620552667,"rotation":0,"isLocked":false,"id":"shape:YD11GnrpT-8nGn4-08IK8","type":"frame","props":{"opacity":"1","w":256.08984375,"h":93.70435909082752,"name":"TldrawWebViewManager"},"parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"aCV","typeName":"shape"},{"x":1106.265148046248,"y":757.5752018812717,"rotation":0,"isLocked":false,"id":"shape:C7V5TiPsBnbop6y3plchZ","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"aG","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":2.3600343955986403,"start":{"type":"binding","boundShapeId":"shape:YD11GnrpT-8nGn4-08IK8","normalizedAnchor":{"x":0.9276612233158547,"y":0.58685277397186},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:iKqC_CvoeAudEOmKppTEy","normalizedAnchor":{"x":0.011024324609946432,"y":0.06297452152966412},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"creates","font":"draw"},"typeName":"shape"},{"x":509.29521096996183,"y":756.8522336251018,"rotation":0,"isLocked":false,"id":"shape:rZUKPO0-ifu3hawAeAqnm","type":"arrow","parentId":"page:X1OJGTODYVEbqMm4emGxU","index":"aCl","props":{"opacity":"1","dash":"draw","size":"s","fill":"none","color":"black","labelColor":"black","bend":0,"start":{"type":"binding","boundShapeId":"shape:Wtlf97-4l2hmPNiDynU74","normalizedAnchor":{"x":0.9760208071478205,"y":0.8269451561252551},"isExact":false},"end":{"type":"binding","boundShapeId":"shape:YD11GnrpT-8nGn4-08IK8","normalizedAnchor":{"x":0.17485880508205695,"y":0.45666115656601974},"isExact":false},"arrowheadStart":"none","arrowheadEnd":"arrow","text":"resolveCustomEditor","font":"draw"},"typeName":"shape"}]} \ No newline at end of file diff --git a/apps/vscode/editor/CHANGELOG.md b/apps/vscode/editor/CHANGELOG.md new file mode 100644 index 000000000..197c4e108 --- /dev/null +++ b/apps/vscode/editor/CHANGELOG.md @@ -0,0 +1,81 @@ +# @tldraw/vscode-editor + +## 1.13.1-alpha.0 + +### Patch Changes + +- Release day! + +## 1.13.0 + +### Minor Changes + +- - Adds missing Arabic translations for dialogs. @abedshamia + - Updates core-example. @brydenfogelman + - Updates Polish translations. @adan2013 + - Adds missing Aria-Labels. @KDSBrowne + - Improves Japanese translation. @yashkumarbarot + - Fixes height and width in app.viewport. @hiroshisuga + - Improves labels on StlyeMenu @proke03 + - Adds missing tooltips to undo / redo buttons. @proke03 + +## 1.12.0 + +### Minor Changes + +- - Improve middle mouse panning + - Fix bug with assets in VS Code plugin + - Improve performance of draw-style shapes + - Fix bug with creating assets + - Fix bug with text align in labels when outputting images + - Fix bug with middle mouse panning on Linux + - Fix bug with zoom shortcuts on number pad + - Fix bug with draw and erase direction when holding shift + +## 1.11.0 + +### Minor Changes + +- d919bd27: Bump dependencies, add international support. + +## 1.11.0-next.0 + +### Minor Changes + +- Bump dependencies, add international support. + +## 1.10.2 + +### Patch Changes + +- Fix tldraw assets for vscode extension. + +## 1.10.1 + +### Patch Changes + +- Fix build. + +## 1.10.0 + +### Minor Changes + +- Fix build error in extension. + +## 1.9.0 + +### Minor Changes + +- Bump underlying packages. + +## 1.8.0 + +### Minor Changes + +- c09d6a3a: Adds text field for page rename, undo buttons on all screen sizes, arrow behavior with alt key. + +## 1.7.1 + +### Patch Changes + +- Fix bug with missing parents / children. diff --git a/apps/vscode/editor/LICENSE b/apps/vscode/editor/LICENSE new file mode 100644 index 000000000..4f227c380 --- /dev/null +++ b/apps/vscode/editor/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2023 tldraw GB Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/apps/vscode/editor/README.md b/apps/vscode/editor/README.md new file mode 100644 index 000000000..61950f935 --- /dev/null +++ b/apps/vscode/editor/README.md @@ -0,0 +1,9 @@ +
+ +
+ +# @tldraw/vscode-editor + +The app for the tldraw VS Code Extension. + +See the README at `vscode` for more about this project. diff --git a/apps/vscode/editor/package.json b/apps/vscode/editor/package.json new file mode 100644 index 000000000..e8f894010 --- /dev/null +++ b/apps/vscode/editor/package.json @@ -0,0 +1,61 @@ +{ + "name": "@tldraw/vscode-editor", + "description": "An an editor for the tldraw vscode extension.", + "version": "2.0.0-alpha.0", + "private": true, + "packageManager": "yarn@3.5.0", + "author": { + "name": "tldraw GB Ltd.", + "email": "hello@tldraw.com" + }, + "homepage": "https://tldraw.dev", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/tldraw/tldraw" + }, + "bugs": { + "url": "https://github.com/tldraw/tldraw/issues" + }, + "keywords": [ + "tldraw", + "drawing", + "app", + "development", + "whiteboard", + "canvas", + "infinite" + ], + "scripts": { + "build": "yarn run -T tsx scripts/build.ts", + "build:vscode-editor": "yarn run -T tsx scripts/build.ts", + "dev:vscode": "yarn run -T tsx scripts/dev.ts", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist", + "lint": "yarn run -T tsx ../../../scripts/lint.ts" + }, + "devDependencies": { + "@tldraw/assets": "workspace:*", + "@tldraw/editor": "workspace:*", + "@tldraw/file-format": "workspace:*", + "@tldraw/tldraw": "workspace:*", + "@tldraw/tlsync-client": "workspace:*", + "@tldraw/ui": "workspace:*", + "@tldraw/utils": "workspace:*", + "@types/fs-extra": "^11.0.1", + "@types/node": "^17.0.14", + "@types/react": "^18.0.24", + "@types/react-dom": "^18.0.6", + "@types/react-router-dom": "^5.1.8", + "concurrently": "7.0.0", + "create-serve": "1.0.1", + "dotenv": "^16.0.3", + "esbuild": "^0.16.7", + "fs-extra": "^11.1.0", + "lazyrepo": "0.0.0-alpha.20", + "nanoid": "^4.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tslib": "^2.4.0" + }, + "gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6" +} diff --git a/apps/vscode/editor/public/index.css b/apps/vscode/editor/public/index.css new file mode 100644 index 000000000..2aa70f680 --- /dev/null +++ b/apps/vscode/editor/public/index.css @@ -0,0 +1,46 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap'); + +html, +body { + padding: 0; + margin: 0; + font-family: 'Inter', sans-serif; + overscroll-behavior: none; + touch-action: none; + overflow: hidden; + min-height: 100vh; + /* mobile viewport bug fix */ + min-height: -webkit-fill-available; +} + +#root { + position: fixed; + width: 100%; + height: 100%; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} + +html, +* { + box-sizing: border-box; +} + +.tldraw--editor { + position: fixed; + top: 0px; + left: 0px; + bottom: 0px; + right: 0px; + width: 100%; + height: 100%; + overflow: hidden; + background-color: var(--color-background); +} diff --git a/apps/vscode/editor/scripts/build.ts b/apps/vscode/editor/scripts/build.ts new file mode 100644 index 000000000..a29ac0fbd --- /dev/null +++ b/apps/vscode/editor/scripts/build.ts @@ -0,0 +1,58 @@ +import esbuild from 'esbuild' +import fs from 'fs' +import fse from 'fs-extra' +import path from 'path' +import { logEnv } from '../../vscode-script-utils/cli' +import { exists, getDirname } from '../../vscode-script-utils/path' + +const rootDir = getDirname(import.meta.url, '../') +const log = logEnv('editor') + +export async function build() { + try { + const targetFolder = `${rootDir}dist/` + if (await exists(targetFolder)) { + log({ cmd: 'remove', args: { target: targetFolder } }) + await fs.promises.rm(targetFolder, { recursive: true }) + } + + await fs.promises.mkdir(targetFolder) + + const topSource = `${rootDir}public` + const files = await fs.promises.readdir(topSource) + for (const file of files) { + const dest = targetFolder + path.basename(file) + const source = path.join(topSource, file) + log({ cmd: 'copy', args: { source, dest } }) + await fse.copy(source, dest) + } + const entryPoints = [`${rootDir}src/index.tsx`] + + log({ cmd: 'esbuild', args: { entryPoints } }) + esbuild.buildSync({ + entryPoints, + outfile: `${rootDir}/dist/index.js`, + minify: false, + bundle: true, + target: 'es6', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + loader: { + '.woff2': 'dataurl', + '.woff': 'dataurl', + '.svg': 'file', + '.png': 'file', + '.json': 'file', + }, + define: { + 'process.env.NODE_ENV': '"production"', + }, + }) + log({ cmd: 'esbuild:success', args: {} }) + } catch (error) { + log({ cmd: 'esbuild:error', args: { error } }) + throw error + } +} + +build() diff --git a/apps/vscode/editor/scripts/dev.ts b/apps/vscode/editor/scripts/dev.ts new file mode 100644 index 000000000..6fecc5a5a --- /dev/null +++ b/apps/vscode/editor/scripts/dev.ts @@ -0,0 +1,71 @@ +import dotenv from 'dotenv' +import esbuild from 'esbuild' +import fs from 'fs' +import fse from 'fs-extra' +import path from 'path' +import { logEnv } from '../../vscode-script-utils/cli' +import { copyEditor } from '../../vscode-script-utils/helpers' +import { exists, getDirname } from '../../vscode-script-utils/path' + +dotenv.config() +const rootDir = getDirname(import.meta.url, '../') +const log = logEnv('editor') + +export async function run() { + try { + const targetFolder = `${rootDir}dist/` + if (await exists(targetFolder)) { + log({ cmd: 'remove', args: { target: targetFolder } }) + await fs.promises.rm(targetFolder, { recursive: true }) + } + + await fs.promises.mkdir(targetFolder) + + const topSource = `${rootDir}public` + const files = await fs.promises.readdir(topSource) + for (const file of files) { + const dest = targetFolder + path.basename(file) + const source = path.join(topSource, file) + log({ cmd: 'copy', args: { source, dest } }) + await fse.copy(source, dest) + } + const entryPoints = [`${rootDir}src/index.tsx`] + + log({ cmd: 'esbuild', args: { entryPoints } }) + esbuild.build({ + entryPoints, + outfile: `${rootDir}/dist/index.js`, + minify: false, + bundle: true, + incremental: true, + target: 'es6', + jsxFactory: 'React.createElement', + jsxFragment: 'React.Fragment', + loader: { + '.woff2': 'dataurl', + '.woff': 'dataurl', + '.svg': 'file', + '.png': 'file', + '.json': 'file', + }, + define: { + 'process.env.NODE_ENV': '"development"', + }, + watch: { + onRebuild(err) { + if (err) { + log({ cmd: 'esbuild:error', args: { err } }) + } else { + copyEditor({ log }) + log({ cmd: 'esbuild:success', args: {} }) + } + }, + }, + }) + } catch (error) { + log({ cmd: 'esbuild:error', args: { error } }) + throw error + } +} + +run() diff --git a/apps/vscode/editor/src/ChangeResponder.tsx b/apps/vscode/editor/src/ChangeResponder.tsx new file mode 100644 index 000000000..f9d412ec4 --- /dev/null +++ b/apps/vscode/editor/src/ChangeResponder.tsx @@ -0,0 +1,77 @@ +import { SyncedStore, TLInstanceId, TLUserId, useApp } from '@tldraw/editor' +import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format' +import { useDefaultHelpers } from '@tldraw/ui' +import { debounce } from '@tldraw/utils' +import React from 'react' +import '../public/index.css' +import { vscode } from './utils/vscode' + +// @ts-ignore +import type { VscodeMessage } from '../../messages' + +export const ChangeResponder = ({ + syncedStore, + userId, + instanceId, +}: { + syncedStore: SyncedStore + userId: TLUserId + instanceId: TLInstanceId +}) => { + const app = useApp() + const { addToast, clearToasts, msg } = useDefaultHelpers() + + React.useEffect(() => { + // When a message is received from the VS Code extension, handle it + function handleMessage({ data: message }: MessageEvent) { + switch (message.type) { + // case 'vscode:undo': { + // app.undo() + // break + // } + // case 'vscode:redo': { + // app.redo() + // break + // } + case 'vscode:revert': { + parseAndLoadDocument(app, message.data.fileContents, msg, addToast) + break + } + } + } + + window.addEventListener('message', handleMessage) + + return () => { + clearToasts() + window.removeEventListener('message', handleMessage) + } + }, [app, userId, instanceId, msg, addToast, clearToasts]) + + React.useEffect(() => { + // When the history changes, send the new file contents to VSCode + const handleChange = debounce(async () => { + if (syncedStore.store) { + vscode.postMessage({ + type: 'vscode:editor-updated', + data: { + fileContents: await serializeTldrawJson(syncedStore.store), + }, + }) + } + }, 250) + + vscode.postMessage({ + type: 'vscode:editor-loaded', + }) + + app.on('change-history', handleChange) + + return () => { + handleChange() + app.off('change-history', handleChange) + } + }, [app, syncedStore, userId, instanceId]) + + return null +} diff --git a/apps/vscode/editor/src/FileOpen.tsx b/apps/vscode/editor/src/FileOpen.tsx new file mode 100644 index 000000000..a6fb0c4ab --- /dev/null +++ b/apps/vscode/editor/src/FileOpen.tsx @@ -0,0 +1,60 @@ +import { TLInstanceId, TLUserId, useApp } from '@tldraw/editor' +import { parseAndLoadDocument } from '@tldraw/file-format' +import { useDefaultHelpers } from '@tldraw/ui' +import React from 'react' +import { vscode } from './utils/vscode' + +export function FileOpen({ + userId, + fileContents, + instanceId, + forceDarkMode, +}: { + instanceId: TLInstanceId + userId: TLUserId + fileContents: string + forceDarkMode: boolean +}) { + const app = useApp() + const { msg, addToast, clearToasts } = useDefaultHelpers() + const [isFileLoaded, setIsFileLoaded] = React.useState(false) + + React.useEffect(() => { + if (isFileLoaded) return + function onV1FileLoad() { + vscode.postMessage({ + type: 'vscode:v1-file-opened', + data: { + description: msg('vscode.file-open.desc'), + backup: msg('vscode.file-open.backup'), + backupSaved: msg('vscode.file-open.backup-saved'), + backupFailed: msg('vscode.file-open.backup-failed'), + dontAskAgain: msg('vscode.file-open.dont-show-again'), + open: msg('vscode.file-open.open'), + }, + }) + } + + async function loadFile() { + await parseAndLoadDocument(app, fileContents, msg, addToast, onV1FileLoad, forceDarkMode) + } + + loadFile() + setIsFileLoaded(true) + return () => { + clearToasts() + } + }, [ + fileContents, + app, + userId, + instanceId, + addToast, + msg, + clearToasts, + forceDarkMode, + isFileLoaded, + ]) + + return null +} diff --git a/apps/vscode/editor/src/FullPageMessage.tsx b/apps/vscode/editor/src/FullPageMessage.tsx new file mode 100644 index 000000000..bfb8ea302 --- /dev/null +++ b/apps/vscode/editor/src/FullPageMessage.tsx @@ -0,0 +1,17 @@ +export function FullPageMessage({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} diff --git a/apps/vscode/editor/src/app.tsx b/apps/vscode/editor/src/app.tsx new file mode 100644 index 000000000..824b22e0d --- /dev/null +++ b/apps/vscode/editor/src/app.tsx @@ -0,0 +1,164 @@ +import { getBundlerAssetUrls } from '@tldraw/assets' +import { + App, + Canvas, + ErrorBoundary, + setRuntimeOverrides, + TldrawEditor, + TLUserId, +} from '@tldraw/editor' +import { linksUiOverrides } from './utils/links' +// eslint-disable-next-line import/no-internal-modules +import '@tldraw/editor/editor.css' +import { TAB_ID, useLocalSyncClient } from '@tldraw/tlsync-client' +import { ContextMenu, MenuSchema, TldrawUi } from '@tldraw/ui' +// eslint-disable-next-line import/no-internal-modules +import '@tldraw/ui/ui.css' +import { useEffect, useMemo, useState } from 'react' +import { VscodeMessage } from '../../messages' +import '../public/index.css' +import { ChangeResponder } from './ChangeResponder' +import { FileOpen } from './FileOpen' +import { FullPageMessage } from './FullPageMessage' +import { onCreateBookmarkFromUrl } from './utils/bookmarks' +import { vscode } from './utils/vscode' + +// @ts-ignore + +setRuntimeOverrides({ + openWindow: (url, target) => { + vscode.postMessage({ + type: 'vscode:open-window', + data: { + url, + target, + }, + }) + }, + refreshPage: () => { + vscode.postMessage({ + type: 'vscode:refresh-page', + }) + }, + hardReset: async () => { + await (window as any).__tldraw__hardReset?.() + vscode.postMessage({ + type: 'vscode:hard-reset', + }) + }, +}) + +const handleError = (error: any) => { + console.error(error.message) +} + +export function WrappedTldrawEditor() { + return ( +
+ Fallback} + onError={handleError} + > + + +
+ ) +} + +const menuOverrides = { + menu: (_app: App, schema: MenuSchema, _helpers: any) => { + schema.forEach((item) => { + if (item.id === 'menu' && item.type === 'group') { + item.children = item.children.filter((menuItem) => { + if (menuItem.id === 'file' && menuItem.type === 'submenu') { + return false + } + return true + }) + } + }) + + return schema + }, +} + +export const TldrawWrapper = () => { + const [tldrawInnerProps, setTldrawInnerProps] = useState(null) + + useEffect(() => { + function handleMessage({ data: message }: MessageEvent) { + switch (message.type) { + case 'vscode:opened-file': { + setTldrawInnerProps({ + assetSrc: message.data.assetSrc, + fileContents: message.data.fileContents, + uri: message.data.uri, + userId: message.data.userId as TLUserId, + isDarkMode: message.data.isDarkMode, + }) + // We only want to listen for this message once + window.removeEventListener('message', handleMessage) + break + } + } + } + + window.addEventListener('message', handleMessage) + + vscode.postMessage({ type: 'vscode:ready-to-receive-file' }) + + return () => { + window.removeEventListener('message', handleMessage) + } + }, [setTldrawInnerProps]) + + return tldrawInnerProps === null ? ( + Loading + ) : ( + + ) +} + +export type TLDrawInnerProps = { + assetSrc: string + fileContents: string + uri: string + userId: TLUserId + isDarkMode: boolean +} + +function TldrawInner({ uri, assetSrc, userId, isDarkMode, fileContents }: TLDrawInnerProps) { + const instanceId = TAB_ID + const syncedStore = useLocalSyncClient({ + universalPersistenceKey: uri, + instanceId, + userId, + }) + + const assetUrls = useMemo(() => getBundlerAssetUrls({ baseUrl: assetSrc }), [assetSrc]) + + return ( + + {/* */} + + + + + + + + + ) +} diff --git a/apps/vscode/editor/src/index.tsx b/apps/vscode/editor/src/index.tsx new file mode 100644 index 000000000..7646a8599 --- /dev/null +++ b/apps/vscode/editor/src/index.tsx @@ -0,0 +1,9 @@ +import * as React from 'react' +import ReactDOM from 'react-dom/client' +import { WrappedTldrawEditor } from './app' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +) diff --git a/apps/vscode/editor/src/utils/bookmarks.ts b/apps/vscode/editor/src/utils/bookmarks.ts new file mode 100644 index 000000000..a8f73e82b --- /dev/null +++ b/apps/vscode/editor/src/utils/bookmarks.ts @@ -0,0 +1,47 @@ +import { rpc } from './rpc' + +async function onCreateBookmarkFromUrlFallback( + url: string +): Promise<{ image: string; title: string; description: string }> { + const meta = { + image: '', + title: '', + description: '', + } + + try { + const resp = await fetch(url, { method: 'GET', mode: 'no-cors' }) + const html = await resp.text() + const doc = new DOMParser().parseFromString(html, 'text/html') + + meta.image = doc.head + .querySelector('meta[property="og:image"]') + ?.getAttribute('content') as string + meta.title = doc.head + .querySelector('meta[property="og:title"]') + ?.getAttribute('content') as string + meta.description = doc.head + .querySelector('meta[property="og:description"]') + ?.getAttribute('content') as string + + return meta + } catch (error) { + console.error(error) + } + + return meta +} + +export async function onCreateBookmarkFromUrl(url: string) { + try { + const data = await rpc('vscode:bookmark', { url }) + + return { + title: data.title || '', + description: data.description || '', + image: data.image || '', + } + } catch (error) { + return onCreateBookmarkFromUrlFallback(url) + } +} diff --git a/apps/vscode/editor/src/utils/links.ts b/apps/vscode/editor/src/utils/links.ts new file mode 100644 index 000000000..34d2f99d9 --- /dev/null +++ b/apps/vscode/editor/src/utils/links.ts @@ -0,0 +1,57 @@ +import { menuGroup, menuItem, TldrawUiOverrides } from '@tldraw/ui' +import { openUrl } from './openUrl' + +export const GITHUB_URL = 'https://github.com/tldraw/tldraw' + +const linksMenuGroup = menuGroup( + 'links', + menuItem({ + id: 'github', + label: 'help-menu.github', + readonlyOk: true, + icon: 'github', + onSelect() { + openUrl(GITHUB_URL) + }, + }), + menuItem({ + id: 'twitter', + label: 'help-menu.twitter', + icon: 'twitter', + readonlyOk: true, + onSelect() { + openUrl('https://twitter.com/tldraw') + }, + }), + menuItem({ + id: 'discord', + label: 'help-menu.discord', + icon: 'discord', + readonlyOk: true, + onSelect() { + openUrl('https://discord.gg/SBBEVCA4PG') + }, + }), + menuItem({ + id: 'about', + label: 'help-menu.about', + icon: 'external-link', + readonlyOk: true, + onSelect() { + openUrl('https://www.tldraw.dev') + }, + }) +)! + +export const linksUiOverrides: TldrawUiOverrides = { + helpMenu(app, schema) { + schema.push(linksMenuGroup) + return schema + }, + menu(app, schema, { isMobile }) { + if (isMobile) { + schema.push(linksMenuGroup) + } + return schema + }, +} diff --git a/apps/vscode/editor/src/utils/openUrl.ts b/apps/vscode/editor/src/utils/openUrl.ts new file mode 100644 index 000000000..bae0c5408 --- /dev/null +++ b/apps/vscode/editor/src/utils/openUrl.ts @@ -0,0 +1,8 @@ +import { vscode } from './vscode' + +export function openUrl(url: string) { + vscode.postMessage({ + type: 'vscode:open-window', + data: { url, target: '_blank' }, + }) +} diff --git a/apps/vscode/editor/src/utils/rpc.ts b/apps/vscode/editor/src/utils/rpc.ts new file mode 100644 index 000000000..eaa06171b --- /dev/null +++ b/apps/vscode/editor/src/utils/rpc.ts @@ -0,0 +1,62 @@ +import { nanoid } from 'nanoid' +import type { VscodeMessagePairs } from '../../../messages' +import { vscode } from './vscode' + +type SimpleRpcOpts = { + timeout: number +} +class SimpleRpcError extends Error { + id: string + data: any + constructor(id: keyof VscodeMessagePairs, data: any) { + super(`Failed ${id}`) + this.id = id + this.data = data + } +} + +export function rpc( + id: keyof VscodeMessagePairs, + data: Omit['data'], + opts: SimpleRpcOpts = { timeout: 5 * 1000 } +) { + const { timeout } = opts + type RequestType = VscodeMessagePairs[typeof id]['request'] + type ResponseType = VscodeMessagePairs[typeof id]['response'] + type ErrorType = VscodeMessagePairs[typeof id]['error'] + + const type = (id + '/request') as RequestType['type'] + const uuid = nanoid() + return new Promise((resolve, reject) => { + const inMessage = { + uuid, + type, + data, + } + vscode.postMessage(inMessage) + + const handler = ({ data: response }: MessageEvent) => { + if (uuid === response.uuid) { + return + } + + const cleanup = () => { + window.removeEventListener('message', handler) + } + + if (response.type === `${id}/response`) { + cleanup() + resolve(response.data as ResponseType['data']) + } + if (response.type === `${id}/error`) { + cleanup() + reject(new SimpleRpcError(id, response.data as ErrorType['data'])) + } + setTimeout(() => { + cleanup() + reject(new SimpleRpcError(id, { timeout: true })) + }, timeout) + } + window.addEventListener('message', handler) + }) +} diff --git a/apps/vscode/editor/src/utils/vscode.ts b/apps/vscode/editor/src/utils/vscode.ts new file mode 100644 index 000000000..8a7af452d --- /dev/null +++ b/apps/vscode/editor/src/utils/vscode.ts @@ -0,0 +1,9 @@ +// @ts-ignore +import type { VscodeMessage } from '../../../messages' + +// Will be placed in global scope by extension +declare function acquireVsCodeApi(): { + postMessage(options: VscodeMessage): void +} + +export const vscode = acquireVsCodeApi() diff --git a/apps/vscode/editor/tsconfig.json b/apps/vscode/editor/tsconfig.json new file mode 100644 index 000000000..815996187 --- /dev/null +++ b/apps/vscode/editor/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": true, + "checkJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "removeComments": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "noEmit": true, + "jsx": "react-jsx", + "incremental": true, + "baseUrl": ".", + "composite": true, + "importHelpers": false, + "skipDefaultLibCheck": true, + "experimentalDecorators": true, + "rootDir": ".." + }, + "include": ["src", "../messages", "scripts", "../vscode-script-utils"], + "references": [ + { "path": "../../../packages/file-format" }, + { "path": "../../../packages/ui" }, + { "path": "../../../packages/editor" }, + { "path": "../../../packages/tlsync-client" }, + { "path": "../../../packages/utils" } + ] +} diff --git a/apps/vscode/extension/.gitignore b/apps/vscode/extension/.gitignore new file mode 100644 index 000000000..5047f2950 --- /dev/null +++ b/apps/vscode/extension/.gitignore @@ -0,0 +1,6 @@ +dist +editor +node_modules +.vscode-test-web/ +*.vsix +.DS_Store \ No newline at end of file diff --git a/apps/vscode/extension/.vscode/extensions.json b/apps/vscode/extension/.vscode/extensions.json new file mode 100644 index 000000000..65254b8a0 --- /dev/null +++ b/apps/vscode/extension/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] +} diff --git a/apps/vscode/extension/.vscode/launch.json b/apps/vscode/extension/.vscode/launch.json new file mode 100644 index 000000000..cf3fcb2d3 --- /dev/null +++ b/apps/vscode/extension/.vscode/launch.json @@ -0,0 +1,23 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Web Extension ", + "type": "extensionHost", + "request": "launch", + "cwd": "${workspaceFolder}", + "args": [ + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "${workspaceFolder}/examples" + ], + "outFiles": ["${workspaceFolder}/dist/web/**/*.js", "!**/node_modules/**"], + "skipFiles": ["/**", "**/node_modules/**"], + "sourceMaps": true + } + ] +} diff --git a/apps/vscode/extension/.vscode/settings.json b/apps/vscode/extension/.vscode/settings.json new file mode 100644 index 000000000..afdab66cc --- /dev/null +++ b/apps/vscode/extension/.vscode/settings.json @@ -0,0 +1,11 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} diff --git a/apps/vscode/extension/.vscodeignore b/apps/vscode/extension/.vscodeignore new file mode 100644 index 000000000..e69de29bb diff --git a/apps/vscode/extension/CHANGELOG.md b/apps/vscode/extension/CHANGELOG.md new file mode 100644 index 000000000..4e1a910af --- /dev/null +++ b/apps/vscode/extension/CHANGELOG.md @@ -0,0 +1,21 @@ +## 2.0.5 + +- Fixed another issue with undo / redo. + +## 2.0.4 + +- Fix issues with undo / redo. +- Fix an issue with extension getting stuck on the loading screen. +- Fix an issue with the extension not saving changes in newly created files. + +## 2.0.3 + +- Fix description in package.json. + +## 2.0.2 + +- Add images to README. + +## 2.0.1 + +- Release! diff --git a/apps/vscode/extension/LICENSE b/apps/vscode/extension/LICENSE new file mode 100644 index 000000000..4f227c380 --- /dev/null +++ b/apps/vscode/extension/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2023 tldraw GB Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/apps/vscode/extension/README.md b/apps/vscode/extension/README.md new file mode 100644 index 000000000..032ef469a --- /dev/null +++ b/apps/vscode/extension/README.md @@ -0,0 +1,25 @@ +## Introduction + +[tldraw](https://tldraw.com) is a very good whiteboard for the web. This extension will let you use tldraw inside of VS Code, using offline `.tldr` files. Create drawings, diagrams, or visual documentation, and then commit the files alongside your code! + +![A screenshot of tldraw in VS Code](https://assets.tldraw.xyz/uploads/6Jnp8CCAr7gn2To06NF8r-vscode-screenshot.png) + +## Usage + +- To create a new tldraw file, use the provided command: "tldraw: New tldraw File". + +![A recording of opening tldraw in VS Code](https://assets.tldraw.xyz/uploads/vx7dEFbx2oURPB3vw8x0E-vscode-recording.gif) + +- To view an existing tldraw file, open a file with the `.tldr` extension in VS Code. + +> **Tip:** The `.tldr` files you create with this extension can also be opened in the tldraw [web app](https://tldraw.com). You can also use this extension to open `.tldr` files saved from the web app. + +## Community + +### Support + +Need help? Please [open an issue](https://github.com/tldraw/tldraw-beta/issues/new/choose) for support. + +### Discussion + +Want to connect with the developers or users? Visit the [Discord channel](https://discord.gg/SBBEVCA4PG). diff --git a/apps/vscode/extension/assets/vscode_open.png b/apps/vscode/extension/assets/vscode_open.png new file mode 100644 index 000000000..277e4b73b Binary files /dev/null and b/apps/vscode/extension/assets/vscode_open.png differ diff --git a/apps/vscode/extension/assets/vscode_recording.gif b/apps/vscode/extension/assets/vscode_recording.gif new file mode 100644 index 000000000..5d699e8f0 Binary files /dev/null and b/apps/vscode/extension/assets/vscode_recording.gif differ diff --git a/apps/vscode/extension/assets/vscode_screenshot.png b/apps/vscode/extension/assets/vscode_screenshot.png new file mode 100644 index 000000000..cfeb5b1a4 Binary files /dev/null and b/apps/vscode/extension/assets/vscode_screenshot.png differ diff --git a/apps/vscode/extension/examples/1.tldr b/apps/vscode/extension/examples/1.tldr new file mode 100644 index 000000000..3da591eb5 --- /dev/null +++ b/apps/vscode/extension/examples/1.tldr @@ -0,0 +1,168 @@ +{ + "fileHandle": null, + "document": { + "id": "doc", + "name": "New Document", + "version": 15.3, + "pages": { + "page": { + "id": "page", + "name": "Page 1", + "childIndex": 1, + "shapes": { + "3805a6e3-b50a-4bb2-0782-e687526cafc8": { + "id": "3805a6e3-b50a-4bb2-0782-e687526cafc8", + "type": "rectangle", + "name": "Rectangle", + "parentId": "page", + "childIndex": 1, + "point": [ + 245.96, + 277.88 + ], + "size": [ + 292.43, + 125.76 + ], + "rotation": 0, + "style": { + "color": "white", + "size": "small", + "isFilled": false, + "dash": "draw" + }, + "label": "Rectangles with labels" + }, + "9a20ffa8-044b-4ca3-0448-632e2b2831d1": { + "id": "9a20ffa8-044b-4ca3-0448-632e2b2831d1", + "type": "ellipse", + "name": "Ellipse", + "parentId": "page", + "childIndex": 3, + "point": [ + 995.24, + 235.11 + ], + "radius": [ + 112.92500000000001, + 108.96000000000004 + ], + "rotation": 0, + "style": { + "color": "white", + "size": "small", + "isFilled": true, + "dash": "draw", + "scale": 1 + }, + "label": "Ovals with labels", + "labelPoint": [ + 0.5, + 0.5 + ] + }, + "c2125c4e-e9ef-4eeb-2011-9e8aaeadd6ea": { + "id": "c2125c4e-e9ef-4eeb-2011-9e8aaeadd6ea", + "type": "arrow", + "name": "Arrow", + "parentId": "page", + "childIndex": 4, + "point": [ + 538.39, + 341.56 + ], + "rotation": 0, + "bend": -0.000013369615345367926, + "handles": { + "start": { + "id": "start", + "index": 0, + "point": [ + 0, + 0 + ], + "canBind": true, + "bindingId": "31007b26-4667-4932-00aa-7f389642b187" + }, + "end": { + "id": "end", + "index": 1, + "point": [ + 448.85, + 2.44 + ], + "canBind": true, + "bindingId": "7b099540-f8b1-4bf3-0d7d-2b74df370067" + }, + "bend": { + "id": "bend", + "index": 2, + "point": [ + 224.43, + 1.22 + ] + } + }, + "decorations": { + "end": "arrow" + }, + "style": { + "color": "white", + "size": "small", + "isFilled": false, + "dash": "draw", + "scale": 1 + }, + "label": "Arrows with labels", + "labelPoint": [ + 0.5, + 0.5 + ] + } + }, + "bindings": { + "7b099540-f8b1-4bf3-0d7d-2b74df370067": { + "id": "7b099540-f8b1-4bf3-0d7d-2b74df370067", + "type": "arrow", + "fromId": "c2125c4e-e9ef-4eeb-2011-9e8aaeadd6ea", + "toId": "9a20ffa8-044b-4ca3-0448-632e2b2831d1", + "handleId": "end", + "point": [ + 0.08, + 0.5 + ], + "distance": 8 + }, + "31007b26-4667-4932-00aa-7f389642b187": { + "id": "31007b26-4667-4932-00aa-7f389642b187", + "type": "arrow", + "fromId": "c2125c4e-e9ef-4eeb-2011-9e8aaeadd6ea", + "toId": "3805a6e3-b50a-4bb2-0782-e687526cafc8", + "handleId": "start", + "point": [ + 0.5, + 0.5 + ], + "distance": 16 + } + } + } + }, + "pageStates": { + "page": { + "id": "page", + "selectedIds": [], + "camera": { + "point": [ + 0, + 0 + ], + "zoom": 1 + }, + "editingId": null + } + }, + "assets": {} + }, + "assets": {} +} \ No newline at end of file diff --git a/apps/vscode/extension/examples/2.tldr b/apps/vscode/extension/examples/2.tldr new file mode 100644 index 000000000..032d528eb --- /dev/null +++ b/apps/vscode/extension/examples/2.tldr @@ -0,0 +1,1026 @@ +{ + "name": "New Document", + "fileHandle": {}, + "document": { + "id": "doc", + "name": "New Document", + "version": 13, + "pages": { + "page": { + "id": "page", + "name": "Page 1", + "childIndex": 1, + "shapes": { + "d8f1475a-d37a-402c-3669-01944f9b84e8": { + "id": "d8f1475a-d37a-402c-3669-01944f9b84e8", + "type": "draw", + "name": "Draw", + "parentId": "page", + "childIndex": 1, + "point": [ + 313.77, + 287.84 + ], + "rotation": 0, + "style": { + "color": "black", + "size": "large", + "isFilled": false, + "dash": "draw" + }, + "points": [ + [ + 0, + 38.91, + 0.5 + ], + [ + 0, + 37.08, + 0.5 + ], + [ + 0, + 35.36, + 0.5 + ], + [ + 6.19, + 25.33, + 0.5 + ], + [ + 12.4, + 17.21, + 0.5 + ], + [ + 20.18, + 9.42, + 0.5 + ], + [ + 26.19, + 4.98, + 0.5 + ], + [ + 32.68, + 1.73, + 0.5 + ], + [ + 40.09, + 0, + 0.5 + ], + [ + 48.14, + 0, + 0.5 + ], + [ + 56.18, + 0, + 0.5 + ], + [ + 63.05, + 2.1599999999999966, + 0.5 + ], + [ + 70.5, + 8.429999999999996, + 0.5 + ], + [ + 74.77, + 15.179999999999996, + 0.5 + ], + [ + 76.5, + 23.619999999999997, + 0.5 + ], + [ + 76.5, + 38.91, + 0.5 + ], + [ + 68.56, + 58.839999999999996, + 0.5 + ], + [ + 57.59, + 75.91, + 0.5 + ], + [ + 42.45, + 97.68, + 0.5 + ], + [ + 30.36, + 115.05, + 0.5 + ], + [ + 25.65, + 122.03, + 0.5 + ], + [ + 21.21, + 128.03, + 0.5 + ], + [ + 19.68, + 130.31, + 0.5 + ], + [ + 19.13, + 131.13, + 0.5 + ], + [ + 19.13, + 131.41, + 0.5 + ], + [ + 21.29, + 129.5, + 0.5 + ], + [ + 25.29, + 126.02, + 0.5 + ], + [ + 32.08, + 122.03, + 0.5 + ], + [ + 41.43, + 118.28999999999999, + 0.5 + ], + [ + 52.94, + 116.24, + 0.5 + ], + [ + 64.4, + 115.53999999999999, + 0.5 + ], + [ + 76.53, + 115.53999999999999, + 0.5 + ], + [ + 86.91, + 115.53999999999999, + 0.5 + ], + [ + 97.28, + 117.47999999999999, + 0.5 + ], + [ + 104.2, + 118.56, + 0.5 + ], + [ + 111.61, + 120.28999999999999, + 0.5 + ], + [ + 115.79, + 121.21, + 0.5 + ], + [ + 118.34, + 121.97, + 0.5 + ], + [ + 118.89, + 122.24, + 0.5 + ], + [ + 119.16, + 122.24, + 0.5 + ] + ], + "isComplete": true + }, + "4a8c1095-335e-42d6-30c9-ff4171dd364e": { + "id": "4a8c1095-335e-42d6-30c9-ff4171dd364e", + "type": "draw", + "name": "Draw", + "parentId": "page", + "childIndex": 2, + "point": [ + 239.55, + 166.36 + ], + "rotation": 0.0759349816929582, + "style": { + "color": "black", + "size": "large", + "isFilled": false, + "dash": "draw" + }, + "points": [ + [ + 72.8938650204388, + 14.590000000000002, + 0.5 + ], + [ + 68.8781905397514, + 19.51, + 0.5 + ], + [ + 61.84336030699925, + 27.34, + 0.5 + ], + [ + 54.16720613998498, + 35.12, + 0.5 + ], + [ + 44.458548427463086, + 44.24, + 0.5 + ], + [ + 23.92631600900976, + 62.11, + 0.5 + ], + [ + 17.207214482355887, + 67.74, + 0.5 + ], + [ + 8.31747810127638, + 73.53, + 0.5 + ], + [ + 3.7196788187202796, + 77.64, + 0.5 + ], + [ + 1.4701117877700842, + 79.15, + 0.5 + ], + [ + 0.552525235671978, + 79.75, + 0.5 + ], + [ + 0, + 80.29, + 0.5 + ], + [ + 0, + 80.57, + 0.5 + ], + [ + 1.9338383248519229, + 80.57, + 0.5 + ], + [ + 7.547889380161841, + 80.57, + 0.5 + ], + [ + 15.756835738716942, + 81.75, + 0.5 + ], + [ + 26.99480437140235, + 83.09, + 0.5 + ], + [ + 40.689536998414944, + 83.09, + 0.5 + ], + [ + 51.35524735129723, + 83.09, + 0.5 + ], + [ + 62.71161424876949, + 83.09, + 0.5 + ], + [ + 70.9205606073246, + 83.09, + 0.5 + ], + [ + 76.5346116626345, + 83.09, + 0.5 + ], + [ + 80.6292183198465, + 83.09, + 0.5 + ], + [ + 82.98731709351797, + 83.09, + 0.5 + ], + [ + 84.26996496204221, + 83.41, + 0.5 + ], + [ + 84.81262367564861, + 83.41, + 0.5 + ], + [ + 85.07901977141903, + 83.41, + 0.5 + ], + [ + 84.32916409443563, + 82.04, + 0.5 + ], + [ + 82.82945274046884, + 77.05, + 0.5 + ], + [ + 80.68841745223992, + 70.45, + 0.5 + ], + [ + 78.12312171519146, + 60.059999999999995, + 0.5 + ], + [ + 75.83408859597897, + 44.82, + 0.5 + ], + [ + 75.02503378660215, + 28.34, + 0.5 + ], + [ + 75.02503378660215, + 16.24, + 0.5 + ], + [ + 74.38370985234003, + 6.879999999999999, + 0.5 + ], + [ + 74.38370985234003, + 2.73, + 0.5 + ], + [ + 74.00878201384833, + 0.93, + 0.5 + ], + [ + 74.00878201384833, + 0, + 0.5 + ], + [ + 74.22584549929091, + 3.552713678800501e-15, + 0.5 + ], + [ + 78.34018520063404, + 0.9100000000000037, + 0.5 + ], + [ + 84.84222324184536, + 1.9900000000000038, + 0.5 + ], + [ + 97.31350713272714, + 2.690000000000003, + 0.5 + ], + [ + 113.48473679819806, + 3.450000000000003, + 0.5 + ], + [ + 133.957770084258, + 3.450000000000003, + 0.5 + ], + [ + 157.27236172520233, + 3.450000000000003, + 0.5 + ], + [ + 177.74539501126222, + 3.450000000000003, + 0.5 + ], + [ + 195.16967297906066, + 3.450000000000003, + 0.5 + ], + [ + 204.61193459581213, + 3.450000000000003, + 0.5 + ], + [ + 212.92941269708854, + 2.9000000000000044, + 0.5 + ], + [ + 217.0240193543005, + 2.9000000000000044, + 0.5 + ], + [ + 219.05652289980813, + 2.9000000000000044, + 0.5 + ], + [ + 219.58931509134896, + 2.9000000000000044, + 0.5 + ], + [ + 219.86557770918498, + 2.9000000000000044, + 0.5 + ], + [ + 219.86557770918498, + 3.780000000000003, + 0.5 + ], + [ + 219.86557770918498, + 7.930000000000003, + 0.5 + ], + [ + 219.86557770918498, + 16.260000000000005, + 0.5 + ], + [ + 219.86557770918498, + 28.90000000000001, + 0.5 + ], + [ + 219.86557770918498, + 44.03, + 0.5 + ], + [ + 220.6055668641028, + 57.87, + 0.5 + ], + [ + 222.1447443063319, + 74.34, + 0.5 + ], + [ + 223.69378827062653, + 90.82000000000001, + 0.5 + ], + [ + 225.34149745557687, + 110.10000000000001, + 0.5 + ], + [ + 227.82786101610077, + 139.69, + 0.5 + ], + [ + 229.58410194377242, + 161.95000000000002, + 0.5 + ], + [ + 231.27127721698508, + 182.52, + 0.5 + ], + [ + 232.07046550429635, + 201.8, + 0.5 + ], + [ + 232.91898640193543, + 222.33, + 0.5 + ], + [ + 233.7181746892467, + 240.09, + 0.5 + ], + [ + 233.7181746892467, + 257.88, + 0.5 + ], + [ + 233.7181746892467, + 274.36, + 0.5 + ], + [ + 234.57656210895138, + 293.6, + 0.5 + ], + [ + 234.57656210895138, + 311.41999999999996, + 0.5 + ], + [ + 235.10935430049219, + 320.99, + 0.5 + ], + [ + 235.8000108450822, + 331.47, + 0.5 + ], + [ + 236.2736039042296, + 337.14, + 0.5 + ], + [ + 236.2736039042296, + 339.52, + 0.5 + ], + [ + 236.54000000000002, + 340.45000000000005, + 0.5 + ], + [ + 236.54000000000002, + 341, + 0.5 + ], + [ + 236.54000000000002, + 341.27, + 0.5 + ], + [ + 236.32293651455745, + 341.27, + 0.5 + ], + [ + 232.19873029114876, + 341.27, + 0.5 + ], + [ + 222.96366563777426, + 341.27, + 0.5 + ], + [ + 210.4825152248269, + 343.37, + 0.5 + ], + [ + 195.5643338616835, + 346.39, + 0.5 + ], + [ + 177.97232501877033, + 348.82000000000005, + 0.5 + ], + [ + 160.38031617585716, + 350.42999999999995, + 0.5 + ], + [ + 142.857372987403, + 351.23, + 0.5 + ], + [ + 122.38433970134314, + 352.1, + 0.5 + ], + [ + 101.91130641528324, + 352.1, + 0.5 + ], + [ + 85.66114457328774, + 352.90999999999997, + 0.5 + ], + [ + 74.99543422040544, + 353.45000000000005, + 0.5 + ], + [ + 65.76036956703096, + 354.09000000000003, + 0.5 + ], + [ + 60.146318511721056, + 354.63, + 0.5 + ], + [ + 57.79808626011515, + 355.06000000000006, + 0.5 + ], + [ + 56.39704012680406, + 355.06000000000006, + 0.5 + ], + [ + 56.13064403103365, + 355.06000000000006, + 0.5 + ], + [ + 55.88398097939437, + 355.06000000000006, + 0.5 + ], + [ + 55.65705097188623, + 355.06000000000006, + 0.5 + ], + [ + 55.420254442312526, + 355.06000000000006, + 0.5 + ], + [ + 55.420254442312526, + 354.83000000000004, + 0.5 + ], + [ + 55.420254442312526, + 354.28, + 0.5 + ], + [ + 55.420254442312526, + 352.58000000000004, + 0.5 + ], + [ + 55.420254442312526, + 351.91999999999996, + 0.5 + ], + [ + 53.14108784516562, + 345.85, + 0.5 + ], + [ + 51.64137649119882, + 340.86, + 0.5 + ], + [ + 49.766737298740324, + 331.5, + 0.5 + ], + [ + 47.94143071660968, + 322.14, + 0.5 + ], + [ + 46.65878284808544, + 311.76, + 0.5 + ], + [ + 44.68547843497124, + 300.24, + 0.5 + ], + [ + 43.560694919496136, + 292.61, + 0.5 + ], + [ + 41.47885876366065, + 281.25, + 0.5 + ], + [ + 38.07490865103865, + 269.57, + 0.5 + ], + [ + 36.46666555435057, + 263.65999999999997, + 0.5 + ], + [ + 34.21709852340037, + 256.25, + 0.5 + ], + [ + 32.07606323517146, + 249.75, + 0.5 + ], + [ + 30.477686660548947, + 243.28, + 0.5 + ], + [ + 28.020922666221765, + 233.93, + 0.5 + ], + [ + 26.146283473763262, + 224.57, + 0.5 + ], + [ + 24.86363560523903, + 214.25, + 0.5 + ], + [ + 22.84099858179697, + 202.72999999999996, + 0.5 + ], + [ + 20.759162425961478, + 191.43, + 0.5 + ], + [ + 18.04586885792944, + 181.42000000000002, + 0.5 + ], + [ + 15.579238341536684, + 172.06, + 0.5 + ], + [ + 13.467802619504488, + 165.54, + 0.5 + ], + [ + 10.053985984816912, + 154.01, + 0.5 + ], + [ + 7.912950696587994, + 147.51, + 0.5 + ], + [ + 6.788167181112896, + 140.09, + 0.5 + ], + [ + 6.255374989572053, + 133.62, + 0.5 + ], + [ + 5.712716275965649, + 127.13, + 0.5 + ], + [ + 5.712716275965649, + 122.97, + 0.5 + ], + [ + 5.712716275965649, + 120.09, + 0.5 + ], + [ + 5.712716275965649, + 118.77000000000002, + 0.5 + ], + [ + 5.712716275965649, + 118.22, + 0.5 + ], + [ + 5.712716275965649, + 117.94, + 0.5 + ], + [ + 5.712716275965649, + 117.69999999999999, + 0.5 + ], + [ + 5.712716275965649, + 117.45, + 0.5 + ], + [ + 5.712716275965649, + 114.57000000000001, + 0.5 + ], + [ + 5.712716275965649, + 109.69999999999999, + 0.5 + ], + [ + 5.712716275965649, + 101.36, + 0.5 + ], + [ + 5.712716275965649, + 96.37, + 0.5 + ], + [ + 5.712716275965649, + 92.81, + 0.5 + ], + [ + 5.712716275965649, + 90.52, + 0.5 + ], + [ + 5.712716275965649, + 89.97, + 0.5 + ], + [ + 5.712716275965649, + 89.68999999999998, + 0.5 + ], + [ + 5.712716275965649, + 89.46000000000001, + 0.5 + ], + [ + 6.2060423792442005, + 89.46000000000001, + 0.5 + ] + ], + "isComplete": true + }, + "9fa6dcc1-3d5e-4b7b-1afd-e9f5dd8742aa": { + "id": "9fa6dcc1-3d5e-4b7b-1afd-e9f5dd8742aa", + "type": "text", + "name": "Text", + "parentId": "page", + "childIndex": 3, + "point": [ + 509.18, + 262.72 + ], + "rotation": 0, + "text": "Save files!", + "style": { + "color": "black", + "size": "large", + "isFilled": false, + "dash": "draw" + } + } + }, + "bindings": {} + } + }, + "pageStates": { + "page": { + "id": "page", + "selectedIds": [ + "9fa6dcc1-3d5e-4b7b-1afd-e9f5dd8742aa" + ], + "camera": { + "point": [ + -130.08, + -111.66 + ], + "zoom": 1.1462851349067762 + } + } + } + }, + "assets": {} +} \ No newline at end of file diff --git a/apps/vscode/extension/examples/3.tldr b/apps/vscode/extension/examples/3.tldr new file mode 100644 index 000000000..a0c8f5f18 --- /dev/null +++ b/apps/vscode/extension/examples/3.tldr @@ -0,0 +1,1104 @@ +{ + "name": "New Document", + "fileHandle": {}, + "document": { + "id": "doc", + "name": "New Document", + "version": 14, + "pages": { + "page": { + "id": "page", + "name": "Page 1", + "childIndex": 1, + "shapes": { + "60a83a6a-6477-45ff-3cd9-5c705c210c3a": { + "id": "60a83a6a-6477-45ff-3cd9-5c705c210c3a", + "type": "draw", + "name": "Draw", + "parentId": "page", + "childIndex": 1, + "point": [ + 211.25, + 246.95 + ], + "rotation": 0, + "style": { + "color": "white", + "size": "large", + "isFilled": false, + "dash": "draw" + }, + "points": [ + [ + 0, + 52.7, + 0.5 + ], + [ + 0.13, + 52.84, + 0.5 + ], + [ + 3.52, + 52.84, + 0.5 + ], + [ + 7.92, + 52.84, + 0.5 + ], + [ + 15.31, + 52.84, + 0.5 + ], + [ + 21.36, + 51.75, + 0.5 + ], + [ + 29.47, + 49.7, + 0.5 + ], + [ + 37.15, + 46.05, + 0.5 + ], + [ + 45.79, + 41.73, + 0.5 + ], + [ + 52.8, + 38.24, + 0.5 + ], + [ + 58.99, + 33.97, + 0.5 + ], + [ + 62.97, + 30.31, + 0.5 + ], + [ + 67.09, + 22.39, + 0.5 + ], + [ + 67.69, + 18.33, + 0.5 + ], + [ + 68, + 12.74, + 0.5 + ], + [ + 68, + 10.09, + 0.5 + ], + [ + 66.16, + 5.89, + 0.5 + ], + [ + 62.82, + 3.46, + 0.5 + ], + [ + 59.09, + 1.82, + 0.5 + ], + [ + 54.8, + 0.32, + 0.5 + ], + [ + 50.88, + 0, + 0.5 + ], + [ + 48.22, + 1.4210854715202004e-14, + 0.5 + ], + [ + 45.85, + 1.15000000000002, + 0.5 + ], + [ + 44.96, + 5.100000000000016, + 0.5 + ], + [ + 44.61, + 12.610000000000014, + 0.5 + ], + [ + 44.61, + 22.87000000000002, + 0.5 + ], + [ + 44.61, + 40.65000000000002, + 0.5 + ], + [ + 44.61, + 52.83000000000002, + 0.5 + ], + [ + 44.61, + 74.87000000000002, + 0.5 + ], + [ + 48.07, + 93.28000000000002, + 0.5 + ], + [ + 51.16, + 106.48000000000002, + 0.5 + ], + [ + 55.96, + 129.24, + 0.5 + ], + [ + 56.48, + 142.85000000000002, + 0.5 + ], + [ + 54.22, + 153.35000000000002, + 0.5 + ], + [ + 48.41, + 160.48000000000002, + 0.5 + ], + [ + 39.35, + 164.79000000000002, + 0.5 + ], + [ + 29.17, + 165.25, + 0.5 + ], + [ + 20.42, + 164.45000000000002, + 0.5 + ], + [ + 14.19, + 159.81, + 0.5 + ], + [ + 8.95, + 152.96000000000004, + 0.5 + ], + [ + 6.37, + 146.16000000000003, + 0.5 + ], + [ + 5.96, + 134.95000000000002, + 0.5 + ], + [ + 8.89, + 124.33000000000001, + 0.5 + ], + [ + 20.38, + 111.74000000000001, + 0.5 + ], + [ + 36.82, + 100.57000000000002, + 0.5 + ], + [ + 56.64, + 90.02000000000001, + 0.5 + ], + [ + 78.06, + 78.35000000000002, + 0.5 + ], + [ + 89.74, + 70.75000000000001, + 0.5 + ], + [ + 100.34, + 61.68000000000002, + 0.5 + ], + [ + 105.51, + 53.48000000000002, + 0.5 + ], + [ + 107.1, + 45.490000000000016, + 0.5 + ], + [ + 107.1, + 38.750000000000014, + 0.5 + ], + [ + 104.94, + 33.73000000000002, + 0.5 + ], + [ + 103.78, + 32.29000000000002, + 0.5 + ], + [ + 102.13, + 31.020000000000017, + 0.5 + ], + [ + 101.84, + 30.87000000000002, + 0.5 + ], + [ + 101.49, + 31.790000000000017, + 0.5 + ], + [ + 101.17, + 38.04000000000002, + 0.5 + ], + [ + 100.76, + 47.58000000000002, + 0.5 + ], + [ + 99, + 66.05000000000001, + 0.5 + ], + [ + 98.08, + 77.33000000000001, + 0.5 + ], + [ + 97.59, + 89.22000000000003, + 0.5 + ], + [ + 97.59, + 101.08000000000001, + 0.5 + ], + [ + 97.59, + 109.85000000000002, + 0.5 + ], + [ + 99.32, + 115.17000000000002, + 0.5 + ], + [ + 101.97, + 119.49000000000002, + 0.5 + ], + [ + 104.27, + 122.43000000000002, + 0.5 + ], + [ + 108.42, + 124.10000000000002, + 0.5 + ], + [ + 112.36, + 124.10000000000002, + 0.5 + ], + [ + 117.77, + 120.96000000000002, + 0.5 + ], + [ + 123.7, + 113.29000000000002, + 0.5 + ], + [ + 129.05, + 103.90000000000002, + 0.5 + ], + [ + 132.64, + 94.09000000000002, + 0.5 + ], + [ + 133.82, + 86.70000000000002, + 0.5 + ], + [ + 133.82, + 82.64000000000001, + 0.5 + ], + [ + 132.32, + 78.95000000000002, + 0.5 + ], + [ + 130.46, + 78.48000000000002, + 0.5 + ], + [ + 128.62, + 78.48000000000002, + 0.5 + ], + [ + 126.02, + 80.21000000000002, + 0.5 + ], + [ + 124.75, + 84.70000000000002, + 0.5 + ], + [ + 124.4, + 90.23000000000002, + 0.5 + ], + [ + 124.4, + 97.56000000000002, + 0.5 + ], + [ + 124.4, + 104.29000000000002, + 0.5 + ], + [ + 127.34, + 109.91000000000003, + 0.5 + ], + [ + 129.95, + 112.26000000000002, + 0.5 + ], + [ + 134.75, + 114.79000000000002, + 0.5 + ], + [ + 139.73, + 115.11000000000001, + 0.5 + ], + [ + 148.71, + 110.61000000000001, + 0.5 + ], + [ + 154.63, + 103.39000000000001, + 0.5 + ], + [ + 164.71, + 87.14000000000001, + 0.5 + ], + [ + 168.65, + 74.91000000000003, + 0.5 + ], + [ + 170.72, + 60.40000000000002, + 0.5 + ], + [ + 171.24, + 45.890000000000015, + 0.5 + ], + [ + 171.24, + 35.62000000000002, + 0.5 + ], + [ + 168.68, + 28.80000000000002, + 0.5 + ], + [ + 167.9, + 27.500000000000018, + 0.5 + ], + [ + 167.15, + 25.87000000000002, + 0.5 + ], + [ + 167.01, + 25.87000000000002, + 0.5 + ], + [ + 167.01, + 28.71000000000002, + 0.5 + ], + [ + 167.01, + 39.33000000000002, + 0.5 + ], + [ + 167.01, + 53.890000000000015, + 0.5 + ], + [ + 167.93, + 65.17000000000002, + 0.5 + ], + [ + 169.94, + 78.78000000000002, + 0.5 + ], + [ + 172.08, + 88.18, + 0.5 + ], + [ + 173.7, + 92.67000000000002, + 0.5 + ], + [ + 175.89, + 96.87000000000002, + 0.5 + ], + [ + 178.82, + 99.18, + 0.5 + ], + [ + 181.84, + 99.18, + 0.5 + ], + [ + 184.49, + 96.29000000000002, + 0.5 + ], + [ + 189.09, + 85.41000000000003, + 0.5 + ], + [ + 191.96, + 74.21000000000002, + 0.5 + ], + [ + 195.5, + 54.350000000000016, + 0.5 + ], + [ + 197.4, + 42.12000000000002, + 0.5 + ], + [ + 198.98, + 26.650000000000016, + 0.5 + ], + [ + 199.78, + 18.08000000000002, + 0.5 + ], + [ + 200.13, + 11.310000000000016, + 0.5 + ], + [ + 200.13, + 8.100000000000016, + 0.5 + ], + [ + 200.13, + 7.960000000000015, + 0.5 + ], + [ + 200.13, + 13.40000000000002, + 0.5 + ], + [ + 200.13, + 20.880000000000017, + 0.5 + ], + [ + 199.15, + 33.490000000000016, + 0.5 + ], + [ + 198.63, + 47.990000000000016, + 0.5 + ], + [ + 198.63, + 62.500000000000014, + 0.5 + ], + [ + 198.63, + 74.38000000000002, + 0.5 + ], + [ + 198.63, + 84.57000000000002, + 0.5 + ], + [ + 200.24, + 89.06000000000002, + 0.5 + ], + [ + 202.34, + 92.71000000000001, + 0.5 + ], + [ + 205.37, + 94.09000000000002, + 0.5 + ], + [ + 208.82, + 94.09000000000002, + 0.5 + ], + [ + 213.66, + 91.21000000000001, + 0.5 + ], + [ + 220.56, + 84.25000000000001, + 0.5 + ], + [ + 227.93, + 75.50000000000001, + 0.5 + ], + [ + 233.45, + 65.37000000000002, + 0.5 + ], + [ + 235.86, + 59.38000000000002, + 0.5 + ], + [ + 237.24, + 53.84000000000002, + 0.5 + ], + [ + 237.24, + 51.19000000000002, + 0.5 + ], + [ + 236.9, + 50.09000000000002, + 0.5 + ], + [ + 234.67, + 49.570000000000014, + 0.5 + ], + [ + 232.08, + 50.09000000000002, + 0.5 + ], + [ + 228.75, + 57.54000000000002, + 0.5 + ], + [ + 228, + 60.890000000000015, + 0.5 + ], + [ + 227.26, + 70.96000000000002, + 0.5 + ], + [ + 227.26, + 77.66000000000003, + 0.5 + ], + [ + 229.33, + 82.84000000000002, + 0.5 + ], + [ + 232.81, + 85.69000000000003, + 0.5 + ], + [ + 237.24, + 86.96000000000001, + 0.5 + ], + [ + 243.37, + 86.96000000000001, + 0.5 + ], + [ + 249.36, + 83.10000000000002, + 0.5 + ], + [ + 253.78, + 76.25000000000001, + 0.5 + ], + [ + 256.24, + 67.50000000000001, + 0.5 + ], + [ + 256.24, + 58.01000000000002, + 0.5 + ], + [ + 254.74, + 50.610000000000014, + 0.5 + ], + [ + 250.43, + 45.920000000000016, + 0.5 + ], + [ + 245.25, + 43.850000000000016, + 0.5 + ], + [ + 239.18, + 43.850000000000016, + 0.5 + ], + [ + 233.05, + 44.22000000000001, + 0.5 + ], + [ + 229.61, + 46.97000000000001, + 0.5 + ], + [ + 226.55, + 49.990000000000016, + 0.5 + ], + [ + 224.69, + 53.58000000000002, + 0.5 + ] + ], + "isComplete": true + }, + "983edee8-07e8-4d06-37ef-8244cd661c4a": { + "id": "983edee8-07e8-4d06-37ef-8244cd661c4a", + "type": "draw", + "name": "Draw", + "parentId": "page", + "childIndex": 2, + "point": [ + 553.77, + 248.23 + ], + "rotation": 0, + "style": { + "color": "black", + "size": "large", + "isFilled": false, + "dash": "draw" + }, + "points": [ + [ + 10.69, + 0.14, + 0.5 + ], + [ + 10.69, + 0, + 0.5 + ], + [ + 10.69, + 0.65, + 0.5 + ], + [ + 8.88, + 9, + 0.5 + ], + [ + 6.21, + 15.94, + 0.5 + ], + [ + 4.84, + 21.4, + 0.5 + ], + [ + 0, + 51.82, + 0.5 + ], + [ + 5.5067062021407764e-14, + 59.930000000000014, + 0.5 + ], + [ + 5.5067062021407764e-14, + 65.52000000000001, + 0.5 + ], + [ + 5.5067062021407764e-14, + 70.61000000000001, + 0.5 + ] + ], + "isComplete": true + }, + "6104b61b-45cb-4425-2c60-79a43467fbd7": { + "id": "6104b61b-45cb-4425-2c60-79a43467fbd7", + "type": "draw", + "name": "Draw", + "parentId": "page", + "childIndex": 3, + "point": [ + 551.74, + 346.04, + 0.5 + ], + "rotation": 0, + "style": { + "color": "black", + "size": "large", + "isFilled": false, + "dash": "draw" + }, + "points": [], + "isComplete": true + }, + "3a0ccf5e-8e12-4780-38ff-1cdc9c1ed0e3": { + "id": "3a0ccf5e-8e12-4780-38ff-1cdc9c1ed0e3", + "type": "draw", + "name": "Draw", + "parentId": "page", + "childIndex": 4, + "point": [ + 456.82, + 377.32, + 0.5 + ], + "rotation": 0, + "style": { + "color": "black", + "size": "small", + "isFilled": false, + "dash": "draw" + }, + "points": [], + "isComplete": true + }, + "efc863d7-6489-4142-32f8-9534fdceaa2b": { + "id": "efc863d7-6489-4142-32f8-9534fdceaa2b", + "type": "draw", + "name": "Draw", + "parentId": "page", + "childIndex": 5, + "point": [ + 488.26, + 369.22, + 0.5 + ], + "rotation": 0, + "style": { + "color": "black", + "size": "small", + "isFilled": false, + "dash": "draw" + }, + "points": [], + "isComplete": true + }, + "250460f4-f56e-4c2f-23ba-91bd98303837": { + "id": "250460f4-f56e-4c2f-23ba-91bd98303837", + "type": "draw", + "name": "Draw", + "parentId": "page", + "childIndex": 6, + "point": [ + 460.44, + 374.14 + ], + "rotation": 0, + "style": { + "color": "black", + "size": "small", + "isFilled": false, + "dash": "draw" + }, + "points": [ + [ + 0, + 14.37, + 0.5 + ], + [ + 0.73, + 15.7, + 0.5 + ], + [ + 1.54, + 16.05, + 0.5 + ], + [ + 2.53, + 16.89, + 0.5 + ], + [ + 3.98, + 17.24, + 0.5 + ], + [ + 6.01, + 17.46, + 0.5 + ], + [ + 8.57, + 17.46, + 0.5 + ], + [ + 11.21, + 17.46, + 0.5 + ], + [ + 15.42, + 17.46, + 0.5 + ], + [ + 19.63, + 17.19, + 0.5 + ], + [ + 24.02, + 14.71, + 0.5 + ], + [ + 28, + 11.3, + 0.5 + ], + [ + 30.3, + 8.51, + 0.5 + ], + [ + 33.21, + 3.9, + 0.5 + ], + [ + 34.4, + 1.92, + 0.5 + ], + [ + 34.88, + 0.75, + 0.5 + ], + [ + 35.14, + 0.22, + 0.5 + ], + [ + 35.14, + 0.11, + 0.5 + ], + [ + 35.14, + 0, + 0.5 + ], + [ + 35.24, + 5.329070518200751e-15, + 0.5 + ] + ], + "isComplete": true + } + }, + "bindings": {} + } + }, + "pageStates": { + "page": { + "id": "page", + "selectedIds": [], + "camera": { + "point": [ + -219.13, + -220.86 + ], + "zoom": 2.462934947049924 + } + } + } + }, + "assets": {} +} \ No newline at end of file diff --git a/apps/vscode/extension/examples/files-with-erros/invalid-record.tldr b/apps/vscode/extension/examples/files-with-erros/invalid-record.tldr new file mode 100644 index 000000000..3d2f300ec --- /dev/null +++ b/apps/vscode/extension/examples/files-with-erros/invalid-record.tldr @@ -0,0 +1 @@ +{"tldrawFileFormatVersion":1,"schema":{"schemaVersion":1,"storeVersion":1,"recordVersions":{"asset":{"version":0,"subTypeKey":"type","subTypeVersions":{"image":2,"video":2,"bookmark":0}},"camera":{"version":0},"document":{"version":0},"instance":{"version":5},"instance_page_state":{"version":0},"page":{"version":0},"shape":{"version":1,"subTypeKey":"type","subTypeVersions":{"draw":1,"text":0,"line":0,"arrow":1,"image":1,"video":1,"geo":2,"note":1,"group":0,"bookmark":1,"embed":1,"frame":0}},"user":{"version":0},"user_document":{"version":2},"user_presence":{"version":0}}},"records":[{"x":0,"y":0,"z":1,"id":"camera:ZEdonVzBw8UQNxHVSfYeN","typeName":"camera"},{"gridSize":10,"id":"document:document","typeName":"document"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"m","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":true,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":1016,"h":1308},"id":"instance:9NyQ7QvIoscZrrjfaLlw3","userId":"user:THIgzt9P6hTGullzaOPpd","currentPageId":"page:DldMYGg34nlqr9pTjmOaV","typeName":"instance"},{"editingId":null,"selectedIds":[],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:l05WL5JCaWeamtf3xX-rR","pageId":"page:DldMYGg34nlqr9pTjmOaV","instanceId":"instance:9NyQ7QvIoscZrrjfaLlw3","cameraId":"camera:ZEdonVzBw8UQNxHVSfYeN","typeName":"instance_page_state"},{"id":"page:DldMYGg34nlqr9pTjmOaV","name":"Page 1","index":"a1","typeName":"page"},{"name":"New User","locale":"en","id":"user:THIgzt9P6hTGullzaOPpd","typeName":"user"},{"isReadOnly":false,"isPenMode":false,"isGridMode":false,"isDarkMode":false,"isMobileMode":false,"isSnapMode":false,"lastUpdatedPageId":"page:DldMYGg34nlqr9pTjmOaV","lastUsedTabId":"instance:Bgu8sMzoEjBFOltNtawzR","id":"user_document:m48KwWKytfJTnajCKC4Ci","userId":"user:THIgzt9P6hTGullzaOPpd","typeName":"user_document"},{"lastUsedInstanceId":"instance:Bgu8sMzoEjBFOltNtawzR","lastActivityTimestamp":1679326918108,"cursor":{"x":1057.125,"y":439.03125,"z":0},"color":"#39B178","id":"user_presence:mWjDj8J3KWsxO9FST9mJY","userId":"user:THIgzt9P6hTGullzaOPpd","typeName":"user_presence"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"m","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":true,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":1016,"h":1308},"id":"instance:qjCFl-gVEHZV6qnavaI5A","userId":"user:THIgzt9P6hTGullzaOPpd","currentPageId":"page:DldMYGg34nlqr9pTjmOaV","typeName":"instance"},{"x":0,"y":0,"z":1,"id":"camera:kDPjsRu3p8aoVfzWbNISY","typeName":"camera"},{"editingId":null,"selectedIds":[],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:80sg16r6TLoBdklDRg4px","pageId":"page:DldMYGg34nlqr9pTjmOaV","instanceId":"instance:qjCFl-gVEHZV6qnavaI5A","cameraId":"camera:kDPjsRu3p8aoVfzWbNISY","typeName":"instance_page_state"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"m","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":true,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":1223,"h":1308},"id":"instance:Bgu8sMzoEjBFOltNtawzR","userId":"user:THIgzt9P6hTGullzaOPpd","currentPageId":"page:DldMYGg34nlqr9pTjmOaV","typeName":"instance"},{"x":0,"y":0,"z":1,"id":"camera:6grbCQvcD63BvbkYvT_zD","typeName":"camera"},{"editingId":null,"selectedIds":["shape:qzz1TZBX_0FfNiKrwE8pI"],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:VIw9Cdw1hWs5ABt9Bpl8U","pageId":"page:DldMYGg34nlqr9pTjmOaV","instanceId":"instance:Bgu8sMzoEjBFOltNtawzR","cameraId":"camera:6grbCQvcD63BvbkYvT_zD","typeName":"instance_page_state"},{"x":454.046875,"y":384.203125,"rotation":0,"isLocked":false,"id":"shape:qzz1TZBX_0FfNiKrwE8pI","type":"geo","props":{"w":97.83984375,"h":103.23828125,"geo":"rectangle","color":"black","labelColor":"black","fill":"none","dash":"draw","size":"m","opacity":"1","font":"draw","text":"","align":"middle","growY":0,"url":""},"parentId":"page:DldMYGg34nlqr9pTjmOaV","index":"a1","typeName":"unknown-type"}]} \ No newline at end of file diff --git a/apps/vscode/extension/examples/files-with-erros/invalid-tldraw-file.tldr b/apps/vscode/extension/examples/files-with-erros/invalid-tldraw-file.tldr new file mode 100644 index 000000000..463405bc0 --- /dev/null +++ b/apps/vscode/extension/examples/files-with-erros/invalid-tldraw-file.tldr @@ -0,0 +1 @@ +This is not a valid tldr file. \ No newline at end of file diff --git a/apps/vscode/extension/examples/files-with-erros/version-too-new.tldr b/apps/vscode/extension/examples/files-with-erros/version-too-new.tldr new file mode 100644 index 000000000..251373c95 --- /dev/null +++ b/apps/vscode/extension/examples/files-with-erros/version-too-new.tldr @@ -0,0 +1 @@ +{"tldrawFileFormatVersion":1000,"schema":{"schemaVersion":1,"storeVersion":1,"recordVersions":{"asset":{"version":0,"subTypeKey":"type","subTypeVersions":{"image":2,"video":2,"bookmark":0}},"camera":{"version":0},"document":{"version":0},"instance":{"version":5},"instance_page_state":{"version":0},"page":{"version":0},"user":{"version":0},"user_document":{"version":2},"user_presence":{"version":0},"shape":{"version":1,"subTypeKey":"type","subTypeVersions":{"draw":1,"text":0,"line":0,"arrow":1,"image":1,"video":1,"geo":2,"note":1,"group":0,"bookmark":1,"embed":1,"frame":0}}}},"records":[{"gridSize":10,"id":"document:document","typeName":"document"},{"isReadOnly":false,"isPenMode":false,"isGridMode":false,"isDarkMode":false,"isMobileMode":false,"isSnapMode":false,"lastUpdatedPageId":"page:wTYNt8AJ-CAe-ozU4bYw0","lastUsedTabId":"instance:H7Ghe-kI0V36JYl-FtXV6","id":"user_document:384AkIr8uUukpLs33G6TO","userId":"user:P_x7HuwQNs0er568NA5nt","typeName":"user_document"},{"id":"page:wTYNt8AJ-CAe-ozU4bYw0","name":"Page 1","index":"a1","typeName":"page"},{"propsForNextShape":{"opacity":"1","color":"black","labelColor":"black","dash":"draw","fill":"none","size":"m","icon":"file","font":"draw","align":"middle","geo":"rectangle","arrowheadStart":"none","arrowheadEnd":"arrow","spline":"line"},"brush":null,"scribble":null,"cursor":{"type":"default","color":"black","rotation":0},"isFocusMode":false,"exportBackground":true,"isDebugMode":false,"isToolLocked":false,"screenBounds":{"x":0,"y":0,"w":1920,"h":1093},"id":"instance:H7Ghe-kI0V36JYl-FtXV6","userId":"user:P_x7HuwQNs0er568NA5nt","currentPageId":"page:wTYNt8AJ-CAe-ozU4bYw0","typeName":"instance"},{"name":"New User","locale":"en","id":"user:P_x7HuwQNs0er568NA5nt","typeName":"user"},{"lastUsedInstanceId":"instance:H7Ghe-kI0V36JYl-FtXV6","lastActivityTimestamp":1679326696474,"cursor":{"x":1218.24609375,"y":608.05859375,"z":0},"color":"#55B467","id":"user_presence:mao_e5ejuI4IdRbJPALvk","userId":"user:P_x7HuwQNs0er568NA5nt","typeName":"user_presence"},{"x":0,"y":0,"z":1,"id":"camera:EJQijEXfj0inf8U-V-cUp","typeName":"camera"},{"editingId":null,"selectedIds":["shape:is_pvm9FlmAIZ2IYg-9jp"],"hoveredId":null,"erasingIds":[],"hintingIds":[],"focusLayerId":null,"id":"instance_page_state:xcQsboc8bb8EOAh_0wA2u","pageId":"page:wTYNt8AJ-CAe-ozU4bYw0","instanceId":"instance:H7Ghe-kI0V36JYl-FtXV6","cameraId":"camera:EJQijEXfj0inf8U-V-cUp","typeName":"instance_page_state"},{"x":696.625,"y":390.11328125,"rotation":0,"isLocked":false,"id":"shape:is_pvm9FlmAIZ2IYg-9jp","type":"geo","props":{"w":174.56640625,"h":209.875,"geo":"rectangle","color":"black","labelColor":"black","fill":"none","dash":"draw","size":"m","opacity":"1","font":"draw","text":"","align":"middle","growY":0,"url":""},"parentId":"page:wTYNt8AJ-CAe-ozU4bYw0","index":"a1","typeName":"shape"}]} \ No newline at end of file diff --git a/apps/vscode/extension/examples/v2.tldr b/apps/vscode/extension/examples/v2.tldr new file mode 100644 index 000000000..bdd2a0cb9 --- /dev/null +++ b/apps/vscode/extension/examples/v2.tldr @@ -0,0 +1,3941 @@ +{ + "tldrawFileFormatVersion": 1, + "schema": { + "schemaVersion": 1, + "storeVersion": 1, + "recordVersions": { + "asset": { + "version": 0, + "subTypeKey": "type", + "subTypeVersions": { + "image": 2, + "video": 2, + "bookmark": 0 + } + }, + "camera": { + "version": 0 + }, + "document": { + "version": 0 + }, + "instance": { + "version": 7 + }, + "instance_page_state": { + "version": 1 + }, + "page": { + "version": 0 + }, + "shape": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": { + "draw": 1, + "text": 1, + "line": 0, + "arrow": 1, + "image": 2, + "video": 1, + "geo": 3, + "note": 2, + "group": 0, + "bookmark": 1, + "embed": 1, + "frame": 0 + } + }, + "user": { + "version": 0 + }, + "user_document": { + "version": 2 + }, + "user_presence": { + "version": 1 + } + } + }, + "records": [ + { + "x": 474.5, + "y": 0, + "z": 1, + "id": "camera:aA0VDIV1wcDWZ5BTFty9V", + "typeName": "camera" + }, + { + "x": -76, + "y": 0, + "z": 1, + "id": "camera:dW6UgPq0ROQETk5ENwAjT", + "typeName": "camera" + }, + { + "gridSize": 10, + "id": "document:document", + "typeName": "document" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 684, + "h": 1308 + }, + "userId": "user:THIgzt9P6hTGullzaOPpd", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:JZYTx9PVjbPgkUrHYmQZz", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1367, + "h": 1308 + }, + "userId": "user:THIgzt9P6hTGullzaOPpd", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:bwmZRPWFbv3bV5PfF3z2O", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:JZYTx9PVjbPgkUrHYmQZz", + "cameraId": "camera:dW6UgPq0ROQETk5ENwAjT", + "croppingId": null, + "id": "instance_page_state:6RtPsoVfg135oflNplW3v", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:bwmZRPWFbv3bV5PfF3z2O", + "cameraId": "camera:aA0VDIV1wcDWZ5BTFty9V", + "croppingId": null, + "id": "instance_page_state:IQXNljqiNrfoAZxM8kxOx", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:ClpbljJ_IKNeaNHepOrNu", + "instanceId": "instance:RRZrHoyNwdltm1qxi6TND", + "cameraId": "camera:W6oe5HCz_NrwZZgcL7N_N", + "croppingId": null, + "id": "instance_page_state:vN3u8Kz_vQpHCte3HTSeR", + "typeName": "instance_page_state" + }, + { + "id": "page:zP-IOnhkEN5y0H4aKK13P", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + { + "name": "New User", + "locale": "en", + "id": "user:THIgzt9P6hTGullzaOPpd", + "typeName": "user" + }, + { + "isReadOnly": false, + "isPenMode": false, + "isGridMode": false, + "isDarkMode": false, + "isMobileMode": false, + "isSnapMode": false, + "lastUpdatedPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "lastUsedTabId": "instance:YB2a8KHI2ZsaYGDKKVA7q", + "id": "user_document:djWJg5klKtcNPkhh2fQ2E", + "userId": "user:THIgzt9P6hTGullzaOPpd", + "typeName": "user_document" + }, + { + "lastUsedInstanceId": "instance:YB2a8KHI2ZsaYGDKKVA7q", + "lastActivityTimestamp": 1681981976188, + "cursor": { + "x": 911.103515625, + "y": -88.603515625, + "z": 0 + }, + "color": "#39B178", + "userId": "user:THIgzt9P6hTGullzaOPpd", + "viewportPageBounds": { + "x": -39.083984375, + "y": -286.712890625, + "w": 1110, + "h": 1308 + }, + "id": "user_presence:DmtQhvbfq_12f2z1JcBI0", + "typeName": "user_presence" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1367, + "h": 1308 + }, + "userId": "user:THIgzt9P6hTGullzaOPpd", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:n7WckCoATtwYXR1WpfXdW", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:hY73bxp83zDthRGqzNEZQ", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:n7WckCoATtwYXR1WpfXdW", + "cameraId": "camera:hY73bxp83zDthRGqzNEZQ", + "croppingId": null, + "id": "instance_page_state:08aik-6TBzgciUl8lbqfq", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 684, + "h": 1308 + }, + "userId": "user:THIgzt9P6hTGullzaOPpd", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:PDkxzmTpzKJMvHqy2XNbC", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:g9EFSCPtCDLVMtmxxce5T", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:PDkxzmTpzKJMvHqy2XNbC", + "cameraId": "camera:g9EFSCPtCDLVMtmxxce5T", + "croppingId": null, + "id": "instance_page_state:poCv_1ehcp8KdTa9R8cmf", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 457, + "h": 1308 + }, + "userId": "user:THIgzt9P6hTGullzaOPpd", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:5lWSlHkwTjTYahZKnDns1", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:GiRToFgusz1WGh2RUgY5x", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:5lWSlHkwTjTYahZKnDns1", + "cameraId": "camera:GiRToFgusz1WGh2RUgY5x", + "croppingId": null, + "id": "instance_page_state:I0Bp7hWctoBzWdxCEHI1B", + "typeName": "instance_page_state" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:29OudOOFv6TZwdvOLOBDd", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:B06TjlcZx4XnpdUmKuhxB", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:GzuameXwutdK_ttLUvZ9p", + "typeName": "camera" + }, + { + "x": 309.5, + "y": 0, + "z": 1, + "id": "camera:HMSTXaV8aW8mir5LQaUwO", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:IbaZB6CXQS1g382PNUU9U", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:JXaP-rjZucHHucu_RJJ89", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:OG0_vnD2Te6OneCmAZtkS", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:Tmcq2fLCvN6sh5DsAXWrs", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:VumMluULw-tXhkupwFcIk", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:WQK6OIIVqaCoAYbBaxP_O", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:Y0_HhHaUwFkEIV3ScugP-", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:eZbIcBV1jAtQyUPKF5tv2", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:qzsQO5qhggkDXXx2SdmWY", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:uWsZWUO395_xU4EjOGf9Y", + "typeName": "camera" + }, + { + "x": 155, + "y": 0, + "z": 1, + "id": "camera:yl9SGtuJdVN_WaqwfzvO8", + "typeName": "camera" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:1hHQ_OcjAm2QvjiV5Vp8I", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:65ci0SA_dhFtP8ucxpCjS", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:6aD0cX1rlbirF-01ZtZIN", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:9t0J867gsW6uvfcZ8XgGK", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1380, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:ASifqiqUizwJJtiv4nTuo", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:CJ0GC_WVxEyqP1v6fYaPp", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 691, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:EJvLlxtplMMTt0ee3kKnn", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:ENnuLvr20uqqFkEaEzzBP", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 500, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:OmHISPjXP8VQo31bBhzo6", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:Q0SVz_RF9Q-2mxaB9qQZ4", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1380, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:UNMoWMYnnkRYHfwNAWmbw", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:VrnFX-clTdX4PXifRSTeg", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:c9NOR-6aA-D-3x8-RaETg", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1380, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:dZ-xjP9oei9PFOjemkRag", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:k1NO4X4bkEq-4n4wwicw2", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:VrnFX-clTdX4PXifRSTeg", + "cameraId": "camera:GzuameXwutdK_ttLUvZ9p", + "croppingId": null, + "id": "instance_page_state:-42BydMYc3SKePjpgI4nw", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:6aD0cX1rlbirF-01ZtZIN", + "cameraId": "camera:Y0_HhHaUwFkEIV3ScugP-", + "croppingId": null, + "id": "instance_page_state:-CkR7EmOM7VIoXx5PzhXd", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:9t0J867gsW6uvfcZ8XgGK", + "cameraId": "camera:IbaZB6CXQS1g382PNUU9U", + "croppingId": null, + "id": "instance_page_state:-nDOOGv_fQ1E9u_QWZKZX", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:65ci0SA_dhFtP8ucxpCjS", + "cameraId": "camera:WQK6OIIVqaCoAYbBaxP_O", + "croppingId": null, + "id": "instance_page_state:2Ak9j6mhrhKw9BZ2J4UMJ", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:OmHISPjXP8VQo31bBhzo6", + "cameraId": "camera:JXaP-rjZucHHucu_RJJ89", + "croppingId": null, + "id": "instance_page_state:I1bAXZ7svj_LP76yhFSi4", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:EJvLlxtplMMTt0ee3kKnn", + "cameraId": "camera:yl9SGtuJdVN_WaqwfzvO8", + "croppingId": null, + "id": "instance_page_state:KvUUTjlcVgcsxzcDjK4i6", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:UNMoWMYnnkRYHfwNAWmbw", + "cameraId": "camera:HMSTXaV8aW8mir5LQaUwO", + "croppingId": null, + "id": "instance_page_state:Q_GaJYYL9BDilAVLyGL-U", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:k1NO4X4bkEq-4n4wwicw2", + "cameraId": "camera:Tmcq2fLCvN6sh5DsAXWrs", + "croppingId": null, + "id": "instance_page_state:StIlLLDvgNZFwRirFTXrw", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:ENnuLvr20uqqFkEaEzzBP", + "cameraId": "camera:eZbIcBV1jAtQyUPKF5tv2", + "croppingId": null, + "id": "instance_page_state:T4GTNo8TSDMh7GiuvWC3E", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:1hHQ_OcjAm2QvjiV5Vp8I", + "cameraId": "camera:OG0_vnD2Te6OneCmAZtkS", + "croppingId": null, + "id": "instance_page_state:Zp6at0awAJEFvBBRjzIJS", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:ASifqiqUizwJJtiv4nTuo", + "cameraId": "camera:29OudOOFv6TZwdvOLOBDd", + "croppingId": null, + "id": "instance_page_state:alJB0fNfLwAVtYRA8BbB4", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:c9NOR-6aA-D-3x8-RaETg", + "cameraId": "camera:qzsQO5qhggkDXXx2SdmWY", + "croppingId": null, + "id": "instance_page_state:nb6J-qTPVVbEbWkKHeYsj", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:Q0SVz_RF9Q-2mxaB9qQZ4", + "cameraId": "camera:uWsZWUO395_xU4EjOGf9Y", + "croppingId": null, + "id": "instance_page_state:v-_DDNNvAoA6CiGPgdATC", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:dZ-xjP9oei9PFOjemkRag", + "cameraId": "camera:VumMluULw-tXhkupwFcIk", + "croppingId": null, + "id": "instance_page_state:vt-Zarpt6TnM7nJKYVbLm", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:CJ0GC_WVxEyqP1v6fYaPp", + "cameraId": "camera:B06TjlcZx4XnpdUmKuhxB", + "croppingId": null, + "id": "instance_page_state:wFkgxYHytbyeCe9ZcuoNn", + "typeName": "instance_page_state" + }, + { + "name": "New User", + "locale": "en", + "id": "user:-MyEsoL1hLKkcuCBtFWCh", + "typeName": "user" + }, + { + "name": "New User", + "locale": "en", + "id": "user:Crg1XY20FhE_7FhyTKkWw", + "typeName": "user" + }, + { + "isReadOnly": false, + "isPenMode": false, + "isGridMode": false, + "isDarkMode": false, + "isMobileMode": false, + "isSnapMode": false, + "lastUpdatedPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "lastUsedTabId": "instance:388ULcMPU9BDdUYCeUgqi", + "id": "user_document:XjsaWIdEUU73rXJ63dg25", + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "typeName": "user_document" + }, + { + "isReadOnly": false, + "isPenMode": false, + "isGridMode": false, + "isDarkMode": false, + "isMobileMode": false, + "isSnapMode": false, + "lastUpdatedPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "lastUsedTabId": "instance:aR6q-ZBu7GfxQE_hGfjTn", + "id": "user_document:zn4Mw6Gnu5NT294aPzuM3", + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "typeName": "user_document" + }, + { + "lastUsedInstanceId": "instance:aR6q-ZBu7GfxQE_hGfjTn", + "lastActivityTimestamp": 1678280566322, + "cursor": { + "x": 967.34375, + "y": 219.03125, + "z": 0 + }, + "color": "#E34BA9", + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "viewportPageBounds": { + "x": 0, + "y": 0, + "w": 1, + "h": 1 + }, + "id": "user_presence:4C0QhUBV80Z6MMvRPcici", + "typeName": "user_presence" + }, + { + "lastUsedInstanceId": "instance:388ULcMPU9BDdUYCeUgqi", + "lastActivityTimestamp": 1678282285858, + "cursor": { + "x": 477.53125, + "y": 391.84375, + "z": 0.5 + }, + "color": "#55B467", + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "viewportPageBounds": { + "x": 0, + "y": 0, + "w": 1, + "h": 1 + }, + "id": "user_presence:MZYonN3GHrFamey0UJVQt", + "typeName": "user_presence" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:Tk51HVg_TO10Muf2L8_jB", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:0ud-LuKSOHdUPS1KnlQDD", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:Tk51HVg_TO10Muf2L8_jB", + "cameraId": "camera:0ud-LuKSOHdUPS1KnlQDD", + "croppingId": null, + "id": "instance_page_state:EQEDofKmix93Si_TdwTjx", + "typeName": "instance_page_state" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:1jVyCpa7W38Ve6lwgwTso", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:7P67D_7biTEkGmQn37sNS", + "typeName": "camera" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:LKzX5wQu2TQ951k7JEM7l", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:nm1lkO5WhjDmrDTTJZ1By", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:nm1lkO5WhjDmrDTTJZ1By", + "cameraId": "camera:7P67D_7biTEkGmQn37sNS", + "croppingId": null, + "id": "instance_page_state:e-KeNOzUhBhRqbm0bMLy5", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:LKzX5wQu2TQ951k7JEM7l", + "cameraId": "camera:1jVyCpa7W38Ve6lwgwTso", + "croppingId": null, + "id": "instance_page_state:rpL0UWSUdnTGXniK6UVWR", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:91gzmo78kPmv3k9xWeQu3", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:Ikw6UvCyL6eOvTeKYB8KX", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:91gzmo78kPmv3k9xWeQu3", + "cameraId": "camera:Ikw6UvCyL6eOvTeKYB8KX", + "croppingId": null, + "id": "instance_page_state:7YzkLbB1Ggbw8FRj6mXR0", + "typeName": "instance_page_state" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:P9bZZSlykIqIHBWiy-Dho", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:U0_kU2M4Yp8DeeRkQESjJ", + "typeName": "camera" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:At5sdrt_z1lcFYFV_LXmV", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:Xd-RTEdw0_rZHXf90_9kb", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:Xd-RTEdw0_rZHXf90_9kb", + "cameraId": "camera:P9bZZSlykIqIHBWiy-Dho", + "croppingId": null, + "id": "instance_page_state:S3sYBR2glTV_hsF09DRtm", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:At5sdrt_z1lcFYFV_LXmV", + "cameraId": "camera:U0_kU2M4Yp8DeeRkQESjJ", + "croppingId": null, + "id": "instance_page_state:jpZtMB5BIeJFdOfTRD3US", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 999, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:uUTAysPUt1kaqrW9nxjhg", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:pl7Vn_Vd3hFqqzOm2DKup", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:uUTAysPUt1kaqrW9nxjhg", + "cameraId": "camera:pl7Vn_Vd3hFqqzOm2DKup", + "croppingId": null, + "id": "instance_page_state:oerfDXMXlbcMZHdqspA0e", + "typeName": "instance_page_state" + }, + { + "x": 233.5, + "y": 0, + "z": 1, + "id": "camera:V1DeL0lRsjS88rPi35870", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:iw2qJLHNHrO0lOzW15EY5", + "typeName": "camera" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:HqSQVyBSq3BHtL9FJQ91y", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:zK8iTl7wfTYGg601o8puv", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:zK8iTl7wfTYGg601o8puv", + "cameraId": "camera:iw2qJLHNHrO0lOzW15EY5", + "croppingId": null, + "id": "instance_page_state:GPJqVnC1c7WK8vImCdA9N", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:HqSQVyBSq3BHtL9FJQ91y", + "cameraId": "camera:V1DeL0lRsjS88rPi35870", + "croppingId": null, + "id": "instance_page_state:jDiuYLEMCn53WsvT-igTx", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:ske4GC--xS1VhQh6q1fmC", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:JqHo9gxGCQN54QdPNi5_A", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:ske4GC--xS1VhQh6q1fmC", + "cameraId": "camera:JqHo9gxGCQN54QdPNi5_A", + "croppingId": null, + "id": "instance_page_state:pUKNSQAe5F5yvKWrm1D4n", + "typeName": "instance_page_state" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:brFzmXe61Ms3zEuaVmL75", + "typeName": "camera" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:r0e0JjwtKRQxQTTSxDLu2", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:r0e0JjwtKRQxQTTSxDLu2", + "cameraId": "camera:brFzmXe61Ms3zEuaVmL75", + "croppingId": null, + "id": "instance_page_state:lhfXXTvkvWueZq5BwXqgD", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:r8IajHhYw3q6LG0P7kOoe", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:5eHPkj559J6oo8kvrXzTP", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:r8IajHhYw3q6LG0P7kOoe", + "cameraId": "camera:5eHPkj559J6oo8kvrXzTP", + "croppingId": null, + "id": "instance_page_state:q9mktUcNKn3SpCiLqypSn", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:gYyhsi7umLIPzuvV1q-Nh", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:VUzRrnahrlWjBfnt8DQha", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:gYyhsi7umLIPzuvV1q-Nh", + "cameraId": "camera:VUzRrnahrlWjBfnt8DQha", + "croppingId": null, + "id": "instance_page_state:MfiFlTNI5iyO0at25igQi", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:n-aij4RiOoLNIy-D3O5TJ", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:oJeKBVF-jSBdFvfeGrkxn", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:n-aij4RiOoLNIy-D3O5TJ", + "cameraId": "camera:oJeKBVF-jSBdFvfeGrkxn", + "croppingId": null, + "id": "instance_page_state:w2Qc120nvUoNolskFqpJM", + "typeName": "instance_page_state" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:pFFus_GGlmOP4xeboJ8Mq", + "typeName": "camera" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:R2J5wdaIOfReUFBLCLZYA", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:R2J5wdaIOfReUFBLCLZYA", + "cameraId": "camera:pFFus_GGlmOP4xeboJ8Mq", + "croppingId": null, + "id": "instance_page_state:XPEwvTZNcADO-1osezhRw", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:C-mjSsZQp3Ii3gOGo_hYA", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:nbAGR8fL5Y5tRItbyVM1t", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:C-mjSsZQp3Ii3gOGo_hYA", + "cameraId": "camera:nbAGR8fL5Y5tRItbyVM1t", + "croppingId": null, + "id": "instance_page_state:u_oBOcQ_YkBHUtJ2Z1Vpi", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:1jt8u2MgxENsIRKLlTvom", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:PpaffxCWjVnFRFfRPtWjN", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:1jt8u2MgxENsIRKLlTvom", + "cameraId": "camera:PpaffxCWjVnFRFfRPtWjN", + "croppingId": null, + "id": "instance_page_state:8sR_6OtDOnZQVTgcNlmsE", + "typeName": "instance_page_state" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:1NIRizKfVbUcFO5TZ-pam", + "typeName": "camera" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:5Glhfxd91BIc5CSzUXCEn", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:5Glhfxd91BIc5CSzUXCEn", + "cameraId": "camera:1NIRizKfVbUcFO5TZ-pam", + "croppingId": null, + "id": "instance_page_state:9cSFhCQWMg0RY5MmSrpsI", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:14iOEGje2Ze0NeugqSZdQ", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:1kyEC07FjFOSodwjT1uLB", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:14iOEGje2Ze0NeugqSZdQ", + "cameraId": "camera:1kyEC07FjFOSodwjT1uLB", + "croppingId": null, + "id": "instance_page_state:XSk0T3NYQzPe_W4JMcKn8", + "typeName": "instance_page_state" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:uzc5gn6Jd21l3ls8-u4CZ", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:v0aRpjXZQjjLmfa0HL6pe", + "typeName": "camera" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "yellow", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:6JKEz0anuolX2dwbGjQKP", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "yellow", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:UC75_LDHYxmRVIIr_jCGL", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:6JKEz0anuolX2dwbGjQKP", + "cameraId": "camera:uzc5gn6Jd21l3ls8-u4CZ", + "croppingId": null, + "id": "instance_page_state:WEgn1MPnX-5hw_sg2oxxq", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:UC75_LDHYxmRVIIr_jCGL", + "cameraId": "camera:v0aRpjXZQjjLmfa0HL6pe", + "croppingId": null, + "id": "instance_page_state:vMDvE3glxqEAM6XozkATu", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:VHMwyBrOhtWOc-Kpbi5t6", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:7FkclMgQHLxKAzBjcIIMx", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:VHMwyBrOhtWOc-Kpbi5t6", + "cameraId": "camera:7FkclMgQHLxKAzBjcIIMx", + "croppingId": null, + "id": "instance_page_state:yF7sx1jg8K6V-Rj_XyyYy", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:Stv54JK2C2X-KpG6AniHT", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:o4PRu-1AHgbB-rnAWkwuy", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:Stv54JK2C2X-KpG6AniHT", + "cameraId": "camera:o4PRu-1AHgbB-rnAWkwuy", + "croppingId": null, + "id": "instance_page_state:vc7CUm1RmmHdkuUGkNuCm", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:RT50lv6-HHPpGZAnzc966", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:wVwtxcLIvtb2CGwL4jDjT", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:RT50lv6-HHPpGZAnzc966", + "cameraId": "camera:wVwtxcLIvtb2CGwL4jDjT", + "croppingId": null, + "id": "instance_page_state:V1D95zri88BJCFXiB5Nc3", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 919, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:AUbvKZtyxlNlGP2ktzhHv", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:3Lf8ZyZ71GBPvhFySJKYV", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:AUbvKZtyxlNlGP2ktzhHv", + "cameraId": "camera:3Lf8ZyZ71GBPvhFySJKYV", + "croppingId": null, + "id": "instance_page_state:hsKfia1ooelgp44Gz6Nwf", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 845, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "violet", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:36IVgwKDuSqpX_mai2HHH", + "typeName": "instance" + }, + { + "x": -37, + "y": 0, + "z": 1, + "id": "camera:laRPg0Amnu9Fm9lhSU-r-", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:36IVgwKDuSqpX_mai2HHH", + "cameraId": "camera:laRPg0Amnu9Fm9lhSU-r-", + "croppingId": null, + "id": "instance_page_state:v1fjNw-EhW2EdWEJIf0JZ", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 847, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:fedFCp0GLedi7OrHaAgOj", + "typeName": "instance" + }, + { + "x": 193.5, + "y": 0, + "z": 1, + "id": "camera:u_qC8oPqOtF5cD6PgnZkv", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:fedFCp0GLedi7OrHaAgOj", + "cameraId": "camera:u_qC8oPqOtF5cD6PgnZkv", + "croppingId": null, + "id": "instance_page_state:wVT0Zo6foh4ziNESYCMoz", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1130, + "h": 1308 + }, + "userId": "user:Crg1XY20FhE_7FhyTKkWw", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:aR6q-ZBu7GfxQE_hGfjTn", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:ogLqM5Wsj95bwA93j3bJ8", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:aR6q-ZBu7GfxQE_hGfjTn", + "cameraId": "camera:ogLqM5Wsj95bwA93j3bJ8", + "croppingId": null, + "id": "instance_page_state:nbhiBUwBMh-OqcPTB0bn-", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 846, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:4pbZCnM3z9StsSgcGsmZ6", + "typeName": "instance" + }, + { + "x": 153.4907897874358, + "y": 923.7630483578448, + "z": 0.31381059609, + "id": "camera:NTD_u2as_5OYeUHz6PO8l", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:4pbZCnM3z9StsSgcGsmZ6", + "cameraId": "camera:NTD_u2as_5OYeUHz6PO8l", + "croppingId": null, + "id": "instance_page_state:8YqI-B0E5VcMYbUib_Kv3", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 846, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:_ZBWBveGxm_mxl-fobJPw", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:MbOTDkgT48T7KVZjQ12G1", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:_ZBWBveGxm_mxl-fobJPw", + "cameraId": "camera:MbOTDkgT48T7KVZjQ12G1", + "croppingId": null, + "id": "instance_page_state:MdtDEU44WOgCEUKZadmtx", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "move", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 846, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:P8El0amX-nqiEoUXzaDmk", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:UUBq2Cp8_2ZbcYfIHG9mW", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:P8El0amX-nqiEoUXzaDmk", + "cameraId": "camera:UUBq2Cp8_2ZbcYfIHG9mW", + "croppingId": null, + "id": "instance_page_state:Xql4_RnW_o1OoXO99MJ3n", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "move", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 846, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:fyFqlNQUnEtwt_FvDgjm6", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:aciUyi3GppBEdjkWknzaQ", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:fyFqlNQUnEtwt_FvDgjm6", + "cameraId": "camera:aciUyi3GppBEdjkWknzaQ", + "croppingId": null, + "id": "instance_page_state:xagSQD-Dmj8mYpFlAQ0Sk", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 846, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:7g63cSMMri7Of8Y-Ye6X2", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:z5FCp8mATtOiK7D2nCe8g", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:7g63cSMMri7Of8Y-Ye6X2", + "cameraId": "camera:z5FCp8mATtOiK7D2nCe8g", + "croppingId": null, + "id": "instance_page_state:j3I6YBpVsVEXaq2s6GEwq", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "move", + "color": "white", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 564, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:DNkK2HGevFlYqeTQqSXP6", + "typeName": "instance" + }, + { + "x": -141, + "y": 0, + "z": 1, + "id": "camera:Lr32hpcbzOzx-rsn-3mEA", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:DNkK2HGevFlYqeTQqSXP6", + "cameraId": "camera:Lr32hpcbzOzx-rsn-3mEA", + "croppingId": null, + "id": "instance_page_state:_9w67YlcAgsjggwhu-Y-6", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "white", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 564, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:PdT4jjfZt-LV_7hiZsHeD", + "typeName": "instance" + }, + { + "x": 449.5657686784008, + "y": 1037.6596831144395, + "z": 0.387420489, + "id": "camera:fnTCvdtmJ81vOFFpY-KMw", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:PdT4jjfZt-LV_7hiZsHeD", + "cameraId": "camera:fnTCvdtmJ81vOFFpY-KMw", + "croppingId": null, + "id": "instance_page_state:hhlQ8NjekO0eL360enc9y", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "white", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 564, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:0uuGCr__7Y39eDTjETk7B", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:dEIG6G4WsB6u-9n8oxGmQ", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:0uuGCr__7Y39eDTjETk7B", + "cameraId": "camera:dEIG6G4WsB6u-9n8oxGmQ", + "croppingId": null, + "id": "instance_page_state:jHhJQrfwox5HGmN65bk3j", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1692, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-blue", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:WXO37SshYK54Ba6fnuHrF", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:tUum8QQDxIDwrBFGUsMfb", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:WXO37SshYK54Ba6fnuHrF", + "cameraId": "camera:tUum8QQDxIDwrBFGUsMfb", + "croppingId": null, + "id": "instance_page_state:yYh3Ta91GBwFtpoasig7w", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1692, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:xMm-ohYZh8K6ku5VYXY7b", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:VM1sBDCM50gWjDh32olBO", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:xMm-ohYZh8K6ku5VYXY7b", + "cameraId": "camera:VM1sBDCM50gWjDh32olBO", + "croppingId": null, + "id": "instance_page_state:2vzs2bGWRjubzGnPliHAf", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "move", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 846, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:388ULcMPU9BDdUYCeUgqi", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:lqyxvAptP0_26M-ICu-JQ", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": "shape:LDQpczsXcH_oFDaCEdc9C", + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:388ULcMPU9BDdUYCeUgqi", + "cameraId": "camera:lqyxvAptP0_26M-ICu-JQ", + "croppingId": null, + "id": "instance_page_state:IyK_vpXYxCov0YEDZd_TI", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:Kcir4oZh2OKiysl8cetfr", + "cameraId": "camera:LZagR3Ks0asAwQVMnmAdD", + "croppingId": null, + "id": "instance_page_state:PkFbyOBDS-3GqrtUP-ntU", + "typeName": "instance_page_state" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 846, + "h": 1308 + }, + "userId": "user:-MyEsoL1hLKkcuCBtFWCh", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "orange", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "followingUserId": null, + "id": "instance:Kcir4oZh2OKiysl8cetfr", + "typeName": "instance" + }, + { + "followingUserId": null, + "propsForNextShape": { + "labelColor": "black", + "opacity": "1", + "color": "light-green", + "dash": "draw", + "fill": "pattern", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1110, + "h": 1308 + }, + "id": "instance:YB2a8KHI2ZsaYGDKKVA7q", + "userId": "user:THIgzt9P6hTGullzaOPpd", + "currentPageId": "page:zP-IOnhkEN5y0H4aKK13P", + "typeName": "instance" + }, + { + "x": 39.083984375, + "y": 286.712890625, + "z": 1, + "id": "camera:Aec4n1NwUpZezcUEt0klO", + "typeName": "camera" + }, + { + "editingId": null, + "croppingId": null, + "selectedIds": [ + "shape:LDQpczsXcH_oFDaCEdc9C" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "id": "instance_page_state:py1SFI269yOJ9joziBDuE", + "pageId": "page:zP-IOnhkEN5y0H4aKK13P", + "instanceId": "instance:YB2a8KHI2ZsaYGDKKVA7q", + "cameraId": "camera:Aec4n1NwUpZezcUEt0klO", + "typeName": "instance_page_state" + }, + { + "x": 411.4296875, + "y": 278.265625, + "rotation": 0, + "type": "geo", + "props": { + "w": 208.97265625, + "h": 178.04296875, + "geo": "rectangle", + "labelColor": "black", + "color": "light-green", + "fill": "pattern", + "dash": "draw", + "size": "m", + "opacity": "1", + "font": "draw", + "text": "", + "align": "middle", + "growY": 0, + "url": "" + }, + "parentId": "page:zP-IOnhkEN5y0H4aKK13P", + "index": "a1", + "isLocked": false, + "id": "shape:LDQpczsXcH_oFDaCEdc9C", + "typeName": "shape" + } + ] +} \ No newline at end of file diff --git a/apps/vscode/extension/examples/v2a.tldr b/apps/vscode/extension/examples/v2a.tldr new file mode 100644 index 000000000..5e1b88729 --- /dev/null +++ b/apps/vscode/extension/examples/v2a.tldr @@ -0,0 +1,3231 @@ +{ + "tldrawFileFormatVersion": 1, + "schema": { + "schemaVersion": 1, + "storeVersion": 1, + "recordVersions": { + "asset": { + "version": 0, + "subTypeKey": "type", + "subTypeVersions": { + "image": 2, + "video": 2, + "bookmark": 0 + } + }, + "camera": { + "version": 0 + }, + "document": { + "version": 0 + }, + "instance": { + "version": 7 + }, + "instance_page_state": { + "version": 1 + }, + "page": { + "version": 0 + }, + "shape": { + "version": 1, + "subTypeKey": "type", + "subTypeVersions": { + "draw": 1, + "text": 1, + "line": 0, + "arrow": 1, + "image": 2, + "video": 1, + "geo": 3, + "note": 2, + "group": 0, + "bookmark": 1, + "embed": 1, + "frame": 0 + } + }, + "user": { + "version": 0 + }, + "user_document": { + "version": 2 + }, + "user_presence": { + "version": 1 + } + } + }, + "records": [ + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:4RyC8f6yoR8SJnpNp2PQX", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:68LDqZiw4zDvdsWBUK_Y1", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:B3YAvSAtDF8GU9RXlcqCW", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:WXQuWEEhPLQR0MmAoprkW", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:cl4G8J2swjqs6m_QocWm2", + "typeName": "camera" + }, + { + "x": -195.91819929162355, + "y": -33.513431642622095, + "z": 1.24146326274048, + "id": "camera:dV8Z0TOVPANBxQxHk7N4t", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:eh5lLxJDUg9NQtV39ff_m", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:fxUlSa_LtHSnz88ukM27W", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:oYosm1BHni8HCtSJsLAJ6", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:ocnwFyfeK6bIrqmqIovPE", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:pZ-fDLYXa29mO1WvKtlCv", + "typeName": "camera" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:zD0QcnFp1C5o92bQRBd2h", + "typeName": "camera" + }, + { + "gridSize": 10, + "id": "document:document", + "typeName": "document" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1029, + "h": 796 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:9kH8WDV1fkp51NxVrDkDJ", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1555, + "h": 725 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:CUeuMrttcCtCuzXfbKIr6", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1555, + "h": 725 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:IC653XjXymaGw5HuuC6_2", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "cross", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1029, + "h": 796 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:PVI3yymQ4VD6iaZNtVQCc", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1080, + "h": 720 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:Y-UHLWi1Pr3-Knm6xR5ZB", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "cross", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1029, + "h": 796 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:YB-DxHmQZkX4R4BJwaZCY", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1029, + "h": 796 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:YEupm9Ts2FS1KvLfSqzlN", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1555, + "h": 725 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:fD7iJ3BTkt6St-VfJtgYs", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1080, + "h": 720 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:l7V8PweWJNRG0PE2KWoNZ", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1555, + "h": 725 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:rfiYDk7mad3GlEhlXqebI", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "cross", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1029, + "h": 796 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:rt7unSTFu8cCO_gWreRoM", + "typeName": "instance" + }, + { + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": false, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1555, + "h": 1040 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "followingUserId": null, + "id": "instance:zKsiYLVqnYkPZt11hjJEk", + "typeName": "instance" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:zKsiYLVqnYkPZt11hjJEk", + "cameraId": "camera:oYosm1BHni8HCtSJsLAJ6", + "croppingId": null, + "id": "instance_page_state:7W3muEotYtqf6M9RmySWA", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:rfiYDk7mad3GlEhlXqebI", + "cameraId": "camera:zD0QcnFp1C5o92bQRBd2h", + "croppingId": null, + "id": "instance_page_state:Ge_1MV1O8jzp0NER3W4un", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:l7V8PweWJNRG0PE2KWoNZ", + "cameraId": "camera:B3YAvSAtDF8GU9RXlcqCW", + "croppingId": null, + "id": "instance_page_state:IiY_cisY-lVIiM1B1gkgg", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:PVI3yymQ4VD6iaZNtVQCc", + "cameraId": "camera:WXQuWEEhPLQR0MmAoprkW", + "croppingId": null, + "id": "instance_page_state:Qmh25_C2H0noDEpLqjsps", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:YB-DxHmQZkX4R4BJwaZCY", + "cameraId": "camera:68LDqZiw4zDvdsWBUK_Y1", + "croppingId": null, + "id": "instance_page_state:frY6xcy4CE8O4R-ivxcHv", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:YEupm9Ts2FS1KvLfSqzlN", + "cameraId": "camera:dV8Z0TOVPANBxQxHk7N4t", + "croppingId": null, + "id": "instance_page_state:gzp8qb1dBhQ0o7scNT3tH", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:fD7iJ3BTkt6St-VfJtgYs", + "cameraId": "camera:cl4G8J2swjqs6m_QocWm2", + "croppingId": null, + "id": "instance_page_state:hIkaM1IldkzCj9Br5PH3N", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:IC653XjXymaGw5HuuC6_2", + "cameraId": "camera:ocnwFyfeK6bIrqmqIovPE", + "croppingId": null, + "id": "instance_page_state:lTVqQGoUh9mJWAIl0qLQk", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:9kH8WDV1fkp51NxVrDkDJ", + "cameraId": "camera:pZ-fDLYXa29mO1WvKtlCv", + "croppingId": null, + "id": "instance_page_state:ls-V9XNEXlEp-9pvhsawH", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:rt7unSTFu8cCO_gWreRoM", + "cameraId": "camera:eh5lLxJDUg9NQtV39ff_m", + "croppingId": null, + "id": "instance_page_state:u-lLNEQBOxehMezerUVQr", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:CUeuMrttcCtCuzXfbKIr6", + "cameraId": "camera:4RyC8f6yoR8SJnpNp2PQX", + "croppingId": null, + "id": "instance_page_state:xWjY9CN4BxrJjlSxR4M9k", + "typeName": "instance_page_state" + }, + { + "editingId": null, + "selectedIds": [], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:Y-UHLWi1Pr3-Knm6xR5ZB", + "cameraId": "camera:fxUlSa_LtHSnz88ukM27W", + "croppingId": null, + "id": "instance_page_state:y5NkMaKvUQXPPy_qDFJs8", + "typeName": "instance_page_state" + }, + { + "id": "page:YVYGLtbdStqZyqN6qFdW4", + "name": "Page 1", + "index": "a1", + "typeName": "page" + }, + { + "name": "New User", + "locale": "en", + "id": "user:6mEo-342XdWQqtfnRnmH_", + "typeName": "user" + }, + { + "isReadOnly": false, + "isPenMode": false, + "isGridMode": false, + "isDarkMode": false, + "isMobileMode": false, + "isSnapMode": false, + "lastUpdatedPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "lastUsedTabId": "instance:mjXWA4_SocZyjxCz5oh3Q", + "id": "user_document:S43QWndENrzGf6aaeSYMz", + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "typeName": "user_document" + }, + { + "lastUsedInstanceId": "instance:mjXWA4_SocZyjxCz5oh3Q", + "lastActivityTimestamp": 1678379400431, + "cursor": { + "x": 932.77734375, + "y": 94.0703125, + "z": 0 + }, + "color": "#E34BA9", + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "viewportPageBounds": { + "x": 0, + "y": 0, + "w": 1, + "h": 1 + }, + "id": "user_presence:SzJLE7LRFPjTfQvO8Exv8", + "typeName": "user_presence" + }, + { + "propsForNextShape": { + "opacity": "1", + "color": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line", + "labelColor": "black" + }, + "brush": null, + "scribble": null, + "cursor": { + "type": "cross", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1555, + "h": 725 + }, + "userId": "user:6mEo-342XdWQqtfnRnmH_", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "followingUserId": null, + "id": "instance:mjXWA4_SocZyjxCz5oh3Q", + "typeName": "instance" + }, + { + "x": 0, + "y": 0, + "z": 1, + "id": "camera:xm3IMYt3QViEnXQemj0VB", + "typeName": "camera" + }, + { + "editingId": null, + "selectedIds": [ + "shape:UTr2Q7s0LckQGV62gL6vz" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:mjXWA4_SocZyjxCz5oh3Q", + "cameraId": "camera:xm3IMYt3QViEnXQemj0VB", + "croppingId": null, + "id": "instance_page_state:nRwCw5JSNvQ_wHSmC5FCD", + "typeName": "instance_page_state" + }, + { + "isReadOnly": false, + "isPenMode": false, + "isGridMode": false, + "isDarkMode": false, + "isMobileMode": false, + "isSnapMode": false, + "lastUpdatedPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "lastUsedTabId": "instance:JuFk62gDDoc8PbkI7Sfa_", + "id": "user_document:Atx3JaQPwIiR9_42tA_9N", + "userId": "user:THIgzt9P6hTGullzaOPpd", + "typeName": "user_document" + }, + { + "followingUserId": null, + "propsForNextShape": { + "opacity": "1", + "color": "black", + "labelColor": "black", + "dash": "draw", + "fill": "none", + "size": "m", + "icon": "file", + "font": "draw", + "align": "middle", + "geo": "rectangle", + "arrowheadStart": "none", + "arrowheadEnd": "arrow", + "spline": "line" + }, + "brush": null, + "scribble": null, + "cursor": { + "type": "default", + "color": "black", + "rotation": 0 + }, + "isFocusMode": false, + "exportBackground": true, + "isDebugMode": true, + "isToolLocked": false, + "screenBounds": { + "x": 0, + "y": 0, + "w": 1110, + "h": 1308 + }, + "id": "instance:JuFk62gDDoc8PbkI7Sfa_", + "userId": "user:THIgzt9P6hTGullzaOPpd", + "currentPageId": "page:YVYGLtbdStqZyqN6qFdW4", + "typeName": "instance" + }, + { + "name": "New User", + "locale": "en", + "id": "user:THIgzt9P6hTGullzaOPpd", + "typeName": "user" + }, + { + "lastUsedInstanceId": "instance:JuFk62gDDoc8PbkI7Sfa_", + "lastActivityTimestamp": 1681981981245, + "cursor": { + "x": 1152.6425336651052, + "y": -376.5930884074941, + "z": 0 + }, + "viewportPageBounds": { + "x": 45.296587382903965, + "y": -469.00710919203743, + "w": 1355.863700234192, + "h": 1597.720468384075 + }, + "color": "#11B3A3", + "id": "user_presence:sDyt0iAM4I-SyXjKMM4MR", + "userId": "user:THIgzt9P6hTGullzaOPpd", + "typeName": "user_presence" + }, + { + "x": -45.296587382903965, + "y": 469.00710919203743, + "z": 0.8186663599064381, + "id": "camera:mDrJQtf2_KXF9dhNZ2iEY", + "typeName": "camera" + }, + { + "editingId": null, + "croppingId": null, + "selectedIds": [ + "shape:UTr2Q7s0LckQGV62gL6vz" + ], + "hoveredId": null, + "erasingIds": [], + "hintingIds": [], + "focusLayerId": null, + "id": "instance_page_state:Tr7OdEDV5cP5WUQ6Cb4t5", + "pageId": "page:YVYGLtbdStqZyqN6qFdW4", + "instanceId": "instance:JuFk62gDDoc8PbkI7Sfa_", + "cameraId": "camera:mDrJQtf2_KXF9dhNZ2iEY", + "typeName": "instance_page_state" + }, + { + "x": 201.6484375, + "y": 369.578125, + "rotation": 0, + "isLocked": false, + "type": "draw", + "props": { + "segments": [ + { + "type": "free", + "points": [ + { + "x": 0, + "y": 0, + "z": 0.5 + }, + { + "x": 0.33, + "y": 0, + "z": 0.5 + }, + { + "x": 7.91, + "y": 0, + "z": 0.5 + }, + { + "x": 12.29, + "y": 0, + "z": 0.5 + }, + { + "x": 17.5, + "y": -0.38, + "z": 0.5 + }, + { + "x": 22.8, + "y": -0.91, + "z": 0.5 + }, + { + "x": 32.91, + "y": -3.04, + "z": 0.5 + }, + { + "x": 47.86, + "y": -6, + "z": 0.5 + }, + { + "x": 56.44, + "y": -7.33, + "z": 0.5 + }, + { + "x": 67.07, + "y": -9.78, + "z": 0.5 + }, + { + "x": 79.63, + "y": -12.92, + "z": 0.5 + }, + { + "x": 91.63, + "y": -16.05, + "z": 0.5 + }, + { + "x": 102.84, + "y": -19.04, + "z": 0.5 + }, + { + "x": 116.12, + "y": -23.66, + "z": 0.5 + }, + { + "x": 129.48, + "y": -28.67, + "z": 0.5 + }, + { + "x": 153.19, + "y": -37.9, + "z": 0.5 + }, + { + "x": 179.5, + "y": -48.2, + "z": 0.5 + }, + { + "x": 196.59, + "y": -57.06, + "z": 0.5 + }, + { + "x": 212.28, + "y": -66.29, + "z": 0.5 + }, + { + "x": 221.98, + "y": -72.25, + "z": 0.5 + }, + { + "x": 229.22, + "y": -76.86, + "z": 0.5 + }, + { + "x": 241.33, + "y": -86.2, + "z": 0.5 + }, + { + "x": 255.17, + "y": -97.27, + "z": 0.5 + }, + { + "x": 269.3, + "y": -109.16, + "z": 0.5 + }, + { + "x": 281.6, + "y": -119.71, + "z": 0.5 + }, + { + "x": 289.79, + "y": -129.28, + "z": 0.5 + }, + { + "x": 298.97, + "y": -140.97, + "z": 0.5 + }, + { + "x": 308.55, + "y": -154.63, + "z": 0.5 + }, + { + "x": 317.24, + "y": -167.68, + "z": 0.5 + }, + { + "x": 320.13, + "y": -174.09, + "z": 0.5 + }, + { + "x": 321.1, + "y": -178.47, + "z": 0.5 + }, + { + "x": 324.26, + "y": -187.24, + "z": 0.5 + }, + { + "x": 327.56, + "y": -195.82, + "z": 0.5 + }, + { + "x": 329.24, + "y": -201.96, + "z": 0.5 + }, + { + "x": 331.09, + "y": -209.37, + "z": 0.5 + }, + { + "x": 332.13, + "y": -217.24, + "z": 0.5 + }, + { + "x": 332.75, + "y": -224.65, + "z": 0.5 + }, + { + "x": 332.9, + "y": -230.28, + "z": 0.5 + }, + { + "x": 332.9, + "y": -235.58, + "z": 0.5 + }, + { + "x": 332.9, + "y": -240.36, + "z": 0.5 + }, + { + "x": 332.9, + "y": -243.88, + "z": 0.5 + }, + { + "x": 332.06, + "y": -246.15, + "z": 0.5 + }, + { + "x": 330.86, + "y": -248.96, + "z": 0.5 + }, + { + "x": 328.31, + "y": -252.36, + "z": 0.5 + }, + { + "x": 325.39, + "y": -255.77, + "z": 0.5 + }, + { + "x": 321.9, + "y": -258.35, + "z": 0.5 + }, + { + "x": 318, + "y": -260.79, + "z": 0.5 + }, + { + "x": 312.46, + "y": -262.98, + "z": 0.5 + }, + { + "x": 305.41, + "y": -264.97, + "z": 0.5 + }, + { + "x": 300.1, + "y": -266.04, + "z": 0.5 + }, + { + "x": 291.42, + "y": -266.57, + "z": 0.5 + }, + { + "x": 278.98, + "y": -266.57, + "z": 0.5 + }, + { + "x": 270.01, + "y": -266.57, + "z": 0.5 + }, + { + "x": 264.4, + "y": -266.57, + "z": 0.5 + }, + { + "x": 257.38, + "y": -266.09, + "z": 0.5 + }, + { + "x": 249.6, + "y": -262.68, + "z": 0.5 + }, + { + "x": 243.52, + "y": -257.09, + "z": 0.5 + }, + { + "x": 239.65, + "y": -251.62, + "z": 0.5 + }, + { + "x": 236.59, + "y": -241.41, + "z": 0.5 + }, + { + "x": 234.74, + "y": -227.7, + "z": 0.5 + }, + { + "x": 234.74, + "y": -214.08, + "z": 0.5 + }, + { + "x": 237.38, + "y": -198.18, + "z": 0.5 + }, + { + "x": 245.11, + "y": -179.43, + "z": 0.5 + }, + { + "x": 254.03, + "y": -161.58, + "z": 0.5 + }, + { + "x": 260.71, + "y": -148.22, + "z": 0.5 + }, + { + "x": 267.58, + "y": -133.98, + "z": 0.5 + }, + { + "x": 274.6, + "y": -119.05, + "z": 0.5 + }, + { + "x": 281.86, + "y": -103.09, + "z": 0.5 + }, + { + "x": 289.25, + "y": -86.48, + "z": 0.5 + }, + { + "x": 296.14, + "y": -69.74, + "z": 0.5 + }, + { + "x": 302.6, + "y": -53.13, + "z": 0.5 + }, + { + "x": 307.88, + "y": -36.88, + "z": 0.5 + }, + { + "x": 312.28, + "y": -21.05, + "z": 0.5 + }, + { + "x": 314.41, + "y": -10.8, + "z": 0.5 + }, + { + "x": 314.98, + "y": -4.49, + "z": 0.5 + }, + { + "x": 315.69, + "y": 5.69, + "z": 0.5 + }, + { + "x": 316.48, + "y": 18.35, + "z": 0.5 + }, + { + "x": 316.79, + "y": 29.32, + "z": 0.5 + }, + { + "x": 316.79, + "y": 39.18, + "z": 0.5 + }, + { + "x": 316.79, + "y": 54, + "z": 0.5 + }, + { + "x": 316.79, + "y": 71.54, + "z": 0.5 + }, + { + "x": 313.91, + "y": 83.04, + "z": 0.5 + }, + { + "x": 309.29, + "y": 90.95, + "z": 0.5 + }, + { + "x": 305.38, + "y": 95.79, + "z": 0.5 + }, + { + "x": 301.97, + "y": 98.71, + "z": 0.5 + }, + { + "x": 295.77, + "y": 103.83, + "z": 0.5 + }, + { + "x": 288.02, + "y": 110.16, + "z": 0.5 + }, + { + "x": 280.63, + "y": 115.37, + "z": 0.5 + }, + { + "x": 273.38, + "y": 119.98, + "z": 0.5 + }, + { + "x": 266.58, + "y": 123.17, + "z": 0.5 + }, + { + "x": 260.27, + "y": 125.46, + "z": 0.5 + }, + { + "x": 256.73, + "y": 126.47, + "z": 0.5 + }, + { + "x": 254.59, + "y": 126.83, + "z": 0.5 + }, + { + "x": 250.89, + "y": 127.61, + "z": 0.5 + }, + { + "x": 246.5, + "y": 128.58, + "z": 0.5 + }, + { + "x": 239.06, + "y": 129.64, + "z": 0.5 + }, + { + "x": 236.02, + "y": 130.05, + "z": 0.5 + }, + { + "x": 233.21, + "y": 130.45, + "z": 0.5 + }, + { + "x": 231.23, + "y": 130.57, + "z": 0.5 + }, + { + "x": 229.65, + "y": 130.57, + "z": 0.5 + }, + { + "x": 228.04, + "y": 130.57, + "z": 0.5 + }, + { + "x": 226.46, + "y": 130.57, + "z": 0.5 + }, + { + "x": 226.01, + "y": 129.44, + "z": 0.5 + }, + { + "x": 226.01, + "y": 127.86, + "z": 0.5 + }, + { + "x": 226.01, + "y": 124.24, + "z": 0.5 + }, + { + "x": 226.01, + "y": 119.86, + "z": 0.5 + }, + { + "x": 227.06, + "y": 115.5, + "z": 0.5 + }, + { + "x": 228.52, + "y": 111.12, + "z": 0.5 + }, + { + "x": 233.92, + "y": 102.68, + "z": 0.5 + }, + { + "x": 240.64, + "y": 92.96, + "z": 0.5 + }, + { + "x": 252.76, + "y": 76.07, + "z": 0.5 + }, + { + "x": 266.48, + "y": 57.07, + "z": 0.5 + }, + { + "x": 278.59, + "y": 41.8, + "z": 0.5 + }, + { + "x": 292.05, + "y": 25.45, + "z": 0.5 + }, + { + "x": 308.11, + "y": 6.1, + "z": 0.5 + }, + { + "x": 324.61, + "y": -13.7, + "z": 0.5 + }, + { + "x": 336.87, + "y": -28.29, + "z": 0.5 + }, + { + "x": 346.05, + "y": -39.14, + "z": 0.5 + }, + { + "x": 360.04, + "y": -56.91, + "z": 0.5 + }, + { + "x": 374.81, + "y": -75.91, + "z": 0.5 + }, + { + "x": 388.45, + "y": -93.69, + "z": 0.5 + }, + { + "x": 401.59, + "y": -110.88, + "z": 0.5 + }, + { + "x": 407.3, + "y": -118.75, + "z": 0.5 + }, + { + "x": 411.55, + "y": -124.82, + "z": 0.5 + }, + { + "x": 418.8, + "y": -136.7, + "z": 0.5 + }, + { + "x": 426.31, + "y": -149.23, + "z": 0.5 + }, + { + "x": 431.81, + "y": -159.85, + "z": 0.5 + }, + { + "x": 437.01, + "y": -170.25, + "z": 0.5 + }, + { + "x": 439.99, + "y": -178.35, + "z": 0.5 + }, + { + "x": 442.45, + "y": -185.76, + "z": 0.5 + }, + { + "x": 443.13, + "y": -189.19, + "z": 0.5 + }, + { + "x": 443.53, + "y": -192, + "z": 0.5 + }, + { + "x": 444.02, + "y": -196.34, + "z": 0.5 + }, + { + "x": 444.5, + "y": -200.72, + "z": 0.5 + }, + { + "x": 444.55, + "y": -204.31, + "z": 0.5 + }, + { + "x": 444.55, + "y": -207.85, + "z": 0.5 + }, + { + "x": 444.55, + "y": -210.17, + "z": 0.5 + }, + { + "x": 444.55, + "y": -212.32, + "z": 0.5 + }, + { + "x": 444.55, + "y": -214.02, + "z": 0.5 + }, + { + "x": 444.55, + "y": -215.61, + "z": 0.5 + }, + { + "x": 443.33, + "y": -216.6, + "z": 0.5 + }, + { + "x": 442.07, + "y": -217.55, + "z": 0.5 + }, + { + "x": 440.5, + "y": -218.2, + "z": 0.5 + }, + { + "x": 438.91, + "y": -218.83, + "z": 0.5 + }, + { + "x": 436.75, + "y": -219.2, + "z": 0.5 + }, + { + "x": 434.6, + "y": -219.56, + "z": 0.5 + }, + { + "x": 432.45, + "y": -219.57, + "z": 0.5 + }, + { + "x": 429.51, + "y": -219.57, + "z": 0.5 + }, + { + "x": 427.88, + "y": -219.57, + "z": 0.5 + }, + { + "x": 423.91, + "y": -214.8, + "z": 0.5 + }, + { + "x": 417.64, + "y": -203.24, + "z": 0.5 + }, + { + "x": 411.91, + "y": -187.87, + "z": 0.5 + }, + { + "x": 406.12, + "y": -166.69, + "z": 0.5 + }, + { + "x": 399.45, + "y": -138.27, + "z": 0.5 + }, + { + "x": 393.16, + "y": -104.92, + "z": 0.5 + }, + { + "x": 386.72, + "y": -69.94, + "z": 0.5 + }, + { + "x": 381.2, + "y": -34.96, + "z": 0.5 + }, + { + "x": 377.6, + "y": -1.61, + "z": 0.5 + }, + { + "x": 375.84, + "y": 20.52, + "z": 0.5 + }, + { + "x": 375.84, + "y": 38.04, + "z": 0.5 + }, + { + "x": 375.84, + "y": 56.66, + "z": 0.5 + }, + { + "x": 377.6, + "y": 67.95, + "z": 0.5 + }, + { + "x": 380.68, + "y": 74.74, + "z": 0.5 + }, + { + "x": 385.41, + "y": 80.55, + "z": 0.5 + }, + { + "x": 390.88, + "y": 84.5, + "z": 0.5 + }, + { + "x": 394.43, + "y": 86.28, + "z": 0.5 + }, + { + "x": 400.34, + "y": 87.39, + "z": 0.5 + }, + { + "x": 407.74, + "y": 88, + "z": 0.5 + }, + { + "x": 413.87, + "y": 88.25, + "z": 0.5 + }, + { + "x": 419.16, + "y": 88.25, + "z": 0.5 + }, + { + "x": 425.74, + "y": 85.02, + "z": 0.5 + }, + { + "x": 433, + "y": 79.73, + "z": 0.5 + }, + { + "x": 440.39, + "y": 72.2, + "z": 0.5 + }, + { + "x": 447.86, + "y": 63.23, + "z": 0.5 + }, + { + "x": 455.34, + "y": 53.34, + "z": 0.5 + }, + { + "x": 463.31, + "y": 42.18, + "z": 0.5 + }, + { + "x": 468.65, + "y": 33.16, + "z": 0.5 + }, + { + "x": 471.73, + "y": 26.37, + "z": 0.5 + }, + { + "x": 478.11, + "y": 13.37, + "z": 0.5 + }, + { + "x": 485.14, + "y": -3.4, + "z": 0.5 + }, + { + "x": 487.96, + "y": -13.26, + "z": 0.5 + }, + { + "x": 489.86, + "y": -20.38, + "z": 0.5 + }, + { + "x": 491.45, + "y": -27.46, + "z": 0.5 + }, + { + "x": 492.51, + "y": -32.76, + "z": 0.5 + }, + { + "x": 493.19, + "y": -37.48, + "z": 0.5 + }, + { + "x": 493.67, + "y": -41.86, + "z": 0.5 + }, + { + "x": 493.83, + "y": -45.2, + "z": 0.5 + }, + { + "x": 493.83, + "y": -48.02, + "z": 0.5 + }, + { + "x": 493.83, + "y": -51.75, + "z": 0.5 + }, + { + "x": 493.13, + "y": -54.91, + "z": 0.5 + }, + { + "x": 492.04, + "y": -55.46, + "z": 0.5 + }, + { + "x": 490.55, + "y": -55.65, + "z": 0.5 + }, + { + "x": 488.96, + "y": -55.65, + "z": 0.5 + }, + { + "x": 487.71, + "y": -55.65, + "z": 0.5 + }, + { + "x": 486.61, + "y": -55.65, + "z": 0.5 + }, + { + "x": 485.16, + "y": -55.2, + "z": 0.5 + }, + { + "x": 483.58, + "y": -54.57, + "z": 0.5 + }, + { + "x": 481.38, + "y": -50.9, + "z": 0.5 + }, + { + "x": 478.66, + "y": -44.82, + "z": 0.5 + }, + { + "x": 477.32, + "y": -41.27, + "z": 0.5 + }, + { + "x": 475.79, + "y": -36.41, + "z": 0.5 + }, + { + "x": 474.2, + "y": -31.11, + "z": 0.5 + }, + { + "x": 473.39, + "y": -25.87, + "z": 0.5 + }, + { + "x": 472.86, + "y": -20.57, + "z": 0.5 + }, + { + "x": 472.36, + "y": -15.98, + "z": 0.5 + }, + { + "x": 471.73, + "y": -8.66, + "z": 0.5 + }, + { + "x": 471.73, + "y": -6.51, + "z": 0.5 + }, + { + "x": 471.73, + "y": -1.1, + "z": 0.5 + }, + { + "x": 471.73, + "y": 5.21, + "z": 0.5 + }, + { + "x": 472.08, + "y": 8.74, + "z": 0.5 + }, + { + "x": 473.33, + "y": 11.78, + "z": 0.5 + }, + { + "x": 474.59, + "y": 13.04, + "z": 0.5 + }, + { + "x": 477.26, + "y": 14.07, + "z": 0.5 + }, + { + "x": 480.07, + "y": 14.87, + "z": 0.5 + }, + { + "x": 482.85, + "y": 15.34, + "z": 0.5 + }, + { + "x": 485.66, + "y": 15.73, + "z": 0.5 + }, + { + "x": 492.43, + "y": 15.81, + "z": 0.5 + }, + { + "x": 501.29, + "y": 14.5, + "z": 0.5 + }, + { + "x": 504.84, + "y": 12.72, + "z": 0.5 + }, + { + "x": 510.5, + "y": 8.05, + "z": 0.5 + }, + { + "x": 516.05, + "y": 3.11, + "z": 0.5 + }, + { + "x": 521.55, + "y": -4.05, + "z": 0.5 + }, + { + "x": 526.83, + "y": -11.32, + "z": 0.5 + }, + { + "x": 529.05, + "y": -15.39, + "z": 0.5 + }, + { + "x": 530.82, + "y": -18.94, + "z": 0.5 + }, + { + "x": 533.74, + "y": -24.32, + "z": 0.5 + }, + { + "x": 537.44, + "y": -31.11, + "z": 0.5 + }, + { + "x": 539.02, + "y": -34.75, + "z": 0.5 + }, + { + "x": 539.73, + "y": -36.9, + "z": 0.5 + }, + { + "x": 541.43, + "y": -40.37, + "z": 0.5 + }, + { + "x": 543.2, + "y": -43.92, + "z": 0.5 + }, + { + "x": 544.25, + "y": -46.32, + "z": 0.5 + }, + { + "x": 545.45, + "y": -49.13, + "z": 0.5 + }, + { + "x": 546.22, + "y": -51.01, + "z": 0.5 + }, + { + "x": 546.85, + "y": -52.59, + "z": 0.5 + }, + { + "x": 547.22, + "y": -53.83, + "z": 0.5 + }, + { + "x": 547.49, + "y": -54.92, + "z": 0.5 + }, + { + "x": 547.75, + "y": -55.6, + "z": 0.5 + }, + { + "x": 547.94, + "y": -55.98, + "z": 0.5 + }, + { + "x": 547.94, + "y": -55.69, + "z": 0.5 + }, + { + "x": 547.54, + "y": -52.62, + "z": 0.5 + }, + { + "x": 546.17, + "y": -45.45, + "z": 0.5 + }, + { + "x": 544.7, + "y": -36.68, + "z": 0.5 + }, + { + "x": 544.21, + "y": -30.72, + "z": 0.5 + }, + { + "x": 544.21, + "y": -25.59, + "z": 0.5 + }, + { + "x": 544.21, + "y": -19.89, + "z": 0.5 + }, + { + "x": 544.21, + "y": -15.59, + "z": 0.5 + }, + { + "x": 544.21, + "y": -12.34, + "z": 0.5 + }, + { + "x": 545.03, + "y": -10.43, + "z": 0.5 + }, + { + "x": 546.94, + "y": -9.34, + "z": 0.5 + }, + { + "x": 548.66, + "y": -9.07, + "z": 0.5 + }, + { + "x": 549.75, + "y": -9.07, + "z": 0.5 + }, + { + "x": 550.84, + "y": -9.23, + "z": 0.5 + }, + { + "x": 551.93, + "y": -9.51, + "z": 0.5 + }, + { + "x": 553.22, + "y": -10.67, + "z": 0.5 + }, + { + "x": 554.65, + "y": -12.46, + "z": 0.5 + }, + { + "x": 555.88, + "y": -14.46, + "z": 0.5 + }, + { + "x": 556.95, + "y": -16.61, + "z": 0.5 + }, + { + "x": 557.71, + "y": -18.16, + "z": 0.5 + }, + { + "x": 558.26, + "y": -19.25, + "z": 0.5 + }, + { + "x": 558.91, + "y": -21, + "z": 0.5 + }, + { + "x": 559.63, + "y": -23.16, + "z": 0.5 + }, + { + "x": 560.1, + "y": -24.94, + "z": 0.5 + }, + { + "x": 560.41, + "y": -26.51, + "z": 0.5 + }, + { + "x": 560.75, + "y": -28.47, + "z": 0.5 + }, + { + "x": 561.11, + "y": -30.62, + "z": 0.5 + }, + { + "x": 561.44, + "y": -32.42, + "z": 0.5 + }, + { + "x": 561.75, + "y": -34, + "z": 0.5 + }, + { + "x": 561.87, + "y": -35.29, + "z": 0.5 + }, + { + "x": 561.87, + "y": -36.39, + "z": 0.5 + }, + { + "x": 561.87, + "y": -37.8, + "z": 0.5 + }, + { + "x": 561.87, + "y": -39.38, + "z": 0.5 + }, + { + "x": 561.87, + "y": -40.65, + "z": 0.5 + }, + { + "x": 561.87, + "y": -41.74, + "z": 0.5 + }, + { + "x": 562.1, + "y": -43.52, + "z": 0.5 + }, + { + "x": 562.46, + "y": -45.67, + "z": 0.5 + }, + { + "x": 562.99, + "y": -47.45, + "z": 0.5 + }, + { + "x": 563.62, + "y": -49.03, + "z": 0.5 + }, + { + "x": 565.04, + "y": -50.55, + "z": 0.5 + }, + { + "x": 566.83, + "y": -51.99, + "z": 0.5 + }, + { + "x": 570.61, + "y": -54.59, + "z": 0.5 + }, + { + "x": 575.38, + "y": -57.78, + "z": 0.5 + }, + { + "x": 578.88, + "y": -59.38, + "z": 0.5 + }, + { + "x": 581.68, + "y": -60.18, + "z": 0.5 + }, + { + "x": 586.25, + "y": -61.18, + "z": 0.5 + }, + { + "x": 591.55, + "y": -62.24, + "z": 0.5 + }, + { + "x": 601.05, + "y": -63.61, + "z": 0.5 + }, + { + "x": 612.26, + "y": -65.11, + "z": 0.5 + }, + { + "x": 622.58, + "y": -65.56, + "z": 0.5 + }, + { + "x": 632.43, + "y": -65.56, + "z": 0.5 + }, + { + "x": 642.4, + "y": -65.56, + "z": 0.5 + }, + { + "x": 652.25, + "y": -65.56, + "z": 0.5 + }, + { + "x": 662.84, + "y": -64.52, + "z": 0.5 + }, + { + "x": 673.93, + "y": -63.04, + "z": 0.5 + }, + { + "x": 685.43, + "y": -57.91, + "z": 0.5 + }, + { + "x": 696.5, + "y": -51.58, + "z": 0.5 + }, + { + "x": 706.16, + "y": -44.28, + "z": 0.5 + }, + { + "x": 715.13, + "y": -36.81, + "z": 0.5 + }, + { + "x": 723.28, + "y": -27.23, + "z": 0.5 + }, + { + "x": 731.18, + "y": -16.95, + "z": 0.5 + }, + { + "x": 738, + "y": -5.89, + "z": 0.5 + }, + { + "x": 744.32, + "y": 5.18, + "z": 0.5 + }, + { + "x": 748.8, + "y": 17.42, + "z": 0.5 + }, + { + "x": 752.75, + "y": 30.05, + "z": 0.5 + }, + { + "x": 754.34, + "y": 45.34, + "z": 0.5 + }, + { + "x": 755.22, + "y": 61.16, + "z": 0.5 + }, + { + "x": 755.41, + "y": 77.27, + "z": 0.5 + }, + { + "x": 755.41, + "y": 93.09, + "z": 0.5 + }, + { + "x": 749.86, + "y": 118.4, + "z": 0.5 + }, + { + "x": 743, + "y": 145.86, + "z": 0.5 + }, + { + "x": 729.42, + "y": 166.91, + "z": 0.5 + }, + { + "x": 714.64, + "y": 185.91, + "z": 0.5 + }, + { + "x": 693.53, + "y": 205.78, + "z": 0.5 + }, + { + "x": 671, + "y": 225.93, + "z": 0.5 + }, + { + "x": 643.74, + "y": 245.36, + "z": 0.5 + }, + { + "x": 616.6, + "y": 263.86, + "z": 0.5 + }, + { + "x": 584.24, + "y": 281.3, + "z": 0.5 + }, + { + "x": 551.16, + "y": 298.5, + "z": 0.5 + }, + { + "x": 533.38, + "y": 305.53, + "z": 0.5 + }, + { + "x": 518.44, + "y": 310.8, + "z": 0.5 + }, + { + "x": 491.39, + "y": 319.85, + "z": 0.5 + }, + { + "x": 462.86, + "y": 329.36, + "z": 0.5 + }, + { + "x": 450.92, + "y": 331.66, + "z": 0.5 + }, + { + "x": 441.07, + "y": 333.07, + "z": 0.5 + }, + { + "x": 423.63, + "y": 335.8, + "z": 0.5 + }, + { + "x": 406.09, + "y": 338.57, + "z": 0.5 + }, + { + "x": 394.2, + "y": 339.47, + "z": 0.5 + }, + { + "x": 382.98, + "y": 340.21, + "z": 0.5 + }, + { + "x": 375.48, + "y": 340.25, + "z": 0.5 + }, + { + "x": 368.1, + "y": 340.25, + "z": 0.5 + }, + { + "x": 360.45, + "y": 339.64, + "z": 0.5 + }, + { + "x": 353.04, + "y": 339.03, + "z": 0.5 + }, + { + "x": 352.86, + "y": 334.77, + "z": 0.5 + }, + { + "x": 352.86, + "y": 325.48, + "z": 0.5 + }, + { + "x": 352.86, + "y": 315.49, + "z": 0.5 + }, + { + "x": 361.63, + "y": 297.38, + "z": 0.5 + }, + { + "x": 400.07, + "y": 253.14, + "z": 0.5 + }, + { + "x": 464.73, + "y": 191.44, + "z": 0.5 + }, + { + "x": 541.16, + "y": 125.75, + "z": 0.5 + }, + { + "x": 632.79, + "y": 50.89, + "z": 0.5 + }, + { + "x": 704.71, + "y": -7.23, + "z": 0.5 + }, + { + "x": 769.49, + "y": -59.01, + "z": 0.5 + }, + { + "x": 850.1, + "y": -124.05, + "z": 0.5 + }, + { + "x": 900.49, + "y": -167.51, + "z": 0.5 + }, + { + "x": 939.75, + "y": -205.86, + "z": 0.5 + }, + { + "x": 985.66, + "y": -252.92, + "z": 0.5 + }, + { + "x": 1008.54, + "y": -279.22, + "z": 0.5 + }, + { + "x": 1019.47, + "y": -297.45, + "z": 0.5 + }, + { + "x": 1035.07, + "y": -330.2, + "z": 0.5 + }, + { + "x": 1043.16, + "y": -355.42, + "z": 0.5 + }, + { + "x": 1043.16, + "y": -364, + "z": 0.5 + }, + { + "x": 1042.43, + "y": -371.92, + "z": 0.5 + }, + { + "x": 1041.2, + "y": -379.33, + "z": 0.5 + }, + { + "x": 1036.08, + "y": -386.59, + "z": 0.5 + }, + { + "x": 1028.34, + "y": -393.63, + "z": 0.5 + }, + { + "x": 1018.11, + "y": -399.32, + "z": 0.5 + }, + { + "x": 1006.23, + "y": -404.07, + "z": 0.5 + }, + { + "x": 985.09, + "y": -410.08, + "z": 0.5 + }, + { + "x": 957.67, + "y": -416.93, + "z": 0.5 + }, + { + "x": 932.47, + "y": -419.7, + "z": 0.5 + }, + { + "x": 909.25, + "y": -419.7, + "z": 0.5 + }, + { + "x": 894.14, + "y": -419.7, + "z": 0.5 + }, + { + "x": 884.28, + "y": -419.7, + "z": 0.5 + }, + { + "x": 868.3, + "y": -416.66, + "z": 0.5 + }, + { + "x": 848.96, + "y": -411.83, + "z": 0.5 + }, + { + "x": 837.48, + "y": -408.09, + "z": 0.5 + }, + { + "x": 830.69, + "y": -405.01, + "z": 0.5 + }, + { + "x": 819.69, + "y": -400.16, + "z": 0.5 + }, + { + "x": 806.32, + "y": -394.31, + "z": 0.5 + }, + { + "x": 798.95, + "y": -390.62, + "z": 0.5 + }, + { + "x": 795.05, + "y": -388.18, + "z": 0.5 + }, + { + "x": 789.21, + "y": -384.89, + "z": 0.5 + }, + { + "x": 782.42, + "y": -381.19, + "z": 0.5 + }, + { + "x": 776.91, + "y": -378.16, + "z": 0.5 + }, + { + "x": 772.13, + "y": -375.51, + "z": 0.5 + }, + { + "x": 769.02, + "y": -373.62, + "z": 0.5 + }, + { + "x": 766.66, + "y": -372.05, + "z": 0.5 + }, + { + "x": 764.83, + "y": -370.28, + "z": 0.5 + }, + { + "x": 763.39, + "y": -368.49, + "z": 0.5 + }, + { + "x": 762.54, + "y": -367.14, + "z": 0.5 + }, + { + "x": 762, + "y": -366.05, + "z": 0.5 + }, + { + "x": 761.82, + "y": -364.96, + "z": 0.5 + }, + { + "x": 761.82, + "y": -363.87, + "z": 0.5 + }, + { + "x": 761.82, + "y": -362.45, + "z": 0.5 + }, + { + "x": 761.82, + "y": -360.88, + "z": 0.5 + }, + { + "x": 761.82, + "y": -360.12, + "z": 0.5 + }, + { + "x": 761.82, + "y": -359.74, + "z": 0.5 + }, + { + "x": 761.82, + "y": -358.86, + "z": 0.5 + }, + { + "x": 761.82, + "y": -357.77, + "z": 0.5 + }, + { + "x": 762.51, + "y": -356.99, + "z": 0.5 + }, + { + "x": 763.81, + "y": -355.92, + "z": 0.5 + }, + { + "x": 764.63, + "y": -355.11, + "z": 0.5 + }, + { + "x": 765.35, + "y": -354.55, + "z": 0.5 + }, + { + "x": 766.04, + "y": -354.1, + "z": 0.5 + }, + { + "x": 766.48, + "y": -353.71, + "z": 0.5 + }, + { + "x": 766.86, + "y": -353.34, + "z": 0.5 + }, + { + "x": 767.24, + "y": -353.09, + "z": 0.5 + }, + { + "x": 767.61, + "y": -352.91, + "z": 0.5 + }, + { + "x": 767.97, + "y": -352.73, + "z": 0.5 + }, + { + "x": 768.34, + "y": -352.55, + "z": 0.5 + }, + { + "x": 768.57, + "y": -352.38, + "z": 0.5 + }, + { + "x": 768.71, + "y": -352.23, + "z": 0.5 + }, + { + "x": 768.84, + "y": -352.1, + "z": 0.5 + }, + { + "x": 768.99, + "y": -351.96, + "z": 0.5 + }, + { + "x": 769.03, + "y": -351.81, + "z": 0.5 + }, + { + "x": 769.03, + "y": -351.67, + "z": 0.5 + }, + { + "x": 769.03, + "y": -351.52, + "z": 0.5 + }, + { + "x": 769.03, + "y": -351.4, + "z": 0.5 + }, + { + "x": 769.14, + "y": -351.39, + "z": 0.5 + }, + { + "x": 769.25, + "y": -351.39, + "z": 0.5 + }, + { + "x": 769.36, + "y": -351.39, + "z": 0.5 + }, + { + "x": 769.47, + "y": -351.39, + "z": 0.5 + }, + { + "x": 769.47, + "y": -351.29, + "z": 0.5 + }, + { + "x": 769.47, + "y": -351.17, + "z": 0.5 + }, + { + "x": 769.47, + "y": -351.05, + "z": 0.5 + }, + { + "x": 769.47, + "y": -350.23, + "z": 0.5 + }, + { + "x": 769.47, + "y": -348.45, + "z": 0.5 + }, + { + "x": 769.47, + "y": -345.81, + "z": 0.5 + }, + { + "x": 769.47, + "y": -342.11, + "z": 0.5 + }, + { + "x": 768.14, + "y": -336.41, + "z": 0.5 + }, + { + "x": 763.72, + "y": -326.07, + "z": 0.5 + }, + { + "x": 759.03, + "y": -316.88, + "z": 0.5 + }, + { + "x": 754, + "y": -308.78, + "z": 0.5 + }, + { + "x": 748.76, + "y": -300.34, + "z": 0.5 + }, + { + "x": 745.57, + "y": -295.57, + "z": 0.5 + }, + { + "x": 743.38, + "y": -292.5, + "z": 0.5 + }, + { + "x": 741.95, + "y": -290.71, + "z": 0.5 + }, + { + "x": 740.4, + "y": -288.54, + "z": 0.5 + }, + { + "x": 738.8, + "y": -286.14, + "z": 0.5 + }, + { + "x": 737.29, + "y": -284.09, + "z": 0.5 + }, + { + "x": 735.86, + "y": -282.3, + "z": 0.5 + }, + { + "x": 734.72, + "y": -280.66, + "z": 0.5 + }, + { + "x": 733.78, + "y": -279.09, + "z": 0.5 + }, + { + "x": 733.11, + "y": -278.03, + "z": 0.5 + }, + { + "x": 732.65, + "y": -277.34, + "z": 0.5 + }, + { + "x": 732.24, + "y": -276.84, + "z": 0.5 + }, + { + "x": 731.86, + "y": -276.46, + "z": 0.5 + }, + { + "x": 731.57, + "y": -276.17, + "z": 0.5 + }, + { + "x": 731.33, + "y": -275.94, + "z": 0.5 + }, + { + "x": 731.18, + "y": -275.8, + "z": 0.5 + }, + { + "x": 731.13, + "y": -275.65, + "z": 0.5 + }, + { + "x": 731.13, + "y": -275.51, + "z": 0.5 + } + ] + } + ], + "color": "black", + "fill": "none", + "dash": "draw", + "size": "m", + "opacity": "1", + "isComplete": true, + "isClosed": false, + "isPen": false + }, + "parentId": "page:YVYGLtbdStqZyqN6qFdW4", + "index": "a1", + "id": "shape:UTr2Q7s0LckQGV62gL6vz", + "typeName": "shape" + } + ] +} \ No newline at end of file diff --git a/apps/vscode/extension/icon.png b/apps/vscode/extension/icon.png new file mode 100644 index 000000000..65026659a Binary files /dev/null and b/apps/vscode/extension/icon.png differ diff --git a/apps/vscode/extension/package.json b/apps/vscode/extension/package.json new file mode 100644 index 000000000..5cd03a700 --- /dev/null +++ b/apps/vscode/extension/package.json @@ -0,0 +1,159 @@ +{ + "name": "tldraw-vscode", + "description": "The tldraw extension for VS Code.", + "version": "2.0.5", + "private": true, + "packageManager": "yarn@3.5.0", + "author": { + "name": "tldraw GB Ltd.", + "email": "hello@tldraw.com" + }, + "homepage": "https://tldraw.dev", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/tldraw/tldraw" + }, + "bugs": { + "url": "https://github.com/tldraw/tldraw/issues" + }, + "keywords": [ + "tldraw", + "drawing", + "app", + "development", + "whiteboard", + "canvas", + "infinite" + ], + "displayName": "tldraw", + "publisher": "tldraw-org", + "icon": "icon.png", + "galleryBanner": { + "color": "#1d1d1d", + "theme": "dark" + }, + "categories": [ + "Visualization" + ], + "engines": { + "vscode": "^1.75.1" + }, + "activationEvents": [], + "browser": "./dist/web/extension.js", + "main": "./dist/web/extension.js", + "extensionKind": [ + "workspace" + ], + "contributes": { + "customEditors": [ + { + "viewType": "tldraw.tldr", + "displayName": "tldraw", + "selector": [ + { + "filenamePattern": "*.tldr" + }, + { + "filenamePattern": "*.tldr.json" + } + ] + } + ], + "keybindings": [ + { + "key": "ctrl+shift+d", + "mac": "cmd+shift+d", + "title": "Toggle Dark Mode", + "command": "tldraw.tldr.toggleDarkMode", + "category": "tldraw", + "when": "resourceExtname == .tldr" + }, + { + "key": "ctrl+numpad_add", + "mac": "cmd+numpad_add", + "title": "Zoom In", + "command": "tldraw.tldr.zoomIn", + "category": "tldraw", + "when": "resourceExtname == .tldr" + }, + { + "key": "ctrl+=", + "mac": "cmd+=", + "title": "Zoom In", + "command": "tldraw.tldr.zoomIn", + "category": "tldraw", + "when": "resourceExtname == .tldr" + }, + { + "key": "ctrl+numpad_subtract", + "mac": "cmd+numpad_subtract", + "title": "Zoom Out", + "command": "tldraw.tldr.zoomOut", + "category": "tldraw", + "when": "resourceExtname == .tldr" + }, + { + "key": "ctrl+-", + "mac": "cmd+-", + "title": "Zoom Out", + "command": "tldraw.tldr.zoomOut", + "category": "tldraw", + "when": "resourceExtname == .tldr" + }, + { + "key": "ctrl+numpad0", + "mac": "cmd+numpad0", + "title": "Reset Zoom", + "command": "tldraw.tldr.resetZoom", + "category": "tldraw", + "when": "resourceExtname == .tldr" + } + ], + "commands": [ + { + "command": "tldraw.tldr.new", + "title": "New Project", + "category": "tldraw" + } + ] + }, + "vsce": { + "dependencies": false + }, + "scripts": { + "dev:vscode": "tsx scripts/dev.ts", + "build": "cd ../editor && yarn build && cd ../extension && tsx scripts/build.ts", + "web": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=.", + "package": "yarn build && tsx scripts/package.ts", + "publish": "vsce publish", + "lint": "yarn run -T tsx ../../../scripts/lint.ts", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist && rm -rf editor && rm -rf temp & yarn" + }, + "devDependencies": { + "@tldraw/editor": "workspace:*", + "@tldraw/file-format": "workspace:*", + "@tldraw/tlstore": "workspace:*", + "@types/fs-extra": "^11.0.1", + "@types/node-fetch": "^2.6.2", + "@types/vscode": "^1.75.1", + "@typescript-eslint/eslint-plugin": "^5.10.2", + "@typescript-eslint/parser": "^5.10.2", + "assert": "^2.0.0", + "esbuild": "^0.16.7", + "fs-extra": "^11.1.0", + "lazyrepo": "0.0.0-alpha.20", + "lodash": "^4.17.21", + "mocha": "^9.1.1", + "process": "^0.11.10", + "ts-loader": "^9.2.5", + "tslib": "^2.4.0", + "tsx": "^3.12.6", + "vsce": "^2.15.0" + }, + "gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296", + "dependencies": { + "nanoid": "^4.0.1", + "node-fetch": "^2.0.0" + } +} diff --git a/apps/vscode/extension/scripts/build.ts b/apps/vscode/extension/scripts/build.ts new file mode 100644 index 000000000..d546ae92c --- /dev/null +++ b/apps/vscode/extension/scripts/build.ts @@ -0,0 +1,42 @@ +import esbuild from 'esbuild' +import { logEnv } from '../../vscode-script-utils/cli' +import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers' + +const log = logEnv('extension') + +async function build() { + await copyEditor({ log }) + await removeDistDirectory({ log }) + + try { + const entryPoints = ['./src/extension.ts'] + log({ cmd: 'esbuild', args: { entryPoints } }) + await esbuild.build({ + entryPoints, + outdir: 'dist/web', + minify: false, + bundle: true, + format: 'cjs', + target: 'es6', + platform: 'node', + define: { + 'process.env.NODE_ENV': '"production"', + }, + tsconfig: './tsconfig.json', + external: ['vscode'], + loader: { + '.woff2': 'dataurl', + '.woff': 'dataurl', + '.svg': 'file', + '.png': 'file', + '.json': 'file', + }, + }) + + log({ cmd: 'esbuild:success', args: { entryPoints } }) + } catch (error) { + log({ cmd: 'esbuild:error', args: { error } }) + throw error + } +} +build() diff --git a/apps/vscode/extension/scripts/dev.ts b/apps/vscode/extension/scripts/dev.ts new file mode 100644 index 000000000..719d3888c --- /dev/null +++ b/apps/vscode/extension/scripts/dev.ts @@ -0,0 +1,54 @@ +import esbuild from 'esbuild' +import { join } from 'path' +import { logEnv } from '../../vscode-script-utils/cli' +import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers' +import { getDirname } from '../../vscode-script-utils/path' + +const rootDir = getDirname(import.meta.url, '../') +const log = logEnv('extension') + +async function dev() { + await copyEditor({ log }) + await removeDistDirectory({ log }) + const entryPoints = [join(rootDir, 'src', 'extension.ts')] + + log({ cmd: 'esbuild', args: { entryPoints } }) + try { + esbuild.build({ + entryPoints, + outdir: join(rootDir, 'dist', 'web'), + minify: false, + bundle: true, + format: 'cjs', + target: 'es6', + sourcemap: 'inline', + platform: 'node', + define: { + 'process.env.NODE_ENV': '"development"', + }, + tsconfig: './tsconfig.json', + external: ['vscode'], + incremental: true, + watch: { + onRebuild(err) { + if (err) { + log({ cmd: 'esbuild:error', args: { error: err } }) + } else { + log({ cmd: 'esbuild:success', args: {} }) + } + }, + }, + loader: { + '.woff2': 'dataurl', + '.woff': 'dataurl', + '.svg': 'file', + '.png': 'file', + '.json': 'file', + }, + }) + } catch (error) { + log({ cmd: 'esbuild:error', args: { error } }) + throw error + } +} +dev() diff --git a/apps/vscode/extension/scripts/package.ts b/apps/vscode/extension/scripts/package.ts new file mode 100644 index 000000000..2e1d95fd3 --- /dev/null +++ b/apps/vscode/extension/scripts/package.ts @@ -0,0 +1,37 @@ +import { exec } from 'child_process' +import * as fs from 'fs' +import * as path from 'path' + +const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')) + +const { log: jslog } = console + +async function main() { + if (fs.existsSync('./editor')) { + fs.rmSync('./editor', { recursive: true }) + } + if (fs.existsSync('./temp')) { + fs.rmSync('./temp', { recursive: true }) + } + + fs.mkdirSync('./temp') + + try { + exec( + `cp -r ../editor/dist editor; vsce package; mv ${pkg.name}-${pkg.version}.vsix ${'./temp'}`, + (error, stdout, stderr) => { + if (error) { + throw new Error(error.message) + } + if (stderr && stderr.search('warning') !== 0) { + throw new Error(stderr) + } + } + ) + } catch (e) { + jslog(`× ${pkg.name}: Build failed due to an error.`) + jslog(e) + } +} + +main() diff --git a/apps/vscode/extension/src/TldrawDocument.ts b/apps/vscode/extension/src/TldrawDocument.ts new file mode 100644 index 000000000..3e710eee5 --- /dev/null +++ b/apps/vscode/extension/src/TldrawDocument.ts @@ -0,0 +1,181 @@ +import { TldrawFile } from '@tldraw/file-format' +import * as vscode from 'vscode' +import { defaultFileContents, fileExists, loadFile } from './file' +import { log } from './utils' + +export type DocumentChangeEventArgs = + | { reason: 'undo' | 'redo' } + | { + reason: 'revert' + fileContents: TldrawFile + } +export class TLDrawDocument implements vscode.CustomDocument { + isBlankDocument = false + lastBackupDestination: vscode.Uri | undefined + + private readonly _onDidChangeDocument = new vscode.EventEmitter() + + /** Fired to notify webviews that the document has changed. */ + public readonly onDidChangeContent = this._onDidChangeDocument.event + + private readonly _onDidChange = new vscode.EventEmitter<{ + readonly label: string + undo(): void + redo(): void + }>() + + public readonly onDidChange = this._onDidChange.event + + private readonly _onDidDispose = new vscode.EventEmitter() + /** Fired when the document is disposed of. */ + public readonly onDidDispose = this._onDidDispose.event + + private disposables: vscode.Disposable[] = [ + this._onDidChange, + this._onDidChangeDocument, + this._onDidDispose, + ] + + private constructor( + public uri: vscode.Uri, + public documentData: TldrawFile, + backupId: string | undefined + ) { + this.isBlankDocument = backupId === 'undefined' + } + + static async create(uri: vscode.Uri, backupId: string | undefined) { + let fileData: TldrawFile + if (typeof backupId === 'string' && (await fileExists(vscode.Uri.parse(backupId)))) { + fileData = await TLDrawDocument.readFile(vscode.Uri.parse(backupId)) + } else { + fileData = await TLDrawDocument.readFile(uri) + } + return new TLDrawDocument(uri, fileData, backupId) + } + + makeEdit(nextFile: TldrawFile) { + log('makeEdit') + const prevData = this.documentData + this.documentData = nextFile + this._onDidChange.fire({ + label: 'edit', + undo: async () => { + log('undo') + this.documentData = prevData + this._onDidChangeDocument.fire({ + reason: 'undo', + }) + }, + redo: async () => { + log('redo') + this.documentData = nextFile + this._onDidChangeDocument.fire({ + reason: 'redo', + }) + }, + }) + } + dispose(): void { + this.disposables.forEach((d) => d.dispose()) + this._onDidDispose.fire() + } + + private static async readFile(uri: vscode.Uri): Promise { + log('readFile') + + if (uri.scheme === 'untitled') { + return defaultFileContents + } + const fileContents = await vscode.workspace.fs.readFile(uri) + return loadFile(Buffer.from(fileContents).toString('utf8')) + } + + async loadBlankDocument() { + this.documentData = defaultFileContents + await this.writeToResource(this.uri) + if (this.lastBackupDestination && (await fileExists(this.lastBackupDestination))) { + await vscode.workspace.fs.delete(this.lastBackupDestination) + } + } + + /** Called by VS Code when the user saves the document. */ + async save(cancellation: vscode.CancellationToken): Promise { + log('save') + await this.saveAs(this.uri, cancellation) + } + + /** Called by VS Code when the user saves the document to a new location. */ + async saveAs(targetResource: vscode.Uri, cancellation: vscode.CancellationToken): Promise { + log('saveAs') + if (cancellation.isCancellationRequested) { + return + } + await this.writeToResource(targetResource) + } + + private async writeToResource(targetResource: vscode.Uri) { + const fileContents = Buffer.from(JSON.stringify(this.documentData, null, 2), 'utf8') + await vscode.workspace.fs.writeFile(targetResource, fileContents) + } + + /** Called by VS Code when the user calls `revert` on a document. */ + async revert(_cancellation: vscode.CancellationToken): Promise { + log('revert') + + const diskContent = await TLDrawDocument.readFile(this.uri) + this.documentData = diskContent + this._onDidChangeDocument.fire({ + reason: 'revert', + fileContents: diskContent, + }) + } + + /** + * Called by VS Code to backup the edited document. + * + * These backups are used to implement hot exit. + */ + async backup( + destination: vscode.Uri, + cancellation: vscode.CancellationToken + ): Promise { + log('backup') + this.lastBackupDestination = destination + + await this.saveAs(destination, cancellation) + + return { + id: destination.toString(), + delete: async () => { + try { + await vscode.workspace.fs.delete(destination) + } catch { + // noop + } + }, + } + } + + async v1Backup(backupSaved: string, backupFailed: string) { + const regex = /\.tldr$/gi + if (!regex.test(this.uri.path)) { + vscode.window.showInformationMessage(backupFailed) + return + } + + let destination = this.uri.with({ path: this.uri.path.replace(/\.tldr$/gi, ' - old.tldr') }) + let exists = await fileExists(destination) + let fileNumber = 1 + while (exists) { + destination = this.uri.with({ + path: this.uri.path.replace(/\.tldr$/gi, ` - old (${fileNumber}).tldr`), + }) + exists = await fileExists(destination) + fileNumber++ + } + await vscode.workspace.fs.copy(this.uri, destination, { overwrite: false }) + const fileName = destination.path.split('/').pop() + vscode.window.showInformationMessage(`${backupSaved}: ${fileName}`) + } +} diff --git a/apps/vscode/extension/src/TldrawEditorProvider.ts b/apps/vscode/extension/src/TldrawEditorProvider.ts new file mode 100644 index 000000000..93a143a5a --- /dev/null +++ b/apps/vscode/extension/src/TldrawEditorProvider.ts @@ -0,0 +1,154 @@ +import * as vscode from 'vscode' +import { DocumentChangeEventArgs, TLDrawDocument } from './TldrawDocument' +import { TldrawWebviewManager } from './TldrawWebviewManager' +import { log } from './utils' + +// @ts-ignore +import type { VscodeMessage } from '../../messages' + +export class TldrawEditorProvider implements vscode.CustomEditorProvider { + private static newTDFileId = 1 + private disposables: vscode.Disposable[] = [] + private static readonly viewType = 'tldraw.tldr' + private webviewPanels: vscode.WebviewPanel[] = [] + + private readonly _onDidChangeCustomDocument = new vscode.EventEmitter< + vscode.CustomDocumentEditEvent + >() + public readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event + + constructor(private readonly context: vscode.ExtensionContext) {} + + public static register = (context: vscode.ExtensionContext): vscode.Disposable => { + // Several commands exist only to prevent the default keyboard shortcuts + const noopCmds = ['zoomIn', 'zoomOut', 'resetZoom', 'toggleDarkMode'] + noopCmds.forEach((name) => + context.subscriptions.push(vscode.commands.registerCommand(`tldraw.tldr.${name}`, () => null)) + ) + + // Register the 'Create New File' command, which creates a temporary + // .tldr file and opens it in the editor. + context.subscriptions.push( + vscode.commands.registerCommand(`tldraw.tldr.new`, () => { + const id = this.newTDFileId++ + const name = id > 1 ? `New Document ${id}.tldr` : `New Document.tldr` + + const workspaceFolders = vscode.workspace.workspaceFolders + const path = workspaceFolders ? workspaceFolders[0].uri : vscode.Uri.parse('') + + vscode.commands.executeCommand( + 'vscode.openWith', + vscode.Uri.joinPath(path, name).with({ scheme: 'untitled' }), + this.viewType + ) + }) + ) + + // Register our editor provider, indicating to VS Code that we can + // handle files with the .tldr extension. + return vscode.window.registerCustomEditorProvider( + this.viewType, + new TldrawEditorProvider(context), + { + webviewOptions: { + retainContextWhenHidden: true, + }, + supportsMultipleEditorsPerDocument: true, + } + ) + } + + async openCustomDocument( + uri: vscode.Uri, + openContext: { backupId?: string }, + _token: vscode.CancellationToken + ): Promise { + log('openCustomDocument') + + const document: TLDrawDocument = await TLDrawDocument.create(uri, openContext.backupId) + this.disposables.push( + document.onDidChange((e) => { + log('onDidChange') + + // Tell VS Code that the document has been edited by the use. + this._onDidChangeCustomDocument.fire({ + document, + ...e, + }) + }) + ) + + this.disposables.push( + document.onDidChangeContent((e: DocumentChangeEventArgs) => { + log('onDidChange') + + this.webviewPanels.forEach((w: vscode.WebviewPanel) => { + if (w.active) { + if (e.reason === 'undo' || e.reason === 'redo') { + w.webview.postMessage({ + type: `vscode:${e.reason}`, + } as VscodeMessage) + } else if (e.reason === 'revert') { + w.webview.postMessage({ + type: `vscode:revert`, + data: { + fileContents: JSON.stringify(e.fileContents), + }, + } as VscodeMessage) + } + } + }) + }) + ) + + document.onDidDispose(() => { + log('onDidDispose document in provider') + this.disposables.forEach((d) => d.dispose()) + }) + + return document + } + + async resolveCustomEditor( + document: TLDrawDocument, + webviewPanel: vscode.WebviewPanel, + _token: vscode.CancellationToken + ): Promise { + log('resolveCustomEditor') + this.webviewPanels.push(webviewPanel) + webviewPanel.onDidDispose(() => { + this.webviewPanels = this.webviewPanels.filter((w) => w !== webviewPanel) + }) + new TldrawWebviewManager(this.context, document, webviewPanel) + } + + public saveCustomDocument( + document: TLDrawDocument, + cancellation: vscode.CancellationToken + ): Thenable { + return document.save(cancellation) + } + + public saveCustomDocumentAs( + document: TLDrawDocument, + destination: vscode.Uri, + cancellation: vscode.CancellationToken + ): Thenable { + return document.saveAs(destination, cancellation) + } + + public revertCustomDocument( + document: TLDrawDocument, + cancellation: vscode.CancellationToken + ): Thenable { + return document.revert(cancellation) + } + + public backupCustomDocument( + document: TLDrawDocument, + context: vscode.CustomDocumentBackupContext, + cancellation: vscode.CancellationToken + ): Thenable { + return document.backup(context.destination, cancellation) + } +} diff --git a/apps/vscode/extension/src/TldrawWebviewManager.ts b/apps/vscode/extension/src/TldrawWebviewManager.ts new file mode 100644 index 000000000..62d170bf8 --- /dev/null +++ b/apps/vscode/extension/src/TldrawWebviewManager.ts @@ -0,0 +1,82 @@ +import { nanoid } from 'nanoid' +import * as vscode from 'vscode' +import { TLDrawDocument } from './TldrawDocument' +import { GlobalStateKeys, WebViewMessageHandler } from './WebViewMessageHandler' +// @ts-ignore + +export class TldrawWebviewManager { + private disposables: vscode.Disposable[] = [] + private webViewMessageHandler: WebViewMessageHandler + + constructor( + context: vscode.ExtensionContext, + document: TLDrawDocument, + webviewPanel: vscode.WebviewPanel + ) { + let userId = context.globalState.get(GlobalStateKeys.UserId) + if (!userId) { + userId = 'user:' + nanoid() + context.globalState.update(GlobalStateKeys.UserId, userId) + } + + const assetSrc = webviewPanel.webview + .asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'editor', '/')) + .toString() + + this.webViewMessageHandler = new WebViewMessageHandler( + document, + webviewPanel, + context, + userId, + assetSrc + ) + // Listen for messages sent from the extensions webview. + webviewPanel.webview.onDidReceiveMessage( + this.webViewMessageHandler.handle, + undefined, + this.disposables + ) + + // Configure the webview. For now all we do is enable scripts and also + // provide the initial webview's html content. + Object.assign(webviewPanel.webview, { + options: { enableScripts: true }, + html: this.getHtmlForWebview(assetSrc), + }) + + const showV1FileOpenWarning = context.globalState.get(GlobalStateKeys.ShowV1FileOpenWarning) + if (showV1FileOpenWarning === undefined) { + context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, true) + } + + // Clean up disposables when the editor is closed. + webviewPanel.onDidDispose(this.handleDidDispose) + } + + private handleDidDispose = () => { + this.disposables.forEach(({ dispose }) => dispose()) + } + + private getHtmlForWebview = (assetSrc: string): string => { + return ` + + + + + + + tldraw + + +
+ + + + + + ` + } +} diff --git a/apps/vscode/extension/src/WebViewMessageHandler.ts b/apps/vscode/extension/src/WebViewMessageHandler.ts new file mode 100644 index 000000000..01ed874f3 --- /dev/null +++ b/apps/vscode/extension/src/WebViewMessageHandler.ts @@ -0,0 +1,215 @@ +import { BaseRecord } from '@tldraw/tlstore' +import { isEqual } from 'lodash' +import fetch from 'node-fetch' +import * as vscode from 'vscode' +import { TLDrawDocument } from './TldrawDocument' +import { loadFile } from './file' + +// @ts-ignore +import type { VscodeMessage } from '../../messages' + +export const GlobalStateKeys = { + ShowV1FileOpenWarning: 'showV1fileOpenWarning', + UserId: 'userId', +} + +export class WebViewMessageHandler { + multiplayerOmitKeys = /^(user_presence:|camera:|user:|user_document:|instance:)/ + newDocumentOmitKeys = /^(user_presence:|camera:|user:|user_document:|instance:|document:|page:)/ + + constructor( + private document: TLDrawDocument, + private webviewPanel: vscode.WebviewPanel, + private context: vscode.ExtensionContext, + private userId: unknown, + private assetSrc: string + ) {} + + isLoaded = false + firstChangeDone = false + + handle = async (e: VscodeMessage) => { + if (!this.document) return + + switch (e.type) { + case 'vscode:ready-to-receive-file': { + // Send the initial document content to bootstrap the Tldraw/Tldraw component. + this.webviewPanel.webview.postMessage({ + type: 'vscode:opened-file', + data: { + fileContents: JSON.stringify(this.document.documentData), + uri: this.document.uri.toString(), + userId: this.userId, + assetSrc: this.assetSrc, + isDarkMode: + this.document.isBlankDocument && + (vscode.window.activeColorTheme.kind === 2 || + vscode.window.activeColorTheme.kind === 3), + }, + } as VscodeMessage) + break + } + case 'vscode:open-window': { + vscode.env.openExternal(vscode.Uri.parse(e.data.url)) + break + } + case 'vscode:undo': { + vscode.commands.executeCommand('undo') + break + } + case 'vscode:redo': { + vscode.commands.executeCommand('redo') + break + } + case 'vscode:refresh-page': { + vscode.commands.executeCommand('workbench.action.reloadWindow') + break + } + case 'vscode:hard-reset': { + await this.document.loadBlankDocument() + vscode.commands.executeCommand('workbench.action.reloadWindow') + break + } + case 'vscode:bookmark/request': { + const url = e.data.url + fetch('https://www.tldraw.com/api/bookmark', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // We can fake the origin here because we're in node.js + origin: 'https://www.tldraw.com', + }, + body: JSON.stringify({ + url, + }), + }) + .then((resp) => { + return resp.json() + }) + .then((json: any) => { + this.webviewPanel.webview.postMessage({ + type: 'vscode:bookmark/response', + uuid: e.uuid, + data: { + url, + title: json.title, + description: json.description, + image: json.image, + }, + }) + }) + .catch((error: any) => { + this.webviewPanel.webview.postMessage({ + type: 'vscode:bookmark/error', + data: { + error: error.toString(), + }, + }) + }) + break + } + case 'vscode:editor-loaded': { + this.isLoaded = true + break + } + case 'vscode:v1-file-opened': { + const showV1FileOpenWarning = this.context.globalState.get( + GlobalStateKeys.ShowV1FileOpenWarning + ) + + if (!showV1FileOpenWarning) return + + const { backup, open, description, dontAskAgain } = e.data + vscode.window + .showInformationMessage(description, open, dontAskAgain, backup) + .then((result) => { + if (result === backup) { + this.document.v1Backup(e.data.backupSaved, e.data.backupFailed) + } else if (result === dontAskAgain) { + this.context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, false) + } + }) + break + } + case 'vscode:editor-updated': { + if (!this.isLoaded) return + + if (!this.firstChangeDone) { + this.firstChangeDone = true + return + } + + const raw = e.data.fileContents + if (!raw) return + + // The event will contain the new TDFile as JSON. + const nextFile = loadFile(raw) + const existingDoc = this.document.documentData + + let isSame = false + + if (existingDoc?.records?.length > 0) { + const oldDoc = this.omit(existingDoc.records, this.multiplayerOmitKeys) + const newDoc = this.omit(nextFile.records, this.multiplayerOmitKeys) + isSame = isEqual(oldDoc, newDoc) + } else { + const newDoc = this.omit(nextFile.records, this.newDocumentOmitKeys) + isSame = isEqual(newDoc, []) + } + + if (!isSame) { + this.document.makeEdit(nextFile) + } + break + } + case 'vscode:hide-v1-file-open-warning': { + this.context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, false) + break + } + case 'vscode:cancel-v1-migrate': { + vscode.commands.executeCommand('workbench.action.closeActiveEditor') + break + } + } + } + + private omit = (records: BaseRecord[], keys: RegExp) => { + return records.filter((record) => { + return !record.id.match(keys) + }) + } + + findDiff(oldDoc: Record, newDoc: Record) { + const newRecords = Object.values(newDoc) + const oldRecords = Object.values(oldDoc) + + for (const oldRecord of oldRecords) { + const newRecord = newRecords.find((r: any) => r.id === oldRecord.id) + if (!newRecord) { + // eslint-disable-next-line no-console + console.log('record missing in new doc', oldRecord) + continue + } else { + if (!isEqual(oldRecord, newRecord)) { + // eslint-disable-next-line no-console + console.log('record different', oldRecord, newRecord) + continue + } + } + } + for (const newRecord of newRecords) { + const oldRecord = oldRecords.find((r: any) => r.id === newRecord.id) + if (!oldRecord) { + // eslint-disable-next-line no-console + console.log('record missing in oldDoc doc', newRecord) + continue + } else { + if (!isEqual(newRecord, oldRecord)) { + // eslint-disable-next-line no-console + console.log('record different', newRecord, oldRecord) + continue + } + } + } + } +} diff --git a/apps/vscode/extension/src/extension.ts b/apps/vscode/extension/src/extension.ts new file mode 100644 index 000000000..53f092f1c --- /dev/null +++ b/apps/vscode/extension/src/extension.ts @@ -0,0 +1,37 @@ +import { watch } from 'fs' +import path from 'path' +import * as vscode from 'vscode' +import { TldrawEditorProvider } from './TldrawEditorProvider' + +export function activate(context: vscode.ExtensionContext) { + try { + if (process.env.NODE_ENV !== 'production') { + const extensionWatcher = watch( + __dirname + '/extension.js', + { persistent: false }, + (eventType, filename) => { + // eslint-disable-next-line no-console + console.log('reloading[%s]', eventType, filename) + extensionWatcher.close() + vscode.commands.executeCommand('workbench.action.reloadWindow') + } + ) + const result = path.dirname(__dirname).split(path.sep).slice(1, -1) + const editorpath = path.join(...result) + const editorWatcher = watch( + editorpath + '/editor/index.js', + { persistent: false }, + (eventType, filename) => { + // eslint-disable-next-line no-console + console.log('reloading[%s]', eventType, filename) + editorWatcher.close() + vscode.commands.executeCommand('workbench.action.reloadWindow') + } + ) + } + + context.subscriptions.push(TldrawEditorProvider.register(context)) + } catch (e) { + console.error(e) + } +} diff --git a/apps/vscode/extension/src/file.ts b/apps/vscode/extension/src/file.ts new file mode 100644 index 000000000..235888916 --- /dev/null +++ b/apps/vscode/extension/src/file.ts @@ -0,0 +1,37 @@ +import { TldrawEditorConfig } from '@tldraw/editor' +import { TldrawFile } from '@tldraw/file-format' +import * as vscode from 'vscode' + +export const defaultFileContents: TldrawFile = { + tldrawFileFormatVersion: 1, + schema: TldrawEditorConfig.default.storeSchema.serialize(), + records: [], +} + +export const fileContentWithErrors: TldrawFile = { + tldrawFileFormatVersion: 1, + schema: TldrawEditorConfig.default.storeSchema.serialize(), + records: [{ typeName: 'shape', id: null } as any], +} + +export function loadFile(fileContents: string): TldrawFile { + if (!fileContents) return defaultFileContents + try { + return JSON.parse(fileContents) as TldrawFile + } catch (e) { + return fileContentWithErrors + } +} + +export async function fileExists(destination: vscode.Uri) { + try { + await vscode.workspace.fs.stat(destination) + return true + } catch (e: any) { + if (e.code !== 'FileNotFound') { + // eslint-disable-next-line no-console + console.log(e) + } + return false + } +} diff --git a/apps/vscode/extension/src/utils.ts b/apps/vscode/extension/src/utils.ts new file mode 100644 index 000000000..d22603d4c --- /dev/null +++ b/apps/vscode/extension/src/utils.ts @@ -0,0 +1,8 @@ +const DEBUG_EVENTS = false + +export const log = (...args: any[]) => { + if (process.env.NODE_ENV !== 'production' && DEBUG_EVENTS) { + // eslint-disable-next-line no-console + console.log(...args) + } +} diff --git a/apps/vscode/extension/tsconfig.json b/apps/vscode/extension/tsconfig.json new file mode 100644 index 000000000..c1cee92fd --- /dev/null +++ b/apps/vscode/extension/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": true, + "checkJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "removeComments": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "noEmit": true, + "jsx": "react-jsx", + "incremental": true, + "baseUrl": ".", + "composite": true, + "importHelpers": false, + "skipDefaultLibCheck": true, + "experimentalDecorators": true, + "rootDir": ".." + }, + "include": ["src", "../messages", "scripts", "../vscode-script-utils"], + "references": [{ "path": "../../../packages/file-format" }] +} diff --git a/apps/vscode/messages.ts b/apps/vscode/messages.ts new file mode 100644 index 000000000..2e7206ffb --- /dev/null +++ b/apps/vscode/messages.ts @@ -0,0 +1,90 @@ +type BookmarkRequest = { + type: 'vscode:bookmark/request' + uuid: string + data: { + url: string + } +} +type BookmarkResponse = { + type: 'vscode:bookmark/response' + uuid: string + data: { + url: string + title?: string + description?: string + image?: string + } +} + +type BookmarkError = { + type: 'vscode:bookmark/error' + uuid: string + data: { + error: string + } +} + +/** @public */ +export type VscodeMessagePairs = { + 'vscode:bookmark': { request: BookmarkRequest; response: BookmarkResponse; error: BookmarkError } +} + +/** @public */ +export type VscodeMessage = + | { + type: 'vscode:editor-loaded' + } + | { + type: 'vscode:ready-to-receive-file' + } + | { + type: 'vscode:v1-file-opened' + data: { + description: string + backup: string + backupSaved: string + backupFailed: string + dontAskAgain: string + open: string + } + } + | { + type: 'vscode:undo' + } + | { + type: 'vscode:redo' + } + | { + type: 'vscode:cancel-v1-migrate' + } + | { + type: 'vscode:editor-updated' | 'vscode:revert' + data: { + fileContents: string + } + } + | { + type: 'vscode:open-window' + data: { + url: string + target: string + } + } + | { + type: 'vscode:opened-file' + data: { + assetSrc: string + fileContents: string + showV1FileOpenWarning: boolean + uri: string + userId: string + isDarkMode: boolean + } + } + | { + type: 'vscode:hide-v1-file-open-warning' + } + | { type: 'vscode:refresh-page' } + | { type: 'vscode:hard-reset' } + | BookmarkRequest + | BookmarkResponse diff --git a/apps/vscode/vscode-script-utils/cli.ts b/apps/vscode/vscode-script-utils/cli.ts new file mode 100644 index 000000000..c56f32be5 --- /dev/null +++ b/apps/vscode/vscode-script-utils/cli.ts @@ -0,0 +1,58 @@ +import path from 'path' + +const displayRelative = (from: string, to: string) => { + const outpath = path.relative(from, to) + if (!outpath.match(/^\./)) { + return `./${outpath}` + } + return outpath +} + +type LogDef = + | { cmd: 'remove'; env: string; args: { target: string } } + | { cmd: 'copy'; env: string; args: { source: string; dest: string } } + | { cmd: 'esbuild'; env: string; args: { entryPoints: string[] } } + | { cmd: 'esbuild:success'; env: string; args: any } + | { cmd: 'esbuild:error'; env: string; args: { error: string } } + | { cmd: 'esbuild:serve'; env: string; args: { host: string; port: number | string } } + +export function log(def: LogDef) { + const printStderr = (icon: string, cmd: string, ...args: unknown[]) => { + console.error(`${icon} [${def.env ?? 'unknown'}/${cmd}]`, ...args) + } + + if (def.cmd === 'remove') { + const { target } = def.args + printStderr('🗑 ', 'remove', displayRelative(process.cwd(), target)) + } else if (def.cmd === 'copy') { + const { source, dest } = def.args + printStderr( + '🏠', + 'copy', + `${displayRelative(process.cwd(), source)} -> ${displayRelative(process.cwd(), dest)}` + ) + } else if (def.cmd === 'esbuild') { + printStderr( + '🤖', + 'esbuild', + `${def.args.entryPoints.map((pathname) => displayRelative(process.cwd(), pathname))}` + ) + } else if (def.cmd === 'esbuild:success') { + printStderr('✅', `esbuild`, `build successful (${new Date().toISOString()})`) + } else if (def.cmd === 'esbuild:error') { + printStderr(`❌`, `esbuild`, `error`) + console.error(def.args.error) + } else if (def.cmd === 'esbuild:serve') { + const { host = 'localhost', port } = def.args + printStderr(`🌎`, `esbuild`, `serving `) + } else { + // @ts-ignore + printStderr(`❓`, def.cmd, JSON.stringify(def.args)) + } +} + +export function logEnv(env: string) { + return (opts: any) => { + log({ ...opts, env }) + } +} diff --git a/apps/vscode/vscode-script-utils/helpers.ts b/apps/vscode/vscode-script-utils/helpers.ts new file mode 100644 index 000000000..0ffa408f3 --- /dev/null +++ b/apps/vscode/vscode-script-utils/helpers.ts @@ -0,0 +1,25 @@ +import fs from 'fs' +import fse from 'fs-extra' +import { join } from 'path' +import { exists, getDirname } from './path' + +const vscodeDir = getDirname(import.meta.url, '../') + +export async function copyEditor({ log }: { log: (opts: any) => void }) { + const editorRoot = join(vscodeDir, 'editor') + const extensionRoot = join(vscodeDir, 'extension') + + const source = join(editorRoot, 'dist') + const dest = join(extensionRoot, 'editor') + + log({ cmd: 'copy', args: { source, dest } }) + await fse.copy(source, dest) +} + +export async function removeDistDirectory({ log }: { log: (opts: any) => void }) { + const target = join(vscodeDir, 'extension', 'dist') + if (await exists(target)) { + log({ cmd: 'remove', args: { target } }) + await fs.promises.rm(target, { recursive: true }) + } +} diff --git a/apps/vscode/vscode-script-utils/path.ts b/apps/vscode/vscode-script-utils/path.ts new file mode 100644 index 000000000..3e7736189 --- /dev/null +++ b/apps/vscode/vscode-script-utils/path.ts @@ -0,0 +1,16 @@ +import fs from 'fs' +import path from 'path' + +export function getDirname(metaUrl: string, targetPath: string) { + const dirname = path.dirname(metaUrl.replace('file://', '')) + return path.normalize(path.join(dirname, targetPath)) +} + +export async function exists(targetFolder: string) { + try { + await fs.promises.access(targetFolder) + return true + } catch (err) { + return false + } +} diff --git a/assets/embed-icons/codepen.png b/assets/embed-icons/codepen.png new file mode 100644 index 000000000..bd47194fa Binary files /dev/null and b/assets/embed-icons/codepen.png differ diff --git a/assets/embed-icons/codesandbox.png b/assets/embed-icons/codesandbox.png new file mode 100644 index 000000000..5fe29a734 Binary files /dev/null and b/assets/embed-icons/codesandbox.png differ diff --git a/assets/embed-icons/excalidraw.png b/assets/embed-icons/excalidraw.png new file mode 100644 index 000000000..fad01b477 Binary files /dev/null and b/assets/embed-icons/excalidraw.png differ diff --git a/assets/embed-icons/felt.png b/assets/embed-icons/felt.png new file mode 100644 index 000000000..e4d94a62b Binary files /dev/null and b/assets/embed-icons/felt.png differ diff --git a/assets/embed-icons/figma.png b/assets/embed-icons/figma.png new file mode 100644 index 000000000..399b965bc Binary files /dev/null and b/assets/embed-icons/figma.png differ diff --git a/assets/embed-icons/github_gist.png b/assets/embed-icons/github_gist.png new file mode 100644 index 000000000..4bc21276f Binary files /dev/null and b/assets/embed-icons/github_gist.png differ diff --git a/assets/embed-icons/google_calendar.png b/assets/embed-icons/google_calendar.png new file mode 100644 index 000000000..758f8af8e Binary files /dev/null and b/assets/embed-icons/google_calendar.png differ diff --git a/assets/embed-icons/google_maps.png b/assets/embed-icons/google_maps.png new file mode 100644 index 000000000..b9422c4c0 Binary files /dev/null and b/assets/embed-icons/google_maps.png differ diff --git a/assets/embed-icons/google_slides.png b/assets/embed-icons/google_slides.png new file mode 100644 index 000000000..818e1018e Binary files /dev/null and b/assets/embed-icons/google_slides.png differ diff --git a/assets/embed-icons/observable.png b/assets/embed-icons/observable.png new file mode 100644 index 000000000..e86f8a1d6 Binary files /dev/null and b/assets/embed-icons/observable.png differ diff --git a/assets/embed-icons/replit.png b/assets/embed-icons/replit.png new file mode 100644 index 000000000..9c26ae736 Binary files /dev/null and b/assets/embed-icons/replit.png differ diff --git a/assets/embed-icons/scratch.png b/assets/embed-icons/scratch.png new file mode 100644 index 000000000..9be22a022 Binary files /dev/null and b/assets/embed-icons/scratch.png differ diff --git a/assets/embed-icons/spotify.png b/assets/embed-icons/spotify.png new file mode 100644 index 000000000..9e888dc2a Binary files /dev/null and b/assets/embed-icons/spotify.png differ diff --git a/assets/embed-icons/tldraw.png b/assets/embed-icons/tldraw.png new file mode 100644 index 000000000..eb38d1f76 Binary files /dev/null and b/assets/embed-icons/tldraw.png differ diff --git a/assets/embed-icons/vimeo.png b/assets/embed-icons/vimeo.png new file mode 100644 index 000000000..a0e361c74 Binary files /dev/null and b/assets/embed-icons/vimeo.png differ diff --git a/assets/embed-icons/youtube.png b/assets/embed-icons/youtube.png new file mode 100644 index 000000000..26bf3334a Binary files /dev/null and b/assets/embed-icons/youtube.png differ diff --git a/assets/fonts/IBMPlexMono-Medium.woff2 b/assets/fonts/IBMPlexMono-Medium.woff2 new file mode 100644 index 000000000..637df267a Binary files /dev/null and b/assets/fonts/IBMPlexMono-Medium.woff2 differ diff --git a/assets/fonts/IBMPlexSans-Medium.woff2 b/assets/fonts/IBMPlexSans-Medium.woff2 new file mode 100644 index 000000000..0e32d3712 Binary files /dev/null and b/assets/fonts/IBMPlexSans-Medium.woff2 differ diff --git a/assets/fonts/IBMPlexSerif-Medium.woff2 b/assets/fonts/IBMPlexSerif-Medium.woff2 new file mode 100644 index 000000000..829423bd4 Binary files /dev/null and b/assets/fonts/IBMPlexSerif-Medium.woff2 differ diff --git a/assets/fonts/Shantell_Sans-Normal-SemiBold.woff2 b/assets/fonts/Shantell_Sans-Normal-SemiBold.woff2 new file mode 100644 index 000000000..b2f3c4793 Binary files /dev/null and b/assets/fonts/Shantell_Sans-Normal-SemiBold.woff2 differ diff --git a/assets/github-hero-dark-draw.png b/assets/github-hero-dark-draw.png new file mode 100644 index 000000000..edd1dfca7 Binary files /dev/null and b/assets/github-hero-dark-draw.png differ diff --git a/assets/github-hero-dark.png b/assets/github-hero-dark.png new file mode 100644 index 000000000..7db66bfab Binary files /dev/null and b/assets/github-hero-dark.png differ diff --git a/assets/github-hero-light-draw.png b/assets/github-hero-light-draw.png new file mode 100644 index 000000000..876da9046 Binary files /dev/null and b/assets/github-hero-light-draw.png differ diff --git a/assets/github-hero-light.png b/assets/github-hero-light.png new file mode 100644 index 000000000..5341bf21a Binary files /dev/null and b/assets/github-hero-light.png differ diff --git a/assets/icons/icon/align-bottom-center.svg b/assets/icons/icon/align-bottom-center.svg new file mode 100644 index 000000000..24d14d203 --- /dev/null +++ b/assets/icons/icon/align-bottom-center.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/icon/align-bottom-left.svg b/assets/icons/icon/align-bottom-left.svg new file mode 100644 index 000000000..45a0e9636 --- /dev/null +++ b/assets/icons/icon/align-bottom-left.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/icon/align-bottom-right.svg b/assets/icons/icon/align-bottom-right.svg new file mode 100644 index 000000000..c7315361d --- /dev/null +++ b/assets/icons/icon/align-bottom-right.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/icon/align-bottom.svg b/assets/icons/icon/align-bottom.svg new file mode 100644 index 000000000..a6933e43d --- /dev/null +++ b/assets/icons/icon/align-bottom.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/align-center-center.svg b/assets/icons/icon/align-center-center.svg new file mode 100644 index 000000000..ca5285fdd --- /dev/null +++ b/assets/icons/icon/align-center-center.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/icon/align-center-horizontal.svg b/assets/icons/icon/align-center-horizontal.svg new file mode 100644 index 000000000..1fe337d2b --- /dev/null +++ b/assets/icons/icon/align-center-horizontal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/align-center-left.svg b/assets/icons/icon/align-center-left.svg new file mode 100644 index 000000000..91bd3891c --- /dev/null +++ b/assets/icons/icon/align-center-left.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/icon/align-center-right.svg b/assets/icons/icon/align-center-right.svg new file mode 100644 index 000000000..26aee67b7 --- /dev/null +++ b/assets/icons/icon/align-center-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/icon/align-center-vertical.svg b/assets/icons/icon/align-center-vertical.svg new file mode 100644 index 000000000..e0095c300 --- /dev/null +++ b/assets/icons/icon/align-center-vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/align-left.svg b/assets/icons/icon/align-left.svg new file mode 100644 index 000000000..bc77f383a --- /dev/null +++ b/assets/icons/icon/align-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/align-right.svg b/assets/icons/icon/align-right.svg new file mode 100644 index 000000000..656704221 --- /dev/null +++ b/assets/icons/icon/align-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/align-top-center.svg b/assets/icons/icon/align-top-center.svg new file mode 100644 index 000000000..7dd7ebb2b --- /dev/null +++ b/assets/icons/icon/align-top-center.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/icon/align-top-left.svg b/assets/icons/icon/align-top-left.svg new file mode 100644 index 000000000..65c2027ac --- /dev/null +++ b/assets/icons/icon/align-top-left.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/icon/align-top-right.svg b/assets/icons/icon/align-top-right.svg new file mode 100644 index 000000000..1a36ab062 --- /dev/null +++ b/assets/icons/icon/align-top-right.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/icon/align-top.svg b/assets/icons/icon/align-top.svg new file mode 100644 index 000000000..08731c6db --- /dev/null +++ b/assets/icons/icon/align-top.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/arrow-left.svg b/assets/icons/icon/arrow-left.svg new file mode 100644 index 000000000..5567a7c35 --- /dev/null +++ b/assets/icons/icon/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/arrowhead-arrow.svg b/assets/icons/icon/arrowhead-arrow.svg new file mode 100644 index 000000000..cffa61493 --- /dev/null +++ b/assets/icons/icon/arrowhead-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/arrowhead-bar.svg b/assets/icons/icon/arrowhead-bar.svg new file mode 100644 index 000000000..114cc4339 --- /dev/null +++ b/assets/icons/icon/arrowhead-bar.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/arrowhead-diamond.svg b/assets/icons/icon/arrowhead-diamond.svg new file mode 100644 index 000000000..ec9b5d6ff --- /dev/null +++ b/assets/icons/icon/arrowhead-diamond.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/arrowhead-dot.svg b/assets/icons/icon/arrowhead-dot.svg new file mode 100644 index 000000000..e693d90fa --- /dev/null +++ b/assets/icons/icon/arrowhead-dot.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/arrowhead-none.svg b/assets/icons/icon/arrowhead-none.svg new file mode 100644 index 000000000..268c5f7ba --- /dev/null +++ b/assets/icons/icon/arrowhead-none.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/arrowhead-square.svg b/assets/icons/icon/arrowhead-square.svg new file mode 100644 index 000000000..2ed2383c9 --- /dev/null +++ b/assets/icons/icon/arrowhead-square.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/arrowhead-triangle-inverted.svg b/assets/icons/icon/arrowhead-triangle-inverted.svg new file mode 100644 index 000000000..d927492d9 --- /dev/null +++ b/assets/icons/icon/arrowhead-triangle-inverted.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/arrowhead-triangle.svg b/assets/icons/icon/arrowhead-triangle.svg new file mode 100644 index 000000000..42721afda --- /dev/null +++ b/assets/icons/icon/arrowhead-triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/aspect-ratio.svg b/assets/icons/icon/aspect-ratio.svg new file mode 100644 index 000000000..38f6452a4 --- /dev/null +++ b/assets/icons/icon/aspect-ratio.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/icon/avatar.svg b/assets/icons/icon/avatar.svg new file mode 100644 index 000000000..622017d61 --- /dev/null +++ b/assets/icons/icon/avatar.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/blob.svg b/assets/icons/icon/blob.svg new file mode 100644 index 000000000..4ff1bafc0 --- /dev/null +++ b/assets/icons/icon/blob.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/bring-forward.svg b/assets/icons/icon/bring-forward.svg new file mode 100644 index 000000000..570408836 --- /dev/null +++ b/assets/icons/icon/bring-forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/bring-to-front.svg b/assets/icons/icon/bring-to-front.svg new file mode 100644 index 000000000..2e4236d6f --- /dev/null +++ b/assets/icons/icon/bring-to-front.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/check.svg b/assets/icons/icon/check.svg new file mode 100644 index 000000000..86caad792 --- /dev/null +++ b/assets/icons/icon/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/checkbox-checked.svg b/assets/icons/icon/checkbox-checked.svg new file mode 100644 index 000000000..9cd8a9d87 --- /dev/null +++ b/assets/icons/icon/checkbox-checked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/checkbox-empty.svg b/assets/icons/icon/checkbox-empty.svg new file mode 100644 index 000000000..5ff6485cf --- /dev/null +++ b/assets/icons/icon/checkbox-empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/chevron-down.svg b/assets/icons/icon/chevron-down.svg new file mode 100644 index 000000000..f43179ca2 --- /dev/null +++ b/assets/icons/icon/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/chevron-left.svg b/assets/icons/icon/chevron-left.svg new file mode 100644 index 000000000..5ec929146 --- /dev/null +++ b/assets/icons/icon/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/chevron-right.svg b/assets/icons/icon/chevron-right.svg new file mode 100644 index 000000000..2432eb346 --- /dev/null +++ b/assets/icons/icon/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/chevron-up.svg b/assets/icons/icon/chevron-up.svg new file mode 100644 index 000000000..d44443df6 --- /dev/null +++ b/assets/icons/icon/chevron-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/chevrons-ne.svg b/assets/icons/icon/chevrons-ne.svg new file mode 100644 index 000000000..3748fdc8e --- /dev/null +++ b/assets/icons/icon/chevrons-ne.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/chevrons-sw.svg b/assets/icons/icon/chevrons-sw.svg new file mode 100644 index 000000000..0773322b7 --- /dev/null +++ b/assets/icons/icon/chevrons-sw.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/clipboard-copy.svg b/assets/icons/icon/clipboard-copy.svg new file mode 100644 index 000000000..b0861dbbd --- /dev/null +++ b/assets/icons/icon/clipboard-copy.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/icon/code.svg b/assets/icons/icon/code.svg new file mode 100644 index 000000000..8386abe1d --- /dev/null +++ b/assets/icons/icon/code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/collab.svg b/assets/icons/icon/collab.svg new file mode 100644 index 000000000..9a891bba8 --- /dev/null +++ b/assets/icons/icon/collab.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/color.svg b/assets/icons/icon/color.svg new file mode 100644 index 000000000..d328079f8 --- /dev/null +++ b/assets/icons/icon/color.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/comment.svg b/assets/icons/icon/comment.svg new file mode 100644 index 000000000..7c726bb29 --- /dev/null +++ b/assets/icons/icon/comment.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/cross-2.svg b/assets/icons/icon/cross-2.svg new file mode 100644 index 000000000..e97b8bd56 --- /dev/null +++ b/assets/icons/icon/cross-2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/cross.svg b/assets/icons/icon/cross.svg new file mode 100644 index 000000000..ce033df93 --- /dev/null +++ b/assets/icons/icon/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/dash-dashed.svg b/assets/icons/icon/dash-dashed.svg new file mode 100644 index 000000000..5f53b6a4e --- /dev/null +++ b/assets/icons/icon/dash-dashed.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/dash-dotted.svg b/assets/icons/icon/dash-dotted.svg new file mode 100644 index 000000000..c731b9aab --- /dev/null +++ b/assets/icons/icon/dash-dotted.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/icon/dash-draw.svg b/assets/icons/icon/dash-draw.svg new file mode 100644 index 000000000..75ecb72b5 --- /dev/null +++ b/assets/icons/icon/dash-draw.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/dash-solid.svg b/assets/icons/icon/dash-solid.svg new file mode 100644 index 000000000..dbbe4aae3 --- /dev/null +++ b/assets/icons/icon/dash-solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/discord.svg b/assets/icons/icon/discord.svg new file mode 100644 index 000000000..46d0f2d9e --- /dev/null +++ b/assets/icons/icon/discord.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/icon/distribute-horizontal.svg b/assets/icons/icon/distribute-horizontal.svg new file mode 100644 index 000000000..e2ba5dbca --- /dev/null +++ b/assets/icons/icon/distribute-horizontal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/icon/distribute-vertical.svg b/assets/icons/icon/distribute-vertical.svg new file mode 100644 index 000000000..ebc9ad4a8 --- /dev/null +++ b/assets/icons/icon/distribute-vertical.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/icon/dot.svg b/assets/icons/icon/dot.svg new file mode 100644 index 000000000..aaedc6a1a --- /dev/null +++ b/assets/icons/icon/dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/dots-horizontal.svg b/assets/icons/icon/dots-horizontal.svg new file mode 100644 index 000000000..ea5ba70f7 --- /dev/null +++ b/assets/icons/icon/dots-horizontal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/dots-vertical.svg b/assets/icons/icon/dots-vertical.svg new file mode 100644 index 000000000..d52e38a83 --- /dev/null +++ b/assets/icons/icon/dots-vertical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/drag-handle-dots.svg b/assets/icons/icon/drag-handle-dots.svg new file mode 100644 index 000000000..836cb0c29 --- /dev/null +++ b/assets/icons/icon/drag-handle-dots.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/icon/duplicate.svg b/assets/icons/icon/duplicate.svg new file mode 100644 index 000000000..1dd299efe --- /dev/null +++ b/assets/icons/icon/duplicate.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/edit.svg b/assets/icons/icon/edit.svg new file mode 100644 index 000000000..58b9fb63a --- /dev/null +++ b/assets/icons/icon/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/external-link.svg b/assets/icons/icon/external-link.svg new file mode 100644 index 000000000..0ffe93970 --- /dev/null +++ b/assets/icons/icon/external-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/file.svg b/assets/icons/icon/file.svg new file mode 100644 index 000000000..c5abf52e4 --- /dev/null +++ b/assets/icons/icon/file.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/fill-none.svg b/assets/icons/icon/fill-none.svg new file mode 100644 index 000000000..858806ce5 --- /dev/null +++ b/assets/icons/icon/fill-none.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/fill-pattern.svg b/assets/icons/icon/fill-pattern.svg new file mode 100644 index 000000000..200390db5 --- /dev/null +++ b/assets/icons/icon/fill-pattern.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/fill-semi.svg b/assets/icons/icon/fill-semi.svg new file mode 100644 index 000000000..2894622af --- /dev/null +++ b/assets/icons/icon/fill-semi.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/fill-solid.svg b/assets/icons/icon/fill-solid.svg new file mode 100644 index 000000000..b2054f091 --- /dev/null +++ b/assets/icons/icon/fill-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/follow.svg b/assets/icons/icon/follow.svg new file mode 100644 index 000000000..fa525c35d --- /dev/null +++ b/assets/icons/icon/follow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/following.svg b/assets/icons/icon/following.svg new file mode 100644 index 000000000..918caa50a --- /dev/null +++ b/assets/icons/icon/following.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/font-draw.svg b/assets/icons/icon/font-draw.svg new file mode 100644 index 000000000..3bcb5509a --- /dev/null +++ b/assets/icons/icon/font-draw.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/font-mono.svg b/assets/icons/icon/font-mono.svg new file mode 100644 index 000000000..3826b56b7 --- /dev/null +++ b/assets/icons/icon/font-mono.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/font-sans.svg b/assets/icons/icon/font-sans.svg new file mode 100644 index 000000000..2a0c28f00 --- /dev/null +++ b/assets/icons/icon/font-sans.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/font-serif.svg b/assets/icons/icon/font-serif.svg new file mode 100644 index 000000000..d3baaceb0 --- /dev/null +++ b/assets/icons/icon/font-serif.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/geo-arrow-down.svg b/assets/icons/icon/geo-arrow-down.svg new file mode 100644 index 000000000..c8de46eec --- /dev/null +++ b/assets/icons/icon/geo-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-arrow-left.svg b/assets/icons/icon/geo-arrow-left.svg new file mode 100644 index 000000000..57e23fd16 --- /dev/null +++ b/assets/icons/icon/geo-arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-arrow-right.svg b/assets/icons/icon/geo-arrow-right.svg new file mode 100644 index 000000000..ee6f147ed --- /dev/null +++ b/assets/icons/icon/geo-arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-arrow-up.svg b/assets/icons/icon/geo-arrow-up.svg new file mode 100644 index 000000000..1b4c0be3a --- /dev/null +++ b/assets/icons/icon/geo-arrow-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-diamond.svg b/assets/icons/icon/geo-diamond.svg new file mode 100644 index 000000000..72174f748 --- /dev/null +++ b/assets/icons/icon/geo-diamond.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-ellipse.svg b/assets/icons/icon/geo-ellipse.svg new file mode 100644 index 000000000..b6a848815 --- /dev/null +++ b/assets/icons/icon/geo-ellipse.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-hexagon.svg b/assets/icons/icon/geo-hexagon.svg new file mode 100644 index 000000000..3cf2c8901 --- /dev/null +++ b/assets/icons/icon/geo-hexagon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-octagon.svg b/assets/icons/icon/geo-octagon.svg new file mode 100644 index 000000000..1f9f324ae --- /dev/null +++ b/assets/icons/icon/geo-octagon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-oval.svg b/assets/icons/icon/geo-oval.svg new file mode 100644 index 000000000..795ad3484 --- /dev/null +++ b/assets/icons/icon/geo-oval.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-pentagon.svg b/assets/icons/icon/geo-pentagon.svg new file mode 100644 index 000000000..3bd2936f3 --- /dev/null +++ b/assets/icons/icon/geo-pentagon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-rectangle.svg b/assets/icons/icon/geo-rectangle.svg new file mode 100644 index 000000000..5e37c9133 --- /dev/null +++ b/assets/icons/icon/geo-rectangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-rhombus-2.svg b/assets/icons/icon/geo-rhombus-2.svg new file mode 100644 index 000000000..2618d5e46 --- /dev/null +++ b/assets/icons/icon/geo-rhombus-2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-rhombus.svg b/assets/icons/icon/geo-rhombus.svg new file mode 100644 index 000000000..177e3f096 --- /dev/null +++ b/assets/icons/icon/geo-rhombus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-star.svg b/assets/icons/icon/geo-star.svg new file mode 100644 index 000000000..698c151e6 --- /dev/null +++ b/assets/icons/icon/geo-star.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-trapezoid.svg b/assets/icons/icon/geo-trapezoid.svg new file mode 100644 index 000000000..659c2bd7c --- /dev/null +++ b/assets/icons/icon/geo-trapezoid.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-triangle.svg b/assets/icons/icon/geo-triangle.svg new file mode 100644 index 000000000..aad8aae4f --- /dev/null +++ b/assets/icons/icon/geo-triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/geo-x-box.svg b/assets/icons/icon/geo-x-box.svg new file mode 100644 index 000000000..0474dff56 --- /dev/null +++ b/assets/icons/icon/geo-x-box.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/github.svg b/assets/icons/icon/github.svg new file mode 100644 index 000000000..12461b1c0 --- /dev/null +++ b/assets/icons/icon/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/group.svg b/assets/icons/icon/group.svg new file mode 100644 index 000000000..e84d19664 --- /dev/null +++ b/assets/icons/icon/group.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/icon/hidden.svg b/assets/icons/icon/hidden.svg new file mode 100644 index 000000000..2ab52e966 --- /dev/null +++ b/assets/icons/icon/hidden.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/image.svg b/assets/icons/icon/image.svg new file mode 100644 index 000000000..5d9041558 --- /dev/null +++ b/assets/icons/icon/image.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/info-circle.svg b/assets/icons/icon/info-circle.svg new file mode 100644 index 000000000..696a14b8d --- /dev/null +++ b/assets/icons/icon/info-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/leading.svg b/assets/icons/icon/leading.svg new file mode 100644 index 000000000..6b661e8d4 --- /dev/null +++ b/assets/icons/icon/leading.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/link.svg b/assets/icons/icon/link.svg new file mode 100644 index 000000000..83dd1b9f3 --- /dev/null +++ b/assets/icons/icon/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/lock-small.svg b/assets/icons/icon/lock-small.svg new file mode 100644 index 000000000..90f0e6d4a --- /dev/null +++ b/assets/icons/icon/lock-small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/lock.svg b/assets/icons/icon/lock.svg new file mode 100644 index 000000000..5693cc4c5 --- /dev/null +++ b/assets/icons/icon/lock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/menu.svg b/assets/icons/icon/menu.svg new file mode 100644 index 000000000..0ee03f0f1 --- /dev/null +++ b/assets/icons/icon/menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/minus.svg b/assets/icons/icon/minus.svg new file mode 100644 index 000000000..2b208b666 --- /dev/null +++ b/assets/icons/icon/minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/mixed.svg b/assets/icons/icon/mixed.svg new file mode 100644 index 000000000..b5d50a2f5 --- /dev/null +++ b/assets/icons/icon/mixed.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/icon/pack.svg b/assets/icons/icon/pack.svg new file mode 100644 index 000000000..9d3880025 --- /dev/null +++ b/assets/icons/icon/pack.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/page.svg b/assets/icons/icon/page.svg new file mode 100644 index 000000000..e2fe4482c --- /dev/null +++ b/assets/icons/icon/page.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/plus.svg b/assets/icons/icon/plus.svg new file mode 100644 index 000000000..bb2dd71f7 --- /dev/null +++ b/assets/icons/icon/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/question-mark-circle.svg b/assets/icons/icon/question-mark-circle.svg new file mode 100644 index 000000000..eedb2cf7f --- /dev/null +++ b/assets/icons/icon/question-mark-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/question-mark.svg b/assets/icons/icon/question-mark.svg new file mode 100644 index 000000000..ef5ecf4fb --- /dev/null +++ b/assets/icons/icon/question-mark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/redo.svg b/assets/icons/icon/redo.svg new file mode 100644 index 000000000..cdb6c7e91 --- /dev/null +++ b/assets/icons/icon/redo.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/reset-zoom.svg b/assets/icons/icon/reset-zoom.svg new file mode 100644 index 000000000..4d90af791 --- /dev/null +++ b/assets/icons/icon/reset-zoom.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/icon/rotate-ccw.svg b/assets/icons/icon/rotate-ccw.svg new file mode 100644 index 000000000..14a5533be --- /dev/null +++ b/assets/icons/icon/rotate-ccw.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/rotate-cw.svg b/assets/icons/icon/rotate-cw.svg new file mode 100644 index 000000000..ba320383b --- /dev/null +++ b/assets/icons/icon/rotate-cw.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/ruler.svg b/assets/icons/icon/ruler.svg new file mode 100644 index 000000000..6784733d7 --- /dev/null +++ b/assets/icons/icon/ruler.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/search.svg b/assets/icons/icon/search.svg new file mode 100644 index 000000000..bdee3c3c2 --- /dev/null +++ b/assets/icons/icon/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/send-backward.svg b/assets/icons/icon/send-backward.svg new file mode 100644 index 000000000..917b9a1af --- /dev/null +++ b/assets/icons/icon/send-backward.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/send-to-back.svg b/assets/icons/icon/send-to-back.svg new file mode 100644 index 000000000..3046efbcc --- /dev/null +++ b/assets/icons/icon/send-to-back.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/settings-horizontal.svg b/assets/icons/icon/settings-horizontal.svg new file mode 100644 index 000000000..a86a03006 --- /dev/null +++ b/assets/icons/icon/settings-horizontal.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/settings-vertical-1.svg b/assets/icons/icon/settings-vertical-1.svg new file mode 100644 index 000000000..16db6e855 --- /dev/null +++ b/assets/icons/icon/settings-vertical-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/settings-vertical.svg b/assets/icons/icon/settings-vertical.svg new file mode 100644 index 000000000..1d4730391 --- /dev/null +++ b/assets/icons/icon/settings-vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/share-1.svg b/assets/icons/icon/share-1.svg new file mode 100644 index 000000000..160f0418a --- /dev/null +++ b/assets/icons/icon/share-1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/share-2.svg b/assets/icons/icon/share-2.svg new file mode 100644 index 000000000..2e0910aea --- /dev/null +++ b/assets/icons/icon/share-2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/size-extra-large.svg b/assets/icons/icon/size-extra-large.svg new file mode 100644 index 000000000..408e3bb66 --- /dev/null +++ b/assets/icons/icon/size-extra-large.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/size-large.svg b/assets/icons/icon/size-large.svg new file mode 100644 index 000000000..011e0f2b4 --- /dev/null +++ b/assets/icons/icon/size-large.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/size-medium.svg b/assets/icons/icon/size-medium.svg new file mode 100644 index 000000000..c5de337cf --- /dev/null +++ b/assets/icons/icon/size-medium.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/size-small.svg b/assets/icons/icon/size-small.svg new file mode 100644 index 000000000..68c4ac464 --- /dev/null +++ b/assets/icons/icon/size-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/spline-cubic.svg b/assets/icons/icon/spline-cubic.svg new file mode 100644 index 000000000..480fe6e14 --- /dev/null +++ b/assets/icons/icon/spline-cubic.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/spline-line.svg b/assets/icons/icon/spline-line.svg new file mode 100644 index 000000000..bdbdf3028 --- /dev/null +++ b/assets/icons/icon/spline-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/stack-horizontal.svg b/assets/icons/icon/stack-horizontal.svg new file mode 100644 index 000000000..2b5d782b4 --- /dev/null +++ b/assets/icons/icon/stack-horizontal.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/stack-vertical.svg b/assets/icons/icon/stack-vertical.svg new file mode 100644 index 000000000..51c48dbaf --- /dev/null +++ b/assets/icons/icon/stack-vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/stretch-horizontal.svg b/assets/icons/icon/stretch-horizontal.svg new file mode 100644 index 000000000..2356c2235 --- /dev/null +++ b/assets/icons/icon/stretch-horizontal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/stretch-vertical.svg b/assets/icons/icon/stretch-vertical.svg new file mode 100644 index 000000000..5fb5d7c3c --- /dev/null +++ b/assets/icons/icon/stretch-vertical.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/text-align-center.svg b/assets/icons/icon/text-align-center.svg new file mode 100644 index 000000000..31f653f2e --- /dev/null +++ b/assets/icons/icon/text-align-center.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/text-align-justify.svg b/assets/icons/icon/text-align-justify.svg new file mode 100644 index 000000000..946e907a9 --- /dev/null +++ b/assets/icons/icon/text-align-justify.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/text-align-left.svg b/assets/icons/icon/text-align-left.svg new file mode 100644 index 000000000..14070feac --- /dev/null +++ b/assets/icons/icon/text-align-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/text-align-right.svg b/assets/icons/icon/text-align-right.svg new file mode 100644 index 000000000..eb3f3690d --- /dev/null +++ b/assets/icons/icon/text-align-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-arrow.svg b/assets/icons/icon/tool-arrow.svg new file mode 100644 index 000000000..4ec0239ca --- /dev/null +++ b/assets/icons/icon/tool-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-embed.svg b/assets/icons/icon/tool-embed.svg new file mode 100644 index 000000000..5cca9c3a1 --- /dev/null +++ b/assets/icons/icon/tool-embed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/tool-eraser.svg b/assets/icons/icon/tool-eraser.svg new file mode 100644 index 000000000..684f50620 --- /dev/null +++ b/assets/icons/icon/tool-eraser.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-frame.svg b/assets/icons/icon/tool-frame.svg new file mode 100644 index 000000000..a3f0ce448 --- /dev/null +++ b/assets/icons/icon/tool-frame.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-hand.svg b/assets/icons/icon/tool-hand.svg new file mode 100644 index 000000000..f38c7bb09 --- /dev/null +++ b/assets/icons/icon/tool-hand.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-highlighter.svg b/assets/icons/icon/tool-highlighter.svg new file mode 100644 index 000000000..62588c091 --- /dev/null +++ b/assets/icons/icon/tool-highlighter.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-line.svg b/assets/icons/icon/tool-line.svg new file mode 100644 index 000000000..db6e280cc --- /dev/null +++ b/assets/icons/icon/tool-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-media.svg b/assets/icons/icon/tool-media.svg new file mode 100644 index 000000000..5d9041558 --- /dev/null +++ b/assets/icons/icon/tool-media.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/tool-note.svg b/assets/icons/icon/tool-note.svg new file mode 100644 index 000000000..2e588e96f --- /dev/null +++ b/assets/icons/icon/tool-note.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-pencil.svg b/assets/icons/icon/tool-pencil.svg new file mode 100644 index 000000000..dd50e2190 --- /dev/null +++ b/assets/icons/icon/tool-pencil.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/tool-pointer.svg b/assets/icons/icon/tool-pointer.svg new file mode 100644 index 000000000..5c2ca6887 --- /dev/null +++ b/assets/icons/icon/tool-pointer.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/tool-text.svg b/assets/icons/icon/tool-text.svg new file mode 100644 index 000000000..b4b237bff --- /dev/null +++ b/assets/icons/icon/tool-text.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/trash.svg b/assets/icons/icon/trash.svg new file mode 100644 index 000000000..8799f732e --- /dev/null +++ b/assets/icons/icon/trash.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/triangle-down.svg b/assets/icons/icon/triangle-down.svg new file mode 100644 index 000000000..7f16828cb --- /dev/null +++ b/assets/icons/icon/triangle-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/triangle-up.svg b/assets/icons/icon/triangle-up.svg new file mode 100644 index 000000000..88587f8c1 --- /dev/null +++ b/assets/icons/icon/triangle-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/twitter.svg b/assets/icons/icon/twitter.svg new file mode 100644 index 000000000..5e2dc0f0d --- /dev/null +++ b/assets/icons/icon/twitter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/undo.svg b/assets/icons/icon/undo.svg new file mode 100644 index 000000000..cc94f8cda --- /dev/null +++ b/assets/icons/icon/undo.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/ungroup.svg b/assets/icons/icon/ungroup.svg new file mode 100644 index 000000000..193da59e7 --- /dev/null +++ b/assets/icons/icon/ungroup.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/icon/unlock-small.svg b/assets/icons/icon/unlock-small.svg new file mode 100644 index 000000000..205980fbb --- /dev/null +++ b/assets/icons/icon/unlock-small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/unlock.svg b/assets/icons/icon/unlock.svg new file mode 100644 index 000000000..b81dc0b4e --- /dev/null +++ b/assets/icons/icon/unlock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/icon/visible.svg b/assets/icons/icon/visible.svg new file mode 100644 index 000000000..a5c205380 --- /dev/null +++ b/assets/icons/icon/visible.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/icon/warning-triangle.svg b/assets/icons/icon/warning-triangle.svg new file mode 100644 index 000000000..70a73aa29 --- /dev/null +++ b/assets/icons/icon/warning-triangle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/icon/zoom-in.svg b/assets/icons/icon/zoom-in.svg new file mode 100644 index 000000000..7aa583e5d --- /dev/null +++ b/assets/icons/icon/zoom-in.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/icon/zoom-out.svg b/assets/icons/icon/zoom-out.svg new file mode 100644 index 000000000..64f0da31e --- /dev/null +++ b/assets/icons/icon/zoom-out.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/translations/ar.json b/assets/translations/ar.json new file mode 100644 index 000000000..0db6b4f7e --- /dev/null +++ b/assets/translations/ar.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "تحويل إلى إشارة مرجعية", + "action.convert-to-embed": "تحويل إلى تضمين", + "action.open-embed-link": "فتح الرابط", + "action.align-bottom": "محاذاة للأسفل", + "action.align-center-horizontal": "محاذاة أفقيًا", + "action.align-center-vertical": "محاذاة عموديًا", + "action.align-center-horizontal.short": "محاذاة أفقية", + "action.align-center-vertical.short": "محاذاة عمودية", + "action.align-left": "محاذاة لليسار", + "action.align-right": "محاذاة لليمين", + "action.align-top": "محاذاة للأعلى", + "action.back-to-content": "العودة إلى المحتوى", + "action.bring-forward": "إحضار إلى الأمام", + "action.bring-to-front": "إحضار إلى المقدمة", + "action.copy-as-json.short": "صيغة JSON", + "action.copy-as-json": "نسخ بصيغة JSON", + "action.copy-as-png.short": "صيغة PNG", + "action.copy-as-png": "نسخ بصيغة PNG", + "action.copy-as-svg.short": "صيغة SVG", + "action.copy-as-svg": "نسخ بصيغة SVG", + "action.copy": "نسخ", + "action.cut": "قص", + "action.delete": "حذف", + "action.distribute-horizontal": "توزيع أفقيًا", + "action.distribute-vertical": "توزيع عموديًا", + "action.distribute-horizontal.short": "توزيع أفقي", + "action.distribute-vertical.short": "توزيع عمودي", + "action.duplicate": "نسخة مكررة", + "action.edit-link": "تعديل الرابط", + "action.exit-pen-mode": "الخروج من وضع القلم", + "action.export-as-json.short": "صيغة JSON", + "action.export-as-json": "تصدير بصيغة JSON", + "action.export-as-png.short": "صيغة PNG", + "action.export-as-png": "تصدير بصيغة PNG", + "action.export-as-svg.short": "صيغة SVG", + "action.export-as-svg": "تصدير بصيغة SVG", + "action.flip-horizontal": "عكس أفقيًا", + "action.flip-vertical": "عكس عموديًا", + "action.flip-horizontal.short": "عكس أفقي", + "action.flip-vertical.short": "عكس عمودي", + "action.group": "مجموعة", + "action.insert-media": "تحميل الوسائط", + "action.new-shared-project": "مشروع جديد تمت مشاركته", + "action.nudge-down": "التحريك لأسفل", + "action.nudge-left": "التحريك لليسار", + "action.nudge-right": "التحريك لليمين", + "action.nudge-up": "التحريك لأعلى", + "action.open-file": "فتح ملف", + "action.pack": "حزمة", + "action.paste": "لصق", + "action.print": "طباعة", + "action.redo": "إعادة", + "action.rotate-ccw": "تدوير عكس اتجاه عقارب الساعة", + "action.rotate-cw": "تدوير في اتجاه عقارب الساعة", + "action.save-copy": "حفظ نسخة", + "action.select-all": "تحديد الكل", + "action.select-none": "عدم تحديد شيء", + "action.send-backward": "إرسال إلى الخلف", + "action.send-to-back": "إرسال إلى خلف", + "action.share-project": "شارك هذا المشروع", + "action.stack-horizontal": "تكديس أفقيًا", + "action.stack-vertical": "تكديس عموديًا", + "action.stack-horizontal.short": "تكديس أفقي", + "action.stack-vertical.short": "تكديس عمودي", + "action.stretch-horizontal": "تمدد أفقيًا", + "action.stretch-vertical": "تمدد عموديًا", + "action.stretch-horizontal.short": "تمدد أفقي", + "action.stretch-vertical.short": "تمدد عمودي", + "action.toggle-auto-size": "التبديل للحجم التلقائي", + "action.toggle-dark-mode.menu": "الوضع المظلم", + "action.toggle-dark-mode": "التبديل للوضع المظلم", + "action.toggle-debug-mode.menu": "وضع التصحيح", + "action.toggle-debug-mode": "التبديل لوضع التصحيح", + "action.toggle-focus-mode.menu": "وضع التركيز", + "action.toggle-focus-mode": "التبديل لوضع التركيز", + "action.toggle-grid.menu": "إظهار الشبكة", + "action.toggle-grid": "التبديل لوضع الشبكة", + "action.toggle-snap-mode.menu": "وضع المحاذاة الدائمة", + "action.toggle-snap-mode": "التبديل لوضع المحاذاة الدائمة", + "action.toggle-tool-lock.menu": "قفل الأداة", + "action.toggle-tool-lock": "تبديل قفل الأداة", + "action.toggle-transparent.context-menu": "شفاف", + "action.toggle-transparent.menu": "شفاف", + "action.toggle-transparent": "تبديل الخلفية الشفافة", + "action.undo": "تراجع", + "action.ungroup": "فك التجميع", + "action.zoom-in": "تكبير", + "action.zoom-out": "تصغير", + "action.zoom-to-100": "التكبير إلى 100%", + "action.zoom-to-fit": "التكبير للملاءمة", + "action.zoom-to-selection": "التكبير للتحديد", + "color-style.black": "أسود", + "color-style.blue": "أزرق", + "color-style.green": "أخضر", + "color-style.grey": "رمادي", + "color-style.light-blue": "أزرق فاتح", + "color-style.light-green": "أخضر فاتح", + "color-style.light-red": "أحمر فاتح", + "color-style.light-violet": "بنفسجي فاتح", + "color-style.orange": "برتقالي", + "color-style.red": "أحمر", + "color-style.violet": "بنفسجي", + "color-style.yellow": "أصفر", + "fill-style.none": "لا يوجد", + "fill-style.semi": "شبه", + "fill-style.solid": "مليء", + "fill-style.pattern": "نمط", + "dash-style.dashed": "بشرطة", + "dash-style.dotted": "منقط", + "dash-style.draw": "رسم", + "dash-style.solid": "مليء", + "size-style.s": "صغير", + "size-style.m": "متوسط", + "size-style.l": "كبير", + "size-style.xl": "كبير جدًا", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "رسم", + "font-style.sans": "غير مذيَّل", + "font-style.serif": "خط مذيَّل", + "font-style.mono": "أحادي", + "align-style.start": "البداية", + "align-style.middle": "الوسط", + "align-style.end": "النهاية", + "align-style.justify": "ضبط", + "geo-style.arrow-down": "سهم للأسفل", + "geo-style.arrow-left": "سهم لليسار", + "geo-style.arrow-right": "سهم لليمين", + "geo-style.arrow-up": "سهم للأعلى", + "geo-style.diamond": "شكل الماسة", + "geo-style.ellipse": "شكل بيضاوي", + "geo-style.hexagon": "شكل سداسي", + "geo-style.octagon": "شكل ثماني", + "geo-style.oval": "شكل بيضاوي", + "geo-style.pentagon": "شكل خماسي", + "geo-style.rectangle": "مستطيل", + "geo-style.rhombus-2": "المعيَّن 2", + "geo-style.rhombus": "معيَّن", + "geo-style.star": "نجمة", + "geo-style.trapezoid": "شبه منحرف", + "geo-style.triangle": "مثلث", + "geo-style.x-box": "مربع X", + "arrowheadStart-style.none": "لا يوجد", + "arrowheadStart-style.arrow": "سهم", + "arrowheadStart-style.bar": "شريط", + "arrowheadStart-style.diamond": "شكل الماسة", + "arrowheadStart-style.dot": "نقطة", + "arrowheadStart-style.inverted": "معكوس", + "arrowheadStart-style.pipe": "أنبوب", + "arrowheadStart-style.square": "مربع", + "arrowheadStart-style.triangle": "مثلث", + "arrowheadEnd-style.none": "لا يوجد", + "arrowheadEnd-style.arrow": "سهم", + "arrowheadEnd-style.bar": "شريط", + "arrowheadEnd-style.diamond": "شكل الماسة", + "arrowheadEnd-style.dot": "نقطة", + "arrowheadEnd-style.inverted": "معكوس", + "arrowheadEnd-style.pipe": "أنبوب", + "arrowheadEnd-style.square": "مربع", + "arrowheadEnd-style.triangle": "مثلث", + "spline-style.line": "خط", + "spline-style.cubic": "مكعب", + "tool.select": "تحديد", + "tool.hand": "أيقونة يد", + "tool.draw": "رسم", + "tool.eraser": "ممحاة", + "tool.arrow-down": "سهم للأسفل", + "tool.arrow-left": "سهم لليسار", + "tool.arrow-right": "سهم لليمين", + "tool.arrow-up": "سهم للأعلى", + "tool.arrow": "سهم", + "tool.diamond": "شكل الماسة", + "tool.ellipse": "شكل بيضاوي", + "tool.hexagon": "شكل سداسي", + "tool.line": "خط", + "tool.octagon": "شكل ثماني", + "tool.oval": "شكل بيضاوي", + "tool.pentagon": "شكل خماسي", + "tool.rectangle": "مستطيل", + "tool.rhombus": "معين", + "tool.star": "نجمة", + "tool.trapezoid": "شبه منحرف", + "tool.triangle": "مثلث", + "tool.x-box": "X box", + "tool.asset": "الأصل", + "tool.frame": "إطار", + "tool.note": "ملحوظة", + "tool.embed": "تضمين", + "tool.text": "النص", + "menu.title": "القائمة", + "menu.copy-as": "النسخ بصيغة", + "menu.edit": "تعديل", + "menu.export-as": "التصدير بصيغة", + "menu.file": "ملف", + "menu.language": "اللغة", + "menu.preferences": "التفضيلات", + "menu.view": "عرض", + "context-menu.arrange": "ترتيب", + "context-menu.copy-as": "النسخ بصيغة", + "context-menu.export-as": "التصدير بصيغة", + "context-menu.move-to-page": "انتقل إلى صفحة", + "context-menu.reorder": "إعادة الترتيب", + "page-menu.title": "الصفحات", + "page-menu.create-new-page": "إنشاء صفحة جديدة", + "page-menu.edit-pages": "تعديل الصفحات", + "page-menu.max-page-count-reached": "تم الوصول إلى الحد الأقصى لعدد الصفحات", + "page-menu.new-page-initial-name": "الصفحة 1", + "page-menu.page": "صفحة", + "page-menu.edit-start": "تعديل", + "page-menu.edit-done": "تم", + "page-menu.submenu.rename": "إعادة تسمية", + "page-menu.submenu.duplicate-page": "نسخة مكررة", + "page-menu.submenu.go-to-page": "انتقل إلى صفحة", + "page-menu.submenu.title": "القائمة", + "page-menu.submenu.move-down": "الانتقال لأسفل", + "page-menu.submenu.move-up": "الانتقال لأعلى", + "page-menu.submenu.delete": "حذف", + "share-menu.title": "مشاركة", + "share-menu.share-project": "مشاركة هذا المشروع", + "share-menu.create-project": "مشروع جديد تمت مشاركته", + "share-menu.copy-link": "نسخ الرابط", + "share-menu.readonly-link": "للقراءة فقط", + "share-menu.copy-readonly-link": "نسخ رابط للقراءة فقط", + "share-menu.offline-note": "ستؤدي مشاركة هذا المشروع إلى إنشاء نسخة مستضافة مباشرة على عنوان URL جديد. كما يمكنك مشاركة عنوان URL مع ما يصل إلى ثلاثين شخصًا آخر لعرض المشروع وتعديله معًا.", + "share-menu.copy-link-note": "سيتمكن أي شخص لديه الرابط من عرض هذا المشروع وتعديله.", + "share-menu.copy-readonly-link-note": "سيتمكن أي شخص لديه الرابط من عرض هذا المشروع (ولكن لن يتمكن من تعديله).", + "share-menu.project-too-large": "عذرًا، لا يمكن مشاركة هذا المشروع لأنه كبير جدًا. نحن نعمل على ذلك!", + "people-menu.title": "الأشخاص", + "people-menu.change-name": "تغيير الاسم", + "people-menu.change-color": "تغيير اللون", + "people-menu.user": "(أنت)", + "people-menu.invite": "دعوة الآخرين", + "debug-menu.hard-reset": "إجراء إعادة تعيين يدوية", + "debug-menu.create-shapes": "إنشاء 100 شكل", + "help-menu.title": "المساعدة والمصادر", + "help-menu.about": "حول", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "اختصارات لوحة المفاتيح", + "help-menu.twitter": "Twitter", + "links-menu.about": "نبذة", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "الإجراءات", + "edit-link-dialog.title": "تعديل الرابط", + "edit-link-dialog.invalid-url": "يجب أن يكون الرابط عنوان URL صالحًا.", + "edit-link-dialog.detail": "سيتم فتح الروابط في علامة تبويب جديدة.", + "edit-link-dialog.url": "عنوان URL", + "edit-link-dialog.clear": "مسح", + "edit-link-dialog.save": "المتابعة", + "edit-link-dialog.cancel": "إلغاء", + "embed-dialog.title": "إنشاء تضمين", + "embed-dialog.url-label": "لصق عنوان URL", + "embed-dialog.back": "العودة", + "embed-dialog.create": "إنشاء", + "embed-dialog.cancel": "إلغاء", + "embed-dialog.url": "عنوان URL", + "embed-dialog.instruction": "الصق عنوان URL الخاص بالموقع لإنشاء التضمين.", + "embed-dialog.invalid-url": "لم نتمكن من إنشاء تضمين من عنوان URL هذا.", + "edit-pages-dialog.title": "تعديل الصفحات", + "edit-pages-dialog.create-new-page": "إنشاء صفحة جديدة", + "edit-pages-dialog.delete": "حذف", + "edit-pages-dialog.duplicate-page": "نسخة مكررة", + "edit-pages-dialog.go-to-page": "انتقل إلى صفحة", + "edit-pages-dialog.max-page-count-reached": "تم الوصول إلى الحد الأقصى لعدد الصفحات", + "edit-pages-dialog.more-menu": "القائمة", + "edit-pages-dialog.move-down": "الانتقال لأسفل", + "edit-pages-dialog.move-up": "الانتقال لأعلى", + "edit-pages-dialog.new-page-initial-name": "الصفحة 1", + "reload-file-dialog.title": "متابعة تعديل الملف", + "reload-file-dialog.description": "لقد كنت تقوم بتعديل ملف. هل ترغب في متابعة تعديله؟", + "reload-file-dialog.failure": "فشلت إعادة تحميل الملف. حاول مرة أخرى؟", + "reload-file-dialog.reload": "متابعة التعديل", + "reload-file-dialog.revert": "لا، شكرًا", + "shortcuts-dialog.title": "اختصارات لوحة المفاتيح", + "shortcuts-dialog.edit": "تعديل", + "shortcuts-dialog.file": "ملف", + "shortcuts-dialog.preferences": "التفضيلات", + "shortcuts-dialog.tools": "الأدوات", + "shortcuts-dialog.transform": "تحويل", + "shortcuts-dialog.view": "عرض", + "shortcuts-dialog.save": "متابعة", + "style-panel.title": "الأنماط", + "style-panel.align": "محاذاة", + "style-panel.arrowheads": "رؤوس الأسهم", + "style-panel.color": "اللون", + "style-panel.dash": "شرطة", + "style-panel.fill": "ملء", + "style-panel.font": "الخط", + "style-panel.geo": "الشكل", + "style-panel.label": "ملصق", + "style-panel.mixed": "مختلط", + "style-panel.opacity": "معدل الشفافية", + "style-panel.size": "الحجم", + "style-panel.spline": "Spline", + "style-panel.text": "النص", + "tool-panel.drawing": "الرسم", + "tool-panel.geo": "الشكل", + "tool-panel.shapes": "الأشكال", + "tool-panel.things": "الأشياء", + "tool-panel.tools": "الأدوات", + "save-changes-prompt.title": "لم تحفظ التغييرات", + "save-changes-prompt.description": "هل ترغب في حفظ التغييرات التي أُجريت على ملفك الحالي؟", + "save-changes-prompt.go-back": "عودة", + "save-changes-prompt.continue": "متابعة", + "navigation-zone.toggle-minimap": "تبديل الخريطة المصغرة", + "navigation-zone.zoom": "تكبير", + "focus-mode.toggle-focus-mode": "تبديل وضع التركيز", + "toast.close": "إغلاق", + "file-system.file-open-error.title": "تعذر فتح الملف", + "file-system.file-open-error.not-a-tldraw-file": "لا يبدو أن الملف الذي حاولت فتحه هو ملف tldraw.", + "file-system.file-open-error.file-format-version-too-new": "الملف الذي حاولت فتحه تابع لإصدار أحدث من tldraw. يُرجى إعادة تحميل الصفحة والمحاولة مرة أخرى.", + "file-system.file-open-error.generic-corrupted-file": "الملف الذي حاولت فتحه تالف.", + "file-system.confirm-open.title": "هل تريد استبدال المشروع الحالي؟", + "file-system.confirm-open.description": "سيؤدي فتح ملف إلى استبدال مشروعك الحالي وستفقد أي تغييرات غير محفوظة. هل أنت متأكد من أنك تريد المتابعة؟", + "file-system.confirm-open.cancel": "إلغاء", + "file-system.confirm-open.open": "فتح الملف", + "file-system.confirm-open.dont-show-again": "لا تسأل مرة أخرى", + "toast.error.export-fail.title": "فشل التصدير", + "toast.error.export-fail.desc": "فشل في تصدير الصورة", + "toast.error.copy-fail.title": "فشل النسخ", + "toast.error.copy-fail.desc": "فشل في نسخ الصورة", + "file-system.shared-document-file-open-error.title": "تعذر فتح الملف", + "file-system.shared-document-file-open-error.description": "فتح الملفات من المشاريع المشتركة غير مدعوم.", + "vscode.file-open.dont-show-again": "لا تسأل مرةً أخرى", + "vscode.file-open.desc": "تم إنشاء هذا الملف باستخدام إصدار سابق من tldraw. هل ترغب في تحديث الملف للعمل مع الإصدار الجديد؟", + "context.pages.new-page": "صفحة جديدة" +} \ No newline at end of file diff --git a/assets/translations/ca.json b/assets/translations/ca.json new file mode 100644 index 000000000..710770879 --- /dev/null +++ b/assets/translations/ca.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "Convertir a marcador", + "action.convert-to-embed": "Convertir a inserció", + "action.open-embed-link": "Obrir enllaç", + "action.align-bottom": "Alinear amb la part inferior", + "action.align-center-horizontal": "Alinear horitzontalment", + "action.align-center-vertical": "Alinear verticalment", + "action.align-center-horizontal.short": "Alinear H", + "action.align-center-vertical.short": "Alinear V", + "action.align-left": "Alinear a l'esquerra", + "action.align-right": "Alinear a la dreta", + "action.align-top": "Alinear amb la part superior", + "action.back-to-content": "Tornar al contingut", + "action.bring-forward": "Portar endavant", + "action.bring-to-front": "Portar al davant", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Copiar com a JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Copiar com a PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Copiar com a SVG", + "action.copy": "Copiar", + "action.cut": "Tallar", + "action.delete": "Esborrar", + "action.distribute-horizontal": "Distribuir horitzontalment", + "action.distribute-vertical": "Distribuir verticalment", + "action.distribute-horizontal.short": "Distribuir H", + "action.distribute-vertical.short": "Distribuir V", + "action.duplicate": "Duplicar", + "action.edit-link": "Editar enllaç", + "action.exit-pen-mode": "Sortir del mode bolígraf", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Exportar com a JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Exportar com a PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Exportar com a SVG", + "action.flip-horizontal": "Girar horitzontalment", + "action.flip-vertical": "Girar verticalment", + "action.flip-horizontal.short": "Girar H", + "action.flip-vertical.short": "Girar V", + "action.group": "Grup", + "action.insert-media": "Pujar contingut multimèdia", + "action.new-shared-project": "Nou projecte compartit", + "action.nudge-down": "Empènyer cap avall", + "action.nudge-left": "Empènyer cap a l'esquerra", + "action.nudge-right": "Empènyer cap a la dreta", + "action.nudge-up": "Empènyer cap amunt", + "action.open-file": "Obrir fitxer", + "action.pack": "Empaquetar", + "action.paste": "Enganxar", + "action.print": "Imprimir", + "action.redo": "Refer", + "action.rotate-ccw": "Girar en sentit antihorari", + "action.rotate-cw": "Girar en sentit horari", + "action.save-copy": "Desar una còpia", + "action.select-all": "Seleccionar tot", + "action.select-none": "Desseleccionar tot", + "action.send-backward": "Enviar cap enrere", + "action.send-to-back": "Enviar al fons", + "action.share-project": "Compartir aquest projecte", + "action.stack-horizontal": "Apilar horitzontalment", + "action.stack-vertical": "Apilar verticalment", + "action.stack-horizontal.short": "Apilar H", + "action.stack-vertical.short": "Apilar V", + "action.stretch-horizontal": "Estirar horitzontalment", + "action.stretch-vertical": "Estirar verticalment", + "action.stretch-horizontal.short": "Estirament H", + "action.stretch-vertical.short": "Estirament V", + "action.toggle-auto-size": "Commutar grandària automàtica", + "action.toggle-dark-mode.menu": "Mode fosc", + "action.toggle-dark-mode": "Commutar mode fosc", + "action.toggle-debug-mode.menu": "Mode de depuració", + "action.toggle-debug-mode": "Commutar mode de depuració", + "action.toggle-focus-mode.menu": "Mode de concentració", + "action.toggle-focus-mode": "Commutar mode de concentració", + "action.toggle-grid.menu": "Mostra la quadrícula", + "action.toggle-grid": "Commutar quadrícula", + "action.toggle-snap-mode.menu": "Acoblament permanent", + "action.toggle-snap-mode": "Commuta acoblament permanent", + "action.toggle-tool-lock.menu": "Bloqueig d'eines", + "action.toggle-tool-lock": "Commutar el bloqueig d'eines", + "action.toggle-transparent.context-menu": "Transparent", + "action.toggle-transparent.menu": "Transparent", + "action.toggle-transparent": "Commutar el fons transparent", + "action.undo": "Desfer", + "action.ungroup": "Desagrupar", + "action.zoom-in": "Apropar-se", + "action.zoom-out": "Allunyar-se", + "action.zoom-to-100": "Amplia fins al 100%", + "action.zoom-to-fit": "Enquadrar", + "action.zoom-to-selection": "Ampliar fins a la selecció", + "color-style.black": "Negre", + "color-style.blue": "Blau", + "color-style.green": "Verd", + "color-style.grey": "Gris", + "color-style.light-blue": "Blau clar", + "color-style.light-green": "Verd clar", + "color-style.light-red": "Vermell clar", + "color-style.light-violet": "Violeta clar", + "color-style.orange": "Taronja", + "color-style.red": "Vermell", + "color-style.violet": "Violeta", + "color-style.yellow": "Groc", + "fill-style.none": "Cap", + "fill-style.semi": "Semi", + "fill-style.solid": "Sòlid", + "fill-style.pattern": "Patró", + "dash-style.dashed": "Guions", + "dash-style.dotted": "Puntejat", + "dash-style.draw": "Dibuixar", + "dash-style.solid": "Sòlid", + "size-style.s": "Petit", + "size-style.m": "Mitjana", + "size-style.l": "Gran", + "size-style.xl": "Extra gran", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Dibuixar", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Inici", + "align-style.middle": "Mig", + "align-style.end": "Fi", + "align-style.justify": "Justificar", + "geo-style.arrow-down": "Fletxa cap avall", + "geo-style.arrow-left": "Fletxa cap a l'esquerra", + "geo-style.arrow-right": "Fletxa cap a la dreta", + "geo-style.arrow-up": "Fletxa cap amunt", + "geo-style.diamond": "Diamant", + "geo-style.ellipse": "El·lipse", + "geo-style.hexagon": "Hexàgon", + "geo-style.octagon": "Octàgon", + "geo-style.oval": "Oval", + "geo-style.pentagon": "Pentàgon", + "geo-style.rectangle": "Rectangle", + "geo-style.rhombus-2": "Rombe 2", + "geo-style.rhombus": "Rombe", + "geo-style.star": "Estrella", + "geo-style.trapezoid": "Trapezoide", + "geo-style.triangle": "Triangle", + "geo-style.x-box": "Caixa X", + "arrowheadStart-style.none": "Cap", + "arrowheadStart-style.arrow": "Fletxa", + "arrowheadStart-style.bar": "Barra", + "arrowheadStart-style.diamond": "Diamant", + "arrowheadStart-style.dot": "Punt", + "arrowheadStart-style.inverted": "Invertit", + "arrowheadStart-style.pipe": "Tub", + "arrowheadStart-style.square": "Quadrat", + "arrowheadStart-style.triangle": "Triangle", + "arrowheadEnd-style.none": "Cap", + "arrowheadEnd-style.arrow": "Fletxa", + "arrowheadEnd-style.bar": "Barra", + "arrowheadEnd-style.diamond": "Diamant", + "arrowheadEnd-style.dot": "Punt", + "arrowheadEnd-style.inverted": "Invertit", + "arrowheadEnd-style.pipe": "Tub", + "arrowheadEnd-style.square": "Quadrat", + "arrowheadEnd-style.triangle": "Triangle", + "spline-style.line": "Línia", + "spline-style.cubic": "Cúbic", + "tool.select": "Seleccionar", + "tool.hand": "Mà", + "tool.draw": "Dibuixar", + "tool.eraser": "Goma d'esborrar", + "tool.arrow-down": "Fletxa cap avall", + "tool.arrow-left": "Fletxa cap a l'esquerra", + "tool.arrow-right": "Fletxa cap a la dreta", + "tool.arrow-up": "Fletxa cap amunt", + "tool.arrow": "Fletxa", + "tool.diamond": "Diamant", + "tool.ellipse": "El·lipse", + "tool.hexagon": "Hexàgon", + "tool.line": "Línia", + "tool.octagon": "Octàgon", + "tool.oval": "Oval", + "tool.pentagon": "Pentàgon", + "tool.rectangle": "Rectangle", + "tool.rhombus": "Rombe", + "tool.star": "Estrella", + "tool.trapezoid": "Trapezoide", + "tool.triangle": "Triangle", + "tool.x-box": "Caixa X", + "tool.asset": "Objecte", + "tool.frame": "Marc", + "tool.note": "Nota", + "tool.embed": "Inserir", + "tool.text": "Text", + "menu.title": "Menú", + "menu.copy-as": "Copiar com a...", + "menu.edit": "Editar", + "menu.export-as": "Exportar com a...", + "menu.file": "Fitxer", + "menu.language": "Llengua", + "menu.preferences": "Preferències", + "menu.view": "Visualització", + "context-menu.arrange": "Organitzar", + "context-menu.copy-as": "Copiar com a...", + "context-menu.export-as": "Exportar com a...", + "context-menu.move-to-page": "Moure a la pàgina", + "context-menu.reorder": "Reordenar", + "page-menu.title": "Pàgines", + "page-menu.create-new-page": "Crear una pàgina nova", + "page-menu.edit-pages": "Editar pàgines", + "page-menu.max-page-count-reached": "S'han arribat al màxim de pàgines", + "page-menu.new-page-initial-name": "Pàgina 1", + "page-menu.page": "Pàgina", + "page-menu.edit-start": "Editar", + "page-menu.edit-done": "Fet", + "page-menu.submenu.rename": "Rebatejar", + "page-menu.submenu.duplicate-page": "Duplicar", + "page-menu.submenu.go-to-page": "Anar a la pàgina", + "page-menu.submenu.title": "Menú", + "page-menu.submenu.move-down": "Baixar", + "page-menu.submenu.move-up": "Pujar", + "page-menu.submenu.delete": "Esborrar", + "share-menu.title": "Compartir", + "share-menu.share-project": "Compartir aquest projecte", + "share-menu.create-project": "Nou projecte compartit", + "share-menu.copy-link": "Copiar l'enllaç", + "share-menu.readonly-link": "Només lectura", + "share-menu.copy-readonly-link": "Copia l'enllaç de només lectura", + "share-menu.offline-note": "Si compartiu aquest projecte, es crearà una còpia allotjada en un URL nou. Podeu compartir l'URL amb un màxim de trenta persones més per veure i editar el projecte junts.", + "share-menu.copy-link-note": "Qualsevol persona que tingui l'enllaç podrà veure i editar aquest projecte.", + "share-menu.copy-readonly-link-note": "Qualsevol persona amb l'enllaç podrà veure (però no editar) aquest projecte.", + "share-menu.project-too-large": "Ho sentim, aquest projecte no es pot compartir perquè és massa gran.", + "people-menu.title": "Gent", + "people-menu.change-name": "Canviar nom", + "people-menu.change-color": "Canviar de color", + "people-menu.user": "(Tu)", + "people-menu.invite": "Convidar altres persones", + "debug-menu.hard-reset": "Reset dur", + "debug-menu.create-shapes": "Crear 100 formes", + "help-menu.title": "Ajuda i recursos", + "help-menu.about": "Sobre nosaltres", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Dreceres de teclat", + "help-menu.twitter": "Twitter", + "links-menu.about": "Sobre nosaltres", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Accions", + "edit-link-dialog.title": "Editar l'enllaç", + "edit-link-dialog.invalid-url": "Un enllaç ha de ser un URL vàlid.", + "edit-link-dialog.detail": "Els enllaços s'obriran en una pestanya nova.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Esborrar tot", + "edit-link-dialog.save": "Continuar", + "edit-link-dialog.cancel": "Cancel·lar", + "embed-dialog.title": "Crear inserció", + "embed-dialog.url-label": "Enganxar l'URL", + "embed-dialog.back": "Enrere", + "embed-dialog.create": "Crear", + "embed-dialog.cancel": "Cancel·lar", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Enganxeu l'URL del lloc per crear la inserció.", + "embed-dialog.invalid-url": "No hem pogut crear una inserció a partir d'aquest URL.", + "edit-pages-dialog.title": "Editar pàgines", + "edit-pages-dialog.create-new-page": "Crear una pàgina nova", + "edit-pages-dialog.delete": "Esborrar", + "edit-pages-dialog.duplicate-page": "Duplicar", + "edit-pages-dialog.go-to-page": "Anar a la pàgina", + "edit-pages-dialog.max-page-count-reached": "S'han arribat al màxim de pàgines", + "edit-pages-dialog.more-menu": "Menú", + "edit-pages-dialog.move-down": "Baixar", + "edit-pages-dialog.move-up": "Pujar", + "edit-pages-dialog.new-page-initial-name": "Pàgina 1", + "reload-file-dialog.title": "Continuar editant el fitxer", + "reload-file-dialog.description": "Estaves editant un fitxer. Vols continuar editant-lo?", + "reload-file-dialog.failure": "No s'ha pogut carregar el fitxer. Tornar a intentar-ho?", + "reload-file-dialog.reload": "Continuar editant", + "reload-file-dialog.revert": "No, gràcies", + "shortcuts-dialog.title": "Dreceres de teclat", + "shortcuts-dialog.edit": "Editar", + "shortcuts-dialog.file": "Fitxer", + "shortcuts-dialog.preferences": "Preferències", + "shortcuts-dialog.tools": "Eines", + "shortcuts-dialog.transform": "Transformar", + "shortcuts-dialog.view": "Vista", + "shortcuts-dialog.save": "Continuar", + "style-panel.title": "Estils", + "style-panel.align": "Alinear", + "style-panel.arrowheads": "Puntes de fletxa", + "style-panel.color": "Color", + "style-panel.dash": "Traça", + "style-panel.fill": "Pintura", + "style-panel.font": "Font", + "style-panel.geo": "Forma", + "style-panel.label": "Etiqueta", + "style-panel.mixed": "Mixt", + "style-panel.opacity": "Opacitat", + "style-panel.size": "Mida", + "style-panel.spline": "Spline", + "style-panel.text": "Text", + "tool-panel.drawing": "Dibuix", + "tool-panel.geo": "Forma", + "tool-panel.shapes": "Formes", + "tool-panel.things": "Coses", + "tool-panel.tools": "Eines", + "save-changes-prompt.title": "Tens canvis no desats", + "save-changes-prompt.description": "Voleu desar els canvis al fitxer actual?", + "save-changes-prompt.go-back": "Tornar enrere", + "save-changes-prompt.continue": "Continuar", + "navigation-zone.toggle-minimap": "Commutar minimapa", + "navigation-zone.zoom": "Zoom", + "focus-mode.toggle-focus-mode": "Commutar mode de concentració", + "toast.close": "Tancar", + "file-system.file-open-error.title": "No s'ha pogut obrir el fitxer", + "file-system.file-open-error.not-a-tldraw-file": "El fitxer que heu intentat obrir no és un fitxer tldraw.", + "file-system.file-open-error.file-format-version-too-new": "El fitxer que heu intentat obrir és d'una versió més recent de tldraw. Torneu a carregar la pàgina i torneu-ho a provar.", + "file-system.file-open-error.generic-corrupted-file": "El fitxer que heu intentat obrir està malmès.", + "file-system.confirm-open.title": "Vols sobreescriure el projecte actual?", + "file-system.confirm-open.description": "L'obertura d'un fitxer substituirà el vostre projecte actual i es perdran els canvis no desats. Voleu continuar?", + "file-system.confirm-open.cancel": "Cancel·lar", + "file-system.confirm-open.open": "Obrir fitxer", + "file-system.confirm-open.dont-show-again": "No preguntar-ho més", + "toast.error.export-fail.title": "No s'ha pogut exportar", + "toast.error.export-fail.desc": "No s'ha pogut exportar la imatge", + "toast.error.copy-fail.title": "No s'ha pogut copiar", + "toast.error.copy-fail.desc": "No s'ha pogut copiar la imatge", + "file-system.shared-document-file-open-error.title": "No s'ha pogut obrir el fitxer", + "file-system.shared-document-file-open-error.description": "No es poden obrir fitxers de projectes compartits.", + "vscode.file-open.dont-show-again": "No preguntar-ho més", + "vscode.file-open.desc": "Aquest fitxer es va crear amb una versió anterior de tldraw. Vols actualitzar-lo perquè funcioni amb la nova versió?", + "context.pages.new-page": "Nova pàgina" +} \ No newline at end of file diff --git a/assets/translations/da.json b/assets/translations/da.json new file mode 100644 index 000000000..0ec05a385 --- /dev/null +++ b/assets/translations/da.json @@ -0,0 +1,194 @@ +{ + "action.convert-to-bookmark": "Konverter til bogmærke", + "action.copy": "Kopier", + "action.cut": "Klip", + "action.delete": "Slet", + "action.duplicate": "Dupliker", + "action.flip-horizontal": "Vend vandret", + "action.flip-vertical": "Vend lodret", + "action.group": "Grupper", + "action.insert-media": "Upload medie", + "action.paste": "Indsæt", + "action.redo": "Gentag", + "action.select-all": "Vælg alt", + "action.select-none": "Fravælg alt", + "action.undo": "Fortryd", + "action.ungroup": "Opdel gruppe", + "action.zoom-in": "Zoom ind", + "action.zoom-out": "Zoom ud", + "action.zoom-to-fit": "Zoom til lærred", + "action.zoom-to-selection": "Zoom til valgte", + "dash-style.draw": "Tegn", + "font-style.draw": "Tegn", + "geo-style.ellipse": "Ellipse", + "geo-style.rectangle": "Rektangel", + "geo-style.triangle": "Trekant", + "geo-style.x-box": "X Boks", + "arrowheadStart-style.none": "Ingen", + "arrowheadStart-style.arrow": "Pil", + "arrowheadStart-style.bar": "Bar", + "arrowheadStart-style.diamond": "Diamant", + "arrowheadStart-style.dot": "Prik", + "arrowheadStart-style.inverted": "Inverteret", + "arrowheadStart-style.pipe": "Rør", + "arrowheadStart-style.square": "Firkant", + "arrowheadStart-style.triangle": "Trekant", + "arrowheadEnd-style.none": "Ingen", + "arrowheadEnd-style.arrow": "Pil", + "arrowheadEnd-style.triangle": "Trekant", + "spline-style.line": "Linje", + "tool.select": "Vælg", + "tool.draw": "Tegn", + "tool.eraser": "Viskelæder", + "tool.arrow": "Pil", + "tool.ellipse": "Ellipse", + "tool.line": "Linje", + "tool.rectangle": "Rektangel", + "tool.trapezoid": "Trapezoid", + "tool.triangle": "Trekant", + "tool.x-box": "X boks", + "tool.asset": "Aktiv", + "tool.frame": "Ramme", + "tool.note": "Note", + "tool.embed": "Indlejre", + "tool.text": "Tekst", + "menu.title": "Menu", + "menu.copy-as": "Kopier som", + "menu.edit": "Rediger", + "menu.export-as": "Eksporter som", + "menu.file": "Fil", + "menu.language": "Sprog", + "menu.preferences": "Indstillinger", + "menu.view": "Vis", + "context-menu.arrange": "Arranger", + "context-menu.copy-as": "Kopier som", + "context-menu.export-as": "Eksporter som", + "context-menu.move-to-page": "Flyt til side", + "context-menu.reorder": "Omarrangere", + "page-menu.title": "Sider", + "page-menu.create-new-page": "Opret ny side", + "page-menu.edit-pages": "Rediger sider", + "page-menu.max-page-count-reached": "Højst antal sider nået", + "page-menu.new-page-initial-name": "Side 1", + "page-menu.page": "Side", + "page-menu.edit-start": "Rediger", + "page-menu.edit-done": "Færdig", + "page-menu.submenu.rename": "Omdøb", + "page-menu.submenu.duplicate-page": "Dupliker", + "page-menu.submenu.go-to-page": "Go til side", + "page-menu.submenu.title": "Menu", + "page-menu.submenu.move-down": "Flyt ned", + "page-menu.submenu.move-up": "Flyt op", + "page-menu.submenu.delete": "Slet", + "share-menu.title": "Del", + "share-menu.share-project": "Del dette projekt", + "share-menu.create-project": "Nyt delt projekt", + "share-menu.copy-link": "Kopier linket", + "share-menu.readonly-link": "Skrivebeskyttet", + "share-menu.copy-readonly-link": "Kopier skrivebeskyttet link", + "share-menu.offline-note": "Deling af dette projekt vil oprette en hosted live-kopi på opgivet URL. Du can dele URL'en med op til 30 andre mennesker som kan se og ændre projektet sammen.", + "share-menu.copy-link-note": "Alle med linket vil være i stand til at se og rediger projektet.", + "share-menu.copy-readonly-link-note": "Alle med linket vil kunne se (men ikke ændre) dette projekt.", + "share-menu.project-too-large": "Beklager, dette projekt kan ikke deles, fordi det er for stort. Vi arbejder på det.", + "people-menu.title": "Folk", + "people-menu.change-name": "Ændre navn", + "people-menu.change-color": "Skift farve", + "people-menu.user": "(Dig)", + "people-menu.invite": "Inviter andre", + "debug-menu.hard-reset": "Hård nulstilling", + "debug-menu.create-shapes": "Opret 100 figurer", + "help-menu.title": "Hjælp og ressourcer", + "help-menu.about": "Om", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Tastaturgenveje", + "help-menu.twitter": "Twitter", + "links-menu.about": "Om", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Handlinger", + "edit-link-dialog.title": "Rediger link", + "edit-link-dialog.invalid-url": "Et link skal være et gyldigt URL", + "edit-link-dialog.detail": "Links vil åbne i nyefaner.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Fjern", + "edit-link-dialog.save": "Fortsæt", + "edit-link-dialog.cancel": "Annuler", + "embed-dialog.title": "Opret indlejring", + "embed-dialog.url-label": "Indsæt URL", + "embed-dialog.back": "Tilbage", + "embed-dialog.create": "Opret", + "embed-dialog.cancel": "Annuller", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Indsæt webstedets URL for at oprette indlejringen.", + "embed-dialog.invalid-url": "Vi kunne ikke oprette en indlejring fra denne URL.", + "edit-pages-dialog.title": "Rediger sider", + "edit-pages-dialog.create-new-page": "Opret ny side", + "edit-pages-dialog.delete": "Slet", + "edit-pages-dialog.duplicate-page": "Dupliker", + "edit-pages-dialog.go-to-page": "Go til side", + "edit-pages-dialog.max-page-count-reached": "Højst antal sider nået", + "edit-pages-dialog.more-menu": "Menu", + "edit-pages-dialog.move-down": "Flyt ned", + "edit-pages-dialog.move-up": "Ryg op", + "edit-pages-dialog.new-page-initial-name": "Side 1", + "reload-file-dialog.title": "Fortsæt med at redigere", + "reload-file-dialog.description": "Du var lige ved at redigere en fil. Vil du fortsætte med at redigere den?", + "reload-file-dialog.failure": "Filen kunne ikke genindlæses. Prøv igen?", + "reload-file-dialog.reload": "Fortsæt med at redigere", + "reload-file-dialog.revert": "Nej tak", + "shortcuts-dialog.title": "Tastaturgenveje", + "shortcuts-dialog.edit": "Rediger", + "shortcuts-dialog.file": "Fil", + "shortcuts-dialog.preferences": "Indstillinger", + "shortcuts-dialog.tools": "Værktøjer", + "shortcuts-dialog.transform": "Flyt & Skalér", + "shortcuts-dialog.view": "Vis", + "shortcuts-dialog.save": "Fortsæt", + "style-panel.title": "Format", + "style-panel.align": "Juster", + "style-panel.arrowheads": "Pilehoveder", + "style-panel.color": "Farve", + "style-panel.dash": "Streg", + "style-panel.fill": "Fyld", + "style-panel.font": "Skrifttype", + "style-panel.geo": "Figur", + "style-panel.label": "Label", + "style-panel.mixed": "Blandet", + "style-panel.opacity": "Gennemsigtighed", + "style-panel.size": "Størrelse", + "style-panel.spline": "Spline", + "style-panel.text": "Tekst", + "tool-panel.drawing": "Tegning", + "tool-panel.geo": "Figur", + "tool-panel.shapes": "Figurer", + "tool-panel.things": "Ting", + "tool-panel.tools": "Værktøjer", + "save-changes-prompt.title": "Du har ugemte ændringer", + "save-changes-prompt.description": "Ønsker du at gemme ændringer i denne fil", + "save-changes-prompt.go-back": "Gå tilbage", + "save-changes-prompt.continue": "Fortsæt", + "navigation-zone.toggle-minimap": "Skift minikort til", + "navigation-zone.zoom": "Zoom ind", + "focus-mode.toggle-focus-mode": "Skift fokustilstand", + "toast.close": "Luk", + "file-system.file-open-error.title": "Kunne ikke åbne filen", + "file-system.file-open-error.not-a-tldraw-file": "Filen, du forsøgte at åbne, ligner ikke en tldraw-fil.", + "file-system.file-open-error.file-format-version-too-new": "Filen du prøver at åbne er fra en nyere version af tldraw. Vær venlig at genindlæs siden og prøv igen.", + "file-system.file-open-error.generic-corrupted-file": "Filen du forsøger at åbne er beskadiget.", + "file-system.confirm-open.title": "Overskriv nuværende projekt", + "file-system.confirm-open.description": "Åbning af fil vil erstatte dit nuværende projekt, og alt ugemte ændringer vil gå tabt. Er du sikker på, at du vil fortsætte?", + "file-system.confirm-open.cancel": "Annuller", + "file-system.confirm-open.open": "Åben fil", + "file-system.confirm-open.dont-show-again": "Spørg ikke igen", + "toast.error.export-fail.title": "Mislykket eksport", + "toast.error.export-fail.desc": "Billedet kunne ikke eksporteres", + "toast.error.copy-fail.title": "Mislykket kopiere", + "toast.error.copy-fail.desc": "Kunne ikke kopiere billedet", + "file-system.shared-document-file-open-error.title": "Kunne ikke åbne filen", + "file-system.shared-document-file-open-error.description": "Åbning af filer fra delte projekter er ikke understøttet.", + "vscode.file-open.dont-show-again": "Spørg ikke igen", + "vscode.file-open.desc": "Denne fil blev oprettet med en tidligere version af tldraw. Vil du opdatere filen så den virker med den nye version?", + "context.pages.new-page": "Ny side" +} \ No newline at end of file diff --git a/assets/translations/de.json b/assets/translations/de.json new file mode 100644 index 000000000..8cfa33f57 --- /dev/null +++ b/assets/translations/de.json @@ -0,0 +1,329 @@ +{ + "action.convert-to-bookmark": "In Lesezeichen umwandeln", + "action.convert-to-embed": "In Einbettung umwandeln", + "action.open-embed-link": "Link öffnen", + "action.align-bottom": "Unten ausrichten", + "action.align-center-horizontal": "Horizontal ausrichten", + "action.align-center-vertical": "Vertikal ausrichten", + "action.align-center-horizontal.short": "Horizontal ausrichten", + "action.align-center-vertical.short": "Vertikal ausrichten", + "action.align-left": "Links ausrichten", + "action.align-right": "Rechts ausrichten", + "action.align-top": "Oben ausrichten", + "action.back-to-content": "Zurück zum Inhalt", + "action.bring-forward": "Vorziehen", + "action.bring-to-front": "Nach vorne bringen", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Kopieren als JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Als PNG kopieren", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Als SVG kopieren", + "action.copy": "Kopieren", + "action.cut": "Ausschneiden", + "action.delete": "Löschen", + "action.distribute-horizontal": "Horizontal verteilen", + "action.distribute-vertical": "Vertikal verteilen", + "action.distribute-horizontal.short": "Horizontal verteilen", + "action.distribute-vertical.short": "Vertikal verteilen", + "action.duplicate": "Duplizieren", + "action.edit-link": "Link bearbeiten", + "action.exit-pen-mode": "Stiftmodus beenden", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Exportieren als JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Als PNG exportieren", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Exportieren als SVG", + "action.flip-horizontal": "Horizontal flippen", + "action.flip-vertical": "Vertikal flippen", + "action.flip-horizontal.short": "Horizontal flippen", + "action.flip-vertical.short": "Vertikal flippen", + "action.group": "Gruppe", + "action.insert-media": "Medien hochladen", + "action.new-shared-project": "Neues gemeinsames Projekt", + "action.nudge-down": "Nach unten schieben", + "action.nudge-left": "Nach links schieben", + "action.nudge-right": "Nach rechts schieben", + "action.nudge-up": "Nach oben schieben", + "action.open-file": "Datei öffnen", + "action.pack": "Verpacken", + "action.paste": "Einfügen", + "action.print": "Drucken", + "action.redo": "Wiederholen", + "action.rotate-ccw": "Drehen gegen den Uhrzeigersinn", + "action.rotate-cw": "Drehen im Uhrzeigersinn", + "action.save-copy": "Eine Kopie speichern", + "action.select-all": "Alle auswählen", + "action.select-none": "Keine auswählen", + "action.send-backward": "Rückwärts senden", + "action.send-to-back": "Zurücksenden", + "action.share-project": "Dieses Projekt teilen", + "action.stack-horizontal": "Horizontal stapeln", + "action.stack-vertical": "Vertikal stapeln", + "action.stack-horizontal.short": "Horizontal stapeln", + "action.stack-vertical.short": "Vertikal stapeln", + "action.stretch-horizontal": "Horizontal auseinanderziehen", + "action.stretch-vertical": "Vertikal auseinanderziehen", + "action.stretch-horizontal.short": "Horizontal auseinanderziehen", + "action.stretch-vertical.short": "Vertikal auseinanderziehen", + "action.toggle-auto-size": "Automatische Größe umschalten", + "action.toggle-dark-mode.menu": "Dunkelmodus", + "action.toggle-dark-mode": "Dunkelmodus umschalten", + "action.toggle-debug-mode.menu": "Debug-Modus", + "action.toggle-debug-mode": "Debug-Modus umschalten", + "action.toggle-focus-mode.menu": "Fokus-Modus", + "action.toggle-focus-mode": "Fokus-Modus umschalten", + "action.toggle-grid.menu": "Gitter anzeigen", + "action.toggle-grid": "Gitter umschalten", + "action.toggle-snap-mode.menu": "Immer an anderen Elementen ausrichten", + "action.toggle-snap-mode": "Umschalten auf immer einrasten", + "action.toggle-tool-lock.menu": "Tool-Sperre", + "action.toggle-tool-lock": "Tool-Sperre umschalten", + "action.toggle-transparent.context-menu": "Transparent", + "action.toggle-transparent.menu": "Transparent", + "action.toggle-transparent": "Transparenten Hintergrund ein- und ausschalten", + "action.undo": "Rückgängig", + "action.ungroup": "Gruppierung auflösen", + "action.zoom-in": "Vergrößern", + "action.zoom-out": "Verkleinern", + "action.zoom-to-100": "Auf 100 % zoomen", + "action.zoom-to-fit": "Anpassen durch Zoomen", + "action.zoom-to-selection": "Zoom auf Auswahl", + "color-style.black": "Schwarz", + "color-style.blue": "Blau", + "color-style.green": "Grün", + "color-style.grey": "Grau", + "color-style.light-blue": "Hellblau", + "color-style.light-green": "Hellgrün", + "color-style.light-red": "Hellrot", + "color-style.light-violet": "Hellviolett", + "color-style.orange": "Orange", + "color-style.red": "Rot", + "color-style.violet": "Lila", + "color-style.yellow": "Gelb", + "fill-style.none": "Keine", + "fill-style.semi": "Semi", + "fill-style.solid": "Solide", + "fill-style.pattern": "Muster", + "dash-style.dashed": "Gestrichelt", + "dash-style.dotted": "Gepunkted", + "dash-style.draw": "Zeichnen", + "dash-style.solid": "Solide", + "size-style.s": "Klein", + "size-style.m": "Mittel", + "size-style.l": "Groß", + "size-style.xl": "Extra groß", + "opacity-style.0.1": "10 %", + "opacity-style.0.25": "25 %", + "opacity-style.0.5": "50 %", + "opacity-style.0.75": "75 %", + "opacity-style.1": "100 %", + "font-style.draw": "Zeichnen", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Start", + "align-style.middle": "Mitte", + "align-style.end": "Beenden", + "align-style.justify": "Rechtfertigen", + "geo-style.arrow-down": "Pfeil nach unten", + "geo-style.arrow-left": "Pfeil nach links", + "geo-style.arrow-right": "Pfeil nach rechts", + "geo-style.arrow-up": "Pfeil nach oben", + "geo-style.diamond": "Diamant", + "geo-style.ellipse": "Ellipse", + "geo-style.hexagon": "Sechseck", + "geo-style.octagon": "Achteck", + "geo-style.oval": "Oval", + "geo-style.pentagon": "Fünfeck", + "geo-style.rectangle": "Rechteck", + "geo-style.rhombus-2": "Rhombus 2", + "geo-style.rhombus": "Rhombus", + "geo-style.star": "Stern", + "geo-style.trapezoid": "Trapez", + "geo-style.triangle": "Dreieck", + "geo-style.x-box": "X Box", + "arrowheadStart-style.none": "Keine", + "arrowheadStart-style.arrow": "Pfeil", + "arrowheadStart-style.bar": "Leiste", + "arrowheadStart-style.diamond": "Diamant", + "arrowheadStart-style.dot": "Punkt", + "arrowheadStart-style.inverted": "Umgekehrt", + "arrowheadStart-style.pipe": "Rohr", + "arrowheadStart-style.square": "Quadrat", + "arrowheadStart-style.triangle": "Dreieck", + "arrowheadEnd-style.none": "Keine", + "arrowheadEnd-style.arrow": "Pfeil", + "arrowheadEnd-style.bar": "Leiste", + "arrowheadEnd-style.diamond": "Diamant", + "arrowheadEnd-style.dot": "Punkt", + "arrowheadEnd-style.inverted": "Umgekehrt", + "arrowheadEnd-style.pipe": "Rohr", + "arrowheadEnd-style.square": "Quadrat", + "arrowheadEnd-style.triangle": "Dreieck", + "spline-style.line": "Linie", + "spline-style.cubic": "Würfel", + "tool.select": "Auswählen", + "tool.hand": "Hand", + "tool.draw": "Zeichnen", + "tool.eraser": "Radiergummi", + "tool.arrow-down": "Pfeil nach unten", + "tool.arrow-left": "Pfeil nach links", + "tool.arrow-right": "Pfeil nach rechts", + "tool.arrow-up": "Pfeil nach oben", + "tool.arrow": "Pfeil", + "tool.diamond": "Diamant", + "tool.ellipse": "Ellipse", + "tool.hexagon": "Sechseck", + "tool.line": "Linie", + "tool.octagon": "Achteck", + "tool.oval": "Oval", + "tool.pentagon": "Fünfeck", + "tool.rectangle": "Rechteck", + "tool.rhombus": "Rhombus", + "tool.star": "Stern", + "tool.trapezoid": "Trapez", + "tool.triangle": "Dreieck", + "tool.x-box": "X box", + "tool.asset": "Asset", + "tool.frame": "Rahmen", + "tool.note": "Hinweis", + "tool.embed": "Einbetten", + "tool.text": "Text", + "menu.title": "Menü", + "menu.copy-as": "Kopieren als", + "menu.edit": "Bearbeiten", + "menu.export-as": "Exportieren als", + "menu.file": "Datei", + "menu.language": "Sprache", + "menu.preferences": "Vorlieben", + "menu.view": "Anzeigen", + "context-menu.arrange": "Anordnen", + "context-menu.copy-as": "Kopieren als", + "context-menu.export-as": "Exportieren als", + "context-menu.move-to-page": "Weiter zur Seite", + "context-menu.reorder": "Neu anordnen", + "page-menu.title": "Seiten", + "page-menu.create-new-page": "Neue Seite erstellen", + "page-menu.edit-pages": "Seiten bearbeiten", + "page-menu.max-page-count-reached": "Maximale Seitenzahl erreicht", + "page-menu.new-page-initial-name": "Seite 1", + "page-menu.page": "Seite", + "page-menu.edit-start": "Bearbeiten", + "page-menu.edit-done": "Fertig", + "page-menu.submenu.rename": "Umbenennen", + "page-menu.submenu.duplicate-page": "Duplizieren", + "page-menu.submenu.go-to-page": "Gehe zu Seite", + "page-menu.submenu.title": "Menü", + "page-menu.submenu.move-down": "Nach unten", + "page-menu.submenu.move-up": "Nach oben", + "page-menu.submenu.delete": "Löschen", + "share-menu.title": "Teilen", + "share-menu.share-project": "Dieses Projekt teilen", + "share-menu.create-project": "Neues gemeinsames Projekt", + "share-menu.copy-link": "Link kopieren", + "share-menu.readonly-link": "Schreibgeschützt", + "share-menu.copy-readonly-link": "Schreibgeschützten Link kopieren", + "share-menu.offline-note": "Wenn Sie dieses Projekt freigeben, wird eine gehostete Live-Kopie unter einer neuen URL erstellt. Sie können die URL mit bis zu dreißig anderen Personen teilen, um das Projekt gemeinsam anzuzeigen und zu bearbeiten.", + "share-menu.copy-link-note": "Jeder, der den Link hat, kann dieses Projekt ansehen und bearbeiten.", + "share-menu.copy-readonly-link-note": "Jeder, der den Link hat, kann dieses Projekt sehen (aber nicht bearbeiten).", + "share-menu.project-too-large": "Dieses Projekt kann leider nicht geteilt werden, da es zu groß ist. Wir arbeiten daran!", + "people-menu.title": "Leute", + "people-menu.change-name": "Name ändern", + "people-menu.change-color": "Farbe ändern", + "people-menu.user": "(Sie)", + "people-menu.invite": "Andere einladen", + "debug-menu.hard-reset": "Harter Reset", + "debug-menu.create-shapes": "100 Formen erstellen", + "help-menu.title": "Hilfe und Ressourcen", + "help-menu.about": "Über", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Tastaturkürzel", + "help-menu.twitter": "Twitter", + "links-menu.about": "Über", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Aktionen", + "edit-link-dialog.title": "Link bearbeiten", + "edit-link-dialog.invalid-url": "Ein Link muss eine gültige URL sein.", + "edit-link-dialog.detail": "Die Links werden in einer neuen Registerkarte geöffnet.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Löschen", + "edit-link-dialog.save": "Weiter", + "edit-link-dialog.cancel": "Abbrechen", + "embed-dialog.title": "Einbettung erstellen", + "embed-dialog.url-label": "URL einfügen", + "embed-dialog.back": "Zurück", + "embed-dialog.create": "Erstellen", + "embed-dialog.cancel": "Abbrechen", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Fügen Sie die URL der Website ein, um die Einbettung zu erstellen.", + "embed-dialog.invalid-url": "Wir konnten keine Einbettung von dieser URL erstellen.", + "edit-pages-dialog.title": "Seiten bearbeiten", + "edit-pages-dialog.create-new-page": "Neue Seite erstellen", + "edit-pages-dialog.delete": "Löschen", + "edit-pages-dialog.duplicate-page": "Duplizieren", + "edit-pages-dialog.go-to-page": "Gehe zu Seite", + "edit-pages-dialog.max-page-count-reached": "Maximale Seitenzahl erreicht", + "edit-pages-dialog.more-menu": "Menü", + "edit-pages-dialog.move-down": "Nach unten", + "edit-pages-dialog.move-up": "Nach oben", + "edit-pages-dialog.new-page-initial-name": "Seite 1", + "reload-file-dialog.title": "Datei weiter bearbeiten", + "reload-file-dialog.description": "Sie haben gerade eine Datei bearbeitet. Möchten Sie sie weiter bearbeiten?", + "reload-file-dialog.failure": "Die Datei konnte nicht neu geladen werden. Erneut versuchen?", + "reload-file-dialog.reload": "Bearbeitung fortsetzen", + "reload-file-dialog.revert": "Nein, danke", + "shortcuts-dialog.title": "Tastaturkürzel", + "shortcuts-dialog.edit": "Bearbeiten", + "shortcuts-dialog.file": "Datei", + "shortcuts-dialog.preferences": "Vorlieben", + "shortcuts-dialog.tools": "Tools", + "shortcuts-dialog.transform": "Transformieren", + "shortcuts-dialog.view": "Anzeigen", + "shortcuts-dialog.save": "Weiter", + "style-panel.title": "Styles", + "style-panel.align": "Ausrichten", + "style-panel.arrowheads": "Pfeilspitzen", + "style-panel.color": "Farbe", + "style-panel.dash": "Dash", + "style-panel.fill": "Füllen", + "style-panel.font": "Schriftart", + "style-panel.geo": "Form", + "style-panel.label": "Label", + "style-panel.mixed": "Gemischt", + "style-panel.opacity": "Deckkraft", + "style-panel.size": "Größe", + "style-panel.spline": "Spline", + "style-panel.text": "Text", + "tool-panel.drawing": "Zeichnung", + "tool-panel.geo": "Form", + "tool-panel.shapes": "Formen", + "tool-panel.things": "Dinge", + "tool-panel.tools": "Tools", + "save-changes-prompt.title": "Sie haben ungespeicherte Änderungen", + "save-changes-prompt.description": "Möchten Sie die Änderungen an Ihrer aktuellen Datei speichern?", + "save-changes-prompt.go-back": "Zurück", + "save-changes-prompt.continue": "Weiter", + "navigation-zone.toggle-minimap": "Minimap umschalten", + "navigation-zone.zoom": "Zoomen", + "focus-mode.toggle-focus-mode": "Fokusmodus umschalten", + "toast.close": "Schließen", + "file-system.file-open-error.title": "Datei konnte nicht geöffnet werden", + "file-system.file-open-error.not-a-tldraw-file": "Die Datei, die Sie zu öffnen versucht haben, sieht nicht wie eine tldraw-Datei aus.", + "file-system.file-open-error.file-format-version-too-new": "Die Datei, die Sie zu öffnen versucht haben, ist von einer neueren Version von tldraw. Bitte laden Sie die Seite neu und versuchen Sie es erneut.", + "file-system.file-open-error.generic-corrupted-file": "Die Datei, die Sie zu öffnen versucht haben, ist beschädigt.", + "file-system.confirm-open.title": "Aktuelles Projekt überschreiben?", + "file-system.confirm-open.description": "Wenn Sie eine Datei öffnen, wird Ihr aktuelles Projekt ersetzt und alle nicht gespeicherten Änderungen gehen verloren. Sind Sie sicher, dass Sie fortfahren möchten?", + "file-system.confirm-open.cancel": "Abbrechen", + "file-system.confirm-open.open": "Datei öffnen", + "file-system.confirm-open.dont-show-again": "Nicht nochmal fragen", + "toast.error.export-fail.title": "Exportieren fehlgeschlagen", + "toast.error.export-fail.desc": "Bild kann nicht exportiert werden", + "toast.error.copy-fail.title": "Kopieren fehlgeschlagen", + "toast.error.copy-fail.desc": "Bild kann nicht kopiert werden", + "file-system.shared-document-file-open-error.title": "Konnte die Datei nicht öffnen" +} \ No newline at end of file diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1 @@ +{} diff --git a/assets/translations/es.json b/assets/translations/es.json new file mode 100644 index 000000000..60a0b089e --- /dev/null +++ b/assets/translations/es.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "Convertir en marcador", + "action.convert-to-embed": "Convertir en incrustación", + "action.open-embed-link": "Abrir enlace", + "action.align-bottom": "Alinear abajo", + "action.align-center-horizontal": "Alinear horizontalmente", + "action.align-center-vertical": "Alinear verticalmente", + "action.align-center-horizontal.short": "Alinear H", + "action.align-center-vertical.short": "Alinear V", + "action.align-left": "Alinear a la izquierda", + "action.align-right": "Alinear a la derecha", + "action.align-top": "Alinear arriba", + "action.back-to-content": "Volver al contenido", + "action.bring-forward": "Traer hacia adelante", + "action.bring-to-front": "Traer al frente", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Copiar como JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Copiar como PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Copiar como SVG", + "action.copy": "Copiar", + "action.cut": "Cortar", + "action.delete": "Eliminar", + "action.distribute-horizontal": "Distribuir horizontalmente", + "action.distribute-vertical": "Distribuir verticalmente", + "action.distribute-horizontal.short": "Distribuir H", + "action.distribute-vertical.short": "Distribuir V", + "action.duplicate": "Duplicar", + "action.edit-link": "Editar enlace", + "action.exit-pen-mode": "Salir de modo lápiz", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Exportar como JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Exportar como PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Exportar como SVG", + "action.flip-horizontal": "Voltear horizontalmente", + "action.flip-vertical": "Voltear verticalmente", + "action.flip-horizontal.short": "Voltear H", + "action.flip-vertical.short": "Voltear V", + "action.group": "Agrupar", + "action.insert-media": "Cargar contenido multimedia", + "action.new-shared-project": "Nuevo proyecto compartido", + "action.nudge-down": "Empujar hacia abajo", + "action.nudge-left": "Empujar a la izquierda", + "action.nudge-right": "Empujar a la derecha", + "action.nudge-up": "Empujar hacia arriba", + "action.open-file": "Abrir archivo", + "action.pack": "Envasar", + "action.paste": "Pegar", + "action.print": "Imprimir", + "action.redo": "Rehacer", + "action.rotate-ccw": "Girar en el sentido contrario de las agujas del reloj", + "action.rotate-cw": "Girar en el sentido de las agujas del reloj", + "action.save-copy": "Guardar una copia", + "action.select-all": "Seleccionar todo", + "action.select-none": "Seleccionar ninguno", + "action.send-backward": "Enviar hacia atrás", + "action.send-to-back": "Enviar al fondo", + "action.share-project": "Compartir este proyecto", + "action.stack-horizontal": "Apilar horizontalmente", + "action.stack-vertical": "Apilar verticalmente", + "action.stack-horizontal.short": "Apilar H", + "action.stack-vertical.short": "Apilar V", + "action.stretch-horizontal": "Estirar horizontalmente", + "action.stretch-vertical": "Estirar verticalmente", + "action.stretch-horizontal.short": "Estirar H", + "action.stretch-vertical.short": "Estirar V", + "action.toggle-auto-size": "Alternar tamaño automático", + "action.toggle-dark-mode.menu": "Modo oscuro", + "action.toggle-dark-mode": "Alternar modo oscuro", + "action.toggle-debug-mode.menu": "Modo de depuración", + "action.toggle-debug-mode": "Alternar modo de depuración", + "action.toggle-focus-mode.menu": "Modo de enfoque", + "action.toggle-focus-mode": "Alternar modo de enfoque", + "action.toggle-grid.menu": "Mostrar cuadrícula", + "action.toggle-grid": "Alternar cuadrícula", + "action.toggle-snap-mode.menu": "Ajustar siempre", + "action.toggle-snap-mode": "Alternar ajustar siempre", + "action.toggle-tool-lock.menu": "Alternar bloqueo", + "action.toggle-tool-lock": "Alternar bloqueo de herramienta", + "action.toggle-transparent.context-menu": "Transparente", + "action.toggle-transparent.menu": "Transparente", + "action.toggle-transparent": "Alternar fondo transparente", + "action.undo": "Deshacer", + "action.ungroup": "Desagrupar", + "action.zoom-in": "Acercar", + "action.zoom-out": "Alejar", + "action.zoom-to-100": "Zoom a 100 %", + "action.zoom-to-fit": "Zoom para ajustar", + "action.zoom-to-selection": "Zoom para seleccionar", + "color-style.black": "Negro", + "color-style.blue": "Azul", + "color-style.green": "Verde", + "color-style.grey": "Gris", + "color-style.light-blue": "Azul claro", + "color-style.light-green": "Verde claro", + "color-style.light-red": "Rojo claro", + "color-style.light-violet": "Violeta claro", + "color-style.orange": "Naranja", + "color-style.red": "Rojo", + "color-style.violet": "Violeta", + "color-style.yellow": "Amarillo", + "fill-style.none": "Ninguno", + "fill-style.semi": "Semi", + "fill-style.solid": "Sólido", + "fill-style.pattern": "Patrón", + "dash-style.dashed": "Discontinuo", + "dash-style.dotted": "Punteado", + "dash-style.draw": "Dibujar", + "dash-style.solid": "Sólido", + "size-style.s": "Pequeño", + "size-style.m": "Medio", + "size-style.l": "Grande", + "size-style.xl": "Extragrande", + "opacity-style.0.1": "10 %", + "opacity-style.0.25": "25 %", + "opacity-style.0.5": "50 %", + "opacity-style.0.75": "75 %", + "opacity-style.1": "100 %", + "font-style.draw": "Dibujar", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Inicio", + "align-style.middle": "Medio", + "align-style.end": "Final", + "align-style.justify": "Justificar", + "geo-style.arrow-down": "Flecha abajo", + "geo-style.arrow-left": "Flecha izquierda", + "geo-style.arrow-right": "Flecha derecha", + "geo-style.arrow-up": "Flecha arriba", + "geo-style.diamond": "Diamante", + "geo-style.ellipse": "Elipse", + "geo-style.hexagon": "Hexágono", + "geo-style.octagon": "Octógono", + "geo-style.oval": "Óvalo", + "geo-style.pentagon": "Pentágono", + "geo-style.rectangle": "Rectángulo", + "geo-style.rhombus-2": "Rombo 2", + "geo-style.rhombus": "Rombo", + "geo-style.star": "Estrella", + "geo-style.trapezoid": "Trapecio", + "geo-style.triangle": "Triángulo", + "geo-style.x-box": "Rectángulo con una X", + "arrowheadStart-style.none": "Ninguno", + "arrowheadStart-style.arrow": "Flecha", + "arrowheadStart-style.bar": "Barra", + "arrowheadStart-style.diamond": "Diamante", + "arrowheadStart-style.dot": "Punto", + "arrowheadStart-style.inverted": "Invertido", + "arrowheadStart-style.pipe": "Tubo", + "arrowheadStart-style.square": "Cuadrado", + "arrowheadStart-style.triangle": "Triángulo", + "arrowheadEnd-style.none": "Ninguno", + "arrowheadEnd-style.arrow": "Flecha", + "arrowheadEnd-style.bar": "Barra", + "arrowheadEnd-style.diamond": "Diamante", + "arrowheadEnd-style.dot": "Punto", + "arrowheadEnd-style.inverted": "Invertido", + "arrowheadEnd-style.pipe": "Tubo", + "arrowheadEnd-style.square": "Cuadrado", + "arrowheadEnd-style.triangle": "Triángulo", + "spline-style.line": "Línea", + "spline-style.cubic": "Cúbico", + "tool.select": "Seleccionar", + "tool.hand": "Mano", + "tool.draw": "Dibujar", + "tool.eraser": "Borrador", + "tool.arrow-down": "Flecha abajo", + "tool.arrow-left": "Flecha izquierda", + "tool.arrow-right": "Flecha derecha", + "tool.arrow-up": "Flecha arriba", + "tool.arrow": "Flecha", + "tool.diamond": "Diamante", + "tool.ellipse": "Elipse", + "tool.hexagon": "Hexágono", + "tool.line": "Línea", + "tool.octagon": "Octógono", + "tool.oval": "Óvalo", + "tool.pentagon": "Pentágono", + "tool.rectangle": "Rectángulo", + "tool.rhombus": "Rombo", + "tool.star": "Estrella", + "tool.trapezoid": "Trapecio", + "tool.triangle": "Triángulo", + "tool.x-box": "Rectángulo con una X", + "tool.asset": "Activo", + "tool.frame": "Marco", + "tool.note": "Nota", + "tool.embed": "Incrustación", + "tool.text": "Texto", + "menu.title": "Menú", + "menu.copy-as": "Copiar como", + "menu.edit": "Editar", + "menu.export-as": "Exportar como", + "menu.file": "Archivo", + "menu.language": "Idioma", + "menu.preferences": "Preferencias", + "menu.view": "Vista", + "context-menu.arrange": "Organizar", + "context-menu.copy-as": "Copiar como", + "context-menu.export-as": "Exportar como", + "context-menu.move-to-page": "Mover a página", + "context-menu.reorder": "Reordenar", + "page-menu.title": "Páginas", + "page-menu.create-new-page": "Crear nueva página", + "page-menu.edit-pages": "Editar páginas", + "page-menu.max-page-count-reached": "Máx. páginas alcanzado", + "page-menu.new-page-initial-name": "Página 1", + "page-menu.page": "Página", + "page-menu.edit-start": "Editar", + "page-menu.edit-done": "Listo", + "page-menu.submenu.rename": "Renombrar", + "page-menu.submenu.duplicate-page": "Duplicar", + "page-menu.submenu.go-to-page": "Ir a página", + "page-menu.submenu.title": "Menú", + "page-menu.submenu.move-down": "Mover hacia abajo", + "page-menu.submenu.move-up": "Mover hacia arriba", + "page-menu.submenu.delete": "Eliminar", + "share-menu.title": "Compartir", + "share-menu.share-project": "Compartir este proyecto", + "share-menu.create-project": "Nuevo proyecto compartido", + "share-menu.copy-link": "Copiar enlace", + "share-menu.readonly-link": "Solo lectura", + "share-menu.copy-readonly-link": "Copiar el enlace de solo lectura", + "share-menu.offline-note": "Al compartir este proyecto se creará una copia en vivo alojada en una nueva URL. Puede compartir la URL hasta con treinta personas para ver y editar el proyecto conjuntamente.", + "share-menu.copy-link-note": "Cualquier persona con el enlace podrá ver y editar este proyecto.", + "share-menu.copy-readonly-link-note": "Cualquier persona con el enlace podrá ver (pero no editar) este proyecto.", + "share-menu.project-too-large": "Lo sentimos, este proyecto no se puede compartir porque es demasiado grande. ¡Estamos trabajando en ello!", + "people-menu.title": "Personas", + "people-menu.change-name": "Cambiar nombre", + "people-menu.change-color": "Cambiar color", + "people-menu.user": "(Usted)", + "people-menu.invite": "Invitar a otros", + "debug-menu.hard-reset": "Reinicio completo", + "debug-menu.create-shapes": "Crear 100 formas", + "help-menu.title": "Ayuda y recursos", + "help-menu.about": "Acerca de", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Atajos de teclado", + "help-menu.twitter": "Twitter", + "links-menu.about": "Acerca de", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Acciones", + "edit-link-dialog.title": "Editar enlace", + "edit-link-dialog.invalid-url": "Un enlace debe ser una URL válida.", + "edit-link-dialog.detail": "Se abrirán enlaces en una nueva pestaña.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Borrar", + "edit-link-dialog.save": "Continuar", + "edit-link-dialog.cancel": "Cancelar", + "embed-dialog.title": "Crear incrustación", + "embed-dialog.url-label": "Pegar URL", + "embed-dialog.back": "Atrás", + "embed-dialog.create": "Crear", + "embed-dialog.cancel": "Cancelar", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Pegar en la URL del sitio para crear la incrustación.", + "embed-dialog.invalid-url": "No pudimos crear una incrustación desde esa URL.", + "edit-pages-dialog.title": "Editar páginas", + "edit-pages-dialog.create-new-page": "Crear nueva página", + "edit-pages-dialog.delete": "Eliminar", + "edit-pages-dialog.duplicate-page": "Duplicar", + "edit-pages-dialog.go-to-page": "Ir a página", + "edit-pages-dialog.max-page-count-reached": "Máx. páginas alcanzado", + "edit-pages-dialog.more-menu": "Menú", + "edit-pages-dialog.move-down": "Mover hacia abajo", + "edit-pages-dialog.move-up": "Mover hacia arriba", + "edit-pages-dialog.new-page-initial-name": "Página 1", + "reload-file-dialog.title": "Seguir editando archivo", + "reload-file-dialog.description": "Usted estaba editando un archivo. ¿Quiere seguir editándolo?", + "reload-file-dialog.failure": "No se pudo volver a cargar el archivo. ¿Intentar de nuevo?", + "reload-file-dialog.reload": "Seguir editando", + "reload-file-dialog.revert": "No, gracias", + "shortcuts-dialog.title": "Atajos de teclado", + "shortcuts-dialog.edit": "Editar", + "shortcuts-dialog.file": "Archivo", + "shortcuts-dialog.preferences": "Preferencias", + "shortcuts-dialog.tools": "Herramientas", + "shortcuts-dialog.transform": "Transformar", + "shortcuts-dialog.view": "Vista", + "shortcuts-dialog.save": "Continuar", + "style-panel.title": "Estilos", + "style-panel.align": "Alinear", + "style-panel.arrowheads": "Puntas de flecha", + "style-panel.color": "Color", + "style-panel.dash": "Raya", + "style-panel.fill": "Relleno", + "style-panel.font": "Fuente", + "style-panel.geo": "Forma", + "style-panel.label": "Etiqueta", + "style-panel.mixed": "Mixto", + "style-panel.opacity": "Opacidad", + "style-panel.size": "Tamaño", + "style-panel.spline": "Spline", + "style-panel.text": "Texto", + "tool-panel.drawing": "Dibujo", + "tool-panel.geo": "Forma", + "tool-panel.shapes": "Formas", + "tool-panel.things": "Cosas", + "tool-panel.tools": "Herramientas", + "save-changes-prompt.title": "Tiene cambios sin guardar", + "save-changes-prompt.description": "¿Le gustaría guardar cambios en su archivo actual?", + "save-changes-prompt.go-back": "Volver", + "save-changes-prompt.continue": "Continuar", + "navigation-zone.toggle-minimap": "Alternar minimapa", + "navigation-zone.zoom": "Zoom", + "focus-mode.toggle-focus-mode": "Alternar modo de enfoque", + "toast.close": "Cerrar", + "file-system.file-open-error.title": "No se pudo abrir el archivo", + "file-system.file-open-error.not-a-tldraw-file": "El archivo que intentó abrir no parece un archivo tldraw.", + "file-system.file-open-error.file-format-version-too-new": "El archivo que intentó abrir es de una versión más reciente de tldraw. Vuelva a cargar la página e inténtelo de nuevo.", + "file-system.file-open-error.generic-corrupted-file": "El archivo que intentó abrir está corrupto.", + "file-system.confirm-open.title": "¿Sobrescribir proyecto actual?", + "file-system.confirm-open.description": "Al abrir un archivo se sustituirá su proyecto actual y cualquier cambio no guardado se perderá. ¿Seguro que desea continuar?", + "file-system.confirm-open.cancel": "Cancelar", + "file-system.confirm-open.open": "Abrir archivo", + "file-system.confirm-open.dont-show-again": "No preguntar de nuevo", + "toast.error.export-fail.title": "Exportación fallida", + "toast.error.export-fail.desc": "No se pudo exportar la imagen", + "toast.error.copy-fail.title": "Copia fallida", + "toast.error.copy-fail.desc": "No se pudo copiar la imagen", + "file-system.shared-document-file-open-error.title": "No se pudo abrir el archivo", + "file-system.shared-document-file-open-error.description": "No es posible abrir archivos de proyectos compartidos.", + "vscode.file-open.dont-show-again": "No volver a preguntar", + "vscode.file-open.desc": "Este archivo se creó con una versión anterior de tldraw. ¿Desea actualizarlo para que funcione con la nueva versión?", + "context.pages.new-page": "Nueva página" +} \ No newline at end of file diff --git a/assets/translations/fa.json b/assets/translations/fa.json new file mode 100644 index 000000000..cd445c179 --- /dev/null +++ b/assets/translations/fa.json @@ -0,0 +1,137 @@ +{ + "action.align-bottom": "تراز به پایین", + "action.align-center-horizontal": "تراز به مرکز افقی", + "action.align-center-vertical": "تراز به مرکز عمودی", + "action.align-center-horizontal.short": "تراز به مرکز افقی", + "action.align-center-vertical.short": "تراز به مرکز عمودی", + "action.align-left": "تراز به چپ", + "action.align-right": "تراز به راست", + "action.align-top": "تراز به بالا", + "action.bring-forward": "انتقال به سمت جلو", + "action.bring-to-front": "انتقال به جلو", + "action.copy": "کپی", + "action.cut": "بریدن", + "action.delete": " پاک‌کردن", + "action.distribute-horizontal": "پخش‌کردن افقی", + "action.distribute-vertical": "پخش‌کردن عمودی", + "action.distribute-horizontal.short": "پخش‌کردن افقی", + "action.distribute-vertical.short": "پخش‌کردن عمودی", + "action.duplicate": "کپی درجا", + "action.flip-horizontal": "برگردون افقی", + "action.flip-vertical": "برگردون عمودی", + "action.flip-horizontal.short": "وارونه‌سازی افقی", + "action.flip-vertical.short": "وارونه‌سازی عمودی", + "action.group": "جمع کن", + "action.insert-media": "آپلود عکس", + "action.paste": "جای‌گذاری", + "action.redo": "یه قدم جلو", + "action.select-all": "انتخاب همه", + "action.select-none": "انتخاب هیچ", + "action.send-backward": "انتقال به سمت عقب", + "action.send-to-back": "انتقال به عقب", + "action.stretch-horizontal": "کش‌آوردن افقی", + "action.stretch-vertical": "کش‌آوردن عمودی", + "action.stretch-horizontal.short": "کش‌آوردن افقی", + "action.stretch-vertical.short": "کش‌آوردن عمودی", + "action.toggle-dark-mode.menu": "حالت تاریک", + "action.toggle-dark-mode": "حالت تاریک", + "action.toggle-debug-mode.menu": "حالت عیب‌یابی", + "action.toggle-debug-mode": "حالت عیب‌یابی", + "action.toggle-focus-mode.menu": "حالت تمرکز", + "action.toggle-focus-mode": "حالت تمرکز", + "action.toggle-grid.menu": "نمایش خطوط راهنما", + "action.toggle-grid": "نمایش خطوط راهنما", + "action.toggle-snap-mode.menu": "همیشه نقاط چسبان را نشان بده", + "action.toggle-snap-mode": "همیشه نقاط چسبان را نشان بده", + "action.toggle-transparent.context-menu": "شفاف", + "action.toggle-transparent.menu": "شفاف", + "action.undo": "یه قدم عقب", + "action.ungroup": "جدا کن", + "action.zoom-in": "زوم جلو", + "action.zoom-out": "زوم عقب", + "action.zoom-to-fit": "نمایش کل صفحه", + "action.zoom-to-selection": "نمایش انتخاب‌شده‌ها", + "color-style.black": "مشکی", + "color-style.blue": "آبی", + "color-style.green": "سبز", + "color-style.grey": "خاکستری", + "color-style.orange": "نارنجی", + "color-style.red": "قرمز", + "color-style.violet": "بنفش", + "color-style.yellow": "زرد", + "fill-style.solid": "توپُر", + "dash-style.dashed": "خط‌چین", + "dash-style.dotted": "نقطه‌چین", + "dash-style.draw": "رسم", + "dash-style.solid": "توپُر", + "size-style.s": "کوچک", + "size-style.m": "متوسط", + "size-style.l": "بزرگ", + "font-style.draw": "رسم", + "geo-style.ellipse": "گردی", + "geo-style.rectangle": "چارگوش", + "geo-style.triangle": "سه‌گوش", + "arrowheadStart-style.arrow": "فلِش", + "arrowheadStart-style.triangle": "سه‌گوش", + "arrowheadEnd-style.arrow": "فلِش", + "arrowheadEnd-style.triangle": "سه‌گوش", + "spline-style.line": "خط", + "tool.select": "انتخاب", + "tool.draw": "رسم", + "tool.eraser": "پاک‌کن", + "tool.arrow": "فلِش", + "tool.ellipse": "گردی", + "tool.line": "خط", + "tool.rectangle": "چارگوش", + "tool.triangle": "سه‌گوش", + "tool.note": "یادداشت", + "tool.text": "متن", + "menu.copy-as": "کپی به‌‌عنوان", + "menu.edit": "ویرایش", + "menu.export-as": "خروجی با فرمت", + "menu.file": "فایل", + "menu.language": "زبان", + "menu.preferences": "تنظیم‌ها", + "menu.view": "نمایش", + "context-menu.copy-as": "کپی به‌‌عنوان", + "context-menu.export-as": "خروجی با فرمت", + "context-menu.move-to-page": "ببر به صفحه", + "page-menu.create-new-page": "ایجاد برگه", + "page-menu.page": "برگه", + "page-menu.edit-start": "ویرایش", + "page-menu.submenu.duplicate-page": "تکثیرکردن", + "page-menu.submenu.delete": " پاک‌کردن", + "share-menu.copy-link": "کپی‌کردن پیوند دعوت", + "share-menu.copy-readonly-link": "کپی‌کردن پیوند غیرقابل ویرایش", + "help-menu.discord": "دیسکورد", + "help-menu.github": "گیت‌هاب", + "help-menu.keyboard-shortcuts": "میان‌برهای صفحه‌کلید", + "help-menu.twitter": "توییتر", + "links-menu.discord": "دیسکورد", + "links-menu.github": "گیت‌هاب", + "links-menu.twitter": "توییتر", + "edit-link-dialog.cancel": "لغو", + "embed-dialog.cancel": "لغو", + "edit-pages-dialog.create-new-page": "ایجاد برگه", + "edit-pages-dialog.delete": "حذف", + "edit-pages-dialog.duplicate-page": "تکثیرکردن", + "shortcuts-dialog.title": "میان‌برهای صفحه‌کلید", + "shortcuts-dialog.edit": "ویرایش", + "shortcuts-dialog.file": "فایل", + "shortcuts-dialog.preferences": "تنظیم‌ها", + "shortcuts-dialog.tools": "ابزارها", + "shortcuts-dialog.transform": "تغییر شکل", + "shortcuts-dialog.view": "نمایش", + "style-panel.title": "استایل‌ها", + "style-panel.align": "تراز", + "style-panel.color": "رنگ", + "style-panel.dash": "خط‌ چین", + "style-panel.fill": "توپُر", + "style-panel.font": "فونت", + "style-panel.size": "اندازه", + "style-panel.text": "متن", + "tool-panel.shapes": "شکل‌ها", + "tool-panel.tools": "ابزارها", + "focus-mode.toggle-focus-mode": "حالت تمرکز", + "file-system.confirm-open.cancel": "لغو" +} diff --git a/assets/translations/fi.json b/assets/translations/fi.json new file mode 100644 index 000000000..1962a06b8 --- /dev/null +++ b/assets/translations/fi.json @@ -0,0 +1,350 @@ +{ + "action.convert-to-bookmark": "Muunna kirjanmerkiksi", + "action.convert-to-embed": "Muunna upotettavaksi", + "action.open-embed-link": "Avaa linkki", + "action.align-bottom": "Tasa alareuna", + "action.align-center-horizontal": "Tasaa pystysuunnassa", + "action.align-center-vertical": "Tasaa vaakasuunnassa", + "action.align-center-horizontal.short": "Tasaa P", + "action.align-center-vertical.short": "Tasaa V", + "action.align-left": "Tasaa vasen reuna", + "action.align-right": "Tasaa oikea reuna", + "action.align-top": "Tasaa yläreuna", + "action.back-to-content": "Takaisin sisältöön", + "action.bring-forward": "Tuo eteenpäin", + "action.bring-to-front": "Tuo eteen", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Kopioi JSON-muodossa", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Kopioi PNG-muodossa", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Kopioi SVG-muodossa", + "action.copy": "Kopioi", + "action.cut": "Leikkaa", + "action.delete": "Poista", + "action.distribute-horizontal": "Jaa pystysuunnassa", + "action.distribute-vertical": "Jaa vaakasuunnassa", + "action.distribute-horizontal.short": "Jaa P", + "action.distribute-vertical.short": "Jaa V", + "action.duplicate": "Luo kopio", + "action.edit-link": "Muokkaa linkkiä", + "action.exit-pen-mode": "Poistu kynätilasta", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Vie JSON-muodossa", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Vie PNG-muodossa", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Vie SVG-muodossa", + "action.flip-horizontal": "Käännä pystysuunnassa", + "action.flip-vertical": "Käännä vaakasuunnassa", + "action.flip-horizontal.short": "Käännä P", + "action.flip-vertical.short": "Käännä V", + "action.group": "Ryhmä", + "action.insert-media": "Lataa media", + "action.new-shared-project": "Uusi jaettu projekti", + "action.nudge-down": "Tönäise alaspäin", + "action.nudge-left": "Tönäise vasemmalle", + "action.nudge-right": "Tönäise oikealle", + "action.nudge-up": "Tönäise ylöspäin", + "action.open-file": "Avaa tiedosto", + "action.pack": "Pakkaa", + "action.paste": "Liitä", + "action.print": "Tulosta", + "action.redo": "Tee uudelleen", + "action.rotate-ccw": "Pyöritä vastapäivään", + "action.rotate-cw": "Pyöritä myötäpäivään", + "action.save-copy": "Tallenna kopio", + "action.select-all": "Valitse kaikki", + "action.select-none": "Poista valinta", + "action.send-backward": "Eteenpäin", + "action.send-to-back": "Taaksepäin", + "action.share-project": "Jaa tämä projekti", + "action.stack-horizontal": "Pinoa pystysuunnassa", + "action.stack-vertical": "Pinoa vaakasuunnassa", + "action.stack-horizontal.short": "Pinoa P", + "action.stack-vertical.short": "Pinoa V", + "action.stretch-horizontal": "Venytä pystysuunnassa", + "action.stretch-vertical": "Venytä vaakasuunnassa", + "action.stretch-horizontal.short": "Venytä P", + "action.stretch-vertical.short": "Venytä V", + "action.toggle-auto-size": "Automaattinen koko päälle/pois", + "action.toggle-dark-mode.menu": "Tumma tila", + "action.toggle-dark-mode": "Tumma tila päälle/pois", + "action.toggle-debug-mode.menu": "Virheenkorjaustila", + "action.toggle-debug-mode": "Virheenkorjaustila päälle/pois", + "action.toggle-focus-mode.menu": "Keskittymistila", + "action.toggle-focus-mode": "Keskittymistila päälle/pois", + "action.toggle-grid.menu": "Näytä ruudukko", + "action.toggle-grid": "Ruudukko päälle/pois", + "action.toggle-snap-mode.menu": "Kohdista aina", + "action.toggle-snap-mode": "Kohdista aina päälle/pois", + "action.toggle-tool-lock.menu": "Työkalun lukitus", + "action.toggle-tool-lock": "Työkalun lukitus päälle/pois", + "action.toggle-transparent.context-menu": "Läpinäkyvä", + "action.toggle-transparent.menu": "Läpinäkyvä", + "action.toggle-transparent": "Läpinäkyvä tausta päälle/pois", + "action.undo": "Peru", + "action.ungroup": "Pura ryhmittely", + "action.zoom-in": "Lähennä", + "action.zoom-out": "Loitonna", + "action.zoom-to-100": "Zoomaa 100 %:iin", + "action.zoom-to-fit": "Zoomaa sopivaksi", + "action.zoom-to-selection": "Zoomaa valintaan", + "color-style.black": "Musta", + "color-style.blue": "Sininen", + "color-style.green": "Vihreä", + "color-style.grey": "Harmaa", + "color-style.light-blue": "Vaaleansininen", + "color-style.light-green": "Vaaleanvihreä", + "color-style.light-red": "Vaalea punainen", + "color-style.light-violet": "Vaalea violetti", + "color-style.orange": "Oranssi", + "color-style.red": "Punainen", + "color-style.violet": "Violetti", + "color-style.yellow": "Keltainen", + "fill-style.none": "Ei täyttöä", + "fill-style.semi": "Osittainen", + "fill-style.solid": "Kiinteä", + "fill-style.pattern": "Kuvio", + "dash-style.dashed": "Katkotettu", + "dash-style.dotted": "Pisteellinen", + "dash-style.draw": "Kynä", + "dash-style.solid": "Kiinteä", + "size-style.s": "Pieni", + "size-style.m": "Keskikokoinen", + "size-style.l": "Suuri", + "size-style.xl": "Erittäin suuri", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Kynä", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Alku", + "align-style.middle": "Keskelle", + "align-style.end": "Loppu", + "align-style.justify": "Tasaa", + "geo-style.arrow-down": "Alanuoli", + "geo-style.arrow-left": "Vasen nuoli", + "geo-style.arrow-right": "Oikea nuoli", + "geo-style.arrow-up": "Ylänuoli", + "geo-style.diamond": "Timantti", + "geo-style.ellipse": "Soikio", + "geo-style.hexagon": "Kuusikulmio", + "geo-style.octagon": "Kahdeksankulmio", + "geo-style.oval": "Soikio", + "geo-style.pentagon": "Viisikulmio", + "geo-style.rectangle": "Suorakulmio", + "geo-style.rhombus-2": "Neljäkäs 2", + "geo-style.rhombus": "Neljäkäs", + "geo-style.star": "Tähti", + "geo-style.trapezoid": "Puolisuunnikas", + "geo-style.triangle": "Kolmio", + "geo-style.x-box": "X laatikko", + "arrowheadStart-style.none": "Ei nuolenpäätä", + "arrowheadStart-style.arrow": "Nuoli", + "arrowheadStart-style.bar": "Viiva", + "arrowheadStart-style.diamond": "Timantti", + "arrowheadStart-style.dot": "Piste", + "arrowheadStart-style.inverted": "Käänteinen", + "arrowheadStart-style.pipe": "Putki", + "arrowheadStart-style.square": "Neliö", + "arrowheadStart-style.triangle": "Kolmio", + "arrowheadEnd-style.none": "Ei nuolenpäätä", + "arrowheadEnd-style.arrow": "Nuoli", + "arrowheadEnd-style.bar": "Viiva", + "arrowheadEnd-style.diamond": "Timantti", + "arrowheadEnd-style.dot": "Piste", + "arrowheadEnd-style.inverted": "Käänteinen", + "arrowheadEnd-style.pipe": "Putki", + "arrowheadEnd-style.square": "Neliö", + "arrowheadEnd-style.triangle": "Kolmio", + "spline-style.line": "Viiva", + "spline-style.cubic": "Kuutio", + "tool.select": "Valitse", + "tool.hand": "Siirrä", + "tool.draw": "Kynä", + "tool.eraser": "Pyyhin", + "tool.arrow-down": "Alanuoli", + "tool.arrow-left": "Vasen nuoli", + "tool.arrow-right": "Oikea nuoli", + "tool.arrow-up": "Ylänuoli", + "tool.arrow": "Nuoli", + "tool.diamond": "Timantti", + "tool.ellipse": "Soikio", + "tool.hexagon": "Kuusikulmio", + "tool.line": "Viiva", + "tool.octagon": "Kahdeksankulmio", + "tool.oval": "Soikio", + "tool.pentagon": "Viisikulmio", + "tool.rectangle": "Suorakulmio", + "tool.rhombus": "Nelikulmio", + "tool.star": "Tähti", + "tool.trapezoid": "Puolisuunnikas", + "tool.triangle": "Kolmio", + "tool.x-box": "X laatikko", + "tool.asset": "Liite", + "tool.frame": "Kehys", + "tool.note": "Muistilappu", + "tool.embed": "Upota", + "tool.text": "Teksti", + "menu.title": "Valikko", + "menu.copy-as": "Kopioi muodossa", + "menu.edit": "Muokkaa", + "menu.export-as": "Vie muodossa", + "menu.file": "Tiedosto", + "menu.language": "Kieli", + "menu.preferences": "Asetukset", + "menu.view": "Näytä", + "context-menu.arrange": "Järjestä", + "context-menu.copy-as": "Kopioi muodossa", + "context-menu.export-as": "Vie muodossa", + "context-menu.move-to-page": "Siirrä sivulle", + "context-menu.reorder": "Uudelleenjärjestä", + "page-menu.title": "Sivut", + "page-menu.create-new-page": "Luo uusi sivu", + "page-menu.edit-pages": "Muokkaa sivuja", + "page-menu.max-page-count-reached": "Maksimi sivumäärä saavutettu", + "page-menu.new-page-initial-name": "Sivu 1", + "page-menu.page": "Sivu", + "page-menu.edit-start": "Muokkaa", + "page-menu.edit-done": "Valmis", + "page-menu.submenu.rename": "Nimeä uudelleen", + "page-menu.submenu.duplicate-page": "Luo kopio", + "page-menu.submenu.go-to-page": "Siirry sivulle", + "page-menu.submenu.title": "Valikko", + "page-menu.submenu.move-down": "Siirrä alaspäin", + "page-menu.submenu.move-up": "Siirrä ylöspäin", + "page-menu.submenu.delete": "Poista", + "share-menu.title": "Jaa", + "share-menu.share-project": "Jaa tämä projekti", + "share-menu.create-project": "Uusi jaettu projekti", + "share-menu.copy-link": "Kopioi linkki", + "share-menu.readonly-link": "Vain luku", + "share-menu.copy-readonly-link": "Kopioi vain lukuoikeuslinkki", + "share-menu.offline-note": "Tämän projektin jakaminen luo live-kopion uuteen URL-osoitteeseen. Voit jakaa osoitteen enintään kolmenkymmenen muun henkilön kanssa, jotta he voivat tarkastella ja muokata projektia yhdessä.", + "share-menu.copy-link-note": "Kuka tahansa, jolla on linkki, voi tarkastella ja muokata tätä projektia.", + "share-menu.copy-readonly-link-note": "Kuka tahansa, jolla on linkki, voi tarkastella (mutta ei muokata) tätä projektia.", + "share-menu.project-too-large": "Valitettavasti tätä projektia ei voi jakaa, koska se on liian suuri. Me työstämme sitä!", + "people-menu.title": "Ihmiset", + "people-menu.change-name": "Muuta nimi", + "people-menu.change-color": "Muuta väri", + "people-menu.user": "(Sinä)", + "people-menu.invite": "Lähetä kutsu", + "debug-menu.hard-reset": "Aloita kokonaan alusta", + "debug-menu.create-shapes": "Luo 100 muotoa", + "help-menu.title": "Ohje ja resurssit", + "help-menu.about": "Tietoja", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Pikanäppäimet", + "help-menu.twitter": "Twitter", + "links-menu.about": "Tietoja", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Toiminnot", + "edit-link-dialog.title": "Linkin muokkaus", + "edit-link-dialog.invalid-url": "Linkin on oltava kelvollinen URL-osoite.", + "edit-link-dialog.detail": "Linkit avautuvat uuteen välilehteen.", + "edit-link-dialog.url": "Osoite", + "edit-link-dialog.clear": "Tyhjennä", + "edit-link-dialog.save": "Jatka", + "edit-link-dialog.cancel": "Peru", + "embed-dialog.title": "Luo upotettu verkko-osa", + "embed-dialog.url-label": "Liitä osoite", + "embed-dialog.back": "Takaisin", + "embed-dialog.create": "Luo", + "embed-dialog.cancel": "Peru", + "embed-dialog.url": "Osoite", + "embed-dialog.instruction": "Luo upotus liittämällä sivuston URL-osoite.", + "embed-dialog.invalid-url": "Upotettua verkko-osaa ei voitu luoda kyseisestä URL-osoitteesta.", + "edit-pages-dialog.title": "Sivujen muokkaus", + "edit-pages-dialog.create-new-page": "Luo uusi sivu", + "edit-pages-dialog.delete": "Poista", + "edit-pages-dialog.duplicate-page": "Luo kopio", + "edit-pages-dialog.go-to-page": "Siirry sivulle", + "edit-pages-dialog.max-page-count-reached": "Maksimi sivumäärä saavutettu", + "edit-pages-dialog.more-menu": "Valikko", + "edit-pages-dialog.move-down": "Siirrä alaspäin", + "edit-pages-dialog.move-up": "Siirrä ylöspäin", + "edit-pages-dialog.new-page-initial-name": "Sivu 1", + "reload-file-dialog.title": "Jatka tiedoston muokkaamista", + "reload-file-dialog.description": "Olit juuri muokkaamassa tiedostoa. Haluatko jatkaa sen muokkaamista?", + "reload-file-dialog.failure": "Tiedoston uudelleenlataus epäonnistui. Yritä uudelleen?", + "reload-file-dialog.reload": "Jatka muokkaamista", + "reload-file-dialog.revert": "Ei kiitos", + "shortcuts-dialog.title": "Pikanäppäimet", + "shortcuts-dialog.edit": "Muokkaa", + "shortcuts-dialog.file": "Tiedosto", + "shortcuts-dialog.preferences": "Asetukset", + "shortcuts-dialog.tools": "Työkalut", + "shortcuts-dialog.transform": "Muunna", + "shortcuts-dialog.view": "Näytä", + "shortcuts-dialog.save": "Jatka", + "style-panel.title": "Tyylit", + "style-panel.align": "Tasaa", + "style-panel.arrowheads": "Nuolenpäät", + "style-panel.color": "Väri", + "style-panel.dash": "Ääriviiva", + "style-panel.fill": "Täyttö", + "style-panel.font": "Fonttiperhe", + "style-panel.geo": "Muoto", + "style-panel.label": "Etiketti", + "style-panel.mixed": "Sekalainen", + "style-panel.opacity": "Peittävyys", + "style-panel.size": "Koko", + "style-panel.spline": "Käyrä", + "style-panel.text": "Teksti", + "tool-panel.drawing": "Kynä", + "tool-panel.geo": "Muoto", + "tool-panel.shapes": "Muodot", + "tool-panel.things": "Tarvikkeet", + "tool-panel.tools": "Työkalut", + "save-changes-prompt.title": "Sinulla on tallentamattomia muutoksia", + "save-changes-prompt.description": "Haluatko tallentaa muutokset nykyiseen tiedostoon?", + "save-changes-prompt.go-back": "Palaa takaisin", + "save-changes-prompt.continue": "Jatka", + "navigation-zone.toggle-minimap": "Vaihda minikartta päälle/pois", + "navigation-zone.zoom": "Zoomaa", + "focus-mode.toggle-focus-mode": "Keskittymistila päälle/pois", + "toast.close": "Sulje", + "file-system.file-open-error.title": "Tiedostoa ei voitu avata", + "file-system.file-open-error.not-a-tldraw-file": "Tiedosto, jonka yritit avata, ei näytä tldraw-tiedostolta.", + "file-system.file-open-error.file-format-version-too-new": "Tiedosto, jonka yritit avata, on uudemmasta tldraw versiosta. Ole hyvä ja yritä uudelleen sivun uudelleenlatauksen jälkeen.", + "file-system.file-open-error.generic-corrupted-file": "Tiedosto, jonka yritit avata, on vioittunut.", + "file-system.confirm-open.title": "Korvataanko nykyinen projekti?", + "file-system.confirm-open.description": "Tiedoston avaaminen korvaa nykyisen projektin ja tallentamattomat muutokset menetetään. Oletko varma, että haluat jatkaa?", + "file-system.confirm-open.cancel": "Peru", + "file-system.confirm-open.open": "Avaa tiedosto", + "file-system.confirm-open.dont-show-again": "Älä kysy uudelleen", + "toast.error.export-fail.title": "Vienti epäonnistui", + "toast.error.export-fail.desc": "Kuvan vienti epäonnistui", + "toast.error.copy-fail.title": "Kopiointi epäonnistui", + "toast.error.copy-fail.desc": "Kuvan kopiointi epäonnistui", + "file-system.shared-document-file-open-error.title": "Tiedostoa ei voitu avata", + "file-system.shared-document-file-open-error.description": "Tiedostojen avaaminen jaetuista projekteista ei ole tuettu.", + "vscode.file-open.dont-show-again": "Älä kysy uudelleen", + "vscode.file-open.desc": "Tämä tiedosto on luoto aiemmalla tldraw versiolla. Haluatko päivittää sen toimimaan uuden version kanssa?", + "context.pages.new-page": "Uusi sivu", + "style-panel.arrowhead-start": "Alku", + "style-panel.arrowhead-end": "Pääty", + "vscode.file-open.open": "Jatka", + "vscode.file-open.backup": "Varmuuskopio", + "vscode.file-open.backup-saved": "Varmuuskopio tallennettu", + "vscode.file-open.backup-failed": "Varmuuskopiointi epäonnistui: tämä ei ole .tldr-tiedosto.", + "tool-panel.more": "Lisää", + "debug-panel.more": "Lisää", + "action.new-project": "Uusi projekti", + "file-system.confirm-clear.title": "Tyhjennetäänkö nykyinen projekti?", + "file-system.confirm-clear.description": "Uuden projektin luominen tyhjentää nykyisen projektin ja kaikki tallentamattomat muutokset menetetään. Oletko varma, että haluat jatkaa?", + "file-system.confirm-clear.cancel": "Peru", + "file-system.confirm-clear.continue": "Jatka", + "file-system.confirm-clear.dont-show-again": "Älä kysy uudelleen", + "action.stop-following": "Lopeta seuraaminen", + "people-menu.follow": "Seuraa", + "style-panel.position": "Sijainti" +} \ No newline at end of file diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 000000000..a252d1540 --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "Convertir en signet", + "action.convert-to-embed": "Convertir en intégration", + "action.open-embed-link": "Ouvrir le lien", + "action.align-bottom": "Aligner en bas", + "action.align-center-horizontal": "Aligner horizontalement", + "action.align-center-vertical": "Aligner verticalement", + "action.align-center-horizontal.short": "Aligner H", + "action.align-center-vertical.short": "Aligner V", + "action.align-left": "Aligner à gauche", + "action.align-right": "Aligner à droite", + "action.align-top": "Aligner en haut", + "action.back-to-content": "Retour au contenu", + "action.bring-forward": "Mettre en avant", + "action.bring-to-front": "Mettre au premier plan", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Copier en tant que JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Copier en tant que PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Copier en tant que SVG", + "action.copy": "Copier", + "action.cut": "Couper", + "action.delete": "Supprimer", + "action.distribute-horizontal": "Distribuer horizontalement", + "action.distribute-vertical": "Distribuer verticalement", + "action.distribute-horizontal.short": "Distribuer H", + "action.distribute-vertical.short": "Distribuer V", + "action.duplicate": "Dupliquer", + "action.edit-link": "Modifier le lien", + "action.exit-pen-mode": "Quitter le mode stylet", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Exporter en tant que JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Exporter en tant que PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Exporter en tant que SVG", + "action.flip-horizontal": "Retourner horizontalement", + "action.flip-vertical": "Retourner verticalement", + "action.flip-horizontal.short": "Retourner H", + "action.flip-vertical.short": "Retourner V", + "action.group": "Grouper", + "action.insert-media": "Charger un média", + "action.new-shared-project": "Nouveau projet partagé", + "action.nudge-down": "Déplacer vers le bas", + "action.nudge-left": "Déplacer à gauche", + "action.nudge-right": "Déplacer à droite", + "action.nudge-up": "Déplacer vers le haut", + "action.open-file": "Ouvrir le fichier", + "action.pack": "Paquet", + "action.paste": "Coller", + "action.print": "Imprimer", + "action.redo": "Rétablir", + "action.rotate-ccw": "Tourner dans le sens inverse des aiguilles d'une montre", + "action.rotate-cw": "Tourner dans le sens des aiguilles d'une montre", + "action.save-copy": "Enregistrer une copie", + "action.select-all": "Sélectionner tout", + "action.select-none": "Sélectionner aucun", + "action.send-backward": "Envoyer vers l'arrière", + "action.send-to-back": "Envoyer à l'arrière", + "action.share-project": "Partager ce projet", + "action.stack-horizontal": "Empiler horizontalement", + "action.stack-vertical": "Empiler verticalement", + "action.stack-horizontal.short": "Empiler H", + "action.stack-vertical.short": "Empiler V", + "action.stretch-horizontal": "Étirer horizontalement", + "action.stretch-vertical": "Étirer verticalement", + "action.stretch-horizontal.short": "Étirer H", + "action.stretch-vertical.short": "Étirer V", + "action.toggle-auto-size": "Activer le dimensionnement automatique", + "action.toggle-dark-mode.menu": "Mode sombre", + "action.toggle-dark-mode": "Activer le mode sombre", + "action.toggle-debug-mode.menu": "Mode débogage", + "action.toggle-debug-mode": "Activer le mode débogage", + "action.toggle-focus-mode.menu": "Mode Focus", + "action.toggle-focus-mode": "Activer le mode Focus", + "action.toggle-grid.menu": "Afficher la grille", + "action.toggle-grid": "Activer la grille", + "action.toggle-snap-mode.menu": "Toujours aligner", + "action.toggle-snap-mode": "Activer la fonction Toujours aligner", + "action.toggle-tool-lock.menu": "Verrouillage de l'outil", + "action.toggle-tool-lock": "Activer le verrouillage de l'outil", + "action.toggle-transparent.context-menu": "Transparent", + "action.toggle-transparent.menu": "Transparent", + "action.toggle-transparent": "Activer l'arrière-plan transparent", + "action.undo": "Annuler", + "action.ungroup": "Dissocier", + "action.zoom-in": "Zoomer", + "action.zoom-out": "Dézoomer", + "action.zoom-to-100": "Zoomer à 100%", + "action.zoom-to-fit": "Zoomer pour ajuster", + "action.zoom-to-selection": "Zoomer sur la sélection", + "color-style.black": "Noir", + "color-style.blue": "Bleu", + "color-style.green": "Vert", + "color-style.grey": "Gris", + "color-style.light-blue": "Bleu clair", + "color-style.light-green": "Vert clair", + "color-style.light-red": "Rouge clair", + "color-style.light-violet": "Violet clair", + "color-style.orange": "Orange", + "color-style.red": "Rouge", + "color-style.violet": "Violet", + "color-style.yellow": "Jaune", + "fill-style.none": "Aucun", + "fill-style.semi": "Semi", + "fill-style.solid": "Solide", + "fill-style.pattern": "Motif", + "dash-style.dashed": "Discontinu", + "dash-style.dotted": "Pointillé", + "dash-style.draw": "Dessiner", + "dash-style.solid": "Solide", + "size-style.s": "Petit", + "size-style.m": "Moyen", + "size-style.l": "Large", + "size-style.xl": "Extra-large", + "opacity-style.0.1": "10 %", + "opacity-style.0.25": "25 %", + "opacity-style.0.5": "50 %", + "opacity-style.0.75": "75 %", + "opacity-style.1": "100 %", + "font-style.draw": "Dessiner", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Début", + "align-style.middle": "Milieu", + "align-style.end": "Fin", + "align-style.justify": "Justifier", + "geo-style.arrow-down": "Flèche vers le bas", + "geo-style.arrow-left": "Flèche vers la gauche", + "geo-style.arrow-right": "Flèche vers la droite", + "geo-style.arrow-up": "Flèche vers le haut", + "geo-style.diamond": "Diamant", + "geo-style.ellipse": "Ellipse", + "geo-style.hexagon": "Hexagone", + "geo-style.octagon": "Octogone", + "geo-style.oval": "Ovale", + "geo-style.pentagon": "Pentagone", + "geo-style.rectangle": "Rectangle", + "geo-style.rhombus-2": "Losange 2", + "geo-style.rhombus": "Losange", + "geo-style.star": "Étoile", + "geo-style.trapezoid": "Trapèze", + "geo-style.triangle": "Triangle", + "geo-style.x-box": "X Box", + "arrowheadStart-style.none": "Aucun", + "arrowheadStart-style.arrow": "Flèche", + "arrowheadStart-style.bar": "Barre", + "arrowheadStart-style.diamond": "Diamant", + "arrowheadStart-style.dot": "Point", + "arrowheadStart-style.inverted": "Inversé", + "arrowheadStart-style.pipe": "Tuyau", + "arrowheadStart-style.square": "Carré", + "arrowheadStart-style.triangle": "Triangle", + "arrowheadEnd-style.none": "Aucun", + "arrowheadEnd-style.arrow": "Flèche", + "arrowheadEnd-style.bar": "Barre", + "arrowheadEnd-style.diamond": "Diamant", + "arrowheadEnd-style.dot": "Point", + "arrowheadEnd-style.inverted": "Inversé", + "arrowheadEnd-style.pipe": "Tuyau", + "arrowheadEnd-style.square": "Carré", + "arrowheadEnd-style.triangle": "Triangle", + "spline-style.line": "Ligne", + "spline-style.cubic": "Cubique", + "tool.select": "Sélection", + "tool.hand": "Main", + "tool.draw": "Dessiner", + "tool.eraser": "Gomme", + "tool.arrow-down": "Flèche vers le bas", + "tool.arrow-left": "Flèche vers la gauche", + "tool.arrow-right": "Flèche vers la droite", + "tool.arrow-up": "Flèche vers le haut", + "tool.arrow": "Flèche", + "tool.diamond": "Diamant", + "tool.ellipse": "Ellipse", + "tool.hexagon": "Hexagone", + "tool.line": "Ligne", + "tool.octagon": "Octogone", + "tool.oval": "Ovale", + "tool.pentagon": "Pentagone", + "tool.rectangle": "Rectangle", + "tool.rhombus": "Losange", + "tool.star": "Étoile", + "tool.trapezoid": "Trapèze", + "tool.triangle": "Triangle", + "tool.x-box": "X box", + "tool.asset": "Actif", + "tool.frame": "Cadre", + "tool.note": "Note", + "tool.embed": "Intégration", + "tool.text": "Texte", + "menu.title": "Menu", + "menu.copy-as": "Copier en tant que", + "menu.edit": "Modifier", + "menu.export-as": "Exporter en tant que", + "menu.file": "Fichier", + "menu.language": "Langue", + "menu.preferences": "Préférences", + "menu.view": "Vue", + "context-menu.arrange": "Organiser", + "context-menu.copy-as": "Copier en tant que", + "context-menu.export-as": "Exporter en tant que", + "context-menu.move-to-page": "Déplacer vers la page", + "context-menu.reorder": "Réorganiser", + "page-menu.title": "Pages", + "page-menu.create-new-page": "Créer une nouvelle page", + "page-menu.edit-pages": "Modifier les pages", + "page-menu.max-page-count-reached": "Nombre maximal de pages atteint", + "page-menu.new-page-initial-name": "Page 1", + "page-menu.page": "Page", + "page-menu.edit-start": "Modifier", + "page-menu.edit-done": "Terminé", + "page-menu.submenu.rename": "Renommer", + "page-menu.submenu.duplicate-page": "Dupliquer", + "page-menu.submenu.go-to-page": "Accéder à la page", + "page-menu.submenu.title": "Menu", + "page-menu.submenu.move-down": "Déplacer vers le bas", + "page-menu.submenu.move-up": "Déplacer vers le haut", + "page-menu.submenu.delete": "Supprimer", + "share-menu.title": "Partager", + "share-menu.share-project": "Partager ce projet", + "share-menu.create-project": "Nouveau projet partagé", + "share-menu.copy-link": "Copier le lien", + "share-menu.readonly-link": "Lecture seule", + "share-menu.copy-readonly-link": "Copier le lien en lecture seule", + "share-menu.offline-note": "Le partage de ce projet créera une copie réelle hébergée sur une nouvelle URL. Vous pourrez partager l'URL avec un maximum de trente personnes pour afficher et modifier le projet ensemble.", + "share-menu.copy-link-note": "Toute personne en possession du lien aura la possibilité de voir et modifier ce projet.", + "share-menu.copy-readonly-link-note": "Toute personne en possession du lien aura la possibilité de voir (mais pas de modifier) ce projet.", + "share-menu.project-too-large": "Désolé, il n'est pas possible de partager ce projet, car il est trop volumineux. Nous travaillons à la résolution de ce problème!", + "people-menu.title": "Personnes", + "people-menu.change-name": "Modifier le nom", + "people-menu.change-color": "Modifier la couleur", + "people-menu.user": "(Vous)", + "people-menu.invite": "Inviter d'autres personnes", + "debug-menu.hard-reset": "Réinitialisation matérielle", + "debug-menu.create-shapes": "Créer 100 formes", + "help-menu.title": "Aide et ressources", + "help-menu.about": "À propos", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Raccourcis clavier", + "help-menu.twitter": "Twitter", + "links-menu.about": "À propos", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Actions", + "edit-link-dialog.title": "Modifier le lien", + "edit-link-dialog.invalid-url": "Un lien doit être une URL valide.", + "edit-link-dialog.detail": "Les liens s'ouvriront dans un nouvel onglet.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Effacer", + "edit-link-dialog.save": "Continuer", + "edit-link-dialog.cancel": "Annuler", + "embed-dialog.title": "Créer une intégration", + "embed-dialog.url-label": "Coller l'URL", + "embed-dialog.back": "Retour", + "embed-dialog.create": "Créer", + "embed-dialog.cancel": "Annuler", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Collez l'URL du site pour créer l'intégration.", + "embed-dialog.invalid-url": "Nous n'avons pas pu créer une intégration (embed) à partir de cette URL.", + "edit-pages-dialog.title": "Modifier les pages", + "edit-pages-dialog.create-new-page": "Créer une nouvelle page", + "edit-pages-dialog.delete": "Supprimer", + "edit-pages-dialog.duplicate-page": "Dupliquer", + "edit-pages-dialog.go-to-page": "Accéder à la page", + "edit-pages-dialog.max-page-count-reached": "Nombre maximal de pages atteint", + "edit-pages-dialog.more-menu": "Menu", + "edit-pages-dialog.move-down": "Déplacer vers le bas", + "edit-pages-dialog.move-up": "Déplacer vers le haut", + "edit-pages-dialog.new-page-initial-name": "Page 1", + "reload-file-dialog.title": "Poursuivre la modification du fichier", + "reload-file-dialog.description": "Vous étiez en train de modifier un fichier. Souhaitez-vous poursuivre la modification?", + "reload-file-dialog.failure": "Échec du rechargement du fichier. Réessayer ?", + "reload-file-dialog.reload": "Poursuivre la modification", + "reload-file-dialog.revert": "Non, merci", + "shortcuts-dialog.title": "Raccourcis clavier", + "shortcuts-dialog.edit": "Modifier", + "shortcuts-dialog.file": "Fichier", + "shortcuts-dialog.preferences": "Préférences", + "shortcuts-dialog.tools": "Outils", + "shortcuts-dialog.transform": "Transformer", + "shortcuts-dialog.view": "Vue", + "shortcuts-dialog.save": "Continuer", + "style-panel.title": "Styles", + "style-panel.align": "Alignement", + "style-panel.arrowheads": "Pointes de flèches", + "style-panel.color": "Couleur", + "style-panel.dash": "Tiret", + "style-panel.fill": "Remplir", + "style-panel.font": "Police", + "style-panel.geo": "Forme", + "style-panel.label": "Étiquette", + "style-panel.mixed": "Mélangé", + "style-panel.opacity": "Opacité", + "style-panel.size": "Taille", + "style-panel.spline": "Spline", + "style-panel.text": "Texte", + "tool-panel.drawing": "Dessin", + "tool-panel.geo": "Forme", + "tool-panel.shapes": "Formes", + "tool-panel.things": "Éléments", + "tool-panel.tools": "Outils", + "save-changes-prompt.title": "Vous avez des modifications non enregistrées", + "save-changes-prompt.description": "Souhaitez-vous enregistrer les modifications apportées à votre fichier actuel ?", + "save-changes-prompt.go-back": "Revenir en arrière", + "save-changes-prompt.continue": "Continuer", + "navigation-zone.toggle-minimap": "Basculer la mini-carte", + "navigation-zone.zoom": "Zoomer", + "focus-mode.toggle-focus-mode": "Basculer le mode Focus", + "toast.close": "Fermer", + "file-system.file-open-error.title": "Ouverture du fichier impossible", + "file-system.file-open-error.not-a-tldraw-file": "Le fichier que vous avez tenté d'ouvrir ne ressemble pas à un fichier tldraw.", + "file-system.file-open-error.file-format-version-too-new": "Le fichier que vous avez tenté d'ouvrir provient d'une version plus récente de tldraw. Veuillez recharger la page et réessayer.", + "file-system.file-open-error.generic-corrupted-file": "Le fichier que vous avez tenté d'ouvrir est corrompu.", + "file-system.confirm-open.title": "Remplacer le projet actuel?", + "file-system.confirm-open.description": "L'ouverture d'un fichier remplacera votre projet actuel et toutes les modifications non enregistrées seront perdues. Voulez-vous vraiment continuer ?", + "file-system.confirm-open.cancel": "Annuler", + "file-system.confirm-open.open": "Ouvrir le fichier", + "file-system.confirm-open.dont-show-again": "Ne plus demander", + "toast.error.export-fail.title": "Échec de l'exportation", + "toast.error.export-fail.desc": "Échec de l'exportation de l'image", + "toast.error.copy-fail.title": "Échec de la copie", + "toast.error.copy-fail.desc": "Échec de la copie de l'image", + "file-system.shared-document-file-open-error.title": "Impossible d'ouvrir le fichier", + "file-system.shared-document-file-open-error.description": "L'ouverture de fichiers à partir de projets partagés n'est pas prise en charge.", + "vscode.file-open.dont-show-again": "Ne plus demander", + "vscode.file-open.desc": "Ce fichier a été créé avec une version antérieure de tldraw. Souhaitez-vous le mettre à jour pour qu’il fonctionne avec la nouvelle version ?", + "context.pages.new-page": "Nouvelle page" +} \ No newline at end of file diff --git a/assets/translations/gl.json b/assets/translations/gl.json new file mode 100644 index 000000000..0f7ddd03a --- /dev/null +++ b/assets/translations/gl.json @@ -0,0 +1,340 @@ +{ + "action.convert-to-bookmark": "Converter a favorito", + "action.convert-to-embed": "Converter a embed", + "action.open-embed-link": "Abrir ligazón", + "action.align-bottom": "Aliñar abaixo", + "action.align-center-horizontal": "Aliñar ao centro horizontalmente", + "action.align-center-vertical": "Aliñar ao centro verticalmente", + "action.align-center-horizontal.short": "Aliñar ao centro horizontalmente", + "action.align-center-vertical.short": "Aliñar ao centro verticalmente", + "action.align-left": "Aliñar á esquerda", + "action.align-right": "Aliñar á dereita", + "action.align-top": "Aliñar arriba", + "action.back-to-content": "Volver ao contido", + "action.bring-forward": "Mover adiante", + "action.bring-to-front": "Mover ao fronte", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Copiar como JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Copiar como PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Copiar como SVG", + "action.copy": "Copiar", + "action.cut": "Cortar", + "action.delete": "Borrar", + "action.distribute-horizontal": "Distribuír horizontalmente", + "action.distribute-vertical": "Distribuír verticalmente", + "action.distribute-horizontal.short": "Distribuír horizontalmente", + "action.distribute-vertical.short": "Distribuír verticalmente", + "action.duplicate": "Duplicar", + "action.edit-link": "Editar ligazón", + "action.exit-pen-mode": "Saír do modo pluma", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Exportar como JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Exportar como PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Exportar como SVG", + "action.flip-horizontal": "Voltear horizontalmente", + "action.flip-vertical": "Voltear verticalmente", + "action.flip-horizontal.short": "Voltear horizontalmente", + "action.flip-vertical.short": "Voltear verticalmente", + "action.group": "Agrupar", + "action.insert-media": "Subir medios", + "action.new-shared-project": "Novo proxecto compartido", + "action.nudge-down": "Mover abaixo", + "action.nudge-left": "Mover á esquerda", + "action.nudge-right": "Move á dereita", + "action.nudge-up": "Mover arriba", + "action.open-file": "Abrir arquivo", + "action.pack": "Empaquetar", + "action.paste": "Pegar", + "action.print": "Imprimir", + "action.redo": "Refacer", + "action.rotate-ccw": "Rotar en sentido antihorario", + "action.rotate-cw": "Rotar en sentido horario", + "action.save-copy": "Gardar unha copia", + "action.select-all": "Selecionar todo", + "action.select-none": "Selecionar nada", + "action.send-backward": "Mover atrás", + "action.send-to-back": "Mover ao fondo", + "action.share-project": "Compartir este proxecto", + "action.stack-horizontal": "Amorear Horizontalmente", + "action.stack-vertical": "Amorear Verticalmente", + "action.stack-horizontal.short": "Amorear H", + "action.stack-vertical.short": "Amorear V", + "action.stretch-horizontal": "Estirar horizontalmente", + "action.stretch-vertical": "Estirar verticalmente", + "action.stretch-horizontal.short": "Estirar horizontalmente", + "action.stretch-vertical.short": "Estirar verticalmente", + "action.toggle-auto-size": "Alternar tamaño automático", + "action.toggle-dark-mode.menu": "Modo escuro", + "action.toggle-dark-mode": "Modo escuro", + "action.toggle-debug-mode.menu": "Modo depuración", + "action.toggle-debug-mode": "Modo depuración", + "action.toggle-focus-mode.menu": "Modo concentración", + "action.toggle-focus-mode": "Modo concentración", + "action.toggle-grid.menu": "Amosar cuadrícula", + "action.toggle-grid": "Amosar cuadrícula", + "action.toggle-snap-mode.menu": "Amosar puntos de axuste", + "action.toggle-snap-mode": "Amosar puntos de axuste", + "action.toggle-tool-lock.menu": "Bloqueo de ferramentas", + "action.toggle-tool-lock": "Alternar bloqueo de ferramentas", + "action.toggle-transparent.context-menu": "Transparente", + "action.toggle-transparent.menu": "Transparente", + "action.toggle-transparent": "Alternar fondo transparente", + "action.undo": "Desfacer", + "action.ungroup": "Desagrupar", + "action.zoom-in": "Achegar", + "action.zoom-out": "Afastar", + "action.zoom-to-100": "Zoom ao 100%", + "action.zoom-to-fit": "Axustar á ventá", + "action.zoom-to-selection": "Achegar á selección", + "color-style.black": "Negro", + "color-style.blue": "Azul", + "color-style.green": "Verde", + "color-style.grey": "Gris", + "color-style.light-blue": "Azul claro", + "color-style.light-green": "Verde claro", + "color-style.light-red": "Vermello claro", + "color-style.light-violet": "Violeta claro", + "color-style.orange": "Laranxa", + "color-style.red": "Vermello", + "color-style.violet": "Violeta", + "color-style.yellow": "Amarelo", + "fill-style.none": "Ningún", + "fill-style.semi": "Media", + "fill-style.solid": "Contínuo", + "fill-style.pattern": "Patrón", + "dash-style.dashed": "Discontínuo", + "dash-style.dotted": "Punteado", + "dash-style.draw": "Debuxar", + "dash-style.solid": "Contínuo", + "size-style.s": "Pequeno", + "size-style.m": "Mediano", + "size-style.l": "Grande", + "size-style.xl": "Extra grande", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Debuxar", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Comezo", + "align-style.middle": "Medio", + "align-style.end": "Fin", + "align-style.justify": "Xustificar", + "geo-style.arrow-down": "Frecha abaixo", + "geo-style.arrow-left": "Frecha esquerda", + "geo-style.arrow-right": "Frecha dereita", + "geo-style.arrow-up": "Frecha arriba", + "geo-style.diamond": "Diamante", + "geo-style.ellipse": "Elipse", + "geo-style.hexagon": "Hexágono", + "geo-style.octagon": "Octógono", + "geo-style.oval": "Óvalo", + "geo-style.pentagon": "Pentágono", + "geo-style.rectangle": "Rectángulo", + "geo-style.rhombus-2": "Rombo 2", + "geo-style.rhombus": "Rombo", + "geo-style.star": "Estrela", + "geo-style.trapezoid": "Trapecio", + "geo-style.triangle": "Triángulo", + "geo-style.x-box": "X Box", + "arrowheadStart-style.none": "Ningún", + "arrowheadStart-style.arrow": "Frecha", + "arrowheadStart-style.bar": "Barra", + "arrowheadStart-style.diamond": "Diamante", + "arrowheadStart-style.dot": "Punto", + "arrowheadStart-style.inverted": "Invertida", + "arrowheadStart-style.pipe": "Tubería", + "arrowheadStart-style.square": "Cadrado", + "arrowheadStart-style.triangle": "Triángulo", + "arrowheadEnd-style.none": "Ningunha", + "arrowheadEnd-style.arrow": "Frecha", + "arrowheadEnd-style.bar": "Barra", + "arrowheadEnd-style.diamond": "Diamante", + "arrowheadEnd-style.dot": "Punto", + "arrowheadEnd-style.inverted": "Invertida", + "arrowheadEnd-style.pipe": "Tubería", + "arrowheadEnd-style.square": "Cadrado", + "arrowheadEnd-style.triangle": "Triángulo", + "spline-style.line": "Liña", + "spline-style.cubic": "Cúbico", + "tool.select": "Seleccionar", + "tool.hand": "Man", + "tool.draw": "Debuxar", + "tool.eraser": "Borrador", + "tool.arrow-down": "Frecha abaixo", + "tool.arrow-left": "Frecha esquerda", + "tool.arrow-right": "Frecha dereita", + "tool.arrow-up": "Frecha arriba", + "tool.arrow": "Frecha", + "tool.diamond": "Diamante", + "tool.ellipse": "Elipse", + "tool.hexagon": "Hexágono", + "tool.line": "Liña", + "tool.octagon": "Octógono", + "tool.oval": "Óvalo", + "tool.pentagon": "Pentágono", + "tool.rectangle": "Rectángulo", + "tool.rhombus": "Rombo", + "tool.star": "Estrela", + "tool.trapezoid": "Trapecio", + "tool.triangle": "Triángulo", + "tool.x-box": "X box", + "tool.asset": "Activo", + "tool.frame": "Marco", + "tool.note": "Pegatina", + "tool.embed": "Embed", + "tool.text": "Texto", + "menu.title": "Menú", + "menu.copy-as": "Copiar como", + "menu.edit": "Editar", + "menu.export-as": "Exportar como", + "menu.file": "Arquivo", + "menu.language": "Idioma", + "menu.preferences": "Preferencias", + "menu.view": "Ver", + "context-menu.arrange": "Organizar", + "context-menu.copy-as": "Copiar como", + "context-menu.export-as": "Exportar como", + "context-menu.move-to-page": "Mover á páxina", + "context-menu.reorder": "Reordenar", + "page-menu.title": "Páxinas", + "page-menu.create-new-page": "Crear páxina", + "page-menu.edit-pages": "Editar páxinas", + "page-menu.max-page-count-reached": "Alcanzouse o máximo de páxinas", + "page-menu.new-page-initial-name": "Páxina 1", + "page-menu.page": "Páxina", + "page-menu.edit-start": "Editar", + "page-menu.edit-done": "Feito", + "page-menu.submenu.rename": "Renomear", + "page-menu.submenu.duplicate-page": "Duplicar", + "page-menu.submenu.go-to-page": "Ir á páxina", + "page-menu.submenu.title": "Menú", + "page-menu.submenu.move-down": "Mover abaixo", + "page-menu.submenu.move-up": "Mover arriba", + "page-menu.submenu.delete": "Borrar", + "share-menu.title": "Compartir", + "share-menu.share-project": "Compartir este proxecto", + "share-menu.create-project": "Novo proxecto compartido", + "share-menu.copy-link": "Copiar invitación", + "share-menu.readonly-link": "Só-Lectura", + "share-menu.copy-readonly-link": "Copiar invitación (só lectura)", + "share-menu.offline-note": "Compartir este proxecto vai crear unha copia aloxada nunha nova URL. Podes compartir a URL con ata trinta personas para ver e editar o proxecto xuntos.", + "share-menu.copy-link-note": "Calquera ca ligazón poderá ver e editar este proxecto.", + "share-menu.copy-readonly-link-note": "Calquera ca ligazón poderá ver (pero non editar) este proxecto.", + "share-menu.project-too-large": "Sentímolo, este proxecto non pode ser compartido porque é moi grande. Estamos traballando nelo!", + "people-menu.title": "Xente", + "people-menu.change-name": "Cambiar nome", + "people-menu.change-color": "Cambiar cor", + "people-menu.user": "(Ti)", + "people-menu.invite": "Invitar a outros", + "debug-menu.hard-reset": "Reinicio completo", + "debug-menu.create-shapes": "Crear 100 formas", + "help-menu.title": "Axuda e recursos", + "help-menu.about": "Sobre", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Atallos de teclado", + "help-menu.twitter": "Twitter", + "links-menu.about": "Sobre", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Accións", + "edit-link-dialog.title": "Editar ligazón", + "edit-link-dialog.invalid-url": "Unha ligazón ten que ser unha URL válida.", + "edit-link-dialog.detail": "As ligazóns abriranse nunha nova lapela", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Limpar", + "edit-link-dialog.save": "Continuar", + "edit-link-dialog.cancel": "Cancelar", + "embed-dialog.title": "Crear embed", + "embed-dialog.url-label": "Pegar URL", + "embed-dialog.back": "Atrás", + "embed-dialog.create": "Crear", + "embed-dialog.cancel": "Cancelar", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Pega a URL do sitio para crear o embed.", + "embed-dialog.invalid-url": "Non puidemos crear o embed de esa URL.", + "edit-pages-dialog.title": "Editar páxinas", + "edit-pages-dialog.create-new-page": "Crear páxina", + "edit-pages-dialog.delete": "Borrar", + "edit-pages-dialog.duplicate-page": "Duplicar", + "edit-pages-dialog.go-to-page": "Ir á páxina", + "edit-pages-dialog.max-page-count-reached": "Alcanzouse o máximo de páxinas", + "edit-pages-dialog.more-menu": "Menú", + "edit-pages-dialog.move-down": "Mover abaixo", + "edit-pages-dialog.move-up": "Mover arriba", + "edit-pages-dialog.new-page-initial-name": "Páxina 1", + "reload-file-dialog.title": "Continuar editando o arquivo", + "reload-file-dialog.description": "Estabas editando un arquivo. Gustaríache seguir editándoo?", + "reload-file-dialog.failure": "Error ao refrescar o arquivo. Queres tentar outra vez?", + "reload-file-dialog.reload": "Continuar editando", + "reload-file-dialog.revert": "Non, grazas", + "shortcuts-dialog.title": "Atallos de teclado", + "shortcuts-dialog.edit": "Editar", + "shortcuts-dialog.file": "Arquivo", + "shortcuts-dialog.preferences": "Preferencias", + "shortcuts-dialog.tools": "Ferramentas", + "shortcuts-dialog.transform": "Transformar", + "shortcuts-dialog.view": "Ver", + "shortcuts-dialog.save": "Continuar", + "style-panel.title": "Estilos", + "style-panel.align": "Aliñamento", + "style-panel.arrowheads": "Puntas de frecha", + "style-panel.color": "Cor", + "style-panel.dash": "Liña", + "style-panel.fill": "Recheo", + "style-panel.font": "Fonte", + "style-panel.geo": "Forma", + "style-panel.label": "Etiqueta", + "style-panel.mixed": "Mesturado", + "style-panel.opacity": "Opacidade", + "style-panel.size": "Tamaño", + "style-panel.spline": "Spline", + "style-panel.text": "Texto", + "tool-panel.drawing": "Debuxo", + "tool-panel.geo": "Forma", + "tool-panel.shapes": "Formas", + "tool-panel.things": "Cousas", + "tool-panel.tools": "Ferramentas", + "save-changes-prompt.title": "Tes cambios sen gardar", + "save-changes-prompt.description": "Queres gardar os cambios no teu arquivo actual?", + "save-changes-prompt.go-back": "Atrás", + "save-changes-prompt.continue": "Continuar", + "navigation-zone.toggle-minimap": "Alternar minimapa", + "navigation-zone.zoom": "Zoom", + "focus-mode.toggle-focus-mode": "Modo concentración", + "toast.close": "Pechar", + "file-system.file-open-error.title": "Non se puido abrir o arquivo", + "file-system.file-open-error.not-a-tldraw-file": "O arquivo que intentaches abrir non parece un arquivo de tldraw.", + "file-system.file-open-error.file-format-version-too-new": "O arquivo que intentaches abrir é dunha versión máis nova de tldraw. Por favor, refresca a páxina e probar outra vez.", + "file-system.file-open-error.generic-corrupted-file": "O arquivo que intentou abrir está corrompido.", + "file-system.confirm-open.title": "Sobrescribir o proxecto actual?", + "file-system.confirm-open.description": "Abrir un arquivo vai remplazar o actual proxecto e calquera cambio sen gardar perderase. Estás seguro de que queres continuar?", + "file-system.confirm-open.cancel": "Cancelar", + "file-system.confirm-open.open": "Abrir arquivo", + "file-system.confirm-open.dont-show-again": "Non preguntar outra vez", + "toast.error.export-fail.title": "Erro na exportación", + "toast.error.export-fail.desc": "Erro ao exportar a imaxe", + "toast.error.copy-fail.title": "Erro na copia", + "toast.error.copy-fail.desc": "Erro ao copiar a imaxe", + "file-system.shared-document-file-open-error.title": "Non se puido abrir o arquivo", + "file-system.shared-document-file-open-error.description": "Abrir arquivos dende proxectos compartidos non está permitido.", + "vscode.file-open.dont-show-again": "Non preguntar outra vez", + "vscode.file-open.desc": "Este arquivo foi creado cunha versión antiga de tldraw. Queres actualizalo para que funcione ca nova versión?", + "context.pages.new-page": "Nova páxina", + "style-panel.arrowhead-start": "Comezar", + "style-panel.arrowhead-end": "Fin", + "vscode.file-open.open": "Continuar", + "vscode.file-open.backup": "Copia de seguridade", + "vscode.file-open.backup-saved": "Copia de seguridade gardada", + "vscode.file-open.backup-failed": "Fallou a copia de seguridade: este non é un arquivo .tldr.", + "style-panel.position": "Posición" +} \ No newline at end of file diff --git a/assets/translations/he.json b/assets/translations/he.json new file mode 100644 index 000000000..231984ba0 --- /dev/null +++ b/assets/translations/he.json @@ -0,0 +1,90 @@ +{ + "action.bring-forward": "הזז קדימה", + "action.bring-to-front": "הבא לחזית", + "action.copy": "העתק", + "action.cut": "גזור", + "action.delete": "מחק", + "action.duplicate": "שכפל", + "action.flip-horizontal": "הפוך אופקית", + "action.flip-vertical": "הפוך אנכית", + "action.flip-horizontal.short": "הפוך אופקית", + "action.flip-vertical.short": "הפוך אנכית", + "action.group": "קבץ", + "action.insert-media": "העלאת מדיה", + "action.paste": "הדבק", + "action.redo": "עשה מחדש", + "action.select-all": "בחר הכל", + "action.select-none": "בטל בחירה", + "action.send-backward": "הזז אחורה", + "action.send-to-back": "הבא לתחתית", + "action.toggle-dark-mode.menu": "מצב חשוך", + "action.toggle-dark-mode": "מצב חשוך", + "action.toggle-debug-mode.menu": "מצב דיבאג", + "action.toggle-debug-mode": "מצב דיבאג", + "action.toggle-focus-mode.menu": "מצב פוקוס", + "action.toggle-focus-mode": "מצב פוקוס", + "action.toggle-grid.menu": "(גריד)הראה רשת עימוד", + "action.toggle-grid": "(גריד)הראה רשת עימוד", + "action.toggle-snap-mode.menu": "הראה קווי מתאר", + "action.toggle-snap-mode": "הראה קווי מתאר", + "action.undo": "בטל", + "action.ungroup": "בטל קיבוץ", + "action.zoom-in": "הגדל תצוגה", + "action.zoom-out": "הקטן תצוגה", + "action.zoom-to-fit": "זום להתאמה", + "action.zoom-to-selection": "זום לבחירה", + "dash-style.draw": "צייר", + "font-style.draw": "צייר", + "geo-style.ellipse": "אליפסה", + "geo-style.rectangle": "מרובע", + "geo-style.triangle": "משולש", + "arrowheadStart-style.arrow": "חץ", + "arrowheadStart-style.triangle": "משולש", + "arrowheadEnd-style.arrow": "חץ", + "arrowheadEnd-style.triangle": "משולש", + "spline-style.line": "קו", + "tool.select": "סמן", + "tool.draw": "צייר", + "tool.eraser": "מחק", + "tool.arrow": "חץ", + "tool.ellipse": "אליפסה", + "tool.line": "קו", + "tool.rectangle": "מרובע", + "tool.triangle": "משולש", + "tool.note": "דביקי", + "tool.text": "טקסט", + "menu.copy-as": "העתק כ", + "menu.edit": "עריכה", + "menu.export-as": "ייצא כ", + "menu.file": "קובץ", + "menu.language": "שפה", + "menu.preferences": "מאפיינים", + "menu.view": "תצוגה", + "context-menu.copy-as": "העתק כ", + "context-menu.export-as": "ייצא כ", + "context-menu.move-to-page": "הזז לדף", + "page-menu.create-new-page": "צור דף", + "page-menu.edit-start": "עריכה", + "page-menu.submenu.duplicate-page": "שכפל", + "page-menu.submenu.delete": "מחק", + "share-menu.copy-link": "העתק קישור הזמנה", + "edit-link-dialog.cancel": "בטל", + "embed-dialog.cancel": "בטל", + "edit-pages-dialog.create-new-page": "צור דף", + "edit-pages-dialog.delete": "מחק", + "edit-pages-dialog.duplicate-page": "שכפל", + "shortcuts-dialog.edit": "עריכה", + "shortcuts-dialog.file": "קובץ", + "shortcuts-dialog.preferences": "מאפיינים", + "shortcuts-dialog.view": "תצוגה", + "style-panel.title": "עיצוב", + "style-panel.align": "יישור", + "style-panel.color": "צבע", + "style-panel.dash": "גבול", + "style-panel.fill": "מלא", + "style-panel.font": "גופן", + "style-panel.size": "גודל", + "style-panel.text": "טקסט", + "focus-mode.toggle-focus-mode": "מצב פוקוס", + "file-system.confirm-open.cancel": "בטל" +} diff --git a/assets/translations/hi-in.json b/assets/translations/hi-in.json new file mode 100644 index 000000000..95a40165d --- /dev/null +++ b/assets/translations/hi-in.json @@ -0,0 +1,328 @@ +{ + "action.convert-to-bookmark": "बुकमार्क में कनवर्ट करें", + "action.convert-to-embed": "एम्बेड में कनवर्ट करें", + "action.open-embed-link": "लिंक खोलें", + "action.align-bottom": "नीचे की तरफ अलाइन करें", + "action.align-center-horizontal": "आड़ा अलाइन करें", + "action.align-center-vertical": "लंबवत अलाइन करें", + "action.align-center-horizontal.short": "H को अलाइन करें", + "action.align-center-vertical.short": "V को अलाइन करें", + "action.align-left": "बाईं तरफ अलाइन करें", + "action.align-right": "दाहिनी तरफ अलाइन करें", + "action.align-top": "ऊपर से अलाइन करें", + "action.back-to-content": "कंटेन्ट पर वापस जाएं", + "action.bring-forward": "आगे लाएं", + "action.bring-to-front": "सामने लाएं", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "JSON के रूप में कॉपी करें", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "PNG के रूप में कॉपी करें", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "SVG के रूप में कॉपी करें", + "action.copy": "कॉपी करें", + "action.cut": "कट करें", + "action.delete": "डिलीट करें", + "action.distribute-horizontal": "आड़ा वितरित करें", + "action.distribute-vertical": "लंबवत वितरित करें", + "action.distribute-horizontal.short": "H को वितरित करें", + "action.distribute-vertical.short": "V को वितरित करें", + "action.duplicate": "डुप्लिकेट", + "action.edit-link": "लिंक को एडिट करें", + "action.exit-pen-mode": "पेन मोड से बाहर निकलें", + "action.export-as-json.short": "JSON", + "action.export-as-json": "JSON के रूप में एक्सपोर्ट करें", + "action.export-as-png.short": "PNG", + "action.export-as-png": "PNG के रूप में एक्सपोर्ट करें", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "SVG के रूप में एक्सपोर्ट करें", + "action.flip-horizontal": "आड़ा फ्लिप करें", + "action.flip-vertical": "लंबवत फ्लिप करें", + "action.flip-horizontal.short": "H को फ्लिप करें", + "action.flip-vertical.short": "V को फ्लिप करें", + "action.group": "ग्रुप", + "action.insert-media": "मीडिया अपलोड करें", + "action.new-shared-project": "नया शेयर किया हुआ प्रोजेक्ट", + "action.nudge-down": "नीचे की तरफ सरकाएं", + "action.nudge-left": "बाईं तरफ सरकाएं", + "action.nudge-right": "दाहिनी तरफ सरकाएं", + "action.nudge-up": "ऊपर की तरफ सरकाएं", + "action.open-file": "फ़ाइल खोलें", + "action.pack": "पैक करें", + "action.paste": "पेस्ट करें", + "action.print": "प्रिंट करें", + "action.redo": "रिडू करें", + "action.rotate-ccw": "घड़ी की सुई के विपरीत दिशा में घुमाएं", + "action.rotate-cw": "घड़ी की सुई की दिशा में घुमाएं", + "action.save-copy": "कॉपी सेव करें", + "action.select-all": "सभी चुनें", + "action.select-none": "कुछ मत चुनें", + "action.send-backward": "पीछे भेजें", + "action.send-to-back": "वापस की तरफ भेजें", + "action.share-project": "इस प्रोजेक्ट को शेयर करें", + "action.stack-horizontal": "आड़ा स्टैक करें", + "action.stack-vertical": "लंबवत स्टैक करें", + "action.stack-horizontal.short": "H को स्टैक करें", + "action.stack-vertical.short": "V को स्टैक करें", + "action.stretch-horizontal": "आड़ा खींचें", + "action.stretch-vertical": "लंबवत खींचे", + "action.stretch-horizontal.short": "H खींचें", + "action.stretch-vertical.short": "V खींचें", + "action.toggle-auto-size": "ऑटो साइज़ टॉगल करें", + "action.toggle-dark-mode.menu": "डार्क मोड", + "action.toggle-dark-mode": "डार्क मोड टॉगल करें", + "action.toggle-debug-mode.menu": "डीबग मोड", + "action.toggle-debug-mode": "डीबग मोड टॉगल करें", + "action.toggle-focus-mode.menu": "फोकस मोड", + "action.toggle-focus-mode": "फोकस मोड टॉगल करें", + "action.toggle-grid.menu": "ग्रिड दिखाएं", + "action.toggle-grid": "ग्रिड टॉगल करें", + "action.toggle-snap-mode.menu": "हमेशा स्नैप करें", + "action.toggle-snap-mode": "टॉगल हमेशा स्नैप करें", + "action.toggle-tool-lock.menu": "टूल लॉक", + "action.toggle-tool-lock": "टूल लॉक टॉगल करें", + "action.toggle-transparent.context-menu": "पारदर्शी", + "action.toggle-transparent.menu": "पारदर्शी", + "action.toggle-transparent": "पारदर्शी बैक्ग्राउण्ड को टॉगल करें", + "action.undo": "अनडू करें", + "action.ungroup": "अनग्रुप करें", + "action.zoom-in": "ज़ूम इन करें", + "action.zoom-out": "ज़ूम आउट करें", + "action.zoom-to-100": "100% तक ज़ूम करें", + "action.zoom-to-fit": "फिट करने तक लिए ज़ूम करें", + "action.zoom-to-selection": "चयन तक ज़ूम करें", + "color-style.black": "काला", + "color-style.blue": "नीला", + "color-style.green": "हरा", + "color-style.grey": "ग्रे", + "color-style.light-blue": "हल्का नीला", + "color-style.light-green": "हल्का हरा", + "color-style.light-red": "हलका लाल", + "color-style.light-violet": "हल्का बैंगनी", + "color-style.orange": "ऑरेंज", + "color-style.red": "लाल", + "color-style.violet": "बैंगनी", + "color-style.yellow": "पीला", + "fill-style.none": "कोई नहीं", + "fill-style.semi": "सेमी", + "fill-style.solid": "ठोस", + "fill-style.pattern": "पैटर्न", + "dash-style.dashed": "डैश्ड", + "dash-style.dotted": "डोटेड", + "dash-style.draw": "ड्रॉ", + "dash-style.solid": "ठोस", + "size-style.s": "छोटा", + "size-style.m": "मध्यम", + "size-style.l": "बड़ा", + "size-style.xl": "बहुत बड़ा", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "ड्रॉ", + "font-style.sans": "सेंस", + "font-style.serif": "सेरिफ़", + "font-style.mono": "मोनो", + "align-style.start": "शुरुआत", + "align-style.middle": "मध्य", + "align-style.end": "अंत", + "align-style.justify": "जस्टीफ़ाई करें", + "geo-style.arrow-down": "नीचे की तरफ एरो करें", + "geo-style.arrow-left": "बाईं तरफ एरो करें", + "geo-style.arrow-right": "दाहिनी तरफ एरो करें", + "geo-style.arrow-up": "ऊपर की तरफ एरो करें", + "geo-style.diamond": "डायमंड", + "geo-style.ellipse": "दीर्घवृत्त", + "geo-style.hexagon": "षट्कोण", + "geo-style.octagon": "अष्टकोण", + "geo-style.oval": "अंडाकार", + "geo-style.pentagon": "पंचकोण", + "geo-style.rectangle": "समकोण", + "geo-style.rhombus-2": "विषमकोण 2", + "geo-style.rhombus": "विषमकोण", + "geo-style.star": "स्टार", + "geo-style.trapezoid": "चतुर्भुज", + "geo-style.triangle": "त्रिकोण", + "geo-style.x-box": "X बॉक्स", + "arrowheadStart-style.none": "कोई नहीं", + "arrowheadStart-style.arrow": "एरो", + "arrowheadStart-style.bar": "बार", + "arrowheadStart-style.diamond": "डायमंड", + "arrowheadStart-style.dot": "डॉट", + "arrowheadStart-style.inverted": "उल्टा", + "arrowheadStart-style.pipe": "पाइप", + "arrowheadStart-style.square": "वर्ग", + "arrowheadStart-style.triangle": "त्रिकोण", + "arrowheadEnd-style.none": "कोई नहीं", + "arrowheadEnd-style.arrow": "एरो", + "arrowheadEnd-style.bar": "बार", + "arrowheadEnd-style.diamond": "डायमंड", + "arrowheadEnd-style.dot": "डॉट", + "arrowheadEnd-style.inverted": "उल्टा", + "arrowheadEnd-style.pipe": "पाइप", + "arrowheadEnd-style.square": "वर्ग", + "arrowheadEnd-style.triangle": "त्रिकोण", + "spline-style.line": "पंक्ति", + "spline-style.cubic": "घन", + "tool.select": "चुनें", + "tool.hand": "हाथ", + "tool.draw": "ड्रॉ", + "tool.eraser": "इरेज़र", + "tool.arrow-down": "नीचे की तरफ एरो करें", + "tool.arrow-left": "बाईं तरफ एरो करें", + "tool.arrow-right": "दाहिनी तरफ एरो करें", + "tool.arrow-up": "ऊपर की तरफ एरो करें", + "tool.arrow": "एरो", + "tool.diamond": "डायमंड", + "tool.ellipse": "दीर्घवृत्त", + "tool.hexagon": "षट्कोण", + "tool.line": "रेखा", + "tool.octagon": "अष्टकोण", + "tool.oval": "अंडाकार", + "tool.pentagon": "पंचकोण", + "tool.rectangle": "समकोण", + "tool.rhombus": "विषमकोण", + "tool.star": "स्टार", + "tool.trapezoid": "चतुर्भुज", + "tool.triangle": "त्रिकोण", + "tool.x-box": "X बॉक्स", + "tool.asset": "संपत्ति", + "tool.frame": "फ्रेम", + "tool.note": "नोट", + "tool.embed": "एम्बेड", + "tool.text": "टेक्स्ट", + "menu.title": "मेन्यू", + "menu.copy-as": "के रूप में कॉपी करें", + "menu.edit": "एडिट करें", + "menu.export-as": "के रूप में एक्सपोर्ट करें", + "menu.file": "फ़ाइल", + "menu.language": "भाषा", + "menu.preferences": "पसंद", + "menu.view": "देखें", + "context-menu.arrange": "व्यवस्थित करें", + "context-menu.copy-as": "के रूप में कॉपी करें", + "context-menu.export-as": "के रूप में एक्सपोर्ट करें", + "context-menu.move-to-page": "पेज पर जाएं", + "context-menu.reorder": "रीआर्डर करें", + "page-menu.title": "पेजिस", + "page-menu.create-new-page": "नया पेज बनाएं", + "page-menu.edit-pages": "पेजिस एडिट करें", + "page-menu.max-page-count-reached": "अधिकतम पेज की सीमा पूर्ण हुई", + "page-menu.new-page-initial-name": "पेज 1", + "page-menu.page": "पेज", + "page-menu.edit-start": "एडिट करें", + "page-menu.edit-done": "हो गया", + "page-menu.submenu.rename": "नाम बदलें", + "page-menu.submenu.duplicate-page": "डुप्लिकेट", + "page-menu.submenu.go-to-page": "पेज पर जाएं", + "page-menu.submenu.title": "मेन्यू", + "page-menu.submenu.move-down": "नीचे जाएं", + "page-menu.submenu.move-up": "ऊपर जाएं", + "page-menu.submenu.delete": "डिलीट करें", + "share-menu.title": "शेयर करें", + "share-menu.share-project": "इस प्रोजेक्ट को शेयर करें", + "share-menu.create-project": "नया शेयर किया हुआ प्रोजेक्ट", + "share-menu.copy-link": "लिंक कॉपी करें", + "share-menu.readonly-link": "रीड-ओनली", + "share-menu.copy-readonly-link": "रीड-ओनली लिंक कॉपी करें", + "share-menu.offline-note": "इस प्रोजेक्ट को शेयर करने से नए URL पर होस्ट की गई लाइव कॉपी बन जाएगी। प्रोजेक्ट को एक साथ देखने और एडिट करने के लिए आप अधिकतम तीस अन्य लोगों के साथ URL शेयर कर सकते हैं।", + "share-menu.copy-link-note": "लिंक वाला कोई भी व्यक्ति इस प्रोजेक्ट को देख और एडिट कर सकेगा।", + "share-menu.copy-readonly-link-note": "लिंक वाला कोई भी व्यक्ति इस प्रोजेक्ट को देख (लेकिन एडिट नहीं) पाएगा।", + "share-menu.project-too-large": "क्षमा करें, यह प्रोजेक्ट शेयर नहीं किया जा सकता क्योंकि यह बहुत बड़ा है। हम इस पर काम कर रहे हैं!", + "people-menu.title": "लोग", + "people-menu.change-name": "नाम बदलें", + "people-menu.change-color": "कलर बदलें", + "people-menu.user": "(आप)", + "people-menu.invite": "दूसरों को इन्वाइट करें", + "debug-menu.hard-reset": "हार्ड रीसेट", + "debug-menu.create-shapes": "100 शेप बनाएं", + "help-menu.title": "सहायता और संसाधन", + "help-menu.about": "के बारे में", + "help-menu.discord": "विवाद", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "किबोर्ड शॉर्टकट", + "help-menu.twitter": "Twitter", + "links-menu.about": "के बारे में", + "links-menu.discord": "विवाद", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "कार्य", + "edit-link-dialog.title": "लिंक को एडिट करें", + "edit-link-dialog.invalid-url": "लिंक एक मान्य URL होना चाहिए।", + "edit-link-dialog.detail": "लिंक एक नए टैब में खुलेंगे।", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "साफ़ करें", + "edit-link-dialog.save": "जारी रखें", + "edit-link-dialog.cancel": "रद्द करें", + "embed-dialog.title": "एम्बेड बनाएं", + "embed-dialog.url-label": "URL पेस्ट करें", + "embed-dialog.back": "वापस जाएं", + "embed-dialog.create": "बनाएं", + "embed-dialog.cancel": "रद्द करें", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "एम्बेड बनाने के लिए साइट के URL में पेस्ट करें।", + "embed-dialog.invalid-url": "हम उस URL से एम्बेड नहीं बना सके।", + "edit-pages-dialog.title": "पेज एडिट करें", + "edit-pages-dialog.create-new-page": "नया पेज बनाएं", + "edit-pages-dialog.delete": "डिलीट करें", + "edit-pages-dialog.duplicate-page": "डुप्लिकेट", + "edit-pages-dialog.go-to-page": "पेज पर जाएं", + "edit-pages-dialog.max-page-count-reached": "अधिकतम पेज की सीमा पूर्ण हुई", + "edit-pages-dialog.more-menu": "मेन्यू", + "edit-pages-dialog.move-down": "नीचे जाएं", + "edit-pages-dialog.move-up": "ऊपर जाएं", + "edit-pages-dialog.new-page-initial-name": "पेज 1", + "reload-file-dialog.title": "फ़ाइल एडिट करना जारी रखें", + "reload-file-dialog.description": "आप बस एक फाइल एडिट कर रहे थे। क्या आप इसको एडिट करना जारी रखना चाहेंगे?", + "reload-file-dialog.failure": "फ़ाइल दोबारा लोड करने में विफल। फिर से कोशिश करें?", + "reload-file-dialog.reload": "एडिट करना जारी रखें", + "reload-file-dialog.revert": "जी नहीं, धन्यवाद", + "shortcuts-dialog.title": "किबोर्ड शॉर्टकट", + "shortcuts-dialog.edit": "एडिट करें", + "shortcuts-dialog.file": "फ़ाइल", + "shortcuts-dialog.preferences": "पसंद", + "shortcuts-dialog.tools": "टूल्स", + "shortcuts-dialog.transform": "परिवर्तन करें", + "shortcuts-dialog.view": "देखें", + "shortcuts-dialog.save": "जारी रखें", + "style-panel.title": "शैलियां", + "style-panel.align": "अलाइन करें", + "style-panel.arrowheads": "तीर", + "style-panel.color": "कलर", + "style-panel.dash": "डैश", + "style-panel.fill": "भरें", + "style-panel.font": "फॉन्ट", + "style-panel.geo": "शेप", + "style-panel.label": "लेबल", + "style-panel.mixed": "मिश्रित", + "style-panel.opacity": "ओपैसटी", + "style-panel.size": "साइज़", + "style-panel.spline": "स्प्लाइन", + "style-panel.text": "टेक्स्ट", + "tool-panel.drawing": "ड्रौइंग", + "tool-panel.geo": "शेप", + "tool-panel.shapes": "शेप्स", + "tool-panel.things": "चीज़ें", + "tool-panel.tools": "टूल्स", + "save-changes-prompt.title": "आपके परिवर्तन सेव नहीं किए गए हैं", + "save-changes-prompt.description": "क्या आप अपनी वर्तमान फ़ाइल में परिवर्तनों को सेव करना चाहेंगे?", + "save-changes-prompt.go-back": "वापस जाएं", + "save-changes-prompt.continue": "जारी रखें", + "navigation-zone.toggle-minimap": "मिनिमैप टॉगल करें", + "navigation-zone.zoom": "ज़ूम करें", + "focus-mode.toggle-focus-mode": "फोकस मोड टॉगल करें", + "toast.close": "बंद करें", + "file-system.file-open-error.title": "फ़ाइल को खोल नहीं सके", + "file-system.file-open-error.not-a-tldraw-file": "आपने जिस फ़ाइल को खोलने का प्रयास किया है वह tldraw फ़ाइल की तरह नहीं दिखती है।", + "file-system.file-open-error.file-format-version-too-new": "आपने जिस फ़ाइल को खोलने का प्रयास किया है वह tldraw के नए वर्ज़न से है। कृपया पेज लोड करें और दोबारा कोशिश करें।", + "file-system.file-open-error.generic-corrupted-file": "आपने जिस फ़ाइल को खोलने का प्रयास किया वह करप्ट है।", + "file-system.confirm-open.title": "वर्तमान प्रोजेक्ट को ओवरराइट करना चाहते हैं?", + "file-system.confirm-open.description": "फ़ाइल खोलने से आपका वर्तमान प्रोजेक्ट बदल जाएगा और सेव नहीं किए गए परिवर्तन खो जाएंगे। क्या आप वाकई जारी रखना चाहते हैं?", + "file-system.confirm-open.cancel": "रद्द करें", + "file-system.confirm-open.open": "फ़ाइल खोलें", + "file-system.confirm-open.dont-show-again": "दोबारा न पूछें", + "toast.error.export-fail.title": "विफल एक्सपोर्ट", + "toast.error.export-fail.desc": "इमेज एक्सपोर्ट करने में विफल", + "toast.error.copy-fail.title": "विफल कॉपी", + "toast.error.copy-fail.desc": "इमेज कॉपी करने में विफल" +} \ No newline at end of file diff --git a/assets/translations/hu.json b/assets/translations/hu.json new file mode 100644 index 000000000..e82d30593 --- /dev/null +++ b/assets/translations/hu.json @@ -0,0 +1,5 @@ +{ + "vscode.file-open.dont-show-again": "Ne kérdezd többet", + "vscode.file-open.desc": "Ez a file egy előző verzióval készült. Frissítsük, hogy az új verzióval is működjön?", + "context.pages.new-page": "Új oldal" +} diff --git a/assets/translations/it.json b/assets/translations/it.json new file mode 100644 index 000000000..5ad4336cb --- /dev/null +++ b/assets/translations/it.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "Converti a segnalibro", + "action.convert-to-embed": "Converti in oggetto incorporato", + "action.open-embed-link": "Apri il collegamento", + "action.align-bottom": "Allinea in basso", + "action.align-center-horizontal": "Allinea orizzontalmente", + "action.align-center-vertical": "Allinea verticalmente", + "action.align-center-horizontal.short": "Allinea orizzontalmente", + "action.align-center-vertical.short": "Allinea verticalmente", + "action.align-left": "Allinea a sinistra", + "action.align-right": "Allinea a destra", + "action.align-top": "Allinea in alto", + "action.back-to-content": "Torna al contenuto", + "action.bring-forward": "Porta avanti", + "action.bring-to-front": "Porta in primo piano", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Copia come JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Copia come PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Copia come SVG", + "action.copy": "Copia", + "action.cut": "Taglia", + "action.delete": "Elimina", + "action.distribute-horizontal": "Distribuire orizzontalmente", + "action.distribute-vertical": "Distribuire verticalmente", + "action.distribute-horizontal.short": "Distribuire orizzontalmente", + "action.distribute-vertical.short": "Distribuire verticalmente", + "action.duplicate": "Duplica", + "action.edit-link": "Modifica il collegamento", + "action.exit-pen-mode": "Esci dalla modalità penna", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Esporta come JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Esporta come PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Esporta come SVG", + "action.flip-horizontal": "Capovolgi orizzontalmente", + "action.flip-vertical": "Capovolgi verticalmente", + "action.flip-horizontal.short": "Capovolgi orizzontalmente", + "action.flip-vertical.short": "Capovolgi verticalmente", + "action.group": "Raggruppa", + "action.insert-media": "Carica contenuti multimediali", + "action.new-shared-project": "Nuovo progetto condiviso", + "action.nudge-down": "Spingi in basso", + "action.nudge-left": "Spingi a sinistra", + "action.nudge-right": "Spingi a destra", + "action.nudge-up": "Spingi verso l'alto", + "action.open-file": "Apri file", + "action.pack": "Pacchetto", + "action.paste": "Incolla", + "action.print": "Stampa", + "action.redo": "Ripristina", + "action.rotate-ccw": "Ruota in senso antiorario", + "action.rotate-cw": "Ruota in senso orario", + "action.save-copy": "Salva una copia", + "action.select-all": "Seleziona tutto", + "action.select-none": "Deseleziona tutto", + "action.send-backward": "Porta indietro", + "action.send-to-back": "Porta in secondo piano", + "action.share-project": "Condividi questo progetto", + "action.stack-horizontal": "Impila orizzontalmente", + "action.stack-vertical": "Impila verticalmente", + "action.stack-horizontal.short": "Impila orizzontalmente", + "action.stack-vertical.short": "Impila verticalmente", + "action.stretch-horizontal": "Allunga orizzontalmente", + "action.stretch-vertical": "Allunga verticalmente", + "action.stretch-horizontal.short": "Allunga orizzontalmente", + "action.stretch-vertical.short": "Allunga verticalmente", + "action.toggle-auto-size": "Alterna le dimensioni automatiche", + "action.toggle-dark-mode.menu": "Tema scuro", + "action.toggle-dark-mode": "Alterna tema scuro", + "action.toggle-debug-mode.menu": "Modalità di debug", + "action.toggle-debug-mode": "Alterna modalità di debug", + "action.toggle-focus-mode.menu": "Modalità concentrazione", + "action.toggle-focus-mode": "Alterna la modalità concentrazione", + "action.toggle-grid.menu": "Mostra griglia", + "action.toggle-grid": "Alterna griglia", + "action.toggle-snap-mode.menu": "Allinea sempre", + "action.toggle-snap-mode": "Alterna allineamento automatico", + "action.toggle-tool-lock.menu": "Blocco degli strumenti", + "action.toggle-tool-lock": "Alterna blocco strumenti", + "action.toggle-transparent.context-menu": "Trasparente", + "action.toggle-transparent.menu": "Trasparente", + "action.toggle-transparent": "Alterna lo sfondo trasparente", + "action.undo": "Annulla", + "action.ungroup": "Separa", + "action.zoom-in": "Ingrandisci", + "action.zoom-out": "Rimpicciolisci", + "action.zoom-to-100": "Ingrandisci al 100%", + "action.zoom-to-fit": "Adatta allo schermo", + "action.zoom-to-selection": "Adatta alla selezione", + "color-style.black": "Nero", + "color-style.blue": "Blu", + "color-style.green": "Verde", + "color-style.grey": "Grigio", + "color-style.light-blue": "Azzurro", + "color-style.light-green": "Verde chiaro", + "color-style.light-red": "Rosso chiaro", + "color-style.light-violet": "Viola chiaro", + "color-style.orange": "Arancione", + "color-style.red": "Rosso", + "color-style.violet": "Viola", + "color-style.yellow": "Giallo", + "fill-style.none": "Nessuno", + "fill-style.semi": "Semi", + "fill-style.solid": "Solido", + "fill-style.pattern": "Modello", + "dash-style.dashed": "Tratteggiato", + "dash-style.dotted": "Punteggiato", + "dash-style.draw": "Matita", + "dash-style.solid": "Solido", + "size-style.s": "Piccolo", + "size-style.m": "Medio", + "size-style.l": "Grande", + "size-style.xl": "Molto grande", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Matita", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Inizio", + "align-style.middle": "In mezzo", + "align-style.end": "Fine", + "align-style.justify": "Giustifica", + "geo-style.arrow-down": "Freccia in giù", + "geo-style.arrow-left": "Freccia a sinistra", + "geo-style.arrow-right": "Freccia a destra", + "geo-style.arrow-up": "Freccia in su", + "geo-style.diamond": "Diamante", + "geo-style.ellipse": "Ellisse", + "geo-style.hexagon": "Esagono", + "geo-style.octagon": "Ottagono", + "geo-style.oval": "Ovale", + "geo-style.pentagon": "Pentagono", + "geo-style.rectangle": "Rettangolo", + "geo-style.rhombus-2": "Rombo 2", + "geo-style.rhombus": "Rombo", + "geo-style.star": "Stella", + "geo-style.trapezoid": "Trapezio", + "geo-style.triangle": "Triangolo", + "geo-style.x-box": "Rettangolo con una X", + "arrowheadStart-style.none": "Nessuna", + "arrowheadStart-style.arrow": "Freccia", + "arrowheadStart-style.bar": "Barra", + "arrowheadStart-style.diamond": "Diamante", + "arrowheadStart-style.dot": "Punto", + "arrowheadStart-style.inverted": "Invertito", + "arrowheadStart-style.pipe": "Tubo", + "arrowheadStart-style.square": "Quadrato", + "arrowheadStart-style.triangle": "Triangolo", + "arrowheadEnd-style.none": "Nessuna", + "arrowheadEnd-style.arrow": "Freccia", + "arrowheadEnd-style.bar": "Barra", + "arrowheadEnd-style.diamond": "Diamante", + "arrowheadEnd-style.dot": "Punto", + "arrowheadEnd-style.inverted": "Invertito", + "arrowheadEnd-style.pipe": "Tubo", + "arrowheadEnd-style.square": "Quadrato", + "arrowheadEnd-style.triangle": "Triangolo", + "spline-style.line": "Linea", + "spline-style.cubic": "Cubico", + "tool.select": "Seleziona", + "tool.hand": "Panoramica", + "tool.draw": "Matita", + "tool.eraser": "Gomma", + "tool.arrow-down": "Freccia in giù", + "tool.arrow-left": "Freccia a sinistra", + "tool.arrow-right": "Freccia a destra", + "tool.arrow-up": "Freccia in su", + "tool.arrow": "Freccia", + "tool.diamond": "Diamante", + "tool.ellipse": "Ellisse", + "tool.hexagon": "Esagono", + "tool.line": "Linea", + "tool.octagon": "Ottagono", + "tool.oval": "Ovale", + "tool.pentagon": "Pentagono", + "tool.rectangle": "Rettangolo", + "tool.rhombus": "Rombo", + "tool.star": "Stella", + "tool.trapezoid": "Trapezio", + "tool.triangle": "Triangolo", + "tool.x-box": "Rettangolo con una X", + "tool.asset": "Risorsa", + "tool.frame": "Cornice", + "tool.note": "Post-it", + "tool.embed": "Oggetto incorporato", + "tool.text": "Casella di testo", + "menu.title": "Menu", + "menu.copy-as": "Copia come", + "menu.edit": "Modifica", + "menu.export-as": "Esporta come", + "menu.file": "File", + "menu.language": "Lingua", + "menu.preferences": "Preferenze", + "menu.view": "Visualizza", + "context-menu.arrange": "Disponi", + "context-menu.copy-as": "Copia come", + "context-menu.export-as": "Esporta come", + "context-menu.move-to-page": "Trasferisci su una pagina", + "context-menu.reorder": "Riordina", + "page-menu.title": "Pagine", + "page-menu.create-new-page": "Crea nuova pagina", + "page-menu.edit-pages": "Modifica pagine", + "page-menu.max-page-count-reached": "Numero massimo di pagine raggiunte", + "page-menu.new-page-initial-name": "Pagina 1", + "page-menu.page": "Pagina", + "page-menu.edit-start": "Modifica", + "page-menu.edit-done": "Finito", + "page-menu.submenu.rename": "Rinomina", + "page-menu.submenu.duplicate-page": "Duplica", + "page-menu.submenu.go-to-page": "Vai alla pagina", + "page-menu.submenu.title": "Menu", + "page-menu.submenu.move-down": "Sposta giù", + "page-menu.submenu.move-up": "Sposta su", + "page-menu.submenu.delete": "Elimina", + "share-menu.title": "Condividi", + "share-menu.share-project": "Condividi questo progetto", + "share-menu.create-project": "Nuovo progetto condiviso", + "share-menu.copy-link": "Copia collegamento", + "share-menu.readonly-link": "Sola lettura", + "share-menu.copy-readonly-link": "Copia il collegamento di sola lettura", + "share-menu.offline-note": "La condivisione di questo progetto creerà una copia in un nuovo URL. Potrai condividere l'URL con un massimo di 30 persone per visualizzare e modificare il progetto insieme", + "share-menu.copy-link-note": "Chiunque abbia il collegamento potrà vedere e modificare questo progetto", + "share-menu.copy-readonly-link-note": "Chiunque abbia il collegamento potrà vedere (ma non modificare) questo progetto", + "share-menu.project-too-large": "Spiacenti, questo progetto non può essere condiviso perché è troppo grande. Ci stiamo lavorando!", + "people-menu.title": "Persone", + "people-menu.change-name": "Cambia nome", + "people-menu.change-color": "Cambia colore", + "people-menu.user": "(Tu)", + "people-menu.invite": "Invita qualcun'altro", + "debug-menu.hard-reset": "Ripristino forzato", + "debug-menu.create-shapes": "Crea 100 forme", + "help-menu.title": "Aiuto e risorse", + "help-menu.about": "A riguardo", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Scorciatoie da tastiera", + "help-menu.twitter": "Twitter", + "links-menu.about": "A riguardo", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Azioni", + "edit-link-dialog.title": "Modifica il collegamento", + "edit-link-dialog.invalid-url": "Un collegamento deve essere un URL valido", + "edit-link-dialog.detail": "I collegamenti verranno aperti in una nuova scheda", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Svuota", + "edit-link-dialog.save": "Continua", + "edit-link-dialog.cancel": "Annulla", + "embed-dialog.title": "Crea oggetto incorporato", + "embed-dialog.url-label": "Incolla URL", + "embed-dialog.back": "Indietro", + "embed-dialog.create": "Crea", + "embed-dialog.cancel": "Annulla", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Incolla qui l'URL del sito per creare l'oggetto incorporato", + "embed-dialog.invalid-url": "Non è stato possibile creare un oggetto incorporato da quell'URL.", + "edit-pages-dialog.title": "Modifica pagine", + "edit-pages-dialog.create-new-page": "Crea nuova pagina", + "edit-pages-dialog.delete": "Elimina", + "edit-pages-dialog.duplicate-page": "Duplica", + "edit-pages-dialog.go-to-page": "Vai alla pagina", + "edit-pages-dialog.max-page-count-reached": "Numero massimo di pagine raggiunte", + "edit-pages-dialog.more-menu": "Menu", + "edit-pages-dialog.move-down": "Sposta giù", + "edit-pages-dialog.move-up": "Sposta su", + "edit-pages-dialog.new-page-initial-name": "Pagina 1", + "reload-file-dialog.title": "Continua a modificare", + "reload-file-dialog.description": "Stavi appena modificando un file. Vorresti continuare a farlo?", + "reload-file-dialog.failure": "Impossibile aprire il file. Riprovare?", + "reload-file-dialog.reload": "Continua a modificare", + "reload-file-dialog.revert": "No, grazie", + "shortcuts-dialog.title": "Scorciatoie da tastiera", + "shortcuts-dialog.edit": "Modifica", + "shortcuts-dialog.file": "File", + "shortcuts-dialog.preferences": "Preferenze", + "shortcuts-dialog.tools": "Strumenti", + "shortcuts-dialog.transform": "Trasforma", + "shortcuts-dialog.view": "Visualizzazione", + "shortcuts-dialog.save": "Continua", + "style-panel.title": "Stile", + "style-panel.align": "Allineamento", + "style-panel.arrowheads": "Punte di freccia", + "style-panel.color": "Colore", + "style-panel.dash": "Tratteggio", + "style-panel.fill": "Riempi", + "style-panel.font": "Font", + "style-panel.geo": "Forma", + "style-panel.label": "Etichetta", + "style-panel.mixed": "Misto", + "style-panel.opacity": "Opacità", + "style-panel.size": "Dimensione", + "style-panel.spline": "Spline", + "style-panel.text": "Casella di testo", + "tool-panel.drawing": "Disegno", + "tool-panel.geo": "Forma", + "tool-panel.shapes": "Forme", + "tool-panel.things": "Cose", + "tool-panel.tools": "Strumenti", + "save-changes-prompt.title": "Hai modifiche non salvate", + "save-changes-prompt.description": "Vorresti salvare le modifiche nel tuo file corrente?", + "save-changes-prompt.go-back": "Torna indietro", + "save-changes-prompt.continue": "Continua", + "navigation-zone.toggle-minimap": "Alterna minimappa", + "navigation-zone.zoom": "Zoom", + "focus-mode.toggle-focus-mode": "Alterna la modalità concentrazione", + "toast.close": "Chiudi", + "file-system.file-open-error.title": "Impossibile aprile il file", + "file-system.file-open-error.not-a-tldraw-file": "Il file che hai provato ad aprire non sembra essere un file tldraw.", + "file-system.file-open-error.file-format-version-too-new": "Il file che hai provato ad aprire viene da una nuova versione di tldraw. Ricarica la pagina e riprova.", + "file-system.file-open-error.generic-corrupted-file": "Il file che hai provato ad aprire è corrotto", + "file-system.confirm-open.title": "Sovrascrivere il progetto corrente?", + "file-system.confirm-open.description": "Aprire un file rimpiazzerà il tuo progetto attuale e ogni modifica non salvata verrà persa. Sei sicuro di voler continuare?", + "file-system.confirm-open.cancel": "Annulla", + "file-system.confirm-open.open": "Apri file", + "file-system.confirm-open.dont-show-again": "Non chiedere di nuovo", + "toast.error.export-fail.title": "Esportazione fallita", + "toast.error.export-fail.desc": "Esportazione dell'immagine fallita", + "toast.error.copy-fail.title": "Copia fallita", + "toast.error.copy-fail.desc": "Copia dell'immagine fallita", + "file-system.shared-document-file-open-error.title": "Impossibile aprire il file", + "file-system.shared-document-file-open-error.description": "L'apertura di file da progetti condivisi non è supportata", + "vscode.file-open.dont-show-again": "Non chiedere di nuovo", + "vscode.file-open.desc": "Questo file è stato creato con una versione precedente di tldraw. Desideri aggiornarlo per farlo funzionare con la nuova versione?", + "context.pages.new-page": "Nuova Pagina" +} \ No newline at end of file diff --git a/assets/translations/ja.json b/assets/translations/ja.json new file mode 100644 index 000000000..e2a38e59b --- /dev/null +++ b/assets/translations/ja.json @@ -0,0 +1,90 @@ +{ + "action.bring-forward": "ひとつ前に移動", + "action.bring-to-front": "最前面に移動", + "action.copy": "コピー", + "action.cut": "切り取り", + "action.delete": "削除", + "action.duplicate": "複製", + "action.flip-horizontal": "水平方向に反転", + "action.flip-vertical": "垂直方向に反転", + "action.flip-horizontal.short": "水平方向に反転", + "action.flip-vertical.short": "垂直方向に反転", + "action.group": "グルーピング", + "action.insert-media": "メディアをアップロード", + "action.paste": "貼り付け", + "action.redo": "やり直し", + "action.select-all": "すべて選択", + "action.select-none": "選択を解除", + "action.send-backward": "ひとつ後ろに移動", + "action.send-to-back": "最背面に移動", + "action.toggle-dark-mode.menu": "ダークモード", + "action.toggle-dark-mode": "ダークモード", + "action.toggle-debug-mode.menu": "デバッグモード", + "action.toggle-debug-mode": "デバッグモード", + "action.toggle-focus-mode.menu": "フォーカスモード", + "action.toggle-focus-mode": "フォーカスモード", + "action.toggle-grid.menu": "グリッドを表示", + "action.toggle-grid": "グリッドを表示", + "action.toggle-snap-mode.menu": "スナップを常に表示", + "action.toggle-snap-mode": "スナップを常に表示", + "action.undo": "元に戻す", + "action.ungroup": "グループ解除", + "action.zoom-in": "拡大", + "action.zoom-out": "縮小", + "action.zoom-to-fit": "拡大してすべてを表示", + "action.zoom-to-selection": "選択したアイテムに合わせて拡大", + "dash-style.draw": "描画", + "font-style.draw": "描画", + "geo-style.ellipse": "楕円形", + "geo-style.rectangle": "長方形", + "geo-style.triangle": "三角形", + "arrowheadStart-style.arrow": "矢印", + "arrowheadStart-style.triangle": "三角形", + "arrowheadEnd-style.arrow": "矢印", + "arrowheadEnd-style.triangle": "三角形", + "spline-style.line": "線", + "tool.select": "選択", + "tool.draw": "描画", + "tool.eraser": "消しゴム", + "tool.arrow": "矢印", + "tool.ellipse": "楕円形", + "tool.line": "線", + "tool.rectangle": "長方形", + "tool.triangle": "三角形", + "tool.note": "ふせん", + "tool.text": "テキスト", + "menu.copy-as": "形式を選択してコピー", + "menu.edit": "編集", + "menu.export-as": "形式を選択してエクスポート", + "menu.file": "ファイル", + "menu.language": "言語", + "menu.preferences": "設定", + "menu.view": "表示", + "context-menu.copy-as": "形式を選択してコピー", + "context-menu.export-as": "形式を選択してエクスポート", + "context-menu.move-to-page": "ページへ移動", + "page-menu.create-new-page": "ページを作成", + "page-menu.edit-start": "編集", + "page-menu.submenu.duplicate-page": "複製", + "page-menu.submenu.delete": "削除", + "share-menu.copy-link": "共有リンクをクリップボードにコピー", + "edit-link-dialog.cancel": "キャンセル", + "embed-dialog.cancel": "キャンセル", + "edit-pages-dialog.create-new-page": "ページを作成", + "edit-pages-dialog.delete": "削除", + "edit-pages-dialog.duplicate-page": "複製", + "shortcuts-dialog.edit": "編集", + "shortcuts-dialog.file": "ファイル", + "shortcuts-dialog.preferences": "設定", + "shortcuts-dialog.view": "表示", + "style-panel.title": "スタイル", + "style-panel.align": "配置", + "style-panel.color": "色", + "style-panel.dash": "線", + "style-panel.fill": "塗りつぶし", + "style-panel.font": "フォント", + "style-panel.size": "大きさ", + "style-panel.text": "テキスト", + "focus-mode.toggle-focus-mode": "フォーカスモード", + "file-system.confirm-open.cancel": "キャンセル" +} diff --git a/assets/translations/ko-kr.json b/assets/translations/ko-kr.json new file mode 100644 index 000000000..0cb320e87 --- /dev/null +++ b/assets/translations/ko-kr.json @@ -0,0 +1,339 @@ +{ + "action.convert-to-bookmark": "북마크로 변환", + "action.convert-to-embed": "임베드로 변환", + "action.open-embed-link": "링크 열기", + "action.align-bottom": "아래쪽 정렬", + "action.align-center-horizontal": "가로 정렬", + "action.align-center-vertical": "세로 정렬", + "action.align-center-horizontal.short": "가로 정렬", + "action.align-center-vertical.short": "세로 정렬", + "action.align-left": "왼쪽 정렬", + "action.align-right": "오른쪽 정렬", + "action.align-top": "위쪽 정렬", + "action.back-to-content": "콘텐츠로 돌아가기", + "action.bring-forward": "앞으로 가져오기", + "action.bring-to-front": "맨 앞으로 가져오기", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "JSON으로 복사", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "PNG로 복사", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "SVG로 복사", + "action.copy": "복사", + "action.cut": "자르기", + "action.delete": "삭제하기", + "action.distribute-horizontal": "가로로 균등 배치", + "action.distribute-vertical": "세로로 균등 배치", + "action.distribute-horizontal.short": "가로로 균등 배치", + "action.distribute-vertical.short": "세로로 균등 배치", + "action.duplicate": "복제하기", + "action.edit-link": "링크 수정", + "action.exit-pen-mode": "펜 모드 종료", + "action.export-as-json.short": "JSON", + "action.export-as-json": "JSON으로 내보내기", + "action.export-as-png.short": "PNG", + "action.export-as-png": "PNG로 내보내기", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "SVG로 내보내기", + "action.flip-horizontal": "가로로 뒤집기", + "action.flip-vertical": "세로로 뒤집기", + "action.flip-horizontal.short": "가로로 뒤집기", + "action.flip-vertical.short": "세로로 뒤집기", + "action.group": "그룹화", + "action.insert-media": "미디어 업로드", + "action.new-shared-project": "새 공유 프로젝트", + "action.nudge-down": "아래로 살짝 밀기", + "action.nudge-left": "왼쪽으로 살짝 밀기", + "action.nudge-right": "오른쪽으로 살짝 밀기", + "action.nudge-up": "위로 살짝 밀기", + "action.open-file": "파일 열기", + "action.pack": "그룹정렬", + "action.paste": "붙여넣기", + "action.print": "인쇄", + "action.redo": "다시 실행", + "action.rotate-ccw": "반시계 반향으로 회전", + "action.rotate-cw": "시계 방향으로 회전", + "action.save-copy": "사본 저장", + "action.select-all": "전체 선택", + "action.select-none": "선택 안함", + "action.send-backward": "뒤로 보내기", + "action.send-to-back": "맨 뒤로 보내기", + "action.share-project": "이 프로젝트 공유", + "action.stack-horizontal": "수평으로 쌓기", + "action.stack-vertical": "수직으로 쌓기", + "action.stack-horizontal.short": "수평으로 쌓기", + "action.stack-vertical.short": "수직으로 쌓기", + "action.stretch-horizontal": "수평으로 늘리기", + "action.stretch-vertical": "수직으로 늘리기", + "action.stretch-horizontal.short": "수평으로 늘리기", + "action.stretch-vertical.short": "수직으로 늘리기", + "action.toggle-auto-size": "자동 크기 전환", + "action.toggle-dark-mode.menu": "다크 모드", + "action.toggle-dark-mode": "다크모드 전환", + "action.toggle-debug-mode.menu": "디버그 모드", + "action.toggle-debug-mode": "디버그 모드 전환", + "action.toggle-focus-mode.menu": "포커스 모드", + "action.toggle-focus-mode": "포커스 모드 전환", + "action.toggle-grid.menu": "그리드 보기", + "action.toggle-grid": "그리드 전환", + "action.toggle-snap-mode.menu": "항상 스냅", + "action.toggle-snap-mode": "항상 스냅 전환", + "action.toggle-tool-lock.menu": "도구 잠금", + "action.toggle-tool-lock": "도구 잠금 전환", + "action.toggle-transparent.context-menu": "투명하게 하기", + "action.toggle-transparent.menu": "투명하게 하기", + "action.toggle-transparent": "투명 배경 전환", + "action.undo": "실행 취소", + "action.ungroup": "그룹화 해제", + "action.zoom-in": "확대", + "action.zoom-out": "축소", + "action.zoom-to-100": "100% 맞추기", + "action.zoom-to-fit": "전체에 맞추기", + "action.zoom-to-selection": "선택 요소에 맞추기", + "color-style.black": "검정", + "color-style.blue": "파랑", + "color-style.green": "초록", + "color-style.grey": "회색", + "color-style.light-blue": "하늘", + "color-style.light-green": "연두", + "color-style.light-red": "연홍", + "color-style.light-violet": "연보라", + "color-style.orange": "주황", + "color-style.red": "빨강", + "color-style.violet": "보라", + "color-style.yellow": "노랑", + "fill-style.none": "없음", + "fill-style.semi": "무색", + "fill-style.solid": "단색", + "fill-style.pattern": "패턴", + "dash-style.dashed": "파선", + "dash-style.dotted": "점선", + "dash-style.draw": "그린선", + "dash-style.solid": "선", + "size-style.s": "소", + "size-style.m": "중", + "size-style.l": "대", + "size-style.xl": "특대", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "필기체", + "font-style.sans": "돋움체", + "font-style.serif": "바탕체", + "font-style.mono": "고정폭", + "align-style.start": "왼쪽", + "align-style.middle": "가운데", + "align-style.end": "오른쪽", + "align-style.justify": "양쪽정렬", + "geo-style.arrow-down": "아래쪽 화살표", + "geo-style.arrow-left": "왼쪽 화살표", + "geo-style.arrow-right": "오른쪽 화살표", + "geo-style.arrow-up": "위쪽 화살표", + "geo-style.diamond": "마름모꼴", + "geo-style.ellipse": "원형", + "geo-style.hexagon": "육각형", + "geo-style.octagon": "팔각형", + "geo-style.oval": "타원형", + "geo-style.pentagon": "오각형", + "geo-style.rectangle": "사각형", + "geo-style.rhombus-2": "평행사변형", + "geo-style.rhombus": "평행사변형", + "geo-style.star": "별", + "geo-style.trapezoid": "사다리꼴", + "geo-style.triangle": "삼각형", + "geo-style.x-box": "X박스", + "arrowheadStart-style.none": "없음", + "arrowheadStart-style.arrow": "화살표", + "arrowheadStart-style.bar": "막대", + "arrowheadStart-style.diamond": "다이아몬드", + "arrowheadStart-style.dot": "원형", + "arrowheadStart-style.inverted": "역삼각형", + "arrowheadStart-style.pipe": "막대", + "arrowheadStart-style.square": "사각형", + "arrowheadStart-style.triangle": "삼각형", + "arrowheadEnd-style.none": "없음", + "arrowheadEnd-style.arrow": "화살표", + "arrowheadEnd-style.bar": "막대", + "arrowheadEnd-style.diamond": "다이아몬드", + "arrowheadEnd-style.dot": "원형", + "arrowheadEnd-style.inverted": "역삼각형", + "arrowheadEnd-style.pipe": "막대", + "arrowheadEnd-style.square": "사각형", + "arrowheadEnd-style.triangle": "삼각형", + "spline-style.line": "선", + "spline-style.cubic": "큐빅", + "tool.select": "선택", + "tool.hand": "핸드툴", + "tool.draw": "그리기", + "tool.eraser": "지우개", + "tool.arrow-down": "아래쪽 화살표", + "tool.arrow-left": "왼쪽 화살표", + "tool.arrow-right": "오른쪽 화살표", + "tool.arrow-up": "위쪽 화살표", + "tool.arrow": "화살표", + "tool.diamond": "마름모꼴", + "tool.ellipse": "원형", + "tool.hexagon": "육각형", + "tool.line": "선", + "tool.octagon": "팔각형", + "tool.oval": "타원", + "tool.pentagon": "오각형", + "tool.rectangle": "사각형", + "tool.rhombus": "마름모", + "tool.star": "별", + "tool.trapezoid": "사다리꼴", + "tool.triangle": "삼각형", + "tool.x-box": "X박스", + "tool.asset": "미디어", + "tool.frame": "프레임", + "tool.note": "메모", + "tool.embed": "임베드", + "tool.text": "문자", + "menu.title": "메뉴", + "menu.copy-as": "복사하기", + "menu.edit": "편집", + "menu.export-as": "내보내기", + "menu.file": "파일", + "menu.language": "언어", + "menu.preferences": "설정", + "menu.view": "보기", + "context-menu.arrange": "정돈", + "context-menu.copy-as": "복사하기", + "context-menu.export-as": "내보내기", + "context-menu.move-to-page": "페이지로 이동", + "context-menu.reorder": "재정렬", + "page-menu.title": "페이지", + "page-menu.create-new-page": "새 페이지 만들기", + "page-menu.edit-pages": "페이지 편집", + "page-menu.max-page-count-reached": "최대 페이지 도달", + "page-menu.new-page-initial-name": "페이지 1", + "page-menu.page": "페이지", + "page-menu.edit-start": "수정", + "page-menu.edit-done": "완료", + "page-menu.submenu.rename": "이름 바꾸기", + "page-menu.submenu.duplicate-page": "복제하기", + "page-menu.submenu.go-to-page": "페이지로 이동", + "page-menu.submenu.title": "메뉴", + "page-menu.submenu.move-down": "아래로 이동", + "page-menu.submenu.move-up": "위로 이동", + "page-menu.submenu.delete": "삭제하기", + "share-menu.title": "공유", + "share-menu.share-project": "이 프로젝트 공유하기", + "share-menu.create-project": "새 공유 프로젝트", + "share-menu.copy-link": "링크 복사", + "share-menu.readonly-link": "읽기 전용", + "share-menu.copy-readonly-link": "읽기 전용 링크 복사", + "share-menu.offline-note": "공유하기를 실행하면 공유 가능한 사본 URL이 생성됩니다. 해당 URL을 통해 최대 30명의 다른 사용자와 함께 프로젝트를 보고 편집할 수 있습니다.", + "share-menu.copy-link-note": "링크가 있는 사람은 누구나 이 프로젝트를 보고 편집할 수 있습니다.", + "share-menu.copy-readonly-link-note": "링크가 있는 사람은 누구나 이 프로젝트를 볼 수 있지만 편집할 수는 없습니다.", + "share-menu.project-too-large": "너무 큰 프로젝트는 공유할 수 없습니다. 현재 개선중이니 참고해주세요!", + "people-menu.title": "사용자", + "people-menu.change-name": "이름 변경", + "people-menu.change-color": "색상 변경", + "people-menu.user": "(나)", + "people-menu.invite": "다른 사용자 초대", + "debug-menu.hard-reset": "전체 초기화", + "debug-menu.create-shapes": "100개의 도형 만들기", + "help-menu.title": "도움말 및 자료", + "help-menu.about": "소개", + "help-menu.discord": "디스코드", + "help-menu.github": "깃허브", + "help-menu.keyboard-shortcuts": "키보드 단축키", + "help-menu.twitter": "트위터", + "links-menu.about": "정보", + "links-menu.discord": "디스코드", + "links-menu.github": "깃허브", + "links-menu.twitter": "트위터", + "actions-menu.title": "액션", + "edit-link-dialog.title": "링크 수정", + "edit-link-dialog.invalid-url": "링크는 유효한 URL이어야 합니다.", + "edit-link-dialog.detail": "새탭으로 링크가 열립니다.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "지우기", + "edit-link-dialog.save": "계속하기", + "edit-link-dialog.cancel": "취소", + "embed-dialog.title": "임베드 만들기", + "embed-dialog.url-label": "URL 붙여넣기", + "embed-dialog.back": "뒤로", + "embed-dialog.create": "생성하기", + "embed-dialog.cancel": "취소", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "임베드를 만들 사이트의 URL을 붙여넣습니다.", + "embed-dialog.invalid-url": "해당 URL에서 임베드를 만들 수 없습니다.", + "edit-pages-dialog.title": "페이지 편집", + "edit-pages-dialog.create-new-page": "새 페이지 만들기", + "edit-pages-dialog.delete": "삭제하기", + "edit-pages-dialog.duplicate-page": "복제하기", + "edit-pages-dialog.go-to-page": "페이지로 이동", + "edit-pages-dialog.max-page-count-reached": "최대 페이지 도달", + "edit-pages-dialog.more-menu": "메뉴", + "edit-pages-dialog.move-down": "아래로 이동", + "edit-pages-dialog.move-up": "위로 이동", + "edit-pages-dialog.new-page-initial-name": "페이지 1", + "reload-file-dialog.title": "파일 수정 계속하기", + "reload-file-dialog.description": "수정중인 파일이 있습니다. 계속 수정하시겠습니까?", + "reload-file-dialog.failure": "파일을 로드하지 못했습니다. 다시 시도하십시오?", + "reload-file-dialog.reload": "계속 수정", + "reload-file-dialog.revert": "진행안함", + "shortcuts-dialog.title": "키보드 단축키", + "shortcuts-dialog.edit": "편집", + "shortcuts-dialog.file": "파일", + "shortcuts-dialog.preferences": "설정", + "shortcuts-dialog.tools": "도구", + "shortcuts-dialog.transform": "변환", + "shortcuts-dialog.view": "보기", + "shortcuts-dialog.save": "계속", + "style-panel.title": "스타일", + "style-panel.align": "정렬", + "style-panel.arrowheads": "화살촉", + "style-panel.color": "색깔", + "style-panel.dash": "테두리", + "style-panel.fill": "채우기", + "style-panel.font": "글꼴", + "style-panel.geo": "모양", + "style-panel.label": "라벨", + "style-panel.mixed": "혼합된", + "style-panel.opacity": "불투명도", + "style-panel.size": "크기", + "style-panel.spline": "스플라인 곡선", + "style-panel.text": "문자", + "tool-panel.drawing": "그리기", + "tool-panel.geo": "모양", + "tool-panel.shapes": "모양", + "tool-panel.things": "개체", + "tool-panel.tools": "도구", + "save-changes-prompt.title": "저장되지 않은 변경사항이 있습니다.", + "save-changes-prompt.description": "현재 파일에 변경 사항을 저장하시겠습니까?", + "save-changes-prompt.go-back": "뒤로가기", + "save-changes-prompt.continue": "계속", + "navigation-zone.toggle-minimap": "미니맵 전환", + "navigation-zone.zoom": "확대/축소", + "focus-mode.toggle-focus-mode": "포커스 모드 전환", + "toast.close": "닫기", + "file-system.file-open-error.title": "파일을 열 수 없습니다.", + "file-system.file-open-error.not-a-tldraw-file": "해당 파일은 tldraw 파일이 아닙니다.", + "file-system.file-open-error.file-format-version-too-new": "해당 파일은 더 최신 버전의 tldraw 파일 입니다. 페이지를 새로고침하고 다시 시도하세요.", + "file-system.file-open-error.generic-corrupted-file": "해당 파일이 손상되었습니다.", + "file-system.confirm-open.title": "현재 프로젝트를 덮어쓰시겠습니까?", + "file-system.confirm-open.description": "파일을 열면 현재 프로젝트가 대체되고 저장되지 않은 변경 내용은 손실됩니다. 계속하시겠습니까?", + "file-system.confirm-open.cancel": "취소", + "file-system.confirm-open.open": "파일 열기", + "file-system.confirm-open.dont-show-again": "다시 묻지 않음", + "toast.error.export-fail.title": "내보내기 실패", + "toast.error.export-fail.desc": "이미지를 내보내지 못했습니다.", + "toast.error.copy-fail.title": "복사 실패", + "toast.error.copy-fail.desc": "이미지를 복사하지 못했습니다.", + "file-system.shared-document-file-open-error.title": "파일을 열 수 없습니다.", + "file-system.shared-document-file-open-error.description": "공유 프로젝트에서 파일 열기는 지원되지 않습니다.", + "vscode.file-open.dont-show-again": "다시 묻지 않음", + "vscode.file-open.desc": "이 파일은 이전 버전의 tldraw로 생성되었습니다. 새 버전에서 작동하도록 업데이트하시겠습니까?", + "context.pages.new-page": "새 페이지", + "style-panel.arrowhead-start": "시작 모양", + "style-panel.arrowhead-end": "끝 모양", + "vscode.file-open.open": "계속하기", + "vscode.file-open.backup": "백업", + "vscode.file-open.backup-saved": "백업 저장완료", + "vscode.file-open.backup-failed": "백업에 실패했습니다: .tldr 파일이 아닙니다." +} \ No newline at end of file diff --git a/assets/translations/ku.json b/assets/translations/ku.json new file mode 100644 index 000000000..4cc4250e9 --- /dev/null +++ b/assets/translations/ku.json @@ -0,0 +1,99 @@ +{ + "action.bring-forward": "بڕۆ پێشەوە", + "action.bring-to-front": "بگوازرێتەوە بۆ پێشەوە", + "action.copy": "کۆپی بکە", + "action.cut": "بڕین", + "action.delete": "سڕینەوە", + "action.duplicate": "دووبارەکردنەوە", + "action.flip-horizontal": "ئاسۆیی وەرگەڕاندن", + "action.flip-vertical": "ستونی وەرگەڕاندن", + "action.flip-horizontal.short": "ئاسۆیی وەرگەڕاندن", + "action.flip-vertical.short": "ستونی وەرگەڕاندن", + "action.group": "کۆمەڵە", + "action.insert-media": "داگرتنی میدیا", + "action.paste": "پەیست بکە", + "action.redo": "دووبارە بیکەرەوە", + "action.select-all": "هەموویان هەڵبژێره", + "action.select-none": "هیچ هه‌ڵمه‌بژێره‌", + "action.send-backward": "بەرەو دواوە", + "action.send-to-back": "بچۆ بۆ پشتەوە", + "action.toggle-dark-mode.menu": "دۆخی تاریک", + "action.toggle-dark-mode": "دۆخی تاریک", + "action.toggle-debug-mode.menu": "مۆدی هەڵەدۆزین", + "action.toggle-debug-mode": "مۆدی هەڵەدۆزین", + "action.toggle-focus-mode.menu": "دۆخی فۆکەس", + "action.toggle-focus-mode": "دۆخی فۆکەس", + "action.toggle-grid.menu": "تۆڕی پیشان بدە", + "action.toggle-grid": "تۆڕی پیشان بدە", + "action.toggle-snap-mode.menu": "هەمیشە وێنەی خێرا پیشان بدە", + "action.toggle-snap-mode": "هەمیشە وێنەی خێرا پیشان بدە", + "action.toggle-transparent.context-menu": "ڕوون", + "action.toggle-transparent.menu": "ڕوون", + "action.undo": "پاشەکشە بکە", + "action.ungroup": "لابردنی کۆمەڵە", + "action.zoom-in": "هێنانە پێشەوە", + "action.zoom-out": "دوور خستنەوە", + "action.zoom-to-fit": "زووم بکە بۆ ئەوەی لەگەڵیدا بگونجێت", + "action.zoom-to-selection": "زووم بکە بۆ هەڵبژاردن", + "dash-style.draw": "وێنەکێشان", + "font-style.draw": "وێنەکێشان", + "geo-style.ellipse": "بیبلی", + "geo-style.rectangle": "لاکێشە", + "geo-style.triangle": "سێگۆشە", + "arrowheadStart-style.arrow": "تیر", + "arrowheadStart-style.triangle": "سێگۆشە", + "arrowheadEnd-style.arrow": "تیر", + "arrowheadEnd-style.triangle": "سێگۆشە", + "spline-style.line": "هێڵ", + "tool.select": "دەسنیاشنکردن", + "tool.draw": "وێنەکێشان", + "tool.eraser": "سڕەرەوە", + "tool.arrow": "تیر", + "tool.ellipse": "بیبلی", + "tool.line": "هێڵ", + "tool.rectangle": "لاکێشە", + "tool.triangle": "سێگۆشە", + "tool.note": "چەسپاو", + "tool.text": "دەق", + "menu.copy-as": "کۆپی وەک", + "menu.edit": "دەستکاری", + "menu.export-as": "هەناردەکردن وەک", + "menu.file": "فایلێک", + "menu.language": "زمان", + "menu.preferences": "خواست", + "menu.view": "دیمەن", + "context-menu.copy-as": "کۆپی وەک", + "context-menu.export-as": "هەناردەکردن وەک", + "context-menu.move-to-page": "بچۆ بۆ لاپەڕە", + "page-menu.create-new-page": "دروستکردنی لاپەڕە", + "page-menu.page": "لاپەڕە", + "page-menu.edit-start": "دەستکاری", + "page-menu.submenu.duplicate-page": "دووبارەکردنەوە", + "page-menu.submenu.delete": "سڕینەوە", + "share-menu.copy-link": "لینکی بانگهێشتکردن کۆپی بکە", + "share-menu.copy-readonly-link": "بە شێوەیەکی هەڕەمەکی کۆپی بکە", + "help-menu.keyboard-shortcuts": "کورتکراوەکانی تەختەکلیل", + "edit-link-dialog.cancel": "ڕەتکردنەوە", + "embed-dialog.cancel": "ڕەتکردنەوە", + "edit-pages-dialog.create-new-page": "دروستکردنی لاپەڕە", + "edit-pages-dialog.delete": "سڕینەوە", + "edit-pages-dialog.duplicate-page": "دووبارەکردنەوە", + "shortcuts-dialog.title": "کورتکراوەکانی تەختەکلیل", + "shortcuts-dialog.edit": "دەستکاری", + "shortcuts-dialog.file": "فایلێک", + "shortcuts-dialog.preferences": "خواست", + "shortcuts-dialog.tools": "ئامرازەکان", + "shortcuts-dialog.transform": "گۆڕین", + "shortcuts-dialog.view": "دیمەن", + "style-panel.title": "نەخشەکان", + "style-panel.align": "ڕێکخستن", + "style-panel.color": "رەنگ", + "style-panel.dash": "لەت لەت", + "style-panel.fill": "پڕکردنەوە", + "style-panel.font": "هێڵ", + "style-panel.size": "قەبارە", + "style-panel.text": "دەق", + "tool-panel.tools": "ئامرازەکان", + "focus-mode.toggle-focus-mode": "دۆخی فۆکەس", + "file-system.confirm-open.cancel": "ڕەتکردنەوە" +} diff --git a/assets/translations/languages.json b/assets/translations/languages.json new file mode 100644 index 000000000..512588c41 --- /dev/null +++ b/assets/translations/languages.json @@ -0,0 +1,134 @@ +[ + { + "locale": "ar", + "label": "عربي" + }, + { + "locale": "ca", + "label": "Català" + }, + { + "locale": "da", + "label": "Danish" + }, + { + "locale": "de", + "label": "Deutsch" + }, + { + "locale": "en", + "label": "English" + }, + { + "locale": "es", + "label": "Español" + }, + { + "locale": "fa", + "label": "فارسی" + }, + { + "locale": "fi", + "label": "Suomi" + }, + { + "locale": "fr", + "label": "Français" + }, + { + "locale": "gl", + "label": "Galego" + }, + { + "locale": "he", + "label": "עברית" + }, + { + "locale": "it", + "label": "Italiano" + }, + { + "locale": "ja", + "label": "日本語" + }, + { + "locale": "ko-kr", + "label": "한국어" + }, + { + "locale": "ku", + "label": "کوردی" + }, + { + "locale": "hi-in", + "label": "हिन्दी" + }, + { + "locale": "hu", + "label": "Magyar" + }, + { + "locale": "my", + "label": "မြန်မာစာ" + }, + { + "locale": "ne", + "label": "नेपाली" + }, + { + "locale": "no", + "label": "Norwegian" + }, + { + "locale": "pl", + "label": "Polski" + }, + { + "locale": "pt-br", + "label": "Português - Brasil" + }, + { + "locale": "pt-pt", + "label": "Português - Europeu" + }, + { + "locale": "ro", + "label": "Română" + }, + { + "locale": "ru", + "label": "Russian" + }, + { + "locale": "sv", + "label": "Svenska" + }, + { + "locale": "te", + "label": "తెలుగు" + }, + { + "locale": "th", + "label": "ภาษาไทย" + }, + { + "locale": "tr", + "label": "Türkçe" + }, + { + "locale": "uk", + "label": "Ukrainian" + }, + { + "locale": "vi", + "label": "Tiếng Việt" + }, + { + "locale": "zh-cn", + "label": "Chinese - Simplified" + }, + { + "locale": "zh-tw", + "label": "繁體中文 (台灣)" + } +] diff --git a/assets/translations/main.json b/assets/translations/main.json new file mode 100644 index 000000000..d275bf44d --- /dev/null +++ b/assets/translations/main.json @@ -0,0 +1,316 @@ +{ + "action.convert-to-bookmark": "Convert to Bookmark", + "action.convert-to-embed": "Convert to Embed", + "action.open-embed-link": "Open link", + "action.align-bottom": "Align bottom", + "action.align-center-horizontal": "Align horizontally", + "action.align-center-vertical": "Align vertically", + "action.align-center-horizontal.short": "Align H", + "action.align-center-vertical.short": "Align V", + "action.align-left": "Align left", + "action.align-right": "Align right", + "action.align-top": "Align top", + "action.back-to-content": "Back to content", + "action.bring-forward": "Bring forward", + "action.bring-to-front": "Bring to front", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Copy as JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Copy as PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Copy as SVG", + "action.copy": "Copy", + "action.cut": "Cut", + "action.delete": "Delete", + "action.distribute-horizontal": "Distribute horizontally", + "action.distribute-vertical": "Distribute vertically", + "action.distribute-horizontal.short": "Distribute H", + "action.distribute-vertical.short": "Distribute V", + "action.duplicate": "Duplicate", + "action.edit-link": "Edit link", + "action.exit-pen-mode": "Exit pen mode", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Export as JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Export as PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Export as SVG", + "action.flip-horizontal": "Flip horizontally", + "action.flip-vertical": "Flip vertically", + "action.flip-horizontal.short": "Flip H", + "action.flip-vertical.short": "Flip V", + "action.group": "Group", + "action.insert-embed": "Insert embed", + "action.insert-media": "Upload media", + "action.new-project": "New project", + "action.new-shared-project": "New shared project", + "action.open-file": "Open file", + "action.pack": "Pack", + "action.paste": "Paste", + "action.print": "Print", + "action.redo": "Redo", + "action.rotate-ccw": "Rotate counterclockwise", + "action.rotate-cw": "Rotate clockwise", + "action.save-copy": "Save a copy", + "action.select-all": "Select all", + "action.select-none": "Select none", + "action.send-backward": "Send backward", + "action.send-to-back": "Send to back", + "action.share-project": "Share this project", + "action.stack-horizontal": "Stack horizontally", + "action.stack-vertical": "Stack vertically", + "action.stack-horizontal.short": "Stack H", + "action.stack-vertical.short": "Stack V", + "action.stop-following": "Stop following", + "action.stretch-horizontal": "Stretch horizontally", + "action.stretch-vertical": "Stretch vertically", + "action.stretch-horizontal.short": "Stretch H", + "action.stretch-vertical.short": "Stretch V", + "action.toggle-auto-size": "Toggle auto size", + "action.toggle-dark-mode.menu": "Dark mode", + "action.toggle-dark-mode": "Toggle dark mode", + "action.toggle-debug-mode.menu": "Debug mode", + "action.toggle-debug-mode": "Toggle debug mode", + "action.toggle-focus-mode.menu": "Focus mode", + "action.toggle-focus-mode": "Toggle focus mode", + "action.toggle-grid.menu": "Show grid", + "action.toggle-grid": "Toggle grid", + "action.toggle-snap-mode.menu": "Always snap", + "action.toggle-snap-mode": "Toggle always snap", + "action.toggle-tool-lock.menu": "Tool lock", + "action.toggle-tool-lock": "Toggle tool lock", + "action.toggle-transparent.context-menu": "Transparent", + "action.toggle-transparent.menu": "Transparent", + "action.toggle-transparent": "Toggle transparent background", + "action.undo": "Undo", + "action.ungroup": "Ungroup", + "action.zoom-in": "Zoom in", + "action.zoom-out": "Zoom out", + "action.zoom-to-100": "Zoom to 100%", + "action.zoom-to-fit": "Zoom to fit", + "action.zoom-to-selection": "Zoom to selection", + "color-style.black": "Black", + "color-style.blue": "Blue", + "color-style.green": "Green", + "color-style.grey": "Grey", + "color-style.light-blue": "Light blue", + "color-style.light-green": "Light green", + "color-style.light-red": "Light red", + "color-style.light-violet": "Light violet", + "color-style.orange": "Orange", + "color-style.red": "Red", + "color-style.violet": "Violet", + "color-style.yellow": "Yellow", + "fill-style.none": "None", + "fill-style.semi": "Semi", + "fill-style.solid": "Solid", + "fill-style.pattern": "Pattern", + "dash-style.dashed": "Dashed", + "dash-style.dotted": "Dotted", + "dash-style.draw": "Draw", + "dash-style.solid": "Solid", + "size-style.s": "Small", + "size-style.m": "Medium", + "size-style.l": "Large", + "size-style.xl": "Extra large", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Draw", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Start", + "align-style.middle": "Middle", + "align-style.end": "End", + "align-style.justify": "Justify", + "geo-style.arrow-down": "Arrow down", + "geo-style.arrow-left": "Arrow left", + "geo-style.arrow-right": "Arrow right", + "geo-style.arrow-up": "Arrow up", + "geo-style.diamond": "Diamond", + "geo-style.ellipse": "Ellipse", + "geo-style.hexagon": "Hexagon", + "geo-style.octagon": "Octagon", + "geo-style.oval": "Oval", + "geo-style.pentagon": "Pentagon", + "geo-style.rectangle": "Rectangle", + "geo-style.rhombus-2": "Rhombus 2", + "geo-style.rhombus": "Rhombus", + "geo-style.star": "Star", + "geo-style.trapezoid": "Trapezoid", + "geo-style.triangle": "Triangle", + "geo-style.x-box": "X Box", + "arrowheadStart-style.none": "None", + "arrowheadStart-style.arrow": "Arrow", + "arrowheadStart-style.bar": "Bar", + "arrowheadStart-style.diamond": "Diamond", + "arrowheadStart-style.dot": "Dot", + "arrowheadStart-style.inverted": "Inverted", + "arrowheadStart-style.pipe": "Pipe", + "arrowheadStart-style.square": "Square", + "arrowheadStart-style.triangle": "Triangle", + "arrowheadEnd-style.none": "None", + "arrowheadEnd-style.arrow": "Arrow", + "arrowheadEnd-style.bar": "Bar", + "arrowheadEnd-style.diamond": "Diamond", + "arrowheadEnd-style.dot": "Dot", + "arrowheadEnd-style.inverted": "Inverted", + "arrowheadEnd-style.pipe": "Pipe", + "arrowheadEnd-style.square": "Square", + "arrowheadEnd-style.triangle": "Triangle", + "spline-style.line": "Line", + "spline-style.cubic": "Cubic", + "tool.select": "Select", + "tool.hand": "Hand", + "tool.draw": "Draw", + "tool.eraser": "Eraser", + "tool.arrow-down": "Arrow down", + "tool.arrow-left": "Arrow left", + "tool.arrow-right": "Arrow right", + "tool.arrow-up": "Arrow up", + "tool.arrow": "Arrow", + "tool.diamond": "Diamond", + "tool.ellipse": "Ellipse", + "tool.hexagon": "Hexagon", + "tool.line": "Line", + "tool.octagon": "Octagon", + "tool.oval": "Oval", + "tool.pentagon": "Pentagon", + "tool.rectangle": "Rectangle", + "tool.rhombus": "Rhombus", + "tool.star": "Star", + "tool.trapezoid": "Trapezoid", + "tool.triangle": "Triangle", + "tool.x-box": "X box", + "tool.asset": "Asset", + "tool.frame": "Frame", + "tool.note": "Note", + "tool.embed": "Embed", + "tool.text": "Text", + "menu.title": "Menu", + "menu.copy-as": "Copy as", + "menu.edit": "Edit", + "menu.export-as": "Export as", + "menu.file": "File", + "menu.language": "Language", + "menu.preferences": "Preferences", + "menu.view": "View", + "context-menu.arrange": "Arrange", + "context-menu.copy-as": "Copy as", + "context-menu.export-as": "Export as", + "context-menu.move-to-page": "Move to page", + "context-menu.reorder": "Reorder", + "page-menu.title": "Pages", + "page-menu.create-new-page": "Create new page", + "page-menu.max-page-count-reached": "Max pages reached", + "page-menu.new-page-initial-name": "Page 1", + "page-menu.edit-start": "Edit", + "page-menu.edit-done": "Done", + "page-menu.go-to-page": "Go to page", + "page-menu.submenu.rename": "Rename", + "page-menu.submenu.duplicate-page": "Duplicate", + "page-menu.submenu.title": "Menu", + "page-menu.submenu.move-down": "Move down", + "page-menu.submenu.move-up": "Move up", + "page-menu.submenu.delete": "Delete", + "share-menu.title": "Share", + "share-menu.share-project": "Share this project", + "share-menu.copy-link": "Copy link", + "share-menu.readonly-link": "Read-only", + "share-menu.copy-readonly-link": "Copy read-only link", + "share-menu.offline-note": "Sharing this project will create a hosted live copy at a new URL. You can share the URL with up to thirty other people to view and edit the project together.", + "share-menu.copy-link-note": "Anyone with the link will be able to view and edit this project.", + "share-menu.copy-readonly-link-note": "Anyone with the link will be able to view (but not edit) this project.", + "share-menu.project-too-large": "Sorry, this project can't be shared because it's too large. We're working on it!", + "people-menu.title": "People", + "people-menu.change-name": "Change name", + "people-menu.change-color": "Change color", + "people-menu.follow": "Following", + "people-menu.following": "Following", + "people-menu.leading": "Following You", + "people-menu.user": "(You)", + "people-menu.invite": "Invite others", + "help-menu.title": "Help and resources", + "help-menu.about": "About", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Keyboard shortcuts", + "help-menu.twitter": "Twitter", + "actions-menu.title": "Actions", + "edit-link-dialog.title": "Edit link", + "edit-link-dialog.invalid-url": "A link must be a valid URL.", + "edit-link-dialog.detail": "Links will open in a new tab.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Clear", + "edit-link-dialog.save": "Continue", + "edit-link-dialog.cancel": "Cancel", + "embed-dialog.title": "Insert embed", + "embed-dialog.back": "Back", + "embed-dialog.create": "Create", + "embed-dialog.cancel": "Cancel", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Paste in the site's URL to create the embed.", + "embed-dialog.invalid-url": "We could not create an embed from that URL.", + "edit-pages-dialog.move-down": "Move down", + "edit-pages-dialog.move-up": "Move up", + "shortcuts-dialog.title": "Keyboard shortcuts", + "shortcuts-dialog.edit": "Edit", + "shortcuts-dialog.file": "File", + "shortcuts-dialog.preferences": "Preferences", + "shortcuts-dialog.tools": "Tools", + "shortcuts-dialog.transform": "Transform", + "shortcuts-dialog.view": "View", + "style-panel.title": "Styles", + "style-panel.align": "Align", + "style-panel.position": "Position", + "style-panel.arrowheads": "Arrowheads", + "style-panel.arrowhead-start": "Start", + "style-panel.arrowhead-end": "End", + "style-panel.color": "Color", + "style-panel.dash": "Dash", + "style-panel.fill": "Fill", + "style-panel.font": "Font", + "style-panel.geo": "Shape", + "style-panel.mixed": "Mixed", + "style-panel.opacity": "Opacity", + "style-panel.size": "Size", + "style-panel.spline": "Spline", + "tool-panel.drawing": "Drawing", + "tool-panel.shapes": "Shapes", + "tool-panel.more": "More", + "debug-panel.more": "More", + "navigation-zone.toggle-minimap": "Toggle minimap", + "navigation-zone.zoom": "Zoom", + "focus-mode.toggle-focus-mode": "Toggle focus mode", + "toast.close": "Close", + "file-system.file-open-error.title": "Could not open file", + "file-system.file-open-error.not-a-tldraw-file": "The file you tried to open doesn't look like a tldraw file.", + "file-system.file-open-error.file-format-version-too-new": "The file you tried to open is from a newer version of tldraw. Please reload the page and try again.", + "file-system.file-open-error.generic-corrupted-file": "The file you tried to open is corrupted.", + "file-system.confirm-open.title": "Overwrite current project?", + "file-system.confirm-open.description": "Opening a file will replace your current project and any unsaved changes will be lost. Are you sure you want to continue?", + "file-system.confirm-open.cancel": "Cancel", + "file-system.confirm-open.open": "Open file", + "file-system.confirm-open.dont-show-again": "Don't ask again", + "file-system.confirm-clear.title": "Clear current project?", + "file-system.confirm-clear.description": "Creating a new project will clear your current project and any unsaved changes will be lost. Are you sure you want to continue?", + "file-system.confirm-clear.cancel": "Cancel", + "file-system.confirm-clear.continue": "Continue", + "file-system.confirm-clear.dont-show-again": "Don't ask again", + "file-system.shared-document-file-open-error.title": "Could not open file", + "file-system.shared-document-file-open-error.description": "Opening files from shared projects is not supported.", + "toast.error.export-fail.title": "Failed export", + "toast.error.export-fail.desc": "Failed to export image", + "toast.error.copy-fail.title": "Failed copy", + "toast.error.copy-fail.desc": "Failed to copy image", + "context.pages.new-page": "New page", + "vscode.file-open.desc": "We've updated this document to work with the current version of tldraw. If you'd like to keep the original version (which will work on old.tldraw.com), click below to create a backup.", + "vscode.file-open.open": "Continue", + "vscode.file-open.backup": "Backup", + "vscode.file-open.backup-saved": "Backup saved", + "vscode.file-open.backup-failed": "Backup failed: this is not a .tldr file.", + "vscode.file-open.dont-show-again": "Don't ask again" +} \ No newline at end of file diff --git a/assets/translations/my.json b/assets/translations/my.json new file mode 100644 index 000000000..2b0e0195d --- /dev/null +++ b/assets/translations/my.json @@ -0,0 +1,121 @@ +{ + "action.align-bottom": "အောက်သို့ ညှိရန်", + "action.align-center-horizontal": "အလျားလိုက် ဗဟိုဆီသို့ ညှိရန်", + "action.align-center-vertical": "ဒေါင်လိုက် ဗဟိုဆီသို့ ညှိရန်", + "action.align-center-horizontal.short": "အလျားလိုက် ဗဟိုဆီသို့ ညှိရန်", + "action.align-center-vertical.short": "ဒေါင်လိုက် ဗဟိုဆီသို့ ညှိရန်", + "action.align-left": "ဘယ်ဖက်သို့ ညှိရန်", + "action.align-right": "ညာဖက်သို့ ညှိရန်", + "action.align-top": "အပေါ်သို့ ညှိရန်", + "action.bring-forward": "ရှေ့သို့ တစ်ဆင့်ပို့မည်", + "action.bring-to-front": "ရှေ့ဆုံးသို့ ပို့မည်", + "action.copy": "ကူးယူ", + "action.cut": "ဖြတ်ယူ", + "action.delete": "ဖျက်မည်", + "action.distribute-horizontal": "အလျားလိုက် ဖြန့်ရန်", + "action.distribute-vertical": "ဒေါင်လိုက် ဖြန့်ရန်", + "action.distribute-horizontal.short": "အလျားလိုက် ဖြန့်ရန်", + "action.distribute-vertical.short": "ဒေါင်လိုက် ဖြန့်ရန်", + "action.duplicate": "ပွားမည်", + "action.flip-horizontal": "အလျားလိုက် လှန်မည်", + "action.flip-vertical": "ဒေါင်လိုက် လှန်မည်", + "action.flip-horizontal.short": "အလျားလိုက် လှန်မည်", + "action.flip-vertical.short": "ဒေါင်လိုက် လှန်မည်", + "action.group": "အုပ်စုဖွဲ့", + "action.insert-media": "မီဒီယာဖိုင်များ တင်မည်", + "action.paste": "ကူးသွင်း", + "action.redo": "ပြန်လုပ်ရန်", + "action.select-all": "အားလုံးကို ရွေးချယ်ရန်", + "action.select-none": "တစ်ခုမှ မရွေးတော့ပါ", + "action.send-backward": "နောက်သို့ တစ်ဆင့်ပို့မည်", + "action.send-to-back": "နောက်ဆုံးသို့ ပို့မည်", + "action.stretch-horizontal": "အလျားလိုက် ဆွဲဆန့်ရန်", + "action.stretch-vertical": "ဒေါင်လိုက် ဆွဲဆန့်ရန်", + "action.stretch-horizontal.short": "အလျားလိုက် ဆွဲဆန့်ရန်", + "action.stretch-vertical.short": "ဒေါင်လိုက် ဆွဲဆန့်ရန်", + "action.toggle-dark-mode.menu": "အမှောင် မုဒ်", + "action.toggle-dark-mode": "အမှောင် မုဒ်", + "action.toggle-debug-mode.menu": "စမ်းသပ် မုဒ်", + "action.toggle-debug-mode": "စမ်းသပ် မုဒ်", + "action.toggle-focus-mode.menu": "ရှင်းရှင်းလင်းလင်း မုဒ်", + "action.toggle-focus-mode": "ရှင်းရှင်းလင်းလင်း မုဒ်", + "action.toggle-grid.menu": "နောက်ခံ ဇယားကွက်ပြရန်", + "action.toggle-grid": "နောက်ခံ ဇယားကွက်ပြရန်", + "action.toggle-snap-mode.menu": "Always Show Snaps", + "action.toggle-snap-mode": "Always Show Snaps", + "action.toggle-transparent.context-menu": "နောက်ခံ အကြည်", + "action.toggle-transparent.menu": "နောက်ခံ အကြည်", + "action.undo": "နဂိုမူလသို့ ပြန်လုပ်ရန်", + "action.ungroup": "အုပ်စုခွဲ", + "action.zoom-in": "အကြီးချဲ့မည်", + "action.zoom-out": "ပြန်ကျဥ်းမည်", + "action.zoom-to-fit": "အံကိုက်ဖြစ်အောင် ချဲ့မည်", + "action.zoom-to-selection": "ရွေးထားသော နေရာသို့ အာရုံပြုမည်", + "dash-style.draw": "ခဲတံ", + "font-style.draw": "ခဲတံ", + "geo-style.ellipse": "ဘဲဥ", + "geo-style.rectangle": "လေထောင့်", + "geo-style.triangle": "တြိဂံ", + "arrowheadStart-style.arrow": "မြှား", + "arrowheadStart-style.triangle": "တြိဂံ", + "arrowheadEnd-style.arrow": "မြှား", + "arrowheadEnd-style.triangle": "တြိဂံ", + "spline-style.line": "မျဥ်း", + "tool.select": "ရွေးချယ်မှု", + "tool.draw": "ခဲတံ", + "tool.eraser": "ခဲဖျက်", + "tool.arrow": "မြှား", + "tool.ellipse": "ဘဲဥ", + "tool.line": "မျဥ်း", + "tool.rectangle": "လေထောင့်", + "tool.triangle": "တြိဂံ", + "tool.note": "ကပ်ခွာမှတ်စု", + "tool.text": "စာသား", + "menu.copy-as": "ကူးယူမည့် ပုံစံ", + "menu.edit": "ပြုပြင်", + "menu.export-as": "ထုတ်ချင်သည့် ပုံစံ", + "menu.file": "ဖိုင်", + "menu.language": "ဘာသာစကား", + "menu.preferences": "ဆက်တင်", + "menu.view": "အမြင်", + "context-menu.copy-as": "ကူးယူမည့် ပုံစံ", + "context-menu.export-as": "ထုတ်ချင်သည့် ပုံစံ", + "context-menu.move-to-page": "စာမျက်နှာတစ်ခုသို့ ရွေးမည်", + "page-menu.create-new-page": "စာမျက်နှာ အသစ်ဖွင့်မည်", + "page-menu.page": "စာမျက်နှာ", + "page-menu.edit-start": "ပြုပြင်", + "page-menu.submenu.duplicate-page": "ပွားမည်", + "page-menu.submenu.delete": "ဖျက်မည်", + "share-menu.copy-link": "ဖိတ်ကြားရန် လင့်ခ်ကူးယူမည်", + "share-menu.copy-readonly-link": "ကြည့်ရူရန်အတွက်သာ လင့်ခ်ကူးယူမည်", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Keyboard shortcuts", + "help-menu.twitter": "Twitter", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "edit-link-dialog.cancel": "မလုပ်တော့ပါ", + "embed-dialog.cancel": "မလုပ်တော့ပါ", + "edit-pages-dialog.create-new-page": "စာမျက်နှာ အသစ်ဖွင့်မည်", + "edit-pages-dialog.delete": "ဖျက်မည်", + "edit-pages-dialog.duplicate-page": "ပွားမည်", + "shortcuts-dialog.title": "Keyboard shortcuts", + "shortcuts-dialog.edit": "ပြုပြင်", + "shortcuts-dialog.file": "ဖိုင်", + "shortcuts-dialog.preferences": "ဆက်တင်", + "shortcuts-dialog.tools": "ကိရိယာများ", + "shortcuts-dialog.transform": "ပြောင်းလည်ရန်", + "shortcuts-dialog.view": "အမြင်", + "style-panel.title": "စတိုင်", + "style-panel.align": "အထားအသို", + "style-panel.color": "အရောင်", + "style-panel.dash": "မျဥ်းစက်", + "style-panel.fill": "အရောင် ထည့်မည်", + "style-panel.font": "စာသားဖောင့်", + "style-panel.size": "အရွယ်အစား", + "style-panel.text": "စာသား", + "tool-panel.tools": "ကိရိယာများ", + "focus-mode.toggle-focus-mode": "ရှင်းရှင်းလင်းလင်း မုဒ်", + "file-system.confirm-open.cancel": "မလုပ်တော့ပါ" +} diff --git a/assets/translations/ne.json b/assets/translations/ne.json new file mode 100644 index 000000000..66af0b431 --- /dev/null +++ b/assets/translations/ne.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "बुकमार्कमा रूपान्तरण गर्नुहोस्", + "action.convert-to-embed": "एम्बेडमा रूपान्तरण गर्नुहोस्", + "action.open-embed-link": "लिङ्क खोल्नुहोस्", + "action.align-bottom": "तल पङ्क्तिबद्ध गर्नुहोस्", + "action.align-center-horizontal": "तेर्सो रूपमा पङ्क्तिबद्ध गर्नुहोस्", + "action.align-center-vertical": "ठाडो रूपमा पङ्क्तिबद्ध गर्नुहोस्", + "action.align-center-horizontal.short": "तेर्सो रूपमा पङ्क्तिबद्ध गर्नुहोस्", + "action.align-center-vertical.short": "ठाडो रूपमा पङ्क्तिबद्ध गर्नुहोस्", + "action.align-left": "बायाँ पङ्क्तिबद्ध गर्नुहोस्", + "action.align-right": "दायाँ पङ्क्तिबद्ध गर्नुहोस्", + "action.align-top": "माथि पङ्क्तिबद्ध गर्नुहोस्", + "action.back-to-content": "सामग्रीमा फर्कनुहोस्", + "action.bring-forward": "अगाडि ल्याउनुहोस्", + "action.bring-to-front": "सबैभन्दा अगाडि ल्याउनुहोस्", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "JSON को रूपमा प्रतिलिपि गर्नुहोस्", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "PNG को रूपमा प्रतिलिपि गर्नुहोस्", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "SVG को रूपमा प्रतिलिपि गर्नुहोस्", + "action.copy": "कपि गर्नुहोस्", + "action.cut": "कट गर्नुहोस्", + "action.delete": "मेटाउनुहोस्", + "action.distribute-horizontal": "तेर्सो रूपमा वितरण गर्नुहोस्", + "action.distribute-vertical": "ठाडो रूपमा वितरण गर्नुहोस्", + "action.distribute-horizontal.short": "तेर्सो रूपमा वितरण गर्नुहोस्", + "action.distribute-vertical.short": "ठाडो रूपमा वितरण गर्नुहोस्", + "action.duplicate": "अनुलिपि गर्नुहोस्", + "action.edit-link": "लिङ्क सम्पादन गर्नुहोस्", + "action.exit-pen-mode": "कलम मोडबाट बाहिर निस्कनुहोस्", + "action.export-as-json.short": "JSON", + "action.export-as-json": "JSON को रूपमा निर्यात गर्नुहोस्", + "action.export-as-png.short": "PNG", + "action.export-as-png": "PNG को रूपमा निर्यात गर्नुहोस्", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "SVG को रूपमा निर्यात गर्नुहोस्", + "action.flip-horizontal": "तेर्सो फ्लिप गर्नुहोस्", + "action.flip-vertical": "ठाडो फ्लिप गर्नुहोस्", + "action.flip-horizontal.short": "तेर्सो रूपमा फ्लिप गर्नुहोस्", + "action.flip-vertical.short": "ठाडो रूपमा फ्लिप गर्नुहोस्", + "action.group": "समूह", + "action.insert-media": "मिडिया अपलोड गर्नुहोस्", + "action.new-shared-project": "नयाँ साझा परियोजना", + "action.nudge-down": "तल धकेल्नुहोस्", + "action.nudge-left": "बायाँ धकेल्नुहोस्", + "action.nudge-right": "दायाँ धकेल्नुहोस्", + "action.nudge-up": "माथि धकेल्नुहोस्", + "action.open-file": "फाइल खोल्नुहोस्", + "action.pack": "प्याक", + "action.paste": "पेस्ट गर्नुहोस्", + "action.print": "मुद्रण गर्नुहोस्", + "action.redo": "पुनः गर्नुहोस्", + "action.rotate-ccw": "घडीको विपरीत दिशामा घुमाउनुहोस्", + "action.rotate-cw": "घडीको दिशामा घुमाउनुहोस्", + "action.save-copy": "एक प्रतिलिपि बचत गर्नुहोस्", + "action.select-all": "सबै छान्नुहोस्", + "action.select-none": "केहि पनि सेलेक्ट नगर्नुहोस्", + "action.send-backward": "पछाडि पठाउनुहोस्", + "action.send-to-back": "सबैभन्दा पछाडि पठाउनुहोस्", + "action.share-project": "यो परियोजना साझेदारी गर्नुहोस्", + "action.stack-horizontal": "तेर्सो रूपमा स्ट्याक गर्नुहोस्", + "action.stack-vertical": "ठाडो रूपमा स्ट्याक गर्नुहोस्", + "action.stack-horizontal.short": "तेर्सो रूपमा स्ट्याक गर्नुहोस्", + "action.stack-vertical.short": "ठाडो रूपमा स्ट्याक गर्नुहोस्", + "action.stretch-horizontal": "तेर्सो रूपमा तन्काउनुहोस्", + "action.stretch-vertical": "ठाडो रूपमा तन्काउनुहोस्", + "action.stretch-horizontal.short": "तेर्सो रूपमा तन्काउनुहोस्", + "action.stretch-vertical.short": "ठाडो रूपमा तन्काउनुहोस्", + "action.toggle-auto-size": "स्वत: आकार टगल गर्नुहोस्", + "action.toggle-dark-mode.menu": "अँध्यारो मोड", + "action.toggle-dark-mode": "अँध्यारो मोड टगल गर्नुहोस्", + "action.toggle-debug-mode.menu": "डिबग मोड", + "action.toggle-debug-mode": "डिबग मोड टगल गर्नुहोस्", + "action.toggle-focus-mode.menu": "फोकस मोड", + "action.toggle-focus-mode": "फोकस मोड टगल गर्नुहोस्", + "action.toggle-grid.menu": "ग्रिड देखाउनुहोस्", + "action.toggle-grid": "ग्रिड टगल गर्नुहोस्", + "action.toggle-snap-mode.menu": "जहिले पनि स्न्याप गर्नुहोस्", + "action.toggle-snap-mode": "जहिले पनि स्न्याप गर्ने टगल गर्नुहोस्", + "action.toggle-tool-lock.menu": "उपकरण लक", + "action.toggle-tool-lock": "उपकरण लक टगल गर्नुहोस्", + "action.toggle-transparent.context-menu": "पारदर्शी", + "action.toggle-transparent.menu": "पारदर्शी", + "action.toggle-transparent": "पारदर्शी पृष्ठभूमि टगल गर्नुहोस्", + "action.undo": "पूर्ववत गर्नुहोस्", + "action.ungroup": "समूह रद्द गर्नुहोस्", + "action.zoom-in": "जुम इन", + "action.zoom-out": "जुम आउट", + "action.zoom-to-100": "१००% मा जुम गर्नुहोस्", + "action.zoom-to-fit": "जुम टु फिट", + "action.zoom-to-selection": "जुम टु सेलेक्सन", + "color-style.black": "कालो", + "color-style.blue": "निलो", + "color-style.green": "हरियो", + "color-style.grey": "खैरो", + "color-style.light-blue": "हल्का निलो", + "color-style.light-green": "हल्का हरियो", + "color-style.light-red": "हल्का रातो", + "color-style.light-violet": "हल्का बैंगनी", + "color-style.orange": "सुन्तला", + "color-style.red": "रातो", + "color-style.violet": "बैंगनी", + "color-style.yellow": "पहेंलो", + "fill-style.none": "कुनै पनि नाई", + "fill-style.semi": "अर्ध", + "fill-style.solid": "ठोस", + "fill-style.pattern": "ढाँचा", + "dash-style.dashed": "ड्यास गरिएको", + "dash-style.dotted": "डट गरिएको", + "dash-style.draw": "चित्र बनाउनु", + "dash-style.solid": "ठोस", + "size-style.s": "सानो", + "size-style.m": "मध्यम", + "size-style.l": "ठूलो", + "size-style.xl": "धेरै ठूलाे", + "opacity-style.0.1": "१०%", + "opacity-style.0.25": "२५%", + "opacity-style.0.5": "५०%", + "opacity-style.0.75": "७५%", + "opacity-style.1": "१००%", + "font-style.draw": "चित्र बनाउनु", + "font-style.sans": "सांस", + "font-style.serif": "सेरिफ", + "font-style.mono": "मोनो", + "align-style.start": "सुरु", + "align-style.middle": "मध्य", + "align-style.end": "अन्त्य", + "align-style.justify": "उचित", + "geo-style.arrow-down": "बाण तल", + "geo-style.arrow-left": "बाण बायाँ", + "geo-style.arrow-right": "बाण दायाँ", + "geo-style.arrow-up": "बाण माथि", + "geo-style.diamond": "हीरा", + "geo-style.ellipse": "दीर्घवृत्त", + "geo-style.hexagon": "हेक्सागन", + "geo-style.octagon": "अष्टकोण", + "geo-style.oval": "ओवल", + "geo-style.pentagon": "पेन्टागन", + "geo-style.rectangle": "आयत", + "geo-style.rhombus-2": "रोम्बस २", + "geo-style.rhombus": "रोम्बस", + "geo-style.star": "तारा", + "geo-style.trapezoid": "Trapezoid", + "geo-style.triangle": "त्रिभुज", + "geo-style.x-box": "X बाकस", + "arrowheadStart-style.none": "कुनै पनि नाई", + "arrowheadStart-style.arrow": "तीर", + "arrowheadStart-style.bar": "बार", + "arrowheadStart-style.diamond": "हीरा", + "arrowheadStart-style.dot": "डट", + "arrowheadStart-style.inverted": "उल्टो", + "arrowheadStart-style.pipe": "पाइप", + "arrowheadStart-style.square": "वर्ग", + "arrowheadStart-style.triangle": "त्रिभुज", + "arrowheadEnd-style.none": "कुनै पनि नाई", + "arrowheadEnd-style.arrow": "तीर", + "arrowheadEnd-style.bar": "बार", + "arrowheadEnd-style.diamond": "हीरा", + "arrowheadEnd-style.dot": "डट", + "arrowheadEnd-style.inverted": "उल्टो", + "arrowheadEnd-style.pipe": "पाइप", + "arrowheadEnd-style.square": "वर्ग", + "arrowheadEnd-style.triangle": "त्रिभुज", + "spline-style.line": "रेखा", + "spline-style.cubic": "घन", + "tool.select": "सेलेक्ट", + "tool.hand": "हात", + "tool.draw": "चित्र बनाउनु", + "tool.eraser": "इरेजर", + "tool.arrow-down": "बाण तल", + "tool.arrow-left": "बाण बायाँ", + "tool.arrow-right": "बाण दायाँ", + "tool.arrow-up": "बाण माथि", + "tool.arrow": "तीर", + "tool.diamond": "हीरा", + "tool.ellipse": "दीर्घवृत्त", + "tool.hexagon": "हेक्सागन", + "tool.line": "रेखा", + "tool.octagon": "अष्टकोण", + "tool.oval": "ओवल", + "tool.pentagon": "पेन्टागन", + "tool.rectangle": "आयत", + "tool.rhombus": "रोम्बस", + "tool.star": "तारा", + "tool.trapezoid": "Trapezoid", + "tool.triangle": "त्रिभुज", + "tool.x-box": "एक्स बाकस", + "tool.asset": "सम्पत्ति", + "tool.frame": "फ्रेम", + "tool.note": "टाँसिने", + "tool.embed": "इम्बेड गर्नुहोस्", + "tool.text": "शब्द", + "menu.title": "मेनु", + "menu.copy-as": "कपि एज", + "menu.edit": "सम्पादन गर्नुहोस्", + "menu.export-as": "एक्सपोर्ट एज", + "menu.file": "फाइल", + "menu.language": "भाषा", + "menu.preferences": "प्राथमिकताहरू", + "menu.view": "भ्यू", + "context-menu.arrange": "व्यवस्थित गर्नुहोस्", + "context-menu.copy-as": "कपि एज", + "context-menu.export-as": "एक्सपोर्ट एज", + "context-menu.move-to-page": "पृष्ठमा सार्नुहोस्", + "context-menu.reorder": "पुन: क्रमबद्ध गर्नुहोस्", + "page-menu.title": "पृष्ठहरू", + "page-menu.create-new-page": "नयाँ पृष्ठ सिर्जना गर्नुहोस्", + "page-menu.edit-pages": "पृष्ठहरू सम्पादन गर्नुहोस्", + "page-menu.max-page-count-reached": "अधिकतम पृष्ठ पुग्यो", + "page-menu.new-page-initial-name": "पृष्ठ १", + "page-menu.page": "पृष्ठ", + "page-menu.edit-start": "सम्पादन", + "page-menu.edit-done": "सकियो", + "page-menu.submenu.rename": "पुन: नामाकरण", + "page-menu.submenu.duplicate-page": "नक्कल", + "page-menu.submenu.go-to-page": "पृष्ठमा जानुहोस्", + "page-menu.submenu.title": "मेनु", + "page-menu.submenu.move-down": "तल सार्नुहोस्", + "page-menu.submenu.move-up": "माथि सार्नुहोस्", + "page-menu.submenu.delete": "मेटाउनुहोस्", + "share-menu.title": "सेयर गर्नुहोस्", + "share-menu.share-project": "यो परियोजना सेयर गर्नुहोस्", + "share-menu.create-project": "नयाँ साझा परियोजना", + "share-menu.copy-link": "लिङ्क प्रतिलिपि गर्नुहोस्", + "share-menu.readonly-link": "पढ्नका लागि मात्र", + "share-menu.copy-readonly-link": "पढ्ने मात्र लिङ्क प्रतिलिपि गर्नुहोस्", + "share-menu.offline-note": "यो परियोजना सेयर गर्नाले नयाँ URL मा होस्ट गरिएको लाइभ प्रतिलिपि सिर्जना गर्नेछ। तपाईंले परियोजना हेर्न र सम्पादन गर्नका लागि तीस अन्य व्यक्तिहरूसँग URL सेयर गर्न सक्नुहुन्छ।", + "share-menu.copy-link-note": "लिङ्क भएका जो कोहीले पनि यो परियोजना हेर्न र सम्पादन गर्न सक्षम हुनेछन्।", + "share-menu.copy-readonly-link-note": "लिङ्क भएको कुनै पनि व्यक्तिले यो परियोजना हेर्न (तर सम्पादन गर्दैन) सक्षम हुनेछ।", + "share-menu.project-too-large": "माफ गर्नुहोस्, यो परियोजना सेयर गर्न सकिँदैन किनभने यो धेरै ठूलो छ। हामी यसमा काम गर्दैछौं!", + "people-menu.title": "मानिस", + "people-menu.change-name": "नाम परिवर्तन गर्नुहोस्", + "people-menu.change-color": "रङ परिवर्तन गर्नुहोस्", + "people-menu.user": "(तिमी)", + "people-menu.invite": "अरूलाई निमन्त्रणा गर्नुहोस्", + "debug-menu.hard-reset": "हार्ड रिसेट गर्नुहोस्", + "debug-menu.create-shapes": "100 आकारहरू सिर्जना गर्नुहोस्", + "help-menu.title": "मद्दत र स्रोतहरू", + "help-menu.about": "tldraw को बारे मा", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "किबोर्ड सर्टकटहरू", + "help-menu.twitter": "ट्विटर", + "links-menu.about": "tldraw को बारे मा", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "ट्विटर", + "actions-menu.title": "कार्यहरू", + "edit-link-dialog.title": "लिङ्क सम्पादन गर्नुहोस्", + "edit-link-dialog.invalid-url": "लिङ्क एक मान्य URL हुनुपर्छ।", + "edit-link-dialog.detail": "लिङ्कहरू नयाँ ट्याबमा खुल्नेछन्।", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "खाली गर्नुहोस्", + "edit-link-dialog.save": "जारी राख्नुहोस्", + "edit-link-dialog.cancel": "रद्द", + "embed-dialog.title": "इम्बेड सिर्जना गर्नुहोस्", + "embed-dialog.url-label": "URL पेस्ट गर्नुहोस्", + "embed-dialog.back": "पछाडी जाउ", + "embed-dialog.create": "सिर्जना गर्नुहोस्", + "embed-dialog.cancel": "रद्द", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "इम्बेड सिर्जना गर्न साइटको URL मा टाँस्नुहोस्।", + "embed-dialog.invalid-url": "हामीले त्यो URL बाट एम्बेड सिर्जना गर्न सकेनौं।", + "edit-pages-dialog.title": "पृष्ठहरू सम्पादन गर्नुहोस्", + "edit-pages-dialog.create-new-page": "नयाँ पृष्ठ सिर्जना गर्नुहोस्", + "edit-pages-dialog.delete": "मेटाउनुहोस्", + "edit-pages-dialog.duplicate-page": "अनुलिपि गर्नुहोस्", + "edit-pages-dialog.go-to-page": "पृष्ठमा जानुहोस्", + "edit-pages-dialog.max-page-count-reached": "अधिकतम पृष्ठ पुग्यो", + "edit-pages-dialog.more-menu": "मेनु", + "edit-pages-dialog.move-down": "तल सार्नुहोस्", + "edit-pages-dialog.move-up": "माथि सार्नुहोस्", + "edit-pages-dialog.new-page-initial-name": "पृष्ठ १", + "reload-file-dialog.title": "फाइल सम्पादन जारी राख्नुहोस्", + "reload-file-dialog.description": "तपाईंले भर्खर एउटा फाइल सम्पादन गर्दै हुनुहुन्थ्यो। के तपाइँ यसलाई सम्पादन जारी राख्न चाहनुहुन्छ?", + "reload-file-dialog.failure": "फाइल पुन: लोड गर्न असफल भयो। फेरि प्रयास गर्ने हो?", + "reload-file-dialog.reload": "जारी राख्नुहोस्", + "reload-file-dialog.revert": "होइन धन्यवाद", + "shortcuts-dialog.title": "किबोर्ड सर्टकटहरू", + "shortcuts-dialog.edit": "सम्पादन गर्नुहोस्", + "shortcuts-dialog.file": "फाइल", + "shortcuts-dialog.preferences": "प्राथमिकताहरू", + "shortcuts-dialog.tools": "उपकरण", + "shortcuts-dialog.transform": "रूपान्तरण", + "shortcuts-dialog.view": "दृश्य", + "shortcuts-dialog.save": "जारी राख्नुहोस्", + "style-panel.title": "शैलीहरू", + "style-panel.align": "पङ्क्तिबद्ध", + "style-panel.arrowheads": "एरोहेडहरू", + "style-panel.color": "रंग", + "style-panel.dash": "धर्का", + "style-panel.fill": "भर्नुहोस्", + "style-panel.font": "फन्ट", + "style-panel.geo": "आकृति", + "style-panel.label": "लेबल", + "style-panel.mixed": "मिश्रित", + "style-panel.opacity": "अस्पष्टता", + "style-panel.size": "आकार", + "style-panel.spline": "स्प्लाइन", + "style-panel.text": "शब्द", + "tool-panel.drawing": "रेखाचित्र", + "tool-panel.geo": "आकृति", + "tool-panel.shapes": "आकृतिहरू", + "tool-panel.things": "चीजहरू", + "tool-panel.tools": "उपकरणहरू", + "save-changes-prompt.title": "तपाईंसँग सुरक्षित नगरिएका परिवर्तनहरू छन्", + "save-changes-prompt.description": "के तपाइँ तपाइँको हालको फाइलमा परिवर्तनहरू सुरक्षित गर्न चाहनुहुन्छ?", + "save-changes-prompt.go-back": "पछाडी जाउ", + "save-changes-prompt.continue": "जारी राख्नुहोस्", + "navigation-zone.toggle-minimap": "मिनिम्याप टगल गर्नुहोस्", + "navigation-zone.zoom": "जुम", + "focus-mode.toggle-focus-mode": "फोकस मोड टगल गर्नुहोस्", + "toast.close": "बन्द", + "file-system.file-open-error.title": "फाइल खोल्न सकिएन", + "file-system.file-open-error.not-a-tldraw-file": "तपाईंले खोल्न खोजेको फाइल tldraw फाइल जस्तो देखिदैन।", + "file-system.file-open-error.file-format-version-too-new": "तपाईंले खोल्न खोजेको फाइल tldraw को नयाँ संस्करणबाट हो। कृपया पृष्ठ पुन: लोड गर्नुहोस् र पुन: प्रयास गर्नुहोस्।", + "file-system.file-open-error.generic-corrupted-file": "तपाईंले खोल्ने प्रयास गर्नुभएको फाइल बिग्रिएको छ।", + "file-system.confirm-open.title": "हालको परियोजना अधिलेखन गर्ने?", + "file-system.confirm-open.description": "फाइल खोल्दा तपाईंको हालको परियोजना प्रतिस्थापन हुनेछ र कुनै पनि बचत नगरिएका परिवर्तनहरू हराउनेछन्। के तपाइँ यहि चाहनुहुन्छ?", + "file-system.confirm-open.cancel": "रद्द", + "file-system.confirm-open.open": "फाइल खोल्नुहोस्", + "file-system.confirm-open.dont-show-again": "फेरि नसोध्नुहोस्", + "toast.error.export-fail.title": "निर्यात गर्न असफल भयो", + "toast.error.export-fail.desc": "छवि निर्यात गर्न असफल भयो", + "toast.error.copy-fail.title": "प्रतिलिपि असफल भयो", + "toast.error.copy-fail.desc": "छवि प्रतिलिपि गर्न असफल भयो", + "file-system.shared-document-file-open-error.title": "फाइल खोल्न सकिएन", + "file-system.shared-document-file-open-error.description": "साझा परियोजनाहरूबाट फाइलहरू खोल्न समर्थित छैन।", + "vscode.file-open.dont-show-again": "फेरि नसोध्नुहोस्", + "vscode.file-open.desc": "यो फाइल tldraw को पुरानो संस्करणबाट सिर्जना गरिएको थियो। के तपाइँ नयाँ संस्करण संग काम गर्न यसलाई upgrade गर्न चाहनुहुन्छ?", + "context.pages.new-page": "नयाँ पृष्ठ" +} \ No newline at end of file diff --git a/assets/translations/no.json b/assets/translations/no.json new file mode 100644 index 000000000..d38c954bf --- /dev/null +++ b/assets/translations/no.json @@ -0,0 +1,63 @@ +{ + "action.copy": "Kopier", + "action.cut": "Klipp ut", + "action.delete": "Slett", + "action.duplicate": "Dupliser", + "action.flip-horizontal": "Snu horisontalt", + "action.flip-vertical": "Snu vertikalt", + "action.group": "Grupper", + "action.insert-media": "Last opp media", + "action.paste": "Lim inn", + "action.redo": "Gjør om", + "action.select-all": "Velg alle", + "action.select-none": "Velg ingen", + "action.undo": "Angre", + "action.ungroup": "Avgrupper", + "action.zoom-in": "Zoom inn", + "action.zoom-out": "Zoom ut", + "action.zoom-to-fit": "Zoom for å passe", + "action.zoom-to-selection": "Zoom til valg", + "dash-style.draw": "Tegn", + "font-style.draw": "Tegn", + "geo-style.ellipse": "Ellipse", + "geo-style.rectangle": "Rektangel", + "geo-style.triangle": "Trekant", + "arrowheadStart-style.arrow": "Pil", + "arrowheadStart-style.triangle": "Trekant", + "arrowheadEnd-style.arrow": "Pil", + "arrowheadEnd-style.triangle": "Trekant", + "spline-style.line": "Linje", + "tool.select": "Velg", + "tool.draw": "Tegn", + "tool.eraser": "Viskelær", + "tool.arrow": "Pil", + "tool.ellipse": "Ellipse", + "tool.line": "Linje", + "tool.rectangle": "Rektangel", + "tool.triangle": "Trekant", + "tool.note": "Lapp", + "tool.text": "Tekst", + "menu.copy-as": "Kopier som", + "menu.edit": "Rediger", + "menu.export-as": "Eksporter som", + "menu.file": "Fil", + "menu.language": "Språk", + "menu.preferences": "Preferanser", + "menu.view": "Vis", + "context-menu.copy-as": "Kopier som", + "context-menu.export-as": "Eksporter som", + "context-menu.move-to-page": "Flytt til side", + "page-menu.submenu.delete": "Slett", + "shortcuts-dialog.edit": "Rediger", + "shortcuts-dialog.file": "Fil", + "shortcuts-dialog.preferences": "Preferanser", + "shortcuts-dialog.view": "Vis", + "style-panel.title": "Stiler", + "style-panel.align": "Juster", + "style-panel.color": "Farge", + "style-panel.dash": "Linje", + "style-panel.fill": "Fyll", + "style-panel.font": "Teksttype", + "style-panel.size": "Størrelse", + "style-panel.text": "Tekst" +} diff --git a/assets/translations/pl.json b/assets/translations/pl.json new file mode 100644 index 000000000..46df5c23f --- /dev/null +++ b/assets/translations/pl.json @@ -0,0 +1,123 @@ +{ + "action.copy-as-json.short": "JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Kopiuj jako PNG", + "action.copy-as-svg.short": "SVG", + "action.copy": "Kopiuj", + "action.cut": "Wytnij", + "action.delete": "Usuń", + "action.duplicate": "Powiel", + "action.export-as-json.short": "JSON", + "action.export-as-png.short": "PNG", + "action.export-as-svg.short": "SVG", + "action.flip-horizontal": "Odwróć w poziomie", + "action.flip-vertical": "Odwróć w pionie", + "action.group": "Grupuj", + "action.insert-media": "Załaduj multimedia", + "action.paste": "Wklej", + "action.redo": "Powtórz", + "action.select-all": "Zaznacz wszystko", + "action.select-none": "Odznacz wszystko", + "action.toggle-dark-mode.menu": "Tryb ciemny", + "action.undo": "Cofnij", + "action.ungroup": "Rozgrupuj", + "action.zoom-in": "Przybliż", + "action.zoom-out": "Oddal", + "action.zoom-to-fit": "Wypełnij ekran", + "action.zoom-to-selection": "Przybliż do zaznaczenia", + "color-style.black": "Czarny", + "color-style.blue": "Niebieski", + "color-style.green": "Zielony", + "color-style.grey": "Szary", + "color-style.light-blue": "Jasnoniebieski", + "color-style.light-green": "Jasnozielony", + "color-style.light-red": "Jasnoczerwony", + "color-style.light-violet": "Jasny fiolet", + "color-style.orange": "Pomarańczowy", + "color-style.red": "Czerwony", + "color-style.violet": "Fioletowy", + "color-style.yellow": "Żółty", + "dash-style.draw": "Rysuj", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Rysuj", + "geo-style.diamond": "Romb", + "geo-style.ellipse": "Elipsa", + "geo-style.rectangle": "Prostokąt", + "geo-style.rhombus-2": "Romb 2", + "geo-style.rhombus": "Romb", + "geo-style.star": "Gwiazda", + "geo-style.trapezoid": "Trapez", + "geo-style.triangle": "Trójkąt", + "arrowheadStart-style.arrow": "Strzałka", + "arrowheadStart-style.diamond": "Romb", + "arrowheadStart-style.triangle": "Trójkąt", + "arrowheadEnd-style.arrow": "Strzałka", + "arrowheadEnd-style.diamond": "Romb", + "arrowheadEnd-style.square": "Kwadrat", + "arrowheadEnd-style.triangle": "Trójkąt", + "spline-style.line": "Linia", + "tool.select": "Zaznacz", + "tool.draw": "Rysuj", + "tool.eraser": "Gumka", + "tool.arrow": "Strzałka", + "tool.diamond": "Romb", + "tool.ellipse": "Elipsa", + "tool.hexagon": "Sześciokąt", + "tool.line": "Linia", + "tool.octagon": "Ośmiokąt", + "tool.oval": "Owal", + "tool.pentagon": "Pięciokąt", + "tool.rectangle": "Prostokąt", + "tool.rhombus": "Romb", + "tool.star": "Gwiazda", + "tool.triangle": "Trójkąt", + "tool.note": "Naklejka", + "tool.text": "Tekst", + "menu.copy-as": "Kopiuj jako", + "menu.edit": "Edycja", + "menu.export-as": "Eksportuj jako", + "menu.file": "Plik", + "menu.language": "Język", + "menu.preferences": "Preferencje", + "menu.view": "Widok", + "context-menu.copy-as": "Kopiuj jako", + "context-menu.export-as": "Eksportuj jako", + "context-menu.move-to-page": "Przenieś na stronę", + "page-menu.new-page-initial-name": "Strona 1", + "page-menu.page": "Strona", + "page-menu.submenu.delete": "Usuń", + "share-menu.create-project": "Nowy wspólny projekt", + "people-menu.title": "Ludzie", + "people-menu.change-color": "Zmień kolor", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.twitter": "Twitter", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "edit-pages-dialog.more-menu": "Menu", + "edit-pages-dialog.new-page-initial-name": "Strona 1", + "reload-file-dialog.revert": "Nie, dziękuję", + "shortcuts-dialog.title": "Skróty klawiaturowe", + "shortcuts-dialog.edit": "Edycja", + "shortcuts-dialog.file": "Plik", + "shortcuts-dialog.preferences": "Preferencje", + "shortcuts-dialog.view": "Widok", + "style-panel.title": "Style", + "style-panel.align": "Wyrównanie", + "style-panel.color": "Kolor", + "style-panel.dash": "Linia", + "style-panel.fill": "Wypełnienie", + "style-panel.font": "Czcionka", + "style-panel.size": "Rozmiar", + "style-panel.text": "Tekst", + "tool-panel.things": "Rzeczy", + "file-system.confirm-open.dont-show-again": "Nie pytaj ponownie", + "vscode.file-open.dont-show-again": "Nie pytaj ponownie", + "context.pages.new-page": "Nowa strona", + "style-panel.position": "Pozycja" +} diff --git a/assets/translations/pt-br.json b/assets/translations/pt-br.json new file mode 100644 index 000000000..793a5918e --- /dev/null +++ b/assets/translations/pt-br.json @@ -0,0 +1,115 @@ +{ + "action.align-bottom": "Alinhar embaixo", + "action.align-center-horizontal": "Alinhar ao centro na horizontal", + "action.align-center-vertical": "Alinhar ao centro na vertical", + "action.align-center-horizontal.short": "Alinhar ao centro na horizontal", + "action.align-center-vertical.short": "Alinhar ao centro na vertical", + "action.align-left": "Alinhar à esquerda", + "action.align-right": "Alinhar à direita", + "action.align-top": "Alinhas em cima", + "action.bring-forward": "Avançar", + "action.bring-to-front": "Trazer para Frente", + "action.copy": "Copiar", + "action.cut": "Cortar", + "action.delete": "Deletar", + "action.distribute-horizontal": "Distruibuir na horizontal", + "action.distribute-vertical": "Distruibuir na vertical", + "action.distribute-horizontal.short": "Distruibuir na horizontal", + "action.distribute-vertical.short": "Distruibuir na vertical", + "action.duplicate": "Duplicar", + "action.flip-horizontal": "Virar Horizontalmente", + "action.flip-vertical": "Virar Verticalmente", + "action.flip-horizontal.short": "Virar Horizontalmente", + "action.flip-vertical.short": "Virar Verticalmente", + "action.group": "Agrupar", + "action.insert-media": "Carregar Mídia", + "action.paste": "Colar", + "action.redo": "Refazer", + "action.select-all": "Selecionar todos", + "action.select-none": "Selecionar nenhum", + "action.send-backward": "Enviar para Trás", + "action.send-to-back": "Recuar", + "action.stretch-horizontal": "Esticar na horizontal", + "action.stretch-vertical": "Esticar na vertical", + "action.stretch-horizontal.short": "Esticar na horizontal", + "action.stretch-vertical.short": "Esticar na vertical", + "action.toggle-dark-mode.menu": "Modo Escuro", + "action.toggle-dark-mode": "Modo Escuro", + "action.toggle-debug-mode.menu": "Modo Debug", + "action.toggle-debug-mode": "Modo Debug", + "action.toggle-focus-mode.menu": "Modo Foco", + "action.toggle-focus-mode": "Modo Foco", + "action.toggle-grid.menu": "Mostrar Grade", + "action.toggle-grid": "Mostrar Grade", + "action.toggle-snap-mode.menu": "Mostrar Pontos de Ajuste", + "action.toggle-snap-mode": "Mostrar Pontos de Ajuste", + "action.toggle-transparent.context-menu": "Transparente", + "action.toggle-transparent.menu": "Transparente", + "action.undo": "Desfazer", + "action.ungroup": "Desagrupar", + "action.zoom-in": "Aumentar zoom", + "action.zoom-out": "Diminuir zoom", + "action.zoom-to-fit": "Zoom para ajuste", + "action.zoom-to-selection": "Zoom para a seleção", + "dash-style.draw": "Desenhar", + "font-style.draw": "Desenhar", + "geo-style.ellipse": "Elipse", + "geo-style.rectangle": "Retângulo", + "geo-style.triangle": "Triângulo", + "arrowheadStart-style.arrow": "Seta", + "arrowheadStart-style.triangle": "Triângulo", + "arrowheadEnd-style.arrow": "Seta", + "arrowheadEnd-style.triangle": "Triângulo", + "spline-style.line": "Linha", + "tool.select": "Selecionar", + "tool.draw": "Desenhar", + "tool.eraser": "Borracha", + "tool.arrow": "Seta", + "tool.ellipse": "Elipse", + "tool.line": "Linha", + "tool.rectangle": "Retângulo", + "tool.triangle": "Triângulo", + "tool.note": "Adesivo", + "tool.text": "Texto", + "menu.copy-as": "Copiar como", + "menu.edit": "Editar", + "menu.export-as": "Exportar como", + "menu.file": "Arquivo", + "menu.language": "Idioma", + "menu.preferences": "Preferências", + "menu.view": "Visualizar", + "context-menu.copy-as": "Copiar como", + "context-menu.export-as": "Exportar como", + "context-menu.move-to-page": "Mover para Página", + "page-menu.create-new-page": "Criar Página", + "page-menu.page": "Página", + "page-menu.edit-start": "Editar", + "page-menu.submenu.duplicate-page": "Duplicar", + "page-menu.submenu.delete": "Deletar", + "share-menu.copy-link": "Copiar Link de Convite", + "share-menu.copy-readonly-link": "Copiar Link ReadOnly", + "help-menu.keyboard-shortcuts": "Atalhos de Teclado", + "edit-link-dialog.cancel": "Cancelar", + "embed-dialog.cancel": "Cancelar", + "edit-pages-dialog.create-new-page": "Criar Página", + "edit-pages-dialog.delete": "Deletar", + "edit-pages-dialog.duplicate-page": "Duplicar", + "shortcuts-dialog.title": "Atalhos de Teclado", + "shortcuts-dialog.edit": "Editar", + "shortcuts-dialog.file": "Arquivo", + "shortcuts-dialog.preferences": "Preferências", + "shortcuts-dialog.tools": "Ferramentas", + "shortcuts-dialog.transform": "Transformar", + "shortcuts-dialog.view": "Visualizar", + "style-panel.title": "Estilos", + "style-panel.align": "Alinhamento", + "style-panel.color": "Cor", + "style-panel.dash": "Traço", + "style-panel.fill": "Preencher", + "style-panel.font": "Fonte", + "style-panel.size": "Tamanho", + "style-panel.text": "Texto", + "tool-panel.tools": "Ferramentas", + "focus-mode.toggle-focus-mode": "Modo Foco", + "file-system.confirm-open.cancel": "Cancelar" +} diff --git a/assets/translations/pt-pt.json b/assets/translations/pt-pt.json new file mode 100644 index 000000000..dd5939bf9 --- /dev/null +++ b/assets/translations/pt-pt.json @@ -0,0 +1,90 @@ +{ + "action.bring-forward": "Mover acima", + "action.bring-to-front": "Colocar à Frente", + "action.copy": "Copiar", + "action.cut": "Cortar", + "action.delete": "Apagar", + "action.duplicate": "Duplicar", + "action.flip-horizontal": "Inverter Horizontalmente", + "action.flip-vertical": "Inverter Verticalmente", + "action.flip-horizontal.short": "Inverter Horizontalmente", + "action.flip-vertical.short": "Inverter Verticalmente", + "action.group": "Agrupar", + "action.insert-media": "Upload Média", + "action.paste": "Colar", + "action.redo": "Refazer", + "action.select-all": "Selecionar todos", + "action.select-none": "Selecionar nenhum", + "action.send-backward": "Mover abaixo", + "action.send-to-back": "Colocar no Fundo", + "action.toggle-dark-mode.menu": "Modo Escuro", + "action.toggle-dark-mode": "Modo Escuro", + "action.toggle-debug-mode.menu": "Modo Debug", + "action.toggle-debug-mode": "Modo Debug", + "action.toggle-focus-mode.menu": "Modo Foco", + "action.toggle-focus-mode": "Modo Foco", + "action.toggle-grid.menu": "Mostrar Grelha", + "action.toggle-grid": "Mostrar Grelha", + "action.toggle-snap-mode.menu": "Mostrar Pontos de Ajuste", + "action.toggle-snap-mode": "Mostrar Pontos de Ajuste", + "action.undo": "Desfazer", + "action.ungroup": "Desagrupar", + "action.zoom-in": "Aumentar zoom", + "action.zoom-out": "Diminuir zoom", + "action.zoom-to-fit": "Zoom para caber", + "action.zoom-to-selection": "Zoom na seleção", + "dash-style.draw": "Desenhar", + "font-style.draw": "Desenhar", + "geo-style.ellipse": "Elipse", + "geo-style.rectangle": "Retângulo", + "geo-style.triangle": "Triângulo", + "arrowheadStart-style.arrow": "Seta", + "arrowheadStart-style.triangle": "Triângulo", + "arrowheadEnd-style.arrow": "Seta", + "arrowheadEnd-style.triangle": "Triângulo", + "spline-style.line": "Linha", + "tool.select": "Selecionar", + "tool.draw": "Desenhar", + "tool.eraser": "Borracha", + "tool.arrow": "Seta", + "tool.ellipse": "Elipse", + "tool.line": "Linha", + "tool.rectangle": "Retângulo", + "tool.triangle": "Triângulo", + "tool.note": "Post-it", + "tool.text": "Texto", + "menu.copy-as": "Copiar como", + "menu.edit": "Editar", + "menu.export-as": "Exportar como", + "menu.file": "Ficheiro", + "menu.language": "Língua", + "menu.preferences": "Preferências", + "menu.view": "Visualizar", + "context-menu.copy-as": "Copiar como", + "context-menu.export-as": "Exportar como", + "context-menu.move-to-page": "Mover para Página", + "page-menu.create-new-page": "Criar Página", + "page-menu.edit-start": "Editar", + "page-menu.submenu.duplicate-page": "Duplicar", + "page-menu.submenu.delete": "Apagar", + "share-menu.copy-link": "Copiar Link de Convite", + "edit-link-dialog.cancel": "Cancelar", + "embed-dialog.cancel": "Cancelar", + "edit-pages-dialog.create-new-page": "Criar Página", + "edit-pages-dialog.delete": "Apagar", + "edit-pages-dialog.duplicate-page": "Duplicar", + "shortcuts-dialog.edit": "Editar", + "shortcuts-dialog.file": "Ficheiro", + "shortcuts-dialog.preferences": "Preferências", + "shortcuts-dialog.view": "Visualizar", + "style-panel.title": "Estilos", + "style-panel.align": "Alinhamento", + "style-panel.color": "Cor", + "style-panel.dash": "Traço", + "style-panel.fill": "Preencher", + "style-panel.font": "Fonte", + "style-panel.size": "Tamanho", + "style-panel.text": "Texto", + "focus-mode.toggle-focus-mode": "Modo Foco", + "file-system.confirm-open.cancel": "Cancelar" +} diff --git a/assets/translations/ro.json b/assets/translations/ro.json new file mode 100644 index 000000000..d48d70b9e --- /dev/null +++ b/assets/translations/ro.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "Convertește la marcaj", + "action.convert-to-embed": "Convertește la înglobare", + "action.open-embed-link": "Deschide legătură", + "action.align-bottom": "Aliniere jos", + "action.align-center-horizontal": "Aliniere orizontală", + "action.align-center-vertical": "Aliniere verticală", + "action.align-center-horizontal.short": "Aliniere orizontală", + "action.align-center-vertical.short": "Aliniere verticală", + "action.align-left": "Aliniere la stânga", + "action.align-right": "Aliniere la dreapta", + "action.align-top": "Aliniere sus", + "action.back-to-content": "Înapoi la conținut", + "action.bring-forward": "Adu în față", + "action.bring-to-front": "Adu în prim plan", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Copiază ca JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Copiază ca PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Copiază ca SVG", + "action.copy": "Copiază", + "action.cut": "Decupează", + "action.delete": "Șterge", + "action.distribute-horizontal": "Distribuit orizontal", + "action.distribute-vertical": "Distribuit vertical", + "action.distribute-horizontal.short": "Distribuit orizontal", + "action.distribute-vertical.short": "Distribuit vertical", + "action.duplicate": "Fă duplicat", + "action.edit-link": "Editează legătură", + "action.exit-pen-mode": "Ieși din modul stilou", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Export ca JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Export ca PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Export ca SVG", + "action.flip-horizontal": "Întoarce pe orizontală", + "action.flip-vertical": "Întoarce pe verticală", + "action.flip-horizontal.short": "Întoarce pe orizontală", + "action.flip-vertical.short": "Întoarce pe verticală", + "action.group": "Grupare", + "action.insert-media": "Încarcă media", + "action.new-shared-project": "Proiect partajat nou", + "action.nudge-down": "Împinge jos", + "action.nudge-left": "Împinge la stânga", + "action.nudge-right": "Împinge la dreapta", + "action.nudge-up": "Împinge în sus", + "action.open-file": "Deschide fișier", + "action.pack": "Pachet", + "action.paste": "Lipește", + "action.print": "Imprimă", + "action.redo": "Refă", + "action.rotate-ccw": "Rotire în sens invers acelor de ceasornic", + "action.rotate-cw": "Rotire în sensul acelor de ceasornic", + "action.save-copy": "Salvează o copie", + "action.select-all": "Selectează tot", + "action.select-none": "Nu selecta nimic", + "action.send-backward": "Trimite în spate", + "action.send-to-back": "Trimite înapoi", + "action.share-project": "Partajează acest proiect", + "action.stack-horizontal": "Stivă orizontal", + "action.stack-vertical": "Stivă vertical", + "action.stack-horizontal.short": "Stivă orizontal", + "action.stack-vertical.short": "Stivă verticală", + "action.stretch-horizontal": "Întinde orizontal", + "action.stretch-vertical": "Întinde vertical", + "action.stretch-horizontal.short": "Întinde orizontal", + "action.stretch-vertical.short": "Întinde vertical", + "action.toggle-auto-size": "Comută dimensiune automată", + "action.toggle-dark-mode.menu": "Mod întunecat", + "action.toggle-dark-mode": "Comută mod întunecat", + "action.toggle-debug-mode.menu": "Mod depanare", + "action.toggle-debug-mode": "Comută mod depanare", + "action.toggle-focus-mode.menu": "Mod focalizare", + "action.toggle-focus-mode": "Comută mod focalizare", + "action.toggle-grid.menu": "Arată grilă", + "action.toggle-grid": "Comută grilă", + "action.toggle-snap-mode.menu": "Rupe întotdeauna", + "action.toggle-snap-mode": "Comută ruptura întotdeauna", + "action.toggle-tool-lock.menu": "Blocare unealtă", + "action.toggle-tool-lock": "Comută blocarea uneltei", + "action.toggle-transparent.context-menu": "Transparent", + "action.toggle-transparent.menu": "Transparent", + "action.toggle-transparent": "Comută fundal transparent", + "action.undo": "Anulează", + "action.ungroup": "Anulează gruparea", + "action.zoom-in": "Mărește", + "action.zoom-out": "Micșorează", + "action.zoom-to-100": "Mărește la 100%", + "action.zoom-to-fit": "Mărește pentru a se potrivi", + "action.zoom-to-selection": "Mărește la selecție", + "color-style.black": "Negru", + "color-style.blue": "Albastru", + "color-style.green": "Verde", + "color-style.grey": "Gri", + "color-style.light-blue": "Albastru deschis", + "color-style.light-green": "Verde deschis", + "color-style.light-red": "Roșu deschis", + "color-style.light-violet": "Violet deschis", + "color-style.orange": "Portocaliu", + "color-style.red": "Roșu", + "color-style.violet": "Violet", + "color-style.yellow": "Galben", + "fill-style.none": "Fără", + "fill-style.semi": "Semi", + "fill-style.solid": "Solid", + "fill-style.pattern": "Model", + "dash-style.dashed": "Întreruptă", + "dash-style.dotted": "Punctată", + "dash-style.draw": "Desenează", + "dash-style.solid": "Solid", + "size-style.s": "Mic", + "size-style.m": "Mediu", + "size-style.l": "Mare", + "size-style.xl": "Foarte mare", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Desenează", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Începe", + "align-style.middle": "Mijloc", + "align-style.end": "Sfârșit", + "align-style.justify": "Justificat", + "geo-style.arrow-down": "Săgeata în jos", + "geo-style.arrow-left": "Săgeată la stânga", + "geo-style.arrow-right": "Săgeată la dreapta", + "geo-style.arrow-up": "Săgeată în sus", + "geo-style.diamond": "Diamant", + "geo-style.ellipse": "Elipsă", + "geo-style.hexagon": "Hexagon", + "geo-style.octagon": "Octogon", + "geo-style.oval": "Oval", + "geo-style.pentagon": "Pentagon", + "geo-style.rectangle": "Dreptunghi", + "geo-style.rhombus-2": "Romb 2", + "geo-style.rhombus": "Romb", + "geo-style.star": "Stea", + "geo-style.trapezoid": "Trapez", + "geo-style.triangle": "Triunghi", + "geo-style.x-box": "Casetă X", + "arrowheadStart-style.none": "Niciunul", + "arrowheadStart-style.arrow": "Săgeată", + "arrowheadStart-style.bar": "Bară", + "arrowheadStart-style.diamond": "Diamant", + "arrowheadStart-style.dot": "Punct", + "arrowheadStart-style.inverted": "Inversat", + "arrowheadStart-style.pipe": "Conductă", + "arrowheadStart-style.square": "Pătrat", + "arrowheadStart-style.triangle": "Triunghi", + "arrowheadEnd-style.none": "Niciunul", + "arrowheadEnd-style.arrow": "Săgeată", + "arrowheadEnd-style.bar": "Bară", + "arrowheadEnd-style.diamond": "Diamant", + "arrowheadEnd-style.dot": "Punct", + "arrowheadEnd-style.inverted": "Inversat", + "arrowheadEnd-style.pipe": "Conductă", + "arrowheadEnd-style.square": "Pătrat", + "arrowheadEnd-style.triangle": "Triunghi", + "spline-style.line": "Linie", + "spline-style.cubic": "Cubic", + "tool.select": "Selectează", + "tool.hand": "Mână", + "tool.draw": "Desenează", + "tool.eraser": "Radieră", + "tool.arrow-down": "Săgeată în jos", + "tool.arrow-left": "Săgeată la stânga", + "tool.arrow-right": "Săgeată la dreapta", + "tool.arrow-up": "Săgeata în sus", + "tool.arrow": "Săgeată", + "tool.diamond": "Diamant", + "tool.ellipse": "Elipsă", + "tool.hexagon": "Hexagon", + "tool.line": "Linie", + "tool.octagon": "Octogon", + "tool.oval": "Oval", + "tool.pentagon": "Pentagon", + "tool.rectangle": "Dreptunghi", + "tool.rhombus": "Romb", + "tool.star": "Stea", + "tool.trapezoid": "Trapez", + "tool.triangle": "Triunghi", + "tool.x-box": "Casetă X", + "tool.asset": "Resursă", + "tool.frame": "Cadru", + "tool.note": "Notă", + "tool.embed": "Înglobare", + "tool.text": "Text", + "menu.title": "Meniu", + "menu.copy-as": "Copiază ca", + "menu.edit": "Editează", + "menu.export-as": "Export ca", + "menu.file": "Fișier", + "menu.language": "Limbi", + "menu.preferences": "Preferințe", + "menu.view": "Vezi", + "context-menu.arrange": "Aranjează", + "context-menu.copy-as": "Copiază ca", + "context-menu.export-as": "Export ca", + "context-menu.move-to-page": "Mută în pagina", + "context-menu.reorder": "Reordonează", + "page-menu.title": "Pagini", + "page-menu.create-new-page": "Creează pagină nouă", + "page-menu.edit-pages": "Editează pagini", + "page-menu.max-page-count-reached": "Numărul maxim de pagini a fost atins", + "page-menu.new-page-initial-name": "Pagina 1", + "page-menu.page": "Pagină", + "page-menu.edit-start": "Editează", + "page-menu.edit-done": "Gata", + "page-menu.submenu.rename": "Redenumește", + "page-menu.submenu.duplicate-page": "Fă duplicat", + "page-menu.submenu.go-to-page": "Mergi la pagina", + "page-menu.submenu.title": "Meniu", + "page-menu.submenu.move-down": "Mută jos", + "page-menu.submenu.move-up": "Mută sus", + "page-menu.submenu.delete": "Șterge", + "share-menu.title": "Partajează", + "share-menu.share-project": "Partajează acest proiect", + "share-menu.create-project": "Proiect partajat nou", + "share-menu.copy-link": "Copiază legătură", + "share-menu.readonly-link": "Doar citire", + "share-menu.copy-readonly-link": "Copiază legătură doar citire", + "share-menu.offline-note": "Partajarea acestui proiect va crea o copie live găzduită la o adresă URL nouă. Poți partaja adresa URL cu până la treizeci de alte persoane pentru a vizualiza și a edita proiectul împreună.", + "share-menu.copy-link-note": "Oricine are legătura va putea vizualiza și edita acest proiect.", + "share-menu.copy-readonly-link-note": "Oricine are legătura va putea vizualiza (dar nu va putea edita) acest proiect.", + "share-menu.project-too-large": "Scuze, acest proiect nu poate fi partajat deoarece este prea mare. Lucrăm la asta!", + "people-menu.title": "Persoane", + "people-menu.change-name": "Schimbă nume", + "people-menu.change-color": "Schimbă culoare", + "people-menu.user": "(Tu)", + "people-menu.invite": "Invită-i pe alții", + "debug-menu.hard-reset": "Resetare hard", + "debug-menu.create-shapes": "Creează 100 de forme", + "help-menu.title": "Ajutor și resurse", + "help-menu.about": "Despre", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Scurtături tastatură", + "help-menu.twitter": "Twitter", + "links-menu.about": "Despre", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Acțiuni", + "edit-link-dialog.title": "Editează legătură", + "edit-link-dialog.invalid-url": "O legătură trebuie să fie un URL valid.", + "edit-link-dialog.detail": "Legăturile se vor deschide într-o filă nouă.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Golește", + "edit-link-dialog.save": "Continuă", + "edit-link-dialog.cancel": "Anulează", + "embed-dialog.title": "Creează înglobare", + "embed-dialog.url-label": "Lipește URL", + "embed-dialog.back": "Înapoi", + "embed-dialog.create": "Creează", + "embed-dialog.cancel": "Anulează", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Introdu URL-ul site-ului pentru a crea înglobarea.", + "embed-dialog.invalid-url": "Nu am putut crea o încorporare din acea adresă URL.", + "edit-pages-dialog.title": "Editează pagini", + "edit-pages-dialog.create-new-page": "Creează o pagină nouă", + "edit-pages-dialog.delete": "Șterge", + "edit-pages-dialog.duplicate-page": "Fă duplicat", + "edit-pages-dialog.go-to-page": "Mergi la pagina", + "edit-pages-dialog.max-page-count-reached": "Numărul maxim de pagini a fost atins", + "edit-pages-dialog.more-menu": "Meniu", + "edit-pages-dialog.move-down": "Mută jos", + "edit-pages-dialog.move-up": "Mută sus", + "edit-pages-dialog.new-page-initial-name": "Pagina 1", + "reload-file-dialog.title": "Continuă editarea fișierului", + "reload-file-dialog.description": "Tocmai editai un fișier. Vrei să continui editarea lui?", + "reload-file-dialog.failure": "Reîncărcarea fișierului a eșuat. Încerci din nou?", + "reload-file-dialog.reload": "Continuă editarea", + "reload-file-dialog.revert": "Nu, mulțumesc", + "shortcuts-dialog.title": "Scurtături tastatură", + "shortcuts-dialog.edit": "Editare", + "shortcuts-dialog.file": "Fișier", + "shortcuts-dialog.preferences": "Preferințe", + "shortcuts-dialog.tools": "Unelte", + "shortcuts-dialog.transform": "Transformare", + "shortcuts-dialog.view": "Vizualizare", + "shortcuts-dialog.save": "Continuă", + "style-panel.title": "Stiluri", + "style-panel.align": "Aliniere", + "style-panel.arrowheads": "Vârfuri de săgeți", + "style-panel.color": "Culoare", + "style-panel.dash": "Liniuță", + "style-panel.fill": "Umplere", + "style-panel.font": "Font", + "style-panel.geo": "Formă", + "style-panel.label": "Etichetă", + "style-panel.mixed": "Amestecat", + "style-panel.opacity": "Opacitate", + "style-panel.size": "Mărime", + "style-panel.spline": "Spline", + "style-panel.text": "Text", + "tool-panel.drawing": "Desen", + "tool-panel.geo": "Formă", + "tool-panel.shapes": "Forme", + "tool-panel.things": "Lucruri", + "tool-panel.tools": "Unelte", + "save-changes-prompt.title": "Ai modificări nesalvate", + "save-changes-prompt.description": "Vrei să salvezi modificările în fișierul curent?", + "save-changes-prompt.go-back": "Înapoi", + "save-changes-prompt.continue": "Continuă", + "navigation-zone.toggle-minimap": "Comută mini-hartă", + "navigation-zone.zoom": "Mărește", + "focus-mode.toggle-focus-mode": "Comută în modul focalizare", + "toast.close": "Închide", + "file-system.file-open-error.title": "Nu s-a putut deschide fișierul", + "file-system.file-open-error.not-a-tldraw-file": "Fișierul pe care ai încercat să îl deschizi nu arată ca un fișier tldraw.", + "file-system.file-open-error.file-format-version-too-new": "Fișierul pe care ai încercat să îl deschizi este dintr-o versiune mai nouă de tldraw. Te rog reîncarcă pagina și încearcă din nou.", + "file-system.file-open-error.generic-corrupted-file": "Fișierul pe care ai încercat să îl deschizi este corupt.", + "file-system.confirm-open.title": "Suprascrii proiectul actual?", + "file-system.confirm-open.description": "Deschiderea unui fișier va înlocui proiectul curent și orice modificări nesalvate se vor pierde. Sigur vrei să continui?", + "file-system.confirm-open.cancel": "Anulează", + "file-system.confirm-open.open": "Deschide fișier", + "file-system.confirm-open.dont-show-again": "Nu mai întreba", + "toast.error.export-fail.title": "Export eșuat", + "toast.error.export-fail.desc": "Exportarea imaginii a eșuat", + "toast.error.copy-fail.title": "Copiere eșuată", + "toast.error.copy-fail.desc": "Copierea imaginii a eșuat", + "file-system.shared-document-file-open-error.title": "Fișierul nu a putut fi deschis", + "file-system.shared-document-file-open-error.description": "Deschiderea fișierelor din proiectele partajate nu este acceptată.", + "vscode.file-open.dont-show-again": "Nu mai întreba", + "vscode.file-open.desc": "Acest fișier a fost creat cu o versiune anterioară de tldraw. Vrei să îl actualizezi pentru a funcționa cu noua versiune?", + "context.pages.new-page": "Pagină nouă" +} \ No newline at end of file diff --git a/assets/translations/ru.json b/assets/translations/ru.json new file mode 100644 index 000000000..0076ba274 --- /dev/null +++ b/assets/translations/ru.json @@ -0,0 +1,350 @@ +{ + "action.convert-to-bookmark": "Конвертировать в закладку", + "action.convert-to-embed": "Конвертировать во встраивание", + "action.open-embed-link": "Открыть ссылку", + "action.align-bottom": "Выровнять по нижнему краю", + "action.align-center-horizontal": "Выровнять по горизонтали", + "action.align-center-vertical": "Выровнять по вертикали", + "action.align-center-horizontal.short": "Выровнять по горизонтали", + "action.align-center-vertical.short": "Выровнять по вертикали", + "action.align-left": "Выровнять по левому краю", + "action.align-right": "Выровнять по правому краю", + "action.align-top": "Выровнять по верхнему краю", + "action.back-to-content": "Назад к содержанию", + "action.bring-forward": "Переместить вперед", + "action.bring-to-front": "На передний план", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Копировать как JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Копировать как PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Копировать как SVG", + "action.copy": "Копировать", + "action.cut": "Вырезать", + "action.delete": "Удалить", + "action.distribute-horizontal": "Распределить по горизонтали", + "action.distribute-vertical": "Распределить по вертикали", + "action.distribute-horizontal.short": "Распределить по горизонтали", + "action.distribute-vertical.short": "Распределить по вертикали", + "action.duplicate": "Дублировать", + "action.edit-link": "Редактировать ссылку", + "action.exit-pen-mode": "Выйти из режима пера", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Экспортировать как JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Экспортировать как PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Экспортировать как SVG", + "action.flip-horizontal": "Зеркально отразить по горизонтали", + "action.flip-vertical": "Зеркально отразить по вертикали", + "action.flip-horizontal.short": "Зеркально отразить по горизонтали", + "action.flip-vertical.short": "Зеркально отразить по вертикали", + "action.group": "Сгруппировать", + "action.insert-media": "Загрузить медиа", + "action.new-shared-project": "Новый совместный проект", + "action.nudge-down": "Сместить вниз", + "action.nudge-left": "Сместить влево", + "action.nudge-right": "Сместить вправо", + "action.nudge-up": "Сместить вверх", + "action.open-file": "Открыть файл", + "action.pack": "Собрать в кучу", + "action.paste": "Вставить", + "action.print": "Печать", + "action.redo": "Повторить", + "action.rotate-ccw": "Вращать против часовой стрелки", + "action.rotate-cw": "Вращаться по часовой стрелке", + "action.save-copy": "Сохранить копию", + "action.select-all": "Выбрать всё", + "action.select-none": "Снять выделение", + "action.send-backward": "Переместить назад", + "action.send-to-back": "На задний план", + "action.share-project": "Поделиться этим проектом", + "action.stack-horizontal": "Разместить горизонтально", + "action.stack-vertical": "Разместить вертикально", + "action.stack-horizontal.short": "Разместить горизонтально", + "action.stack-vertical.short": "Разместить вертикально", + "action.stretch-horizontal": "Растянуть по горизонтали", + "action.stretch-vertical": "Растянуть по вертикали", + "action.stretch-horizontal.short": "Растянуть по горизонтали", + "action.stretch-vertical.short": "Растянуть по вертикали", + "action.toggle-auto-size": "Переключить автоматический размер", + "action.toggle-dark-mode.menu": "Темный режим", + "action.toggle-dark-mode": "Переключить темный режим", + "action.toggle-debug-mode.menu": "Режим отладки", + "action.toggle-debug-mode": "Переключить режим отладки", + "action.toggle-focus-mode.menu": "Режим концентрации", + "action.toggle-focus-mode": "Переключить режим концентрации", + "action.toggle-grid.menu": "Показать сетку", + "action.toggle-grid": "Переключить сетку", + "action.toggle-snap-mode.menu": "Всегда привязываться", + "action.toggle-snap-mode": "Переключить всегда привязываться", + "action.toggle-tool-lock.menu": "Блокировка инструмента", + "action.toggle-tool-lock": "Переключить блокировку инструмента", + "action.toggle-transparent.context-menu": "Прозрачный", + "action.toggle-transparent.menu": "Прозрачный", + "action.toggle-transparent": "Переключить прозрачный фон", + "action.undo": "Отменить", + "action.ungroup": "Разгруппировать", + "action.zoom-in": "Увеличить", + "action.zoom-out": "Уменьшить", + "action.zoom-to-100": "Масштабировать до 100%", + "action.zoom-to-fit": "Масштабировать до размера окна", + "action.zoom-to-selection": "Масштабировать до выделения", + "color-style.black": "Черный", + "color-style.blue": "Синий", + "color-style.green": "Зеленый", + "color-style.grey": "Серый", + "color-style.light-blue": "Голубой", + "color-style.light-green": "Светло-зеленый", + "color-style.light-red": "Светло-красный", + "color-style.light-violet": "Светло-фиолетовый", + "color-style.orange": "Оранжевый", + "color-style.red": "Красный", + "color-style.violet": "Фиолетовый", + "color-style.yellow": "Желтый", + "fill-style.none": "Без", + "fill-style.semi": "Полу", + "fill-style.solid": "Сплошная", + "fill-style.pattern": "Узор", + "dash-style.dashed": "Штриховой", + "dash-style.dotted": "Пунктирный", + "dash-style.draw": "Художественный", + "dash-style.solid": "Сплошной", + "size-style.s": "Маленький", + "size-style.m": "Средний", + "size-style.l": "Большой", + "size-style.xl": "Очень большой", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Художественный", + "font-style.sans": "Без засечек", + "font-style.serif": "С засечками", + "font-style.mono": "Моноширинный", + "align-style.start": "По левому краю", + "align-style.middle": "По центру", + "align-style.end": "По правому краю", + "align-style.justify": "По ширине", + "geo-style.arrow-down": "Стрелка вниз", + "geo-style.arrow-left": "Стрелка влево", + "geo-style.arrow-right": "Стрелка вправо", + "geo-style.arrow-up": "Стрелка вверх", + "geo-style.diamond": "Ромб", + "geo-style.ellipse": "Эллипс", + "geo-style.hexagon": "Шестиугольник", + "geo-style.octagon": "Восьмиугольник", + "geo-style.oval": "Овал", + "geo-style.pentagon": "Пятиугольник", + "geo-style.rectangle": "Прямоугольник", + "geo-style.rhombus-2": "Ромб 2", + "geo-style.rhombus": "Ромб", + "geo-style.star": "Звезда", + "geo-style.trapezoid": "Трапеция", + "geo-style.triangle": "Треугольник", + "geo-style.x-box": "X квадрат", + "arrowheadStart-style.none": "Без", + "arrowheadStart-style.arrow": "Стрелка", + "arrowheadStart-style.bar": "Линия", + "arrowheadStart-style.diamond": "Ромб", + "arrowheadStart-style.dot": "Круг", + "arrowheadStart-style.inverted": "Обратная", + "arrowheadStart-style.pipe": "Труба", + "arrowheadStart-style.square": "Квадрат", + "arrowheadStart-style.triangle": "Треугольник", + "arrowheadEnd-style.none": "Без", + "arrowheadEnd-style.arrow": "Стрелка", + "arrowheadEnd-style.bar": "Линия", + "arrowheadEnd-style.diamond": "Ромб", + "arrowheadEnd-style.dot": "Круг", + "arrowheadEnd-style.inverted": "Обратная", + "arrowheadEnd-style.pipe": "Труба", + "arrowheadEnd-style.square": "Квадрат", + "arrowheadEnd-style.triangle": "Треугольник", + "spline-style.line": "Прямой", + "spline-style.cubic": "Кубический", + "tool.select": "Перемещение", + "tool.hand": "Рука", + "tool.draw": "Карандаш", + "tool.eraser": "Ластик", + "tool.arrow-down": "Стрелка вниз", + "tool.arrow-left": "Стрелка влево", + "tool.arrow-right": "Стрелка вправо", + "tool.arrow-up": "Стрелка вверх", + "tool.arrow": "Стрелка", + "tool.diamond": "Ромбоид", + "tool.ellipse": "Эллипс", + "tool.hexagon": "Шестиугольник", + "tool.line": "Линия", + "tool.octagon": "Восьмиугольник", + "tool.oval": "Овал", + "tool.pentagon": "Пятиугольник", + "tool.rectangle": "Прямоугольник", + "tool.rhombus": "Ромб", + "tool.star": "Звезда", + "tool.trapezoid": "Трапеция", + "tool.triangle": "Треугольник", + "tool.x-box": "X квадрат", + "tool.asset": "Ресурс", + "tool.frame": "Рамка", + "tool.note": "Заметка", + "tool.embed": "Встраивание", + "tool.text": "Текст", + "menu.title": "Меню", + "menu.copy-as": "Скопировать как", + "menu.edit": "Правка", + "menu.export-as": "Экспортировать как", + "menu.file": "Файл", + "menu.language": "Язык", + "menu.preferences": "Настройки", + "menu.view": "Вид", + "context-menu.arrange": "Организовать", + "context-menu.copy-as": "Скопировать как", + "context-menu.export-as": "Экспортировать как", + "context-menu.move-to-page": "Перенести на страницу", + "context-menu.reorder": "Переупорядочить", + "page-menu.title": "Страницы", + "page-menu.create-new-page": "Создать новую страницу", + "page-menu.edit-pages": "Редактировать страницы", + "page-menu.max-page-count-reached": "Достигнуто максимальное количество страниц", + "page-menu.new-page-initial-name": "Страница 1", + "page-menu.page": "Страница", + "page-menu.edit-start": "Редактировать", + "page-menu.edit-done": "Выполнено", + "page-menu.submenu.rename": "Переименовать", + "page-menu.submenu.duplicate-page": "Дублировать", + "page-menu.submenu.go-to-page": "Перейти на страницу", + "page-menu.submenu.title": "Меню", + "page-menu.submenu.move-down": "Переместить вниз", + "page-menu.submenu.move-up": "Переместить вверх", + "page-menu.submenu.delete": "Удалить", + "share-menu.title": "Поделиться", + "share-menu.share-project": "Поделиться этим проектом", + "share-menu.create-project": "Новый совместный проект", + "share-menu.copy-link": "Копировать ссылку", + "share-menu.readonly-link": "Только для чтения", + "share-menu.copy-readonly-link": "Скопировать ссылку только для чтения", + "share-menu.offline-note": "Совместное использование этого проекта создаст размещенную онлайн копию по новому URL-адресу. Вы можете поделиться URL-адресом с тридцатью другими людьми для совместного просмотра и редактирования проекта.", + "share-menu.copy-link-note": "Любой, у кого есть ссылка, сможет просматривать и редактировать этот проект.", + "share-menu.copy-readonly-link-note": "Любой, у кого есть ссылка, сможет просматривать (но не редактировать) этот проект.", + "share-menu.project-too-large": "К сожалению, этим проектом нельзя поделиться, потому что он слишком большой. Мы работаем над этим!", + "people-menu.title": "Люди", + "people-menu.change-name": "Изменить имя", + "people-menu.change-color": "Изменить цвет", + "people-menu.user": "(Вы)", + "people-menu.invite": "Пригласить других пользователей", + "debug-menu.hard-reset": "Жесткий сброс", + "debug-menu.create-shapes": "Создать 100 форм", + "help-menu.title": "Помощь и материалы", + "help-menu.about": "О нас", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Сочетания клавиш", + "help-menu.twitter": "Twitter", + "links-menu.about": "О нас", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Действия", + "edit-link-dialog.title": "Редактировать ссылку", + "edit-link-dialog.invalid-url": "Ссылка должна быть действительным URL-адресом.", + "edit-link-dialog.detail": "Ссылки откроются в новой вкладке.", + "edit-link-dialog.url": "URL-адрес", + "edit-link-dialog.clear": "Очистить", + "edit-link-dialog.save": "Продолжить", + "edit-link-dialog.cancel": "Отмена", + "embed-dialog.title": "Создать встраивание", + "embed-dialog.url-label": "Вставить URL-адрес", + "embed-dialog.back": "Назад", + "embed-dialog.create": "Создать", + "embed-dialog.cancel": "Отмена", + "embed-dialog.url": "URL-адрес", + "embed-dialog.instruction": "Вставьте URL-адрес сайта, чтобы создать встраивание.", + "embed-dialog.invalid-url": "Нам не удалось создать встраивание из этого URL-адреса.", + "edit-pages-dialog.title": "Редактировать страницы", + "edit-pages-dialog.create-new-page": "Создать новую страницу", + "edit-pages-dialog.delete": "Удалить", + "edit-pages-dialog.duplicate-page": "Дублировать", + "edit-pages-dialog.go-to-page": "Перейти на страницу", + "edit-pages-dialog.max-page-count-reached": "Достигнуто максимальное количество страниц", + "edit-pages-dialog.more-menu": "Меню", + "edit-pages-dialog.move-down": "Переместить вниз", + "edit-pages-dialog.move-up": "Переместить вверх", + "edit-pages-dialog.new-page-initial-name": "Страница 1", + "reload-file-dialog.title": "Продолжить редактирование файла", + "reload-file-dialog.description": "Вы только что редактировали файл. Хотите ли продолжить его редактирование?", + "reload-file-dialog.failure": "Не удалось перезагрузить файл. Попробуйте еще раз?", + "reload-file-dialog.reload": "Продолжить редактирование", + "reload-file-dialog.revert": "Нет, спасибо", + "shortcuts-dialog.title": "Сочетания клавиш", + "shortcuts-dialog.edit": "Правка", + "shortcuts-dialog.file": "Файл", + "shortcuts-dialog.preferences": "Настройки", + "shortcuts-dialog.tools": "Инструменты", + "shortcuts-dialog.transform": "Преобразования", + "shortcuts-dialog.view": "Вид", + "shortcuts-dialog.save": "Продолжить", + "style-panel.title": "Стили", + "style-panel.align": "Выровнять", + "style-panel.arrowheads": "Стрелка", + "style-panel.color": "Цвет", + "style-panel.dash": "Контур", + "style-panel.fill": "Заливка", + "style-panel.font": "Шрифт", + "style-panel.geo": "Форма", + "style-panel.label": "Метка", + "style-panel.mixed": "Смешанный", + "style-panel.opacity": "Непрозрачность", + "style-panel.size": "Размер", + "style-panel.spline": "Сплайн", + "style-panel.text": "Текст", + "tool-panel.drawing": "Рисунок", + "tool-panel.geo": "Форма", + "tool-panel.shapes": "Формы", + "tool-panel.things": "Предметы", + "tool-panel.tools": "Инструменты", + "save-changes-prompt.title": "У вас есть несохраненные изменения", + "save-changes-prompt.description": "Хотите сохранить изменения в текущем файле?", + "save-changes-prompt.go-back": "Вернуться назад", + "save-changes-prompt.continue": "Продолжить", + "navigation-zone.toggle-minimap": "Переключить мини-карту", + "navigation-zone.zoom": "Увеличить", + "focus-mode.toggle-focus-mode": "Переключить режим концентрации", + "toast.close": "Закрыть", + "file-system.file-open-error.title": "Не удалось открыть файл", + "file-system.file-open-error.not-a-tldraw-file": "Файл, который вы пытались открыть, не похож на файл tldraw.", + "file-system.file-open-error.file-format-version-too-new": "Файл, который вы пытались открыть, относится к более новой версии tldraw. Пожалуйста, обновите страницу и попробуйте еще раз.", + "file-system.file-open-error.generic-corrupted-file": "Файл, который вы пытались открыть, поврежден.", + "file-system.confirm-open.title": "Перезаписать текущий проект?", + "file-system.confirm-open.description": "Открытие файла заменит ваш текущий проект, и все несохраненные изменения будут потеряны. Вы уверены, что хотите продолжить?", + "file-system.confirm-open.cancel": "Отмена", + "file-system.confirm-open.open": "Открыть файл", + "file-system.confirm-open.dont-show-again": "Больше не спрашивать", + "toast.error.export-fail.title": "Ошибка экспорта", + "toast.error.export-fail.desc": "Не удалось экспортировать изображение", + "toast.error.copy-fail.title": "Не удалось скопировать", + "toast.error.copy-fail.desc": "Не удалось скопировать изображение", + "file-system.shared-document-file-open-error.title": "Не удалось открыть файл", + "file-system.shared-document-file-open-error.description": "Открытие файлов из совместных проектов не поддерживается.", + "vscode.file-open.dont-show-again": "Больше не спрашивать", + "vscode.file-open.desc": "Этот файл был создан в более ранней версии tldraw. Хотите обновить его для работы с новой версией?", + "context.pages.new-page": "Новая страница", + "style-panel.arrowhead-start": "Наконечник", + "style-panel.arrowhead-end": "Хвост", + "vscode.file-open.open": "Продолжить", + "vscode.file-open.backup": "Резервная копия", + "vscode.file-open.backup-saved": "Резервная копия сохранена", + "vscode.file-open.backup-failed": "Резервное копирование не удалось: это не файл .tldr", + "tool-panel.more": "Подробнее", + "debug-panel.more": "Подробнее", + "action.new-project": "Новый проект", + "file-system.confirm-clear.title": "Очистить текущий проект?", + "file-system.confirm-clear.description": "Создание нового проекта очистит ваш текущий проект, и все несохраненные изменения будут потеряны. Вы уверены, что хотите продолжить?", + "file-system.confirm-clear.cancel": "Отмена", + "file-system.confirm-clear.continue": "Продолжить", + "file-system.confirm-clear.dont-show-again": "Больше не спрашивать", + "action.stop-following": "Перестать следовать", + "people-menu.follow": "Следовать", + "style-panel.position": "Позиция" +} \ No newline at end of file diff --git a/assets/translations/sv.json b/assets/translations/sv.json new file mode 100644 index 000000000..55d8375ee --- /dev/null +++ b/assets/translations/sv.json @@ -0,0 +1,105 @@ +{ + "action.bring-forward": "Flytta framåt", + "action.bring-to-front": "Placera längst fram", + "action.copy": "Kopiera", + "action.cut": "Klipp ut", + "action.delete": "Radera", + "action.duplicate": "Duplicera", + "action.flip-horizontal": "Vänd horisontellt", + "action.flip-vertical": "Vänd vertikalt", + "action.flip-horizontal.short": "Vänd horisontellt", + "action.flip-vertical.short": "Vänd vertikalt", + "action.group": "Gruppera", + "action.insert-media": "Ladda upp media", + "action.paste": "Klistra in", + "action.redo": "Gör om", + "action.select-all": "Välj alla", + "action.select-none": "Välj ingen", + "action.send-backward": "Flytta bakåt", + "action.send-to-back": "Placera längst bak", + "action.toggle-dark-mode.menu": "Mörkt läge", + "action.toggle-dark-mode": "Mörkt läge", + "action.toggle-debug-mode.menu": "Debugläge", + "action.toggle-debug-mode": "Debugläge", + "action.toggle-focus-mode.menu": "Fokusläge", + "action.toggle-focus-mode": "Fokusläge", + "action.toggle-grid.menu": "Visa rutnät", + "action.toggle-grid": "Visa rutnät", + "action.toggle-snap-mode.menu": "Visa alltid fästpunkter", + "action.toggle-snap-mode": "Visa alltid fästpunkter", + "action.toggle-transparent.context-menu": "Transparent", + "action.toggle-transparent.menu": "Transparent", + "action.undo": "Ångra", + "action.ungroup": "Avgruppera", + "action.zoom-in": "Zooma in", + "action.zoom-out": "Zooma ut", + "action.zoom-to-fit": "Anpassa zoom till skärm", + "action.zoom-to-selection": "Anpassa zoom till urval", + "dash-style.draw": "Rita", + "font-style.draw": "Rita", + "geo-style.ellipse": "Ellips", + "geo-style.rectangle": "Rektangel", + "geo-style.triangle": "Triangel", + "arrowheadStart-style.arrow": "Pil", + "arrowheadStart-style.triangle": "Triangel", + "arrowheadEnd-style.arrow": "Pil", + "arrowheadEnd-style.triangle": "Triangel", + "spline-style.line": "Linje", + "tool.select": "Välj", + "tool.draw": "Rita", + "tool.eraser": "Radera", + "tool.arrow": "Pil", + "tool.ellipse": "Ellips", + "tool.line": "Linje", + "tool.rectangle": "Rektangel", + "tool.triangle": "Triangel", + "tool.note": "Klisterlapp", + "tool.text": "Text", + "menu.copy-as": "Kopiera som", + "menu.edit": "Redigera", + "menu.export-as": "Exportera till", + "menu.file": "Arkiv", + "menu.language": "Språk", + "menu.preferences": "Inställningar", + "menu.view": "Innehåll", + "context-menu.copy-as": "Kopiera som", + "context-menu.export-as": "Exportera till", + "context-menu.move-to-page": "Flytta till sida", + "page-menu.create-new-page": "Skapa sida", + "page-menu.page": "Sida", + "page-menu.edit-start": "Redigera", + "page-menu.submenu.duplicate-page": "Duplicera", + "page-menu.submenu.delete": "Radera", + "share-menu.copy-link": "Kopiera länk med redigeringsrättigheter", + "share-menu.copy-readonly-link": "Kopiera länk med läsrättigheter", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Tangentbordsgenvägar", + "help-menu.twitter": "Twitter", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "edit-link-dialog.cancel": "Avbryt", + "embed-dialog.cancel": "Avbryt", + "edit-pages-dialog.create-new-page": "Skapa sida", + "edit-pages-dialog.delete": "Radera", + "edit-pages-dialog.duplicate-page": "Duplicera", + "shortcuts-dialog.title": "Tangentbordsgenvägar", + "shortcuts-dialog.edit": "Redigera", + "shortcuts-dialog.file": "Arkiv", + "shortcuts-dialog.preferences": "Inställningar", + "shortcuts-dialog.tools": "Verktyg", + "shortcuts-dialog.transform": "Transform", + "shortcuts-dialog.view": "Innehåll", + "style-panel.title": "Utseende", + "style-panel.align": "Justera", + "style-panel.color": "Färg", + "style-panel.dash": "Streck", + "style-panel.fill": "Ifylld", + "style-panel.font": "Typsnitt", + "style-panel.size": "Storlek", + "style-panel.text": "Text", + "tool-panel.tools": "Verktyg", + "focus-mode.toggle-focus-mode": "Fokusläge", + "file-system.confirm-open.cancel": "Avbryt" +} diff --git a/assets/translations/te.json b/assets/translations/te.json new file mode 100644 index 000000000..cfc846b7b --- /dev/null +++ b/assets/translations/te.json @@ -0,0 +1,121 @@ +{ + "action.align-bottom": "అడుగున కుదుర్చు", + "action.align-center-horizontal": "అడ్డంగా మధ్యలో కుదుర్చు", + "action.align-center-vertical": "నిలువుగా మధ్యలో కుదుర్చు", + "action.align-center-horizontal.short": "అడ్డంగా మధ్యలో కుదుర్చు", + "action.align-center-vertical.short": "నిలువుగా మధ్యలో కుదుర్చు", + "action.align-left": "ఎడమవైపుకు కుదుర్చు", + "action.align-right": "కుడివైపుకు కుదుర్చు", + "action.align-top": "పైకి కుదుర్చు", + "action.bring-forward": "ముందుకు జరుపు", + "action.bring-to-front": "మొదటికి జరుపు", + "action.copy": "నకలు", + "action.cut": "కత్తిరించు", + "action.delete": "తొలగించు", + "action.distribute-horizontal": "అడ్డంగా పంచు", + "action.distribute-vertical": "నిలువుగా పంచు", + "action.distribute-horizontal.short": "అడ్డంగా పంచు", + "action.distribute-vertical.short": "నిలువుగా పంచు", + "action.duplicate": "మారుప్రతి", + "action.flip-horizontal": "అడ్డంగా పల్టీ", + "action.flip-vertical": "నిలువుగా పల్టీ", + "action.flip-horizontal.short": "అడ్డంగా పల్టీ", + "action.flip-vertical.short": "నిలువుగా పల్టీ", + "action.group": "గుంపుగా ఏర్పరచు", + "action.insert-media": "ఎగుమతి మాధ్యం", + "action.paste": "అతికించు", + "action.redo": "మళ్ళీ మార్చు", + "action.select-all": "అన్నీ ఎంపికచెయ్యి", + "action.select-none": "ఎదీ ఎంపికచెయ్యవద్దు", + "action.send-backward": "వెనుకకు జరుపు", + "action.send-to-back": "ఆఖరికి జరుపు", + "action.stretch-horizontal": "అడ్డంగా లాగు", + "action.stretch-vertical": "నిలువుగా లాగు", + "action.stretch-horizontal.short": "అడ్డంగా లాగు", + "action.stretch-vertical.short": "నిలువుగా లాగు", + "action.toggle-dark-mode.menu": "చీకటైన క్రమం", + "action.toggle-dark-mode": "చీకటైన క్రమం", + "action.toggle-debug-mode.menu": "తప్పులేరు క్రమం", + "action.toggle-debug-mode": "తప్పులేరు క్రమం", + "action.toggle-focus-mode.menu": "తీక్షణ క్రమం", + "action.toggle-focus-mode": "తీక్షణ క్రమం", + "action.toggle-grid.menu": "చట్రం చూపు", + "action.toggle-grid": "చట్రం చూపు", + "action.toggle-snap-mode.menu": "ఎప్పుడూ దృశ్యభాగం చూపు", + "action.toggle-snap-mode": "ఎప్పుడూ దృశ్యభాగం చూపు", + "action.toggle-transparent.context-menu": "కాంతి భేద్యము", + "action.toggle-transparent.menu": "కాంతి భేద్యము", + "action.undo": "మార్పుని తిరుగగొట్టు", + "action.ungroup": "గుంపును చెదరగొట్టు", + "action.zoom-in": "దగ్గరగా చూపు", + "action.zoom-out": "దూరంగా చూపు", + "action.zoom-to-fit": "సరిపెట్టి చూపు", + "action.zoom-to-selection": "ఎంచుకున్న విషయాన్నే చూపు", + "dash-style.draw": "గీయ్యి", + "font-style.draw": "గీయ్యి", + "geo-style.ellipse": "దీర్ఘవృత్తం", + "geo-style.rectangle": "దీర్ఘచతురస్రం", + "geo-style.triangle": "త్రిభుజం", + "arrowheadStart-style.arrow": "బాణం", + "arrowheadStart-style.triangle": "త్రిభుజం", + "arrowheadEnd-style.arrow": "బాణం", + "arrowheadEnd-style.triangle": "త్రిభుజం", + "spline-style.line": "గీత", + "tool.select": "ఎంపికచెయ్యి", + "tool.draw": "గీయ్యి", + "tool.eraser": "Eraser", + "tool.arrow": "బాణం", + "tool.ellipse": "దీర్ఘవృత్తం", + "tool.line": "గీత", + "tool.rectangle": "దీర్ఘచతురస్రం", + "tool.triangle": "త్రిభుజం", + "tool.note": "అతుక్కునే", + "tool.text": "అక్షరములు", + "menu.copy-as": "నకలుప్రతిగా ఇక్కడికి", + "menu.edit": "పరిష్కరించు", + "menu.export-as": "ఎగుమతి ఇక్కడికి", + "menu.file": "ఫైల్", + "menu.language": "భాష", + "menu.preferences": "ఎంచుకొన్నవి", + "menu.view": "చూపు", + "context-menu.copy-as": "నకలుప్రతిగా ఇక్కడికి", + "context-menu.export-as": "ఎగుమతి ఇక్కడికి", + "context-menu.move-to-page": "పుటలోకి జరుపు", + "page-menu.create-new-page": "పుట కల్పించు", + "page-menu.page": "పుత", + "page-menu.edit-start": "పరిష్కరించు", + "page-menu.submenu.duplicate-page": "మారుప్రతి", + "page-menu.submenu.delete": "తొలగించు", + "share-menu.copy-link": "అహ్వాన లింకు రాయి", + "share-menu.copy-readonly-link": "మారనిప్రతి లింకు రాయి", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "కీబోర్డ్ సత్వరమార్గం", + "help-menu.twitter": "Twitter", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "edit-link-dialog.cancel": "రద్దుచేయి", + "embed-dialog.cancel": "రద్దుచేయి", + "edit-pages-dialog.create-new-page": "పుట కల్పించు", + "edit-pages-dialog.delete": "తొలగించు", + "edit-pages-dialog.duplicate-page": "మారుప్రతి", + "shortcuts-dialog.title": "కీబోర్డ్ సత్వరమార్గం", + "shortcuts-dialog.edit": "పరిష్కరించు", + "shortcuts-dialog.file": "ఫైల్", + "shortcuts-dialog.preferences": "ఎంచుకొన్నవి", + "shortcuts-dialog.tools": "పరికరాలు", + "shortcuts-dialog.transform": "మార్చు", + "shortcuts-dialog.view": "చూపు", + "style-panel.title": "విధములు", + "style-panel.align": "సరిపరచు", + "style-panel.color": "రంగు", + "style-panel.dash": "అడ్డ గీత", + "style-panel.fill": "నింపు", + "style-panel.font": "అక్షరాకృతి", + "style-panel.size": "పరిమాణం", + "style-panel.text": "అక్షరములు", + "tool-panel.tools": "పరికరాలు", + "focus-mode.toggle-focus-mode": "తీక్షణ క్రమం", + "file-system.confirm-open.cancel": "రద్దుచేయి" +} diff --git a/assets/translations/th.json b/assets/translations/th.json new file mode 100644 index 000000000..3e2d1d8a4 --- /dev/null +++ b/assets/translations/th.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "แปลงเป็นบุ๊กมาร์ก", + "action.convert-to-embed": "แปลงเป็น Embed", + "action.open-embed-link": "เปิดลิงก์", + "action.align-bottom": "จัดให้ชิดด้านล่าง", + "action.align-center-horizontal": "จัดให้กึ่งกลางแนวนอน", + "action.align-center-vertical": "ซ้อนต่อกันแนวนอน", + "action.align-center-horizontal.short": "จัดให้อยู่กึ่งกลางแนวนอน", + "action.align-center-vertical.short": "จัดให้อยู่กึ่งกลางแนวตั้ง", + "action.align-left": "จัดให้ชิดซ้าย", + "action.align-right": "จัดให้ชิดขวา", + "action.align-top": "จัดให้ชิดด้านบน", + "action.back-to-content": "กลับไปยังเนื้อหา", + "action.bring-forward": "ย้ายไปข้างหน้า", + "action.bring-to-front": "ย้ายไปด้านหน้าสุด", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "คัดลอกเป็น JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "คัดลอกเป็น PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "คัดลอกเป็น SVG", + "action.copy": "คัดลอก", + "action.cut": "ตัด", + "action.delete": "ลบ", + "action.distribute-horizontal": "กระจายในแนวนอน", + "action.distribute-vertical": "กระจายในแนวตั้ง", + "action.distribute-horizontal.short": "กระจายแนวนอน", + "action.distribute-vertical.short": "กระจายแนวตั้ง", + "action.duplicate": "ทำซ้ำ", + "action.edit-link": "แก้ไขลิงก์", + "action.exit-pen-mode": "ออกจากโหมดปากกา", + "action.export-as-json.short": "JSON", + "action.export-as-json": "ส่งออกเป็น JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "ส่งออกเป็น PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "ส่งออกเป็น SVG", + "action.flip-horizontal": "พลิกแนวนอน", + "action.flip-vertical": "พลิกแนวตั้ง", + "action.flip-horizontal.short": "พลิกแนวตั้ง", + "action.flip-vertical.short": "พลิกแนวนอน", + "action.group": "จัดกลุ่ม", + "action.insert-media": "อัปโหลดสื่อ", + "action.new-shared-project": "โครงการใหม่ที่ใช้ร่วมกัน", + "action.nudge-down": "เขยิบลง", + "action.nudge-left": "เขยิบไปทางซ้าย", + "action.nudge-right": "เขยิบไปทางขวา", + "action.nudge-up": "เขยิบขึ้น", + "action.open-file": "เปิดไฟล์", + "action.pack": "บรรจุ", + "action.paste": "วาง", + "action.print": "พิมพ์", + "action.redo": "ทำซ้ำ", + "action.rotate-ccw": "หมุนทวนเข็มนาฬิกา", + "action.rotate-cw": "หมุนตามเข็มนาฬิกา", + "action.save-copy": "บันทึกสำเนา", + "action.select-all": "เลือกทั้งหมด", + "action.select-none": "ไม่เลือกทั้งหมด", + "action.send-backward": "ย้ายไปข้างหลัง", + "action.send-to-back": "ย้ายไปด้านหลังสุด", + "action.share-project": "แบ่งปันโครงการนี้", + "action.stack-horizontal": "ซ้อนต่อกันแนวนอน", + "action.stack-vertical": "ซ้อนต่อกันแนวตั้ง", + "action.stack-horizontal.short": "ซ้อนต่อกันแนวนอน", + "action.stack-vertical.short": "ซ้อนต่อกันแนวตั้ง", + "action.stretch-horizontal": "ยืดในแนวนอน", + "action.stretch-vertical": "ยืดในแนวตั้ง", + "action.stretch-horizontal.short": "ยืดแนวนอน", + "action.stretch-vertical.short": "ยืดแนวตั้ง", + "action.toggle-auto-size": "สลับขนาดอัตโนมัติ", + "action.toggle-dark-mode.menu": "โหมดมืด", + "action.toggle-dark-mode": "โหมดมืด", + "action.toggle-debug-mode.menu": "โหมดดีบัก", + "action.toggle-debug-mode": "โหมดดีบัก", + "action.toggle-focus-mode.menu": "โหมดโฟกัส", + "action.toggle-focus-mode": "โหมดโฟกัส", + "action.toggle-grid.menu": "แสดงตาราง", + "action.toggle-grid": "แสดงตาราง", + "action.toggle-snap-mode.menu": "แสดงสแนปตลอด", + "action.toggle-snap-mode": "สลับสแนปตลอด", + "action.toggle-tool-lock.menu": "ล็อคเครื่องมือ", + "action.toggle-tool-lock": "สลับล็อคเครื่องมือ", + "action.toggle-transparent.context-menu": "โปร่งใส", + "action.toggle-transparent.menu": "โปร่งใส", + "action.toggle-transparent": "สลับพื้นหลังโปร่งใส", + "action.undo": "เลิกทำ", + "action.ungroup": "ยกเลิกจัดกลุ่ม", + "action.zoom-in": "ขยาย", + "action.zoom-out": "ย่อ", + "action.zoom-to-100": "ขยายเป็น 100%", + "action.zoom-to-fit": "ขยายให้พอดี", + "action.zoom-to-selection": "ขยายให้พอดีกับสิ่งที่เลือก", + "color-style.black": "สีดำ", + "color-style.blue": "สีฟ้า", + "color-style.green": "สีเขียว", + "color-style.grey": "สีเทา", + "color-style.light-blue": "สีม่วงอ่อน", + "color-style.light-green": "สีม่วงอ่อน", + "color-style.light-red": "สีม่วงอ่อน", + "color-style.light-violet": "สีม่วงอ่อน", + "color-style.orange": "สีส้ม", + "color-style.red": "สีแดง", + "color-style.violet": "สีม่วง", + "color-style.yellow": "สีเหลือง", + "fill-style.none": "ไม่มี", + "fill-style.semi": "กึ่ง", + "fill-style.solid": "หนาแข็ง", + "fill-style.pattern": "ลวดลาย", + "dash-style.dashed": "ประ", + "dash-style.dotted": "จุด", + "dash-style.draw": "ดินสอ", + "dash-style.solid": "หนาแข็ง", + "size-style.s": "เล็ก", + "size-style.m": "ปานกลาง", + "size-style.l": "ใหญ่", + "size-style.xl": "ขนาดใหญ่พิเศษ", + "opacity-style.0.1": "๑๐%", + "opacity-style.0.25": "๒๕%", + "opacity-style.0.5": "๕๐%", + "opacity-style.0.75": "๗๕%", + "opacity-style.1": "๑๐๐%", + "font-style.draw": "ดินสอ", + "font-style.sans": "แซนส์", + "font-style.serif": "เซริฟ", + "font-style.mono": "โมโน", + "align-style.start": "เริ่มต้น", + "align-style.middle": "กลาง", + "align-style.end": "สิ้นสุด", + "align-style.justify": "ปรับ", + "geo-style.arrow-down": "ลูกศรลง", + "geo-style.arrow-left": "ลูกศรซ้าย", + "geo-style.arrow-right": "ลูกศรขวา", + "geo-style.arrow-up": "ลูกศรขึ้น", + "geo-style.diamond": "สี่เหลี่ยมเพชร", + "geo-style.ellipse": "วงรี", + "geo-style.hexagon": "หกเหลี่ยม", + "geo-style.octagon": "แปดเหลี่ยม", + "geo-style.oval": "วงรี", + "geo-style.pentagon": "ห้าเหลี่ยม", + "geo-style.rectangle": "สี่เหลี่ยมผืนผ้า", + "geo-style.rhombus-2": "สี่เหลี่ยมขนมเปียกปูน", + "geo-style.rhombus": "สี่เหลี่ยมขนมเปียกปูน", + "geo-style.star": "ดาว", + "geo-style.trapezoid": "สี่เหลี่ยมคางหมู", + "geo-style.triangle": "สามเหลี่ยม", + "geo-style.x-box": "สี่เหลี่ยมพื้นผ้า X", + "arrowheadStart-style.none": "ไม่มี", + "arrowheadStart-style.arrow": "ลูกศร", + "arrowheadStart-style.bar": "แถบ", + "arrowheadStart-style.diamond": "สี่เหลี่ยมเพชร", + "arrowheadStart-style.dot": "จุด", + "arrowheadStart-style.inverted": "กลับด้าน", + "arrowheadStart-style.pipe": "ท่อ", + "arrowheadStart-style.square": "สี่เหลี่ยมจัตุรัส", + "arrowheadStart-style.triangle": "สามเหลี่ยม", + "arrowheadEnd-style.none": "ไม่มี", + "arrowheadEnd-style.arrow": "ลูกศร", + "arrowheadEnd-style.bar": "แถบ", + "arrowheadEnd-style.diamond": "สี่เหลี่ยมเพชร", + "arrowheadEnd-style.dot": "จุด", + "arrowheadEnd-style.inverted": "กลับด้าน", + "arrowheadEnd-style.pipe": "ท่อ", + "arrowheadEnd-style.square": "สี่เหลี่ยมจัตุรัส", + "arrowheadEnd-style.triangle": "สามเหลี่ยม", + "spline-style.line": "เส้น", + "spline-style.cubic": "ลูกบาศก์", + "tool.select": "เลือก", + "tool.hand": "มือ", + "tool.draw": "ดินสอ", + "tool.eraser": "ยางลบ", + "tool.arrow-down": "ลูกศรลง", + "tool.arrow-left": "ลูกศรซ้าย", + "tool.arrow-right": "ลูกศรขวา", + "tool.arrow-up": "ลูกศรขึ้น", + "tool.arrow": "ลูกศร", + "tool.diamond": "สี่เหลี่ยมเพชร", + "tool.ellipse": "วงรี", + "tool.hexagon": "หกเหลี่ยม", + "tool.line": "เส้น", + "tool.octagon": "แปดเหลี่ยม", + "tool.oval": "วงรี", + "tool.pentagon": "ห้าเหลี่ยม", + "tool.rectangle": "สี่เหลี่ยมผืนผ้า", + "tool.rhombus": "สี่เหลี่ยมขนมเปียกปูน", + "tool.star": "ดาว", + "tool.trapezoid": "สี่เหลี่ยมคางหมู", + "tool.triangle": "สามเหลี่ยม", + "tool.x-box": "สี่เหลี่ยมพื้นผ้า X", + "tool.asset": "มีเดีย", + "tool.frame": "กรอบ", + "tool.note": "บันทึกย่อ", + "tool.embed": "Embed", + "tool.text": "กล่องข้อความ", + "menu.title": "เมนู", + "menu.copy-as": "คัดลอกเป็น", + "menu.edit": "แก้ไข", + "menu.export-as": "ส่งออกเป็น", + "menu.file": "ไฟล์", + "menu.language": "ภาษา", + "menu.preferences": "ตั้งค่า", + "menu.view": "มุมมอง", + "context-menu.arrange": "จัดระเบียบ", + "context-menu.copy-as": "คัดลอกเป็น", + "context-menu.export-as": "ส่งออกเป็น", + "context-menu.move-to-page": "ย้ายไปยังหน้า", + "context-menu.reorder": "จัดเรียงลำดับ", + "page-menu.title": "หน้า", + "page-menu.create-new-page": "สร้างหน้า", + "page-menu.edit-pages": "แก้ไขหน้า", + "page-menu.max-page-count-reached": "จํานวนหน้าสูงสุดแล้ว", + "page-menu.new-page-initial-name": "หน้า 1", + "page-menu.page": "หน้า", + "page-menu.edit-start": "แก้ไข", + "page-menu.edit-done": "เสร็จแล้ว", + "page-menu.submenu.rename": "เปลี่ยนชื่อ", + "page-menu.submenu.duplicate-page": "สำเนา", + "page-menu.submenu.go-to-page": "ไปยังหน้า", + "page-menu.submenu.title": "เมนู", + "page-menu.submenu.move-down": "เลื่อนลง", + "page-menu.submenu.move-up": "เลื่อนขึ้น", + "page-menu.submenu.delete": "ลบ", + "share-menu.title": "แบ่งปัน", + "share-menu.share-project": "แบ่งปันโครงการนี้", + "share-menu.create-project": "โครงการใหม่ที่ใช้งานร่วมกัน", + "share-menu.copy-link": "คัดลอกลิงก์เชิญ", + "share-menu.readonly-link": "อ่านอย่างเดียว", + "share-menu.copy-readonly-link": "คัดลอกลิงก์แบบให้อ่านอย่างเดียว", + "share-menu.offline-note": "การแชร์โครงการต์นี้จะสร้างสําเนาสดที่โฮสต์ที่ URL ใหม่ คุณสามารถแชร์ URL กับบุคคลอื่นได้สูงสุดสามสิบคนเพื่อดูและแก้ไขโครงการด้วยกัน", + "share-menu.copy-link-note": "ทุกคนที่มีลิงก์จะสามารถดูและแก้ไขโครงการนี้ได้", + "share-menu.copy-readonly-link-note": "ทุกคนที่มีลิงก์จะสามารถดู (แต่ไม่สามารถแก้ไข) โครงการนี้ได้", + "share-menu.project-too-large": "ขออภัย ไม่สามารถแชร์โครงการนี้ได้เนื่องจากมีขนาดใหญ่เกินไป เรากำลังดำเนินการอยู่!", + "people-menu.title": "ผู้คน", + "people-menu.change-name": "เปลี่ยนชื่อ", + "people-menu.change-color": "เปลี่ยนสี", + "people-menu.user": "(คุณ)", + "people-menu.invite": "เชิญผู้อื่น", + "debug-menu.hard-reset": "ล้างค่าระบบ", + "debug-menu.create-shapes": "สร้าง 100 รูปทรง", + "help-menu.title": "ความช่วยเหลือและแหล่งข้อมูล", + "help-menu.about": "เกี่ยวกับ", + "help-menu.discord": "ดิสคอร์ด", + "help-menu.github": "กิตฮับ", + "help-menu.keyboard-shortcuts": "แป้นพิมพ์ลัด", + "help-menu.twitter": "ทวิตเตอร์", + "links-menu.about": "เกี่ยวกับ", + "links-menu.discord": "ดิสคอร์ด", + "links-menu.github": "กิตฮับ", + "links-menu.twitter": "ทวิตเตอร์", + "actions-menu.title": "การกระทำ", + "edit-link-dialog.title": "แก้ไขลิงค์", + "edit-link-dialog.invalid-url": "ลิงก์ต้องเป็น URL ที่ถูกต้อง", + "edit-link-dialog.detail": "ลิงก์จะเปิดขึ้นในแท็บใหม่", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "ล้าง", + "edit-link-dialog.save": "ดำเนินการต่อ", + "edit-link-dialog.cancel": "ยกเลิก", + "embed-dialog.title": "สร้าง embed", + "embed-dialog.url-label": "วาง URL", + "embed-dialog.back": "กลับ", + "embed-dialog.create": "สร้าง", + "embed-dialog.cancel": "ยกเลิก", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "วาง URL ของไซต์เพื่อสร้าง embed", + "embed-dialog.invalid-url": "เราไม่สามารถสร้าง embed จาก URL นั้นได้", + "edit-pages-dialog.title": "แก้ไขหน้า", + "edit-pages-dialog.create-new-page": "สร้างหน้า", + "edit-pages-dialog.delete": "ลบ", + "edit-pages-dialog.duplicate-page": "สำเนา", + "edit-pages-dialog.go-to-page": "ไปยังหน้า", + "edit-pages-dialog.max-page-count-reached": "จํานวนหน้าสูงสุดแล้ว", + "edit-pages-dialog.more-menu": "เมนู", + "edit-pages-dialog.move-down": "เลื่อนลง", + "edit-pages-dialog.move-up": "เลื่อนขึ้น", + "edit-pages-dialog.new-page-initial-name": "หน้า 1", + "reload-file-dialog.title": "ดำเนินการแก้ไขต่อไป", + "reload-file-dialog.description": "คุณเพิ่งแก้ไขไฟล์ คุณต้องการแก้ไขต่อไปหรือไม่", + "reload-file-dialog.failure": "การโหลดไฟล์ซ้ำไม่สำเร็จ ลองอีกครั้งไหม", + "reload-file-dialog.reload": "ดําเนินการแก้ไขต่อ", + "reload-file-dialog.revert": "ไม่เป็นไรขอบคุณ", + "shortcuts-dialog.title": "แป้นพิมพ์ลัด", + "shortcuts-dialog.edit": "แก้ไข", + "shortcuts-dialog.file": "ไฟล์", + "shortcuts-dialog.preferences": "ตั้งค่า", + "shortcuts-dialog.tools": "เครื่องมือ", + "shortcuts-dialog.transform": "แปลง", + "shortcuts-dialog.view": "มุมมอง", + "shortcuts-dialog.save": "ดำเนินการต่อ", + "style-panel.title": "รูปแบบ", + "style-panel.align": "จัดแนว", + "style-panel.arrowheads": "หัวลูกศร", + "style-panel.color": "สี", + "style-panel.dash": "เส้นขีด", + "style-panel.fill": "เติม", + "style-panel.font": "แบบอักษร", + "style-panel.geo": "รูปร่าง", + "style-panel.label": "ป้ายกำกับ", + "style-panel.mixed": "ผสม", + "style-panel.opacity": "ความทึบ", + "style-panel.size": "ขนาด", + "style-panel.spline": "เส้นโค้ง", + "style-panel.text": "กล่องข้อความ", + "tool-panel.drawing": "การวาดภาพ", + "tool-panel.geo": "รูปร่าง", + "tool-panel.shapes": "รูปทรง", + "tool-panel.things": "สิ่งของ", + "tool-panel.tools": "เครื่องมือ", + "save-changes-prompt.title": "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก", + "save-changes-prompt.description": "คุณต้องการบันทึกการเปลี่ยนแปลงในไฟล์ปัจจุบันของคุณหรือไม่", + "save-changes-prompt.go-back": "ย้อนกลับ", + "save-changes-prompt.continue": "ดำเนินการต่อ", + "navigation-zone.toggle-minimap": "สลับแผนที่ย่อ", + "navigation-zone.zoom": "ขยาย", + "focus-mode.toggle-focus-mode": "โหมดโฟกัส", + "toast.close": "ปิด", + "file-system.file-open-error.title": "ไม่สามารถเปิดไฟล์ได้", + "file-system.file-open-error.not-a-tldraw-file": "ไฟล์ที่คุณพยายามเปิดดูไม่เหมือนไฟล์ tldraw", + "file-system.file-open-error.file-format-version-too-new": "ไฟล์ที่คุณพยายามเปิดมาจาก tldraw รุ่นใหม่กว่า โปรดโหลดหน้าเว็บและลองอีกครั้ง", + "file-system.file-open-error.generic-corrupted-file": "ไฟล์ที่คุณพยายามเปิดเสียหาย", + "file-system.confirm-open.title": "เขียนทับโครงการปัจจุบันใช่หรือไม่", + "file-system.confirm-open.description": "การเปิดไฟล์จะแทนที่โครงการปัจจุบันของคุณและการเปลี่ยนแปลงที่ไม่ได้บันทึกจะหายไป คุณแน่ใจหรือไม่ว่าต้องการดําเนินการต่อ", + "file-system.confirm-open.cancel": "ยกเลิก", + "file-system.confirm-open.open": "เปิดไฟล์", + "file-system.confirm-open.dont-show-again": "ไม่ต้องถามอีก", + "toast.error.export-fail.title": "การส่งออกล้มเหลว", + "toast.error.export-fail.desc": "การส่งออกรูปภาพไม่สำเร็จ", + "toast.error.copy-fail.title": "การคัดลอกล้มเหลว", + "toast.error.copy-fail.desc": "คัดลอกรูปภาพไม่สําเร็จ", + "file-system.shared-document-file-open-error.title": "ไม่สามารถเปิดไฟล์ได้", + "file-system.shared-document-file-open-error.description": "ไม่รองรับการเปิดไฟล์จากโครงการที่ใช้งานร่วมกัน", + "vscode.file-open.dont-show-again": "ไม่ต้องถามอีก", + "vscode.file-open.desc": "ไฟล์นี้ถูกสร้างขึ้นด้วย tldraw รุ่นก่อนหน้า คุณต้องการอัปเดตเพื่อทํางานกับรุ่นใหม่หรือไม่", + "context.pages.new-page": "หน้าใหม่" +} \ No newline at end of file diff --git a/assets/translations/tr.json b/assets/translations/tr.json new file mode 100644 index 000000000..390a4ada3 --- /dev/null +++ b/assets/translations/tr.json @@ -0,0 +1,332 @@ +{ + "action.convert-to-bookmark": "Yer işaretine dönüştür", + "action.convert-to-embed": "Yerleştirmeye dönüştür", + "action.open-embed-link": "Bağlantıyı aç", + "action.align-bottom": "Aşağı hizala", + "action.align-center-horizontal": "Yatay olanak hizala", + "action.align-center-vertical": "Dikey olarak hizala", + "action.align-center-horizontal.short": "Yatay hizala", + "action.align-center-vertical.short": "Dikey hizala", + "action.align-left": "Sola hizala", + "action.align-right": "Sağa hizala", + "action.align-top": "Yukarı hizala", + "action.back-to-content": "İçeriğe geri dön", + "action.bring-forward": "Öne doğru getir", + "action.bring-to-front": "Öne getir", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "JSON olarak kopyala", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "PNG olarak kopyala", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "SVG olarak kopyala", + "action.copy": "Kopyala", + "action.cut": "Kes", + "action.delete": "Sil", + "action.distribute-horizontal": "Yatay olarak dağıt", + "action.distribute-vertical": "Dikey olarak dağıt", + "action.distribute-horizontal.short": "Yatay dağıt", + "action.distribute-vertical.short": "Dikey dağıt", + "action.duplicate": "Kopya oluştur", + "action.edit-link": "Bağlantıyı düzenle", + "action.exit-pen-mode": "Kalem modundan çık", + "action.export-as-json.short": "JSON", + "action.export-as-json": "JSON olarak dışa aktar", + "action.export-as-png.short": "PNG", + "action.export-as-png": "PNG olarak dışa aktar", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "SVG olarak dışa aktar", + "action.flip-horizontal": "Yatay Çevir", + "action.flip-vertical": "Dikey Çevir", + "action.flip-horizontal.short": "Yatay döndür", + "action.flip-vertical.short": "Dikey döndür", + "action.group": "Grupla", + "action.insert-media": "Medya Yükle", + "action.new-shared-project": "Yeni ortak proje", + "action.nudge-down": "Aşağı it", + "action.nudge-left": "Sola it", + "action.nudge-right": "Sağa it", + "action.nudge-up": "Yukarı it", + "action.open-file": "Dosya aç", + "action.pack": "Paket", + "action.paste": "Yapıştır", + "action.print": "Yazdır", + "action.redo": "Yinele", + "action.rotate-ccw": "Saat yönünün aksine döndür", + "action.rotate-cw": "Saat yönünde döndür", + "action.save-copy": "Bir kopyasını kaydet", + "action.select-all": "Hepsini Seç", + "action.select-none": "Hiçbirini Seçme", + "action.send-backward": "Arkaya doğru gönder", + "action.send-to-back": "Arkaya gönder", + "action.share-project": "Bu projeyi paylaş", + "action.stack-horizontal": "Yatay olarak istifle", + "action.stack-vertical": "Dikey olarak istifle", + "action.stack-horizontal.short": "Yatay istifle", + "action.stack-vertical.short": "Dikey istifle", + "action.stretch-horizontal": "Yatay olanak esnet", + "action.stretch-vertical": "Dikey olarak esnet", + "action.stretch-horizontal.short": "Yatay esnet", + "action.stretch-vertical.short": "Dikey esnet", + "action.toggle-auto-size": "Otomatik boyuta geçiş yap", + "action.toggle-dark-mode.menu": "Karanlık mod", + "action.toggle-dark-mode": "Karanlık moda geçiş yap", + "action.toggle-debug-mode.menu": "Hata ayıklama modu", + "action.toggle-debug-mode": "Hata ayıklama moduna geçiş yap", + "action.toggle-focus-mode.menu": "Odak modu", + "action.toggle-focus-mode": "Odak moduna geçiş yap", + "action.toggle-grid.menu": "Izgarayı göster", + "action.toggle-grid": "Izgarayı değiştir", + "action.toggle-snap-mode.menu": "Her zaman tuttur", + "action.toggle-snap-mode": "Her zaman tuttura geçiş yap", + "action.toggle-tool-lock.menu": "Araç kilidi", + "action.toggle-tool-lock": "Araç kilidine geçiş yap", + "action.toggle-transparent.context-menu": "Şeffaf", + "action.toggle-transparent.menu": "Şeffaf", + "action.toggle-transparent": "Şeffaf arkaplana geçiş yap", + "action.undo": "Geri Al", + "action.ungroup": "Gruplamayı Kaldır", + "action.zoom-in": "Yakınlaştır", + "action.zoom-out": "Uzaklaştır", + "action.zoom-to-100": "%100 yakınlaştır", + "action.zoom-to-fit": "Sığdırmak için Yakınlaştır", + "action.zoom-to-selection": "Seçime Yakınlaştır", + "color-style.black": "Siyah", + "color-style.blue": "Mavi", + "color-style.green": "Yeşil", + "color-style.grey": "Gri", + "color-style.light-blue": "Açık mavi", + "color-style.light-green": "Açık yeşil", + "color-style.light-red": "Açık kırmızı", + "color-style.light-violet": "Açık eflatun", + "color-style.orange": "Turuncu", + "color-style.red": "Kırmızı", + "color-style.violet": "Eflatun", + "color-style.yellow": "Sarı", + "fill-style.none": "Hiçbiri", + "fill-style.semi": "Yarı", + "fill-style.solid": "Düz", + "fill-style.pattern": "Desen", + "dash-style.dashed": "Kesik çizgili", + "dash-style.dotted": "Noktalı", + "dash-style.draw": "Çizim", + "dash-style.solid": "Düz", + "size-style.s": "Küçük", + "size-style.m": "Orta boy", + "size-style.l": "Büyük", + "size-style.xl": "Ekstra büyük", + "opacity-style.0.1": "%10", + "opacity-style.0.25": "%25", + "opacity-style.0.5": "%50", + "opacity-style.0.75": "%75", + "opacity-style.1": "%100", + "font-style.draw": "Çizim", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Başla", + "align-style.middle": "Orta", + "align-style.end": "Son", + "align-style.justify": "Blokla", + "geo-style.arrow-down": "Aşağı ok", + "geo-style.arrow-left": "Sol ok", + "geo-style.arrow-right": "Sağ ok", + "geo-style.arrow-up": "Yukarı ok", + "geo-style.diamond": "Karo", + "geo-style.ellipse": "Elips", + "geo-style.hexagon": "Altıgen", + "geo-style.octagon": "Sekizgen", + "geo-style.oval": "Oval", + "geo-style.pentagon": "Beşgen", + "geo-style.rectangle": "Dikdörtgen", + "geo-style.rhombus-2": "Eşkenar Dörtgen 2", + "geo-style.rhombus": "Eşkenar dörtgen", + "geo-style.star": "Yıldız", + "geo-style.trapezoid": "Yamuk", + "geo-style.triangle": "Üçgen", + "geo-style.x-box": "X Kutusu", + "arrowheadStart-style.none": "Hiçbiri", + "arrowheadStart-style.arrow": "Ok", + "arrowheadStart-style.bar": "Çubuk", + "arrowheadStart-style.diamond": "Karo", + "arrowheadStart-style.dot": "Nokta", + "arrowheadStart-style.inverted": "Ters", + "arrowheadStart-style.pipe": "Boru", + "arrowheadStart-style.square": "Kare", + "arrowheadStart-style.triangle": "Üçgen", + "arrowheadEnd-style.none": "Hiçbiri", + "arrowheadEnd-style.arrow": "Ok", + "arrowheadEnd-style.bar": "Çubuk", + "arrowheadEnd-style.diamond": "Karo", + "arrowheadEnd-style.dot": "Nokta", + "arrowheadEnd-style.inverted": "Ters", + "arrowheadEnd-style.pipe": "Boru", + "arrowheadEnd-style.square": "Kare", + "arrowheadEnd-style.triangle": "Üçgen", + "spline-style.line": "Çizgi", + "spline-style.cubic": "Kübik", + "tool.select": "Seç", + "tool.hand": "El", + "tool.draw": "Çizim", + "tool.eraser": "Silgi", + "tool.arrow-down": "Aşağı ok", + "tool.arrow-left": "Sol ok", + "tool.arrow-right": "Sağ ok", + "tool.arrow-up": "Yukarı ok", + "tool.arrow": "Ok", + "tool.diamond": "Karo", + "tool.ellipse": "Elips", + "tool.hexagon": "Altıgen", + "tool.line": "Çizgi", + "tool.octagon": "Sekizgen", + "tool.oval": "Oval", + "tool.pentagon": "Beşgen", + "tool.rectangle": "Dikdörtgen", + "tool.rhombus": "Eşkenar dörtgen", + "tool.star": "Yıldız", + "tool.trapezoid": "Yamuk", + "tool.triangle": "Üçgen", + "tool.x-box": "X kutusu", + "tool.asset": "Varlık", + "tool.frame": "Çerçeve", + "tool.note": "Yapışkan", + "tool.embed": "Yerleştir", + "tool.text": "Yazı", + "menu.title": "Menü", + "menu.copy-as": "Olarak Kopyala", + "menu.edit": "Düzenle", + "menu.export-as": "Olarak Dışarı Aktar", + "menu.file": "Dosya", + "menu.language": "Dil", + "menu.preferences": "Tercihler", + "menu.view": "Görüntü", + "context-menu.arrange": "Düzenle", + "context-menu.copy-as": "Olarak Kopyala", + "context-menu.export-as": "Olarak Dışarı Aktar", + "context-menu.move-to-page": "Sayfaya Taşı", + "context-menu.reorder": "Yeniden sırala", + "page-menu.title": "Sayfalar", + "page-menu.create-new-page": "Yeni sayfa oluştur", + "page-menu.edit-pages": "Sayfaları düzenle", + "page-menu.max-page-count-reached": "Maksimum sayfaya ulaşıldı.", + "page-menu.new-page-initial-name": "Sayfa 1", + "page-menu.page": "Sayfa", + "page-menu.edit-start": "Düzenle", + "page-menu.edit-done": "Bitti", + "page-menu.submenu.rename": "Yeniden adlandır", + "page-menu.submenu.duplicate-page": "Kopya oluştur", + "page-menu.submenu.go-to-page": "Sayfaya git", + "page-menu.submenu.title": "Menü", + "page-menu.submenu.move-down": "Aşağı taşı", + "page-menu.submenu.move-up": "Yukarı taşı", + "page-menu.submenu.delete": "Sil", + "share-menu.title": "Paylaş", + "share-menu.share-project": "Bu projeyi paylaş", + "share-menu.create-project": "Yeni ortak proje", + "share-menu.copy-link": "Bağlantıyı kopyala", + "share-menu.readonly-link": "Salt okunur", + "share-menu.copy-readonly-link": "Salt okunur bağlantıyı kopyala", + "share-menu.offline-note": "Bu projeyi paylaşmak, yeni bir URL'de barındırılan canlı bir kopya oluşturacaktır. Projeyi birlikte görüntülemek ve düzenlemek için bağlantıyı en fazla otuz kişiyle paylaşabilirsiniz.", + "share-menu.copy-link-note": "Bağlantıya sahip olan herkes bu projeyi görüntüleyebilir ve düzenleyebilir.", + "share-menu.copy-readonly-link-note": "Bağlantıya sahip olan herkes bu projeyi görüntüleyebilir (ancak düzenleyemez)", + "share-menu.project-too-large": "Üzgünüz, bu proje çok büyük olduğu için paylaşılamıyor. Üzerinde çalışıyoruz!", + "people-menu.title": "Kişiler", + "people-menu.change-name": "İsmi değiştir", + "people-menu.change-color": "Rengi değiştir", + "people-menu.user": "(Siz)", + "people-menu.invite": "Başkalarını davet et", + "debug-menu.hard-reset": "Donanım sıfırlama", + "debug-menu.create-shapes": "100 şekil oluştur", + "help-menu.title": "Yardım ve kaynaklar", + "help-menu.about": "Hakkında", + "help-menu.discord": "Discord", + "help-menu.github": "Github", + "help-menu.keyboard-shortcuts": "Klavye kısayolları", + "help-menu.twitter": "Twitter", + "links-menu.about": "Hakkımızda", + "links-menu.discord": "Discord", + "links-menu.github": "Github", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Eylemler", + "edit-link-dialog.title": "Bağlantıyı düzenle", + "edit-link-dialog.invalid-url": "Bağlantı geçerli bir URL olmalıdır.", + "edit-link-dialog.detail": "Bağlantılar yeni sekmede açılacaktır.", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "Temizle", + "edit-link-dialog.save": "Devam et", + "edit-link-dialog.cancel": "İptal", + "embed-dialog.title": "Yerleştirme oluştur", + "embed-dialog.url-label": "URL'i yapıştır", + "embed-dialog.back": "Geri", + "embed-dialog.create": "Oluştur", + "embed-dialog.cancel": "İptal", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "Yerleştirmeyi oluşturmak için sitenin URL adresini yapıştırın.", + "embed-dialog.invalid-url": "Bu URL'den yerleştirme oluşturamadık.", + "edit-pages-dialog.title": "Sayfaları düzenle", + "edit-pages-dialog.create-new-page": "Yeni sayfa oluştur", + "edit-pages-dialog.delete": "Sil", + "edit-pages-dialog.duplicate-page": "Kopya oluştur", + "edit-pages-dialog.go-to-page": "Sayfaya git", + "edit-pages-dialog.max-page-count-reached": "Maksimum sayfa sayısına ulaşıldı", + "edit-pages-dialog.more-menu": "Menü", + "edit-pages-dialog.move-down": "Aşağı taşı", + "edit-pages-dialog.move-up": "Yukarı taşı", + "edit-pages-dialog.new-page-initial-name": "Sayfa 1", + "reload-file-dialog.title": "Dosyayı düzenlemeye devam et", + "reload-file-dialog.description": "Az önce bir dosyayı düzenliyordunuz. Düzenlemeye devam etmek ister misiniz?", + "reload-file-dialog.failure": "Dosya tekrar yüklenemedi. Tekrar denemek ister misiniz?", + "reload-file-dialog.reload": "Düzenlemeye devam et", + "reload-file-dialog.revert": "Hayır, teşekkürler", + "shortcuts-dialog.title": "Klavye kısayolları", + "shortcuts-dialog.edit": "Düzenle", + "shortcuts-dialog.file": "Dosya", + "shortcuts-dialog.preferences": "Tercihler", + "shortcuts-dialog.tools": "Araçlar", + "shortcuts-dialog.transform": "Dönüştür", + "shortcuts-dialog.view": "Görüntü", + "shortcuts-dialog.save": "Devam et", + "style-panel.title": "Stiller", + "style-panel.align": "Hizala", + "style-panel.arrowheads": "Ok uçları", + "style-panel.color": "Renk", + "style-panel.dash": "Çizgi", + "style-panel.fill": "Doldur", + "style-panel.font": "Yazı Tipi", + "style-panel.geo": "Şekil", + "style-panel.label": "Etiket", + "style-panel.mixed": "Karışık", + "style-panel.opacity": "Opaklık", + "style-panel.size": "Boyut", + "style-panel.text": "Yazı", + "tool-panel.drawing": "Çizim", + "tool-panel.geo": "Şekil", + "tool-panel.shapes": "Şekiller", + "tool-panel.things": "Şeyler", + "tool-panel.tools": "Araçlar", + "save-changes-prompt.title": "Kaydedilmemiş değişiklikleriniz mevcut", + "save-changes-prompt.description": "Mevcut dosyanızdaki değişiklikleri kaydemek ister misiniz?", + "save-changes-prompt.go-back": "Geri git", + "save-changes-prompt.continue": "Devam et", + "navigation-zone.toggle-minimap": "Mini haritaya geçiş yap", + "navigation-zone.zoom": "Yakınlaştır", + "focus-mode.toggle-focus-mode": "Odak modunu değiştir", + "toast.close": "Kapat", + "file-system.file-open-error.title": "Dosya açılamadı", + "file-system.file-open-error.not-a-tldraw-file": "Açmaya çalıştığınız dosya tldraw dosyasına benzemiyor.", + "file-system.file-open-error.file-format-version-too-new": "Açmaya çalıştığınız dosya daha yeni bir tldraw versiyonuna ait. Lütfen sayfayı yenileyip tekrar deneyin.", + "file-system.file-open-error.generic-corrupted-file": "Açmaya çalıştığınız dosya bozuk", + "file-system.confirm-open.title": "Mevcut projenin üzerine yazılsın mı?", + "file-system.confirm-open.description": "Açtığınız yeni dosya mevcut projenizin yerini alacak ve kaydedilmemiş tüm değişiklikler kaybolacaktır. Devam etmek istediğinize emin misiniz?", + "file-system.confirm-open.cancel": "İptal", + "file-system.confirm-open.open": "Dosya Aç", + "file-system.confirm-open.dont-show-again": "Bir daha sorma", + "toast.error.export-fail.title": "Dışa aktarma başarısız", + "toast.error.export-fail.desc": "Görsel dışa aktarılamadı", + "toast.error.copy-fail.title": "Kopyalama başarısız", + "toast.error.copy-fail.desc": "Görsel kopyalanamadı", + "file-system.shared-document-file-open-error.title": "Dosya açılamadı", + "file-system.shared-document-file-open-error.description": "Paylaşılan projelerdeki dosyaların açılmaası desteklenmemektedir.", + "vscode.file-open.dont-show-again": "Bir daha sorma", + "vscode.file-open.desc": "Bu dosya daha önceki bir tldraw versiyonu ile oluşturulmuştur. Yeni versiyonla çalışmak için güncellemek ister misiniz?", + "context.pages.new-page": "Yeni sayfa" +} \ No newline at end of file diff --git a/assets/translations/uk.json b/assets/translations/uk.json new file mode 100644 index 000000000..438d6550b --- /dev/null +++ b/assets/translations/uk.json @@ -0,0 +1,350 @@ +{ + "action.convert-to-bookmark": "Перетворити у закладку", + "action.convert-to-embed": "Конвертувати у вбудовування", + "action.open-embed-link": "Відкрити посилання", + "action.align-bottom": "Вирівняти за нижнім краєм", + "action.align-center-horizontal": "Вирівняти по горизонталі", + "action.align-center-vertical": "Вирівняти по вертикалі", + "action.align-center-horizontal.short": "Вирівняти по горизонталі", + "action.align-center-vertical.short": "Вирівняти по вертикалі", + "action.align-left": "Вирівняти за лівим краєм", + "action.align-right": "Вирівняти за правим краєм", + "action.align-top": "Вирівняти за верхнім краєм", + "action.back-to-content": "Повернутися до змісту", + "action.bring-forward": "Перемістити вперед", + "action.bring-to-front": "На передній план", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Копіювати як JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Копіювати як PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Копіювати як SVG", + "action.copy": "Копіювати", + "action.cut": "Вирізати", + "action.delete": "Видалити", + "action.distribute-horizontal": "Розподілити по горизонталі", + "action.distribute-vertical": "Розподілити по вертикалі", + "action.distribute-horizontal.short": "Розподілити по горизонталі", + "action.distribute-vertical.short": "Розподілити по вертикалі", + "action.duplicate": "Дублювати", + "action.edit-link": "Редагувати посилання", + "action.exit-pen-mode": "Вийти з режиму пера", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Експортувати як JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Експортувати як PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Експортувати як SVG", + "action.flip-horizontal": "Віддзеркалити по горизонталі", + "action.flip-vertical": "Віддзеркалити по вертикалі", + "action.flip-horizontal.short": "Віддзеркалити по горизонталі", + "action.flip-vertical.short": "Віддзеркалити по вертикалі", + "action.group": "Згрупувати", + "action.insert-media": "Завантажити медіа", + "action.new-shared-project": "Новий спільний проєкт", + "action.nudge-down": "Змістити вниз", + "action.nudge-left": "Змістити вліво", + "action.nudge-right": "Змістити вправо", + "action.nudge-up": "Змістити вгору", + "action.open-file": "Відкрити файл", + "action.pack": "Зібрати до купи", + "action.paste": "Вставити", + "action.print": "Друк", + "action.redo": "Повторити", + "action.rotate-ccw": "Обернути проти годинникової стрілки", + "action.rotate-cw": "Обернути за годинниковою стрілкою", + "action.save-copy": "Зберегти копію", + "action.select-all": "Обрати все", + "action.select-none": "Зняти виділення", + "action.send-backward": "Перемістити назад", + "action.send-to-back": "На задній план", + "action.share-project": "Поділитися цим проєктом", + "action.stack-horizontal": "Розмістити горизонтально", + "action.stack-vertical": "Розмістити вертикально", + "action.stack-horizontal.short": "Розмістити горизонтально", + "action.stack-vertical.short": "Розмістити вертикально", + "action.stretch-horizontal": "Розтягнути по горизонталі", + "action.stretch-vertical": "Розтягнути по вертикалі", + "action.stretch-horizontal.short": "Розтягнути по горизонталі", + "action.stretch-vertical.short": "Розтягнути по вертикалі", + "action.toggle-auto-size": "Перемикнути автоматичний розмір", + "action.toggle-dark-mode.menu": "Темний режим", + "action.toggle-dark-mode": "Перемкнути темний режим", + "action.toggle-debug-mode.menu": "Режим налагодження", + "action.toggle-debug-mode": "Перемкнути режим налагодження", + "action.toggle-focus-mode.menu": "Режим концентрації", + "action.toggle-focus-mode": "Перемкнути режим концентрації", + "action.toggle-grid.menu": "Показати сітку", + "action.toggle-grid": "Перемкнути сітку", + "action.toggle-snap-mode.menu": "Завжди прив'язуватися", + "action.toggle-snap-mode": "Перемкнути постійне прив’язування", + "action.toggle-tool-lock.menu": "Блокування інструменту", + "action.toggle-tool-lock": "Перемкнути блокування інструменту", + "action.toggle-transparent.context-menu": "Прозорий", + "action.toggle-transparent.menu": "Прозорий", + "action.toggle-transparent": "Перемкнути прозорий фон", + "action.undo": "Скасувати", + "action.ungroup": "Розгрупувати", + "action.zoom-in": "Збільшити", + "action.zoom-out": "Зменшити", + "action.zoom-to-100": "Масштабувати до 100%", + "action.zoom-to-fit": "Масштабувати до розміру вікна", + "action.zoom-to-selection": "Масштабувати до виділення", + "color-style.black": "Чорний", + "color-style.blue": "Синій", + "color-style.green": "Зелений", + "color-style.grey": "Сірий", + "color-style.light-blue": "Блакитний", + "color-style.light-green": "Світло-зелений", + "color-style.light-red": "Світло-червоний", + "color-style.light-violet": "Світло-фіолетовий", + "color-style.orange": "Помаранчевий", + "color-style.red": "Червоний", + "color-style.violet": "Фіолетовий", + "color-style.yellow": "Жовтий", + "fill-style.none": "Без", + "fill-style.semi": "Напів", + "fill-style.solid": "Суцільне", + "fill-style.pattern": "Візерунок", + "dash-style.dashed": "Штриховий", + "dash-style.dotted": "Пунктирний", + "dash-style.draw": "Художній", + "dash-style.solid": "Суцільний", + "size-style.s": "Маленький", + "size-style.m": "Середній", + "size-style.l": "Великий", + "size-style.xl": "Дуже великий", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Художній", + "font-style.sans": "Без засічок", + "font-style.serif": "Із засічками", + "font-style.mono": "Моноширинний", + "align-style.start": "За лівим краєм", + "align-style.middle": "За центром", + "align-style.end": "За правим краєм", + "align-style.justify": "За шириною", + "geo-style.arrow-down": "Стрілка вниз", + "geo-style.arrow-left": "Стрілка вліво", + "geo-style.arrow-right": "Стрілка вправо", + "geo-style.arrow-up": "Стрілка вгору", + "geo-style.diamond": "Ромб", + "geo-style.ellipse": "Еліпс", + "geo-style.hexagon": "Шестикутник", + "geo-style.octagon": "Восьмикутник", + "geo-style.oval": "Овал", + "geo-style.pentagon": "П'ятикутник", + "geo-style.rectangle": "Прямокутник", + "geo-style.rhombus-2": "Ромб 2", + "geo-style.rhombus": "Ромб", + "geo-style.star": "Зірка", + "geo-style.trapezoid": "Трапеція", + "geo-style.triangle": "Трикутник", + "geo-style.x-box": "X квадрат", + "arrowheadStart-style.none": "Без", + "arrowheadStart-style.arrow": "Стрілка", + "arrowheadStart-style.bar": "Лінія", + "arrowheadStart-style.diamond": "Ромб", + "arrowheadStart-style.dot": "Коло", + "arrowheadStart-style.inverted": "Обернена", + "arrowheadStart-style.pipe": "Труба", + "arrowheadStart-style.square": "Квадрат", + "arrowheadStart-style.triangle": "Трикутник", + "arrowheadEnd-style.none": "Без", + "arrowheadEnd-style.arrow": "Стрілка", + "arrowheadEnd-style.bar": "Лінія", + "arrowheadEnd-style.diamond": "Ромб", + "arrowheadEnd-style.dot": "Коло", + "arrowheadEnd-style.inverted": "Обернена", + "arrowheadEnd-style.pipe": "Труба", + "arrowheadEnd-style.square": "Квадрат", + "arrowheadEnd-style.triangle": "Трикутник", + "spline-style.line": "Прямий", + "spline-style.cubic": "Кубічний", + "tool.select": "Переміщення", + "tool.hand": "Рука", + "tool.draw": "Олівець", + "tool.eraser": "Гумка", + "tool.arrow-down": "Стрілка вниз", + "tool.arrow-left": "Стрілка вліво", + "tool.arrow-right": "Стрілка вправо", + "tool.arrow-up": "Стрілка вгору", + "tool.arrow": "Стрілка", + "tool.diamond": "Ромбоїд", + "tool.ellipse": "Еліпс", + "tool.hexagon": "Шестикутник", + "tool.line": "Лінія", + "tool.octagon": "Восьмикутник", + "tool.oval": "Овал", + "tool.pentagon": "П'ятикутник", + "tool.rectangle": "Прямокутник", + "tool.rhombus": "Ромб", + "tool.star": "Зірка", + "tool.trapezoid": "Трапеція", + "tool.triangle": "Трикутник", + "tool.x-box": "X квадрат", + "tool.asset": "Ресурс", + "tool.frame": "Рамка", + "tool.note": "Нотатка", + "tool.embed": "Вбудовування", + "tool.text": "Текст", + "menu.title": "Меню", + "menu.copy-as": "Скопіювати як", + "menu.edit": "Правка", + "menu.export-as": "Експортувати як", + "menu.file": "Файл", + "menu.language": "Мова", + "menu.preferences": "Налаштування", + "menu.view": "Вигляд", + "context-menu.arrange": "Організувати", + "context-menu.copy-as": "Скопіювати як", + "context-menu.export-as": "Експортувати як", + "context-menu.move-to-page": "Перенести на сторінку", + "context-menu.reorder": "Перевпорядкувати", + "page-menu.title": "Сторінки", + "page-menu.create-new-page": "Створити нову сторінку", + "page-menu.edit-pages": "Редагувати сторінки", + "page-menu.max-page-count-reached": "Досягнуто максимальної кількості сторінок", + "page-menu.new-page-initial-name": "Сторінка 1", + "page-menu.page": "Сторінка", + "page-menu.edit-start": "Редагувати", + "page-menu.edit-done": "Готово", + "page-menu.submenu.rename": "Перейменувати", + "page-menu.submenu.duplicate-page": "Дублювати", + "page-menu.submenu.go-to-page": "Перейти на сторінку", + "page-menu.submenu.title": "Меню", + "page-menu.submenu.move-down": "Перемістити вниз", + "page-menu.submenu.move-up": "Перемістити вгору", + "page-menu.submenu.delete": "Видалити", + "share-menu.title": "Поділитися", + "share-menu.share-project": "Поділитися цим проєктом", + "share-menu.create-project": "Новий спільний проєкт", + "share-menu.copy-link": "Копіювати посилання", + "share-menu.readonly-link": "Лише для читання", + "share-menu.copy-readonly-link": "Скопіювати посилання, доступне лише для читання", + "share-menu.offline-note": "Спільний доступ до цього проєкту створить розміщену онлайн копію за новою URL-адресою. Ви можете поділитися URL-адресою з тридцятьма іншими людьми, щоб вони мали змогу переглядати та редагувати проєкт разом з вами.", + "share-menu.copy-link-note": "Будь-хто, маючи посилання, зможе переглянути та відредагувати цей проєкт.", + "share-menu.copy-readonly-link-note": "Будь-хто, маючи посилання, зможе переглянути (але не редагувати) цей проєкт.", + "share-menu.project-too-large": "На жаль, цим проєктом не можна поділитися, оскільки він занадто великий. Ми працюємо над цим!", + "people-menu.title": "Люди", + "people-menu.change-name": "Змінити ім'я", + "people-menu.change-color": "Змінити колір", + "people-menu.user": "(Ви)", + "people-menu.invite": "Запросити інших користувачів", + "debug-menu.hard-reset": "Повне перезавантаження", + "debug-menu.create-shapes": "Створити 100 форм", + "help-menu.title": "Довідка та матеріали", + "help-menu.about": "Про нас", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "Комбінації клавіш", + "help-menu.twitter": "Twitter", + "links-menu.about": "Про нас", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Дії", + "edit-link-dialog.title": "Редагувати посилання", + "edit-link-dialog.invalid-url": "Посилання має бути дійсною URL-адресою.", + "edit-link-dialog.detail": "Посилання відкриються в новій вкладці.", + "edit-link-dialog.url": "URL-адреса", + "edit-link-dialog.clear": "Очистити", + "edit-link-dialog.save": "Далі", + "edit-link-dialog.cancel": "Скасувати", + "embed-dialog.title": "Створити вбудовування", + "embed-dialog.url-label": "Вставити URL-адресу", + "embed-dialog.back": "Назад", + "embed-dialog.create": "Створити", + "embed-dialog.cancel": "Скасувати", + "embed-dialog.url": "URL-адреса", + "embed-dialog.instruction": "Вставте URL-адресу сайту, щоб створити вбудовування.", + "embed-dialog.invalid-url": "Ми не змогли створити вбудовування з цієї URL-адреси.", + "edit-pages-dialog.title": "Редагувати сторінки", + "edit-pages-dialog.create-new-page": "Створити нову сторінку", + "edit-pages-dialog.delete": "Видалити", + "edit-pages-dialog.duplicate-page": "Дублювати", + "edit-pages-dialog.go-to-page": "Перейти на сторінку", + "edit-pages-dialog.max-page-count-reached": "Досягнуто максимальної кількості сторінок", + "edit-pages-dialog.more-menu": "Меню", + "edit-pages-dialog.move-down": "Перемістити вниз", + "edit-pages-dialog.move-up": "Перемістити вгору", + "edit-pages-dialog.new-page-initial-name": "Сторінка 1", + "reload-file-dialog.title": "Продовжити редагування файлу", + "reload-file-dialog.description": "Ви щойно редагували файл. Чи бажаєте продовжити його редагування?", + "reload-file-dialog.failure": "Не вдалося перезавантажити файл. Спробувати ще раз?", + "reload-file-dialog.reload": "Продовжити редагування", + "reload-file-dialog.revert": "Ні, дякую", + "shortcuts-dialog.title": "Комбінації клавіш", + "shortcuts-dialog.edit": "Правка", + "shortcuts-dialog.file": "Файл", + "shortcuts-dialog.preferences": "Налаштування", + "shortcuts-dialog.tools": "Інструменти", + "shortcuts-dialog.transform": "Перетворення", + "shortcuts-dialog.view": "Вигляд", + "shortcuts-dialog.save": "Далі", + "style-panel.title": "Стилі", + "style-panel.align": "Вирівняти", + "style-panel.arrowheads": "Стрілка", + "style-panel.color": "Колір", + "style-panel.dash": "Контур", + "style-panel.fill": "Заливка", + "style-panel.font": "Шрифт", + "style-panel.geo": "Форма", + "style-panel.label": "Позначка", + "style-panel.mixed": "Змішаний", + "style-panel.opacity": "Непрозорість", + "style-panel.size": "Розмір", + "style-panel.spline": "Сплайн", + "style-panel.text": "Текст", + "tool-panel.drawing": "Малюнок", + "tool-panel.geo": "Форма", + "tool-panel.shapes": "Форми", + "tool-panel.things": "Предмети", + "tool-panel.tools": "Інструменти", + "save-changes-prompt.title": "У вас є незбережені зміни", + "save-changes-prompt.description": "Бажаєте зберегти зміни у поточному файлі?", + "save-changes-prompt.go-back": "Повернутися", + "save-changes-prompt.continue": "Далі", + "navigation-zone.toggle-minimap": "Перемкнути мінімапу", + "navigation-zone.zoom": "Збільшити", + "focus-mode.toggle-focus-mode": "Перемкнути режим концентрації", + "toast.close": "Закрити", + "file-system.file-open-error.title": "Не вдалося відкрити файл", + "file-system.file-open-error.not-a-tldraw-file": "Файл, який ви намагалися відкрити, не схожий на файл tldraw.", + "file-system.file-open-error.file-format-version-too-new": "Файл, який ви намагалися відкрити, належить до новішої версії tldraw. Будь ласка, перезавантажте сторінку і спробуйте ще раз.", + "file-system.file-open-error.generic-corrupted-file": "Файл, який ви намагалися відкрити, пошкоджено.", + "file-system.confirm-open.title": "Перезаписати поточний проєкт?", + "file-system.confirm-open.description": "Відкриття файлу замінить ваш поточний проєкт, і будь-які незбережені зміни будуть втрачені. Ви дійсно бажаєте продовжити?", + "file-system.confirm-open.cancel": "Скасувати", + "file-system.confirm-open.open": "Відкрити файл", + "file-system.confirm-open.dont-show-again": "Не запитувати більше", + "toast.error.export-fail.title": "Помилка експорту", + "toast.error.export-fail.desc": "Не вдалося експортувати зображення", + "toast.error.copy-fail.title": "Не вдалося скопіювати", + "toast.error.copy-fail.desc": "Не вдалося скопіювати зображення", + "file-system.shared-document-file-open-error.title": "Не вдалося відкрити файл", + "file-system.shared-document-file-open-error.description": "Відкриття файлів зі спільних проєктів не підтримується.", + "vscode.file-open.dont-show-again": "Більше не запитувати", + "vscode.file-open.desc": "Цей файл було створено у попередній версії tldraw. Бажаєте оновити його для роботи з новою версією?", + "context.pages.new-page": "Нова сторінка", + "style-panel.arrowhead-start": "Наконечник", + "style-panel.arrowhead-end": "Хвіст", + "vscode.file-open.open": "Далі", + "vscode.file-open.backup": "Резервна копія", + "vscode.file-open.backup-saved": "Резервна копія збережена", + "vscode.file-open.backup-failed": "Резервне копіювання не вдалося: це не файл .tldr.", + "tool-panel.more": "Детальніше", + "debug-panel.more": "Детальніше", + "action.new-project": "Новий проєкт", + "file-system.confirm-clear.title": "Очистити поточний проєкт?", + "file-system.confirm-clear.description": "Створення нового проєкту очистить ваш поточний проєкт, і всі незбережені зміни будуть втрачені. Ви впевнені, що хочете продовжити?", + "file-system.confirm-clear.cancel": "Скасувати", + "file-system.confirm-clear.continue": "Далі", + "file-system.confirm-clear.dont-show-again": "Більше не запитувати", + "action.stop-following": "Припинити стежити", + "people-menu.follow": "Стежити", + "style-panel.position": "Позиція" +} \ No newline at end of file diff --git a/assets/translations/vi.json b/assets/translations/vi.json new file mode 100644 index 000000000..d671b09e8 --- /dev/null +++ b/assets/translations/vi.json @@ -0,0 +1,333 @@ +{ + "action.convert-to-bookmark": "Chuyển đổi thành Dấu trang", + "action.convert-to-embed": "Chuyển đổi sang Nhúng", + "action.open-embed-link": "Mở liên kết", + "action.align-bottom": "Căn chỉnh dưới", + "action.align-center-horizontal": "Căn chỉnh ngang", + "action.align-center-vertical": "Căn chỉnh dọc", + "action.align-center-horizontal.short": "Căn chỉnh ngang", + "action.align-center-vertical.short": "Căn chỉnh dọc", + "action.align-left": "Căn chỉnh trái", + "action.align-right": "Căn chỉnh phải", + "action.align-top": "Căn chỉnh đầu", + "action.back-to-content": "Quay lại nội dung", + "action.bring-forward": "Mang ra phía sau", + "action.bring-to-front": "Mang ra phía trước", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "Sao chép định dạng JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "Sao chép định dạng PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "Sao chép định dạng SVG", + "action.copy": "Sao chép", + "action.cut": "Cắt", + "action.delete": "Xoá", + "action.distribute-horizontal": "Phân phối ngang", + "action.distribute-vertical": "Phân phối dọc", + "action.distribute-horizontal.short": "Phân phối ngang", + "action.distribute-vertical.short": "Phân phối dọc", + "action.duplicate": "Nhân đôi", + "action.edit-link": "Chỉnh sửa liên kết", + "action.exit-pen-mode": "Thoát khỏi chế độ bút", + "action.export-as-json.short": "JSON", + "action.export-as-json": "Xuất định dạng JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "Xuất định dạng PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "Xuất định dạng SVG", + "action.flip-horizontal": "Lật ngang", + "action.flip-vertical": "Lật dọc", + "action.flip-horizontal.short": "Lật ngang", + "action.flip-vertical.short": "Lật dọc", + "action.group": "Nhóm", + "action.insert-media": "Tải phương tiện lên", + "action.new-shared-project": "Dự án chia sẻ mới", + "action.nudge-down": "Nhích xuống", + "action.nudge-left": "Nhích trái", + "action.nudge-right": "Nhích phải", + "action.nudge-up": "Nhích lên", + "action.open-file": "Mở tệp", + "action.pack": "Đóng gói", + "action.paste": "Dán", + "action.print": "In", + "action.redo": "Làm lại", + "action.rotate-ccw": "Xoay ngược chiều kim đồng hồ", + "action.rotate-cw": "Xoay theo chiều kim đồng hồ", + "action.save-copy": "Lưu bản sao", + "action.select-all": "Lựa chọn tất cả", + "action.select-none": "Không lựa chọn", + "action.send-backward": "Quay lại sau", + "action.send-to-back": "Quay lại trước", + "action.share-project": "Chia sẻ dự án này", + "action.stack-horizontal": "Căng chiều ngang", + "action.stack-vertical": "Căng chiều đứng", + "action.stack-horizontal.short": "Căng ngang", + "action.stack-vertical.short": "Căng dọc", + "action.stretch-horizontal": "Căng chiều ngang", + "action.stretch-vertical": "Căng chiều đứng", + "action.stretch-horizontal.short": "Căng ngang", + "action.stretch-vertical.short": "Căng dọc", + "action.toggle-auto-size": "Bật/tắt kích thước tự động", + "action.toggle-dark-mode.menu": "Chế độ tối", + "action.toggle-dark-mode": "Bật/tắt chế độ tối", + "action.toggle-debug-mode.menu": "Chế độ tìm lỗi", + "action.toggle-debug-mode": "Bật/tắt chế độ tìm lỗi", + "action.toggle-focus-mode.menu": "Chế độ tập trung", + "action.toggle-focus-mode": "Bật/tắt chế độ tập trung", + "action.toggle-grid.menu": "Hiển thị lưới", + "action.toggle-grid": "Bật/tắt lưới", + "action.toggle-snap-mode.menu": "Luôn khớp", + "action.toggle-snap-mode": "Bật/tắt luôn khớp", + "action.toggle-tool-lock.menu": "Khóa công cụ", + "action.toggle-tool-lock": "Bật/tắt khóa công cụ", + "action.toggle-transparent.context-menu": "Trong suốt", + "action.toggle-transparent.menu": "Trong suốt", + "action.toggle-transparent": "Bật/tắt nền trong suốt", + "action.undo": "Quay lại", + "action.ungroup": "Bỏ nhóm", + "action.zoom-in": "Phóng to", + "action.zoom-out": "Thu nhỏ", + "action.zoom-to-100": "Phóng to 100%", + "action.zoom-to-fit": "Phóng to vừa", + "action.zoom-to-selection": "Phóng to đến đang chọn", + "color-style.black": "Đen", + "color-style.blue": "Xanh dương", + "color-style.green": "Xanh lá", + "color-style.grey": "Xám", + "color-style.light-blue": "Xanh dương tươi", + "color-style.light-green": "Xanh lá tươi", + "color-style.light-red": "Đỏ tươi", + "color-style.light-violet": "Tím tươi", + "color-style.orange": "Cam", + "color-style.red": "Đỏ", + "color-style.violet": "Tím", + "color-style.yellow": "Vàng", + "fill-style.none": "Không", + "fill-style.semi": "Một nữa", + "fill-style.solid": "Trơn", + "fill-style.pattern": "Mẫu", + "dash-style.dashed": "Nét đứt", + "dash-style.dotted": "Chấm", + "dash-style.draw": "Vẽ", + "dash-style.solid": "Trơn", + "size-style.s": "Nhỏ", + "size-style.m": "Trung bình", + "size-style.l": "Lớn", + "size-style.xl": "Cực lớn", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "Vẽ", + "font-style.sans": "Sans", + "font-style.serif": "Serif", + "font-style.mono": "Mono", + "align-style.start": "Đầu", + "align-style.middle": "Chính giữa", + "align-style.end": "Cuối", + "align-style.justify": "Căn chỉnh", + "geo-style.arrow-down": "Mũi tên xuống dưới", + "geo-style.arrow-left": "Mũi tên sang trái", + "geo-style.arrow-right": "Mũi tên sang phải", + "geo-style.arrow-up": "Mũi tên hướng lên", + "geo-style.diamond": "Kim cương", + "geo-style.ellipse": "Hình Elip", + "geo-style.hexagon": "Hình lục giác", + "geo-style.octagon": "Hình bát giác", + "geo-style.oval": "Hình bầu", + "geo-style.pentagon": "Hình ngũ giác", + "geo-style.rectangle": "Hình chữ nhật", + "geo-style.rhombus-2": "Hình thoi 2", + "geo-style.rhombus": "Hình thoi", + "geo-style.star": "Ngôi sao", + "geo-style.trapezoid": "Hình thang", + "geo-style.triangle": "Hình tam giác", + "geo-style.x-box": "Khung X", + "arrowheadStart-style.none": "Không", + "arrowheadStart-style.arrow": "Mũi tên", + "arrowheadStart-style.bar": "Thanh", + "arrowheadStart-style.diamond": "Kim cương", + "arrowheadStart-style.dot": "Chấm", + "arrowheadStart-style.inverted": "Đảo ngược", + "arrowheadStart-style.pipe": "Đường ống", + "arrowheadStart-style.square": "Hình vuông", + "arrowheadStart-style.triangle": "Tam giác", + "arrowheadEnd-style.none": "Không", + "arrowheadEnd-style.arrow": "Mũi tên", + "arrowheadEnd-style.bar": "Thanh", + "arrowheadEnd-style.diamond": "Kim cương", + "arrowheadEnd-style.dot": "Chấm", + "arrowheadEnd-style.inverted": "Đảo ngược", + "arrowheadEnd-style.pipe": "Đường ống", + "arrowheadEnd-style.square": "Hình vuông", + "arrowheadEnd-style.triangle": "Tam giác", + "spline-style.line": "Đường", + "spline-style.cubic": "Khối", + "tool.select": "Lựa chọn", + "tool.hand": "Tay", + "tool.draw": "Vẽ", + "tool.eraser": "Tẩy", + "tool.arrow-down": "Mũi tên xuống dưới", + "tool.arrow-left": "Mũi tên qua trái", + "tool.arrow-right": "Mũi tên qua phải", + "tool.arrow-up": "Mũi tên lên trên", + "tool.arrow": "Mũi tên", + "tool.diamond": "Hình kim cương", + "tool.ellipse": "Hình Elip", + "tool.hexagon": "Hình lục giác", + "tool.line": "Đường", + "tool.octagon": "Hình bát giác", + "tool.oval": "Hình bầu", + "tool.pentagon": "Hình ngũ giác", + "tool.rectangle": "Hình chữ nhật", + "tool.rhombus": "Hình thoi", + "tool.star": "Ngôi sao", + "tool.trapezoid": "Hình thang", + "tool.triangle": "Tam giác", + "tool.x-box": "Khung X", + "tool.asset": "Thư viện", + "tool.frame": "Khung", + "tool.note": "Ghi chú", + "tool.embed": "Nhúng", + "tool.text": "Chữ", + "menu.title": "Menu", + "menu.copy-as": "Sao chép dưới dạng", + "menu.edit": "Chỉnh sửa", + "menu.export-as": "Xuất dưới dạng", + "menu.file": "Tệp", + "menu.language": "Ngôn ngữ", + "menu.preferences": "Cài đặt", + "menu.view": "Lượt xem", + "context-menu.arrange": "Vị trí", + "context-menu.copy-as": "Sao chép dưới dạng", + "context-menu.export-as": "Xuất dưới dạng", + "context-menu.move-to-page": "Đi đến trang", + "context-menu.reorder": "Sắp xếp", + "page-menu.title": "Các trang", + "page-menu.create-new-page": "Tạo trang mới", + "page-menu.edit-pages": "Chỉnh sửa trang", + "page-menu.max-page-count-reached": "Đã đạt đến số trang tối đa", + "page-menu.new-page-initial-name": "Trang 1", + "page-menu.page": "Trang", + "page-menu.edit-start": "Sửa", + "page-menu.edit-done": "Xong", + "page-menu.submenu.rename": "Đổi tên", + "page-menu.submenu.duplicate-page": "Nhân đôi", + "page-menu.submenu.go-to-page": "Đi đến trang", + "page-menu.submenu.title": "Menu", + "page-menu.submenu.move-down": "Đi xuống", + "page-menu.submenu.move-up": "Đi lên", + "page-menu.submenu.delete": "Xoá", + "share-menu.title": "Chia sẻ", + "share-menu.share-project": "Chia sẻ dự án này", + "share-menu.create-project": "Dự án chia sẻ mới", + "share-menu.copy-link": "Sao chép liên kết", + "share-menu.readonly-link": "Chỉ có thể đọc", + "share-menu.copy-readonly-link": "Sao chép liên kết chỉ có thể đọc", + "share-menu.offline-note": "Chia sẻ dự án này sẽ tạo một bản sao trực tiếp được lưu trữ tại một đường dẫn mới. Bạn có thể chia sẻ đường dẫn với tối đa 30 người khác để cùng xem và chỉnh sửa dự án.", + "share-menu.copy-link-note": "Bất kỳ ai có liên kết đều có thể xem và chỉnh sửa dự án này.", + "share-menu.copy-readonly-link-note": "Bất kỳ ai có liên kết đều có thể xem (nhưng không thể chỉnh sửa) dự án này.", + "share-menu.project-too-large": "Rất tiếc, không thể chia sẻ dự án này vì dự án quá lớn. Chúng tôi đang làm việc để cải thiện điều này!", + "people-menu.title": "Mọi người", + "people-menu.change-name": "Thay đổi tên", + "people-menu.change-color": "Thay đổi màu", + "people-menu.user": "(Bạn)", + "people-menu.invite": "Mời người khác", + "debug-menu.hard-reset": "Đặt lại", + "debug-menu.create-shapes": "Tạo 100 hình dạng", + "help-menu.title": "Trợ giúp và thông tin khác", + "help-menu.about": "Về chúng tôi", + "help-menu.discord": "Discord", + "help-menu.github": "Github", + "help-menu.keyboard-shortcuts": "Phím tắt bàn phím", + "help-menu.twitter": "Twitter", + "links-menu.about": "Về chúng tôi", + "links-menu.discord": "Discord", + "links-menu.github": "Github", + "links-menu.twitter": "Twitter", + "actions-menu.title": "Hành động", + "edit-link-dialog.title": "Chỉnh sửa liên kết", + "edit-link-dialog.invalid-url": "Một liên kết phải là một đường dẫn hợp lệ.", + "edit-link-dialog.detail": "Liên kết sẽ mở trong một tab mới.", + "edit-link-dialog.url": "Đường dẫn", + "edit-link-dialog.clear": "Xoá", + "edit-link-dialog.save": "Tiếp tục", + "edit-link-dialog.cancel": "Huỷ", + "embed-dialog.title": "Tạo nhúng", + "embed-dialog.url-label": "Dán đường dẫn", + "embed-dialog.back": "Quay lại", + "embed-dialog.create": "Tạo", + "embed-dialog.cancel": "Huỷ", + "embed-dialog.url": "Đường dẫn", + "embed-dialog.instruction": "Dán đường dẫn của trang web để tạo nhúng.", + "embed-dialog.invalid-url": "Chúng tôi không thể tạo nhúng từ đường dẫn đó.", + "edit-pages-dialog.title": "Chỉnh sửa trang", + "edit-pages-dialog.create-new-page": "Tạo trang mới", + "edit-pages-dialog.delete": "Xoá", + "edit-pages-dialog.duplicate-page": "Nhân đôi", + "edit-pages-dialog.go-to-page": "Đi đến trang", + "edit-pages-dialog.max-page-count-reached": "Đã đạt đến số trang tối đa", + "edit-pages-dialog.more-menu": "Menu", + "edit-pages-dialog.move-down": "Xuống dưới", + "edit-pages-dialog.move-up": "Lên trên", + "edit-pages-dialog.new-page-initial-name": "Trang 1", + "reload-file-dialog.title": "Tiếp tục chỉnh sửa tệp", + "reload-file-dialog.description": "Bạn vừa chỉnh sửa một tệp. Bạn có muốn tiếp tục chỉnh sửa nó không?", + "reload-file-dialog.failure": "Tải lại tệp thất bại. Thử lại?", + "reload-file-dialog.reload": "Tiếp tục chỉnh sửa", + "reload-file-dialog.revert": "Không cảm ơn", + "shortcuts-dialog.title": "Phím tắt bàn phím", + "shortcuts-dialog.edit": "Sửa", + "shortcuts-dialog.file": "Tệp", + "shortcuts-dialog.preferences": "Cài đặt", + "shortcuts-dialog.tools": "Các công cụ", + "shortcuts-dialog.transform": "Biến đổi", + "shortcuts-dialog.view": "Xem", + "shortcuts-dialog.save": "Tiếp tục", + "style-panel.title": "Kiểu", + "style-panel.align": "Căn chỉnh", + "style-panel.arrowheads": "Đầu mũi tên", + "style-panel.color": "Màu", + "style-panel.dash": "Nét đứt", + "style-panel.fill": "Tô", + "style-panel.font": "Phông", + "style-panel.geo": "Hình", + "style-panel.label": "Nhãn", + "style-panel.mixed": "Trộn", + "style-panel.opacity": "Độ mờ", + "style-panel.size": "Kích thước", + "style-panel.spline": "Đường cong", + "style-panel.text": "Chữ", + "tool-panel.drawing": "Vẽ", + "tool-panel.geo": "Khối", + "tool-panel.shapes": "Các khối", + "tool-panel.things": "Linh tinh", + "tool-panel.tools": "Công cụ", + "save-changes-prompt.title": "Bạn có các thay đổi chưa lưu", + "save-changes-prompt.description": "Bạn có muốn lưu các thay đổi vào tệp hiện tại của mình không?", + "save-changes-prompt.go-back": "Quay lại", + "save-changes-prompt.continue": "Tiếp tục", + "navigation-zone.toggle-minimap": "Bật/tắt bản đồ nhỏ", + "navigation-zone.zoom": "Phóng to", + "focus-mode.toggle-focus-mode": "Bật/tắt chế độ tập trung", + "toast.close": "Đóng", + "file-system.file-open-error.title": "Không thể mở tệp", + "file-system.file-open-error.not-a-tldraw-file": "Tệp bạn cố mở không giống tệp tldraw.", + "file-system.file-open-error.file-format-version-too-new": "Tệp bạn cố mở là từ phiên bản mới hơn của tldraw. Hãy tải lại trang và thử lại.", + "file-system.file-open-error.generic-corrupted-file": "Tệp bạn cố mở bị hỏng.", + "file-system.confirm-open.title": "Ghi đè lên dự án hiện tại?", + "file-system.confirm-open.description": "Việc mở tệp sẽ thay thế dự án hiện tại của bạn và mọi thay đổi chưa được lưu sẽ bị mất. Bạn có chắc chắn muốn tiếp tục không?", + "file-system.confirm-open.cancel": "Huỷ", + "file-system.confirm-open.open": "Mở tệp", + "file-system.confirm-open.dont-show-again": "Đừng hỏi lại", + "toast.error.export-fail.title": "Xuất thất bại", + "toast.error.export-fail.desc": "Xuất ảnh thất bại", + "toast.error.copy-fail.title": "Sao chép thất bại", + "toast.error.copy-fail.desc": "Không thể sao chép hình ảnh", + "file-system.shared-document-file-open-error.title": "Không thể mở tệp", + "file-system.shared-document-file-open-error.description": "Mở tệp từ các dự án được chia sẻ hiện tại không được hỗ trợ.", + "vscode.file-open.dont-show-again": "Đừng hỏi lại", + "vscode.file-open.desc": "Tệp này được tạo bằng phiên bản cũ hơn của tldraw. Bạn có muốn cập nhật nó để hoạt động với phiên bản mới không?", + "context.pages.new-page": "Trang mới" +} \ No newline at end of file diff --git a/assets/translations/zh-cn.json b/assets/translations/zh-cn.json new file mode 100644 index 000000000..8cd128186 --- /dev/null +++ b/assets/translations/zh-cn.json @@ -0,0 +1,328 @@ +{ + "action.convert-to-bookmark": "转换为书签", + "action.convert-to-embed": "转换为嵌入", + "action.open-embed-link": "打开链接", + "action.align-bottom": "底端对齐", + "action.align-center-horizontal": "水平对齐", + "action.align-center-vertical": "垂直对齐", + "action.align-center-horizontal.short": "水平对齐", + "action.align-center-vertical.short": "垂直对齐", + "action.align-left": "左对齐", + "action.align-right": "右对齐", + "action.align-top": "顶端对齐", + "action.back-to-content": "返回内容", + "action.bring-forward": "上移一层", + "action.bring-to-front": "置顶", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "复制为 JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "复制为 PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "复制为 SVG", + "action.copy": "复制", + "action.cut": "剪切", + "action.delete": "删除", + "action.distribute-horizontal": "横向分布", + "action.distribute-vertical": "纵向分布", + "action.distribute-horizontal.short": "横向分布", + "action.distribute-vertical.short": "纵向分布", + "action.duplicate": "复制", + "action.edit-link": "编辑链接", + "action.exit-pen-mode": "退出钢笔模式", + "action.export-as-json.short": "JSON", + "action.export-as-json": "导出为 JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "导出为 PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "导出为 SVG", + "action.flip-horizontal": "水平翻转", + "action.flip-vertical": "垂直翻转", + "action.flip-horizontal.short": "水平翻转", + "action.flip-vertical.short": "垂直翻转", + "action.group": "分组", + "action.insert-media": "上传媒体文件", + "action.new-shared-project": "新建共享项目", + "action.nudge-down": "向下微移", + "action.nudge-left": "向左微移", + "action.nudge-right": "向右微移", + "action.nudge-up": "向上微移", + "action.open-file": "打开文件", + "action.pack": "打包", + "action.paste": "粘贴", + "action.print": "打印", + "action.redo": "重做", + "action.rotate-ccw": "逆时针旋转", + "action.rotate-cw": "顺时针旋转", + "action.save-copy": "保存副本", + "action.select-all": "选中全部", + "action.select-none": "取消选中", + "action.send-backward": "下移一层", + "action.send-to-back": "置底", + "action.share-project": "共享此项目", + "action.stack-horizontal": "横排", + "action.stack-vertical": "竖排", + "action.stack-horizontal.short": "横排", + "action.stack-vertical.short": "竖排", + "action.stretch-horizontal": "水平拉伸", + "action.stretch-vertical": "垂直拉伸", + "action.stretch-horizontal.short": "水平拉伸", + "action.stretch-vertical.short": "垂直拉伸", + "action.toggle-auto-size": "切换自动大小", + "action.toggle-dark-mode.menu": "暗黑模式", + "action.toggle-dark-mode": "切换暗黑模式", + "action.toggle-debug-mode.menu": "调试模式", + "action.toggle-debug-mode": "切换调试模式", + "action.toggle-focus-mode.menu": "专注模式", + "action.toggle-focus-mode": "切换专注模式", + "action.toggle-grid.menu": "显示网格", + "action.toggle-grid": "切换网格", + "action.toggle-snap-mode.menu": "始终吸附", + "action.toggle-snap-mode": "切换始终吸附", + "action.toggle-tool-lock.menu": "工具锁定", + "action.toggle-tool-lock": "切换工具锁定", + "action.toggle-transparent.context-menu": "透明", + "action.toggle-transparent.menu": "透明", + "action.toggle-transparent": "切换透明背景", + "action.undo": "撤销", + "action.ungroup": "取消分组", + "action.zoom-in": "放大", + "action.zoom-out": "缩小", + "action.zoom-to-100": "缩放至 100%", + "action.zoom-to-fit": "自适应缩放", + "action.zoom-to-selection": "缩放至显示选中内容", + "color-style.black": "黑色", + "color-style.blue": "蓝色", + "color-style.green": "绿色", + "color-style.grey": "灰色", + "color-style.light-blue": "浅蓝色", + "color-style.light-green": "浅绿色", + "color-style.light-red": "浅红色", + "color-style.light-violet": "浅紫色", + "color-style.orange": "橙色", + "color-style.red": "红色", + "color-style.violet": "紫色", + "color-style.yellow": "黄色", + "fill-style.none": "无", + "fill-style.semi": "半填充", + "fill-style.solid": "实心", + "fill-style.pattern": "图案", + "dash-style.dashed": "虚线", + "dash-style.dotted": "虚点", + "dash-style.draw": "画笔", + "dash-style.solid": "实心", + "size-style.s": "小", + "size-style.m": "中", + "size-style.l": "大", + "size-style.xl": "加大", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "画笔", + "font-style.sans": "无衬线", + "font-style.serif": "衬线", + "font-style.mono": "黑白", + "align-style.start": "开始", + "align-style.middle": "中间", + "align-style.end": "结束", + "align-style.justify": "两端对齐", + "geo-style.arrow-down": "向下箭头", + "geo-style.arrow-left": "向左箭头", + "geo-style.arrow-right": "向右箭头", + "geo-style.arrow-up": "向上箭头", + "geo-style.diamond": "菱形", + "geo-style.ellipse": "椭圆形", + "geo-style.hexagon": "六边形", + "geo-style.octagon": "八边形", + "geo-style.oval": "卵形", + "geo-style.pentagon": "五边形", + "geo-style.rectangle": "矩形", + "geo-style.rhombus-2": "菱形 2", + "geo-style.rhombus": "菱形", + "geo-style.star": "星形", + "geo-style.trapezoid": "梯形", + "geo-style.triangle": "三角形", + "geo-style.x-box": "X 框", + "arrowheadStart-style.none": "无", + "arrowheadStart-style.arrow": "箭头", + "arrowheadStart-style.bar": "条", + "arrowheadStart-style.diamond": "菱形", + "arrowheadStart-style.dot": "点", + "arrowheadStart-style.inverted": "反转", + "arrowheadStart-style.pipe": "管道", + "arrowheadStart-style.square": "正方形", + "arrowheadStart-style.triangle": "三角形", + "arrowheadEnd-style.none": "无", + "arrowheadEnd-style.arrow": "箭头", + "arrowheadEnd-style.bar": "条", + "arrowheadEnd-style.diamond": "菱形", + "arrowheadEnd-style.dot": "点", + "arrowheadEnd-style.inverted": "反转", + "arrowheadEnd-style.pipe": "管道", + "arrowheadEnd-style.square": "正方形", + "arrowheadEnd-style.triangle": "三角形", + "spline-style.line": "直线", + "spline-style.cubic": "立方形", + "tool.select": "选择", + "tool.hand": "手形", + "tool.draw": "画笔", + "tool.eraser": "橡皮", + "tool.arrow-down": "向下箭头", + "tool.arrow-left": "向左箭头", + "tool.arrow-right": "向右箭头", + "tool.arrow-up": "向上箭头", + "tool.arrow": "箭头", + "tool.diamond": "菱形", + "tool.ellipse": "椭圆形", + "tool.hexagon": "六边形", + "tool.line": "直线", + "tool.octagon": "八边形", + "tool.oval": "卵形", + "tool.pentagon": "五边形", + "tool.rectangle": "矩形", + "tool.rhombus": "菱形", + "tool.star": "星形", + "tool.trapezoid": "梯形", + "tool.triangle": "三角形", + "tool.x-box": "X 框", + "tool.asset": "图片", + "tool.frame": "框架", + "tool.note": "便笺", + "tool.embed": "嵌入", + "tool.text": "文本", + "menu.title": "菜单", + "menu.copy-as": "复制为", + "menu.edit": "编辑", + "menu.export-as": "导出为", + "menu.file": "文件", + "menu.language": "语言", + "menu.preferences": "偏好", + "menu.view": "视图", + "context-menu.arrange": "排列", + "context-menu.copy-as": "复制为", + "context-menu.export-as": "导出为", + "context-menu.move-to-page": "移动到页面", + "context-menu.reorder": "重新排序", + "page-menu.title": "页面", + "page-menu.create-new-page": "创建新页面", + "page-menu.edit-pages": "编辑页面", + "page-menu.max-page-count-reached": "达到最大页数", + "page-menu.new-page-initial-name": "页面 1", + "page-menu.page": "页面", + "page-menu.edit-start": "编辑", + "page-menu.edit-done": "完成", + "page-menu.submenu.rename": "重命名", + "page-menu.submenu.duplicate-page": "复制", + "page-menu.submenu.go-to-page": "转到页面", + "page-menu.submenu.title": "菜单", + "page-menu.submenu.move-down": "下移", + "page-menu.submenu.move-up": "上移", + "page-menu.submenu.delete": "删除", + "share-menu.title": "共享", + "share-menu.share-project": "共享此项目", + "share-menu.create-project": "新建共享项目", + "share-menu.copy-link": "复制链接", + "share-menu.readonly-link": "只读", + "share-menu.copy-readonly-link": "复制只读链接", + "share-menu.offline-note": "共享此项目将以新 URL 创建托管活动副本。您可以与不超过三十人共享此 URL,一起查看和编辑项目。", + "share-menu.copy-link-note": "任何人使用此链接都能查看和编辑此项目。", + "share-menu.copy-readonly-link-note": "任何人使用此链接都能查看(但不能编辑)此项目。", + "share-menu.project-too-large": "抱歉,此项目太大,无法共享。我们正在努力解决!", + "people-menu.title": "人员", + "people-menu.change-name": "更改名称", + "people-menu.change-color": "更改颜色", + "people-menu.user": "(您)", + "people-menu.invite": "邀请他人", + "debug-menu.hard-reset": "硬重置", + "debug-menu.create-shapes": "创建 100 个形状", + "help-menu.title": "帮助和资源", + "help-menu.about": "关于", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "键盘快捷方式", + "help-menu.twitter": "Twitter", + "links-menu.about": "关于", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "操作", + "edit-link-dialog.title": "编辑链接", + "edit-link-dialog.invalid-url": "链接必须是有效 URL。", + "edit-link-dialog.detail": "链接将在新标签页中打开。", + "edit-link-dialog.url": "URL", + "edit-link-dialog.clear": "清除", + "edit-link-dialog.save": "继续", + "edit-link-dialog.cancel": "取消", + "embed-dialog.title": "创建嵌入", + "embed-dialog.url-label": "粘贴 URL", + "embed-dialog.back": "返回", + "embed-dialog.create": "创建", + "embed-dialog.cancel": "取消", + "embed-dialog.url": "URL", + "embed-dialog.instruction": "粘贴网站 URL 创建嵌入。", + "embed-dialog.invalid-url": "我们无法从该 URL 创建嵌入。", + "edit-pages-dialog.title": "编辑页面", + "edit-pages-dialog.create-new-page": "创建新页面", + "edit-pages-dialog.delete": "删除", + "edit-pages-dialog.duplicate-page": "复制", + "edit-pages-dialog.go-to-page": "转到页面", + "edit-pages-dialog.max-page-count-reached": "达到最大页数", + "edit-pages-dialog.more-menu": "菜单", + "edit-pages-dialog.move-down": "下移", + "edit-pages-dialog.move-up": "上移", + "edit-pages-dialog.new-page-initial-name": "页面 1", + "reload-file-dialog.title": "继续编辑文件", + "reload-file-dialog.description": "您刚才编辑了一个文件。是否要继续编辑它?", + "reload-file-dialog.failure": "无法重新加载文件。是否重试?", + "reload-file-dialog.reload": "继续编辑", + "reload-file-dialog.revert": "不,谢谢", + "shortcuts-dialog.title": "键盘快捷方式", + "shortcuts-dialog.edit": "编辑", + "shortcuts-dialog.file": "文件", + "shortcuts-dialog.preferences": "偏好", + "shortcuts-dialog.tools": "工具", + "shortcuts-dialog.transform": "转换", + "shortcuts-dialog.view": "视图", + "shortcuts-dialog.save": "继续", + "style-panel.title": "样式", + "style-panel.align": "对齐", + "style-panel.arrowheads": "箭头", + "style-panel.color": "颜色", + "style-panel.dash": "划线", + "style-panel.fill": "填充", + "style-panel.font": "字体", + "style-panel.geo": "形状", + "style-panel.label": "标签", + "style-panel.mixed": "混合", + "style-panel.opacity": "不透明度", + "style-panel.size": "大小", + "style-panel.spline": "曲线", + "style-panel.text": "文本", + "tool-panel.drawing": "绘图", + "tool-panel.geo": "形状", + "tool-panel.shapes": "形状", + "tool-panel.things": "物品", + "tool-panel.tools": "工具", + "save-changes-prompt.title": "您有未保存的更改", + "save-changes-prompt.description": "是否要将更改保存到当前文件?", + "save-changes-prompt.go-back": "返回", + "save-changes-prompt.continue": "继续", + "navigation-zone.toggle-minimap": "切换小地图", + "navigation-zone.zoom": "缩放", + "focus-mode.toggle-focus-mode": "切换专注模式", + "toast.close": "关闭", + "file-system.file-open-error.title": "无法打开文件", + "file-system.file-open-error.not-a-tldraw-file": "您试图打开的文件看起来不像 tldraw 文件。", + "file-system.file-open-error.file-format-version-too-new": "您试图打开的文件来自 tldraw 新版本。请重新加载页面,然后重试。", + "file-system.file-open-error.generic-corrupted-file": "您试图打开的文件已损坏。", + "file-system.confirm-open.title": "覆盖当前项目?", + "file-system.confirm-open.description": "打开文件将替换您的当前项目,任何未保存的更改将丢失。确定要继续吗?", + "file-system.confirm-open.cancel": "取消", + "file-system.confirm-open.open": "打开文件", + "file-system.confirm-open.dont-show-again": "不再询问", + "toast.error.export-fail.title": "导出失败", + "toast.error.export-fail.desc": "无法导出图像", + "toast.error.copy-fail.title": "复制失败", + "toast.error.copy-fail.desc": "无法复制图像" +} \ No newline at end of file diff --git a/assets/translations/zh-tw.json b/assets/translations/zh-tw.json new file mode 100644 index 000000000..216c75eb2 --- /dev/null +++ b/assets/translations/zh-tw.json @@ -0,0 +1,350 @@ +{ + "action.convert-to-bookmark": "轉換為書籤", + "action.convert-to-embed": "轉換為嵌入", + "action.open-embed-link": "開啟連結", + "action.align-bottom": "置底", + "action.align-center-horizontal": "水平置中", + "action.align-center-vertical": "垂直置中", + "action.align-center-horizontal.short": "水平置中", + "action.align-center-vertical.short": "垂直置中", + "action.align-left": "置左", + "action.align-right": "置右", + "action.align-top": "置頂", + "action.back-to-content": "回到內容", + "action.bring-forward": "前移一層", + "action.bring-to-front": "移至頂層", + "action.copy-as-json.short": "JSON", + "action.copy-as-json": "複製為 JSON", + "action.copy-as-png.short": "PNG", + "action.copy-as-png": "複製為 PNG", + "action.copy-as-svg.short": "SVG", + "action.copy-as-svg": "複製為 SVG", + "action.copy": "複製", + "action.cut": "剪下", + "action.delete": "刪除", + "action.distribute-horizontal": "水平分布", + "action.distribute-vertical": "垂直分布", + "action.distribute-horizontal.short": "水平分布", + "action.distribute-vertical.short": "垂直分布", + "action.duplicate": "複製", + "action.edit-link": "編輯連結", + "action.exit-pen-mode": "退出畫筆模式", + "action.export-as-json.short": "JSON", + "action.export-as-json": "匯出成 JSON", + "action.export-as-png.short": "PNG", + "action.export-as-png": "匯出成 PNG", + "action.export-as-svg.short": "SVG", + "action.export-as-svg": "匯出成 SVG", + "action.flip-horizontal": "水平翻轉", + "action.flip-vertical": "垂直翻轉", + "action.flip-horizontal.short": "水平翻轉", + "action.flip-vertical.short": "垂直翻轉", + "action.group": "群組", + "action.insert-media": "上傳媒體檔", + "action.new-shared-project": "新建共享專案", + "action.nudge-down": "向下微調", + "action.nudge-left": "向左微調", + "action.nudge-right": "向右微調", + "action.nudge-up": "向上微調", + "action.open-file": "開啟檔案", + "action.pack": "打包", + "action.paste": "貼上", + "action.print": "列印", + "action.redo": "取消復原", + "action.rotate-ccw": "逆時針旋轉", + "action.rotate-cw": "順時針旋轉", + "action.save-copy": "存為副本", + "action.select-all": "全選", + "action.select-none": "取消選取", + "action.send-backward": "後移一層", + "action.send-to-back": "移至底層", + "action.share-project": "共享此專案", + "action.stack-horizontal": "水平堆疊", + "action.stack-vertical": "垂直堆疊", + "action.stack-horizontal.short": "水平堆疊", + "action.stack-vertical.short": "垂直堆疊", + "action.stretch-horizontal": "水平延展", + "action.stretch-vertical": "垂直延展", + "action.stretch-horizontal.short": "水平延展", + "action.stretch-vertical.short": "垂直延展", + "action.toggle-auto-size": "切換自動大小", + "action.toggle-dark-mode.menu": "深色模式", + "action.toggle-dark-mode": "切換深色模式", + "action.toggle-debug-mode.menu": "除錯模式", + "action.toggle-debug-mode": "切換除錯模式", + "action.toggle-focus-mode.menu": "專注模式", + "action.toggle-focus-mode": "切換專注模式", + "action.toggle-grid.menu": "顯示網格", + "action.toggle-grid": "切換網格", + "action.toggle-snap-mode.menu": "始終貼齊", + "action.toggle-snap-mode": "切換始終貼齊", + "action.toggle-tool-lock.menu": "工具鎖定", + "action.toggle-tool-lock": "切換工具鎖定", + "action.toggle-transparent.context-menu": "透明", + "action.toggle-transparent.menu": "透明", + "action.toggle-transparent": "切換透明背景", + "action.undo": "復原", + "action.ungroup": "取消群組", + "action.zoom-in": "放大", + "action.zoom-out": "縮小", + "action.zoom-to-100": "縮放至 100%", + "action.zoom-to-fit": "縮放至適當大小", + "action.zoom-to-selection": "縮放至選取範圍", + "color-style.black": "黑色", + "color-style.blue": "藍色", + "color-style.green": "綠色", + "color-style.grey": "灰色", + "color-style.light-blue": "淺藍色", + "color-style.light-green": "淺綠色", + "color-style.light-red": "淺紅色", + "color-style.light-violet": "淺紫色", + "color-style.orange": "橘色", + "color-style.red": "紅色", + "color-style.violet": "紫色", + "color-style.yellow": "黃色", + "fill-style.none": "無", + "fill-style.semi": "半填滿", + "fill-style.solid": "填滿", + "fill-style.pattern": "圖案", + "dash-style.dashed": "虛線", + "dash-style.dotted": "虛點", + "dash-style.draw": "手繪", + "dash-style.solid": "實線", + "size-style.s": "小", + "size-style.m": "中", + "size-style.l": "大", + "size-style.xl": "特大", + "opacity-style.0.1": "10%", + "opacity-style.0.25": "25%", + "opacity-style.0.5": "50%", + "opacity-style.0.75": "75%", + "opacity-style.1": "100%", + "font-style.draw": "手繪", + "font-style.sans": "無襯線", + "font-style.serif": "襯線", + "font-style.mono": "等寬", + "align-style.start": "前端", + "align-style.middle": "置中", + "align-style.end": "末端", + "align-style.justify": "兩端", + "geo-style.arrow-down": "向下箭頭", + "geo-style.arrow-left": "向左箭頭", + "geo-style.arrow-right": "向右箭頭", + "geo-style.arrow-up": "向上箭頭", + "geo-style.diamond": "菱形 2", + "geo-style.ellipse": "橢圓形", + "geo-style.hexagon": "六邊形", + "geo-style.octagon": "八邊形", + "geo-style.oval": "卵形", + "geo-style.pentagon": "五邊形", + "geo-style.rectangle": "矩形", + "geo-style.rhombus-2": "菱形 2", + "geo-style.rhombus": "菱形", + "geo-style.star": "星形", + "geo-style.trapezoid": "梯形", + "geo-style.triangle": "三角形", + "geo-style.x-box": "X 框", + "arrowheadStart-style.none": "無", + "arrowheadStart-style.arrow": "箭頭", + "arrowheadStart-style.bar": "條狀", + "arrowheadStart-style.diamond": "菱形", + "arrowheadStart-style.dot": "點", + "arrowheadStart-style.inverted": "反轉", + "arrowheadStart-style.pipe": "管狀", + "arrowheadStart-style.square": "正方形", + "arrowheadStart-style.triangle": "三角形", + "arrowheadEnd-style.none": "無", + "arrowheadEnd-style.arrow": "箭頭", + "arrowheadEnd-style.bar": "條狀", + "arrowheadEnd-style.diamond": "菱形", + "arrowheadEnd-style.dot": "點", + "arrowheadEnd-style.inverted": "反轉", + "arrowheadEnd-style.pipe": "管狀", + "arrowheadEnd-style.square": "正方形", + "arrowheadEnd-style.triangle": "三角形", + "spline-style.line": "直線", + "spline-style.cubic": "立方體", + "tool.select": "選取", + "tool.hand": "抓取", + "tool.draw": "手繪", + "tool.eraser": "橡皮擦", + "tool.arrow-down": "向下箭頭", + "tool.arrow-left": "向左箭頭", + "tool.arrow-right": "向右箭頭", + "tool.arrow-up": "向上箭頭", + "tool.arrow": "箭頭", + "tool.diamond": "菱形 2", + "tool.ellipse": "橢圓形", + "tool.hexagon": "六邊形", + "tool.line": "直線", + "tool.octagon": "八邊形", + "tool.oval": "卵形", + "tool.pentagon": "五邊形", + "tool.rectangle": "矩形", + "tool.rhombus": "菱形", + "tool.star": "星形", + "tool.trapezoid": "梯形", + "tool.triangle": "三角形", + "tool.x-box": "X 框", + "tool.asset": "資源", + "tool.frame": "框架", + "tool.note": "便利貼", + "tool.embed": "嵌入", + "tool.text": "文字", + "menu.title": "選單", + "menu.copy-as": "複製成", + "menu.edit": "編輯", + "menu.export-as": "匯出成", + "menu.file": "檔案", + "menu.language": "語言", + "menu.preferences": "選項", + "menu.view": "檢視", + "context-menu.arrange": "佈局", + "context-menu.copy-as": "複製成", + "context-menu.export-as": "匯出成", + "context-menu.move-to-page": "移至頁面", + "context-menu.reorder": "重新排序", + "page-menu.title": "頁面", + "page-menu.create-new-page": "建立新頁面", + "page-menu.edit-pages": "編輯頁面", + "page-menu.max-page-count-reached": "達到上限頁數", + "page-menu.new-page-initial-name": "頁面 1", + "page-menu.page": "頁面", + "page-menu.edit-start": "編輯", + "page-menu.edit-done": "完成", + "page-menu.submenu.rename": "重新命名", + "page-menu.submenu.duplicate-page": "複製", + "page-menu.submenu.go-to-page": "進到頁面", + "page-menu.submenu.title": "選單", + "page-menu.submenu.move-down": "下移", + "page-menu.submenu.move-up": "上移", + "page-menu.submenu.delete": "刪除", + "share-menu.title": "共享", + "share-menu.share-project": "共享此專案", + "share-menu.create-project": "新建共享專案", + "share-menu.copy-link": "複製連結", + "share-menu.readonly-link": "唯讀", + "share-menu.copy-readonly-link": "複製唯讀連結", + "share-menu.offline-note": "共享此專案將建立即時副本,並產生新網址。您可以分享網址,與其他至多三十人一起檢視和編輯副本。", + "share-menu.copy-link-note": "任何人透過連結都能檢視和編輯此專案。", + "share-menu.copy-readonly-link-note": "任何人透過連結都能檢視(但不能編輯)此專案。", + "share-menu.project-too-large": "抱歉,無法共享此專案,因為檔案過大。我們已在調整這件事!", + "people-menu.title": "人員", + "people-menu.change-name": "更改名稱", + "people-menu.change-color": "更改顏色", + "people-menu.user": "(您)", + "people-menu.invite": "邀請其他人", + "debug-menu.hard-reset": "強制重置", + "debug-menu.create-shapes": "建立 100 個形狀", + "help-menu.title": "幫助和資源", + "help-menu.about": "關於", + "help-menu.discord": "Discord", + "help-menu.github": "GitHub", + "help-menu.keyboard-shortcuts": "鍵盤快捷鍵", + "help-menu.twitter": "Twitter", + "links-menu.about": "關於", + "links-menu.discord": "Discord", + "links-menu.github": "GitHub", + "links-menu.twitter": "Twitter", + "actions-menu.title": "操作", + "edit-link-dialog.title": "編輯連結", + "edit-link-dialog.invalid-url": "連結必須是有效網址。", + "edit-link-dialog.detail": "連結將在新分頁開啟。", + "edit-link-dialog.url": "網址", + "edit-link-dialog.clear": "清除", + "edit-link-dialog.save": "繼續", + "edit-link-dialog.cancel": "取消", + "embed-dialog.title": "建立嵌入", + "embed-dialog.url-label": "貼上網址", + "embed-dialog.back": "返回", + "embed-dialog.create": "建立", + "embed-dialog.cancel": "取消", + "embed-dialog.url": "網址", + "embed-dialog.instruction": "貼上網站連結以建立嵌入。", + "embed-dialog.invalid-url": "我們沒辦法從該網址建立嵌入。", + "edit-pages-dialog.title": "編輯頁面", + "edit-pages-dialog.create-new-page": "建立新頁面", + "edit-pages-dialog.delete": "刪除", + "edit-pages-dialog.duplicate-page": "複製", + "edit-pages-dialog.go-to-page": "進到頁面", + "edit-pages-dialog.max-page-count-reached": "達到上限頁數", + "edit-pages-dialog.more-menu": "選單", + "edit-pages-dialog.move-down": "下移", + "edit-pages-dialog.move-up": "上移", + "edit-pages-dialog.new-page-initial-name": "頁面 1", + "reload-file-dialog.title": "繼續編輯檔案", + "reload-file-dialog.description": "您剛才正編輯一個檔案。要繼續編輯嗎?", + "reload-file-dialog.failure": "無法重新載入檔案。是否重試?", + "reload-file-dialog.reload": "繼續編輯", + "reload-file-dialog.revert": "不,謝謝", + "shortcuts-dialog.title": "鍵盤快捷鍵", + "shortcuts-dialog.edit": "編輯", + "shortcuts-dialog.file": "檔案", + "shortcuts-dialog.preferences": "選項", + "shortcuts-dialog.tools": "工具", + "shortcuts-dialog.transform": "轉換", + "shortcuts-dialog.view": "檢視", + "shortcuts-dialog.save": "繼續", + "style-panel.title": "樣式", + "style-panel.align": "對齊", + "style-panel.arrowheads": "箭頭", + "style-panel.color": "顏色", + "style-panel.dash": "虛線", + "style-panel.fill": "填充", + "style-panel.font": "字型", + "style-panel.geo": "形狀", + "style-panel.label": "標籤", + "style-panel.mixed": "混合", + "style-panel.opacity": "不透明度", + "style-panel.size": "大小", + "style-panel.spline": "曲線", + "style-panel.text": "文字", + "tool-panel.drawing": "繪圖", + "tool-panel.geo": "形狀", + "tool-panel.shapes": "形狀", + "tool-panel.things": "物件", + "tool-panel.tools": "工具", + "save-changes-prompt.title": "您有未儲存的更動", + "save-changes-prompt.description": "您想儲存更動至當前檔案嗎?", + "save-changes-prompt.go-back": "返回", + "save-changes-prompt.continue": "繼續", + "navigation-zone.toggle-minimap": "切換小地圖", + "navigation-zone.zoom": "縮放", + "focus-mode.toggle-focus-mode": "切換專注模式", + "toast.close": "關閉", + "file-system.file-open-error.title": "無法開啟檔案", + "file-system.file-open-error.not-a-tldraw-file": "您嘗試開啟的檔案不像 tldraw 檔。", + "file-system.file-open-error.file-format-version-too-new": "你嘗試開啟的檔案來自新版 tldraw。請重新載入頁面,然後再試一次。", + "file-system.file-open-error.generic-corrupted-file": "您嘗試開啟的檔案已損毀。", + "file-system.confirm-open.title": "覆蓋當前檔案?", + "file-system.confirm-open.description": "開啟檔案將置換您的當前專案,任何未儲存的更動都會消失。您確定要繼續嗎?", + "file-system.confirm-open.cancel": "取消", + "file-system.confirm-open.open": "開啟檔案", + "file-system.confirm-open.dont-show-again": "不再詢問", + "toast.error.export-fail.title": "匯出失敗", + "toast.error.export-fail.desc": "圖片匯出失敗", + "toast.error.copy-fail.title": "複製失敗", + "toast.error.copy-fail.desc": "圖片複製失敗", + "file-system.shared-document-file-open-error.title": "無法開啟檔案", + "file-system.shared-document-file-open-error.description": "不支援從共享專案開啟檔案。", + "vscode.file-open.dont-show-again": "不再詢問", + "vscode.file-open.desc": "此檔案由較早版本的 tldraw 所建立。您想要更新它以用於新版本上嗎?", + "context.pages.new-page": "新頁面", + "style-panel.arrowhead-start": "前端", + "style-panel.arrowhead-end": "末端", + "vscode.file-open.open": "繼續", + "vscode.file-open.backup": "備份", + "vscode.file-open.backup-saved": "已備份", + "vscode.file-open.backup-failed": "備份失敗:這不是 .tldr 檔。", + "tool-panel.more": "更多", + "debug-panel.more": "更多", + "action.new-project": "新建專案", + "file-system.confirm-clear.title": "清除當前專案?", + "file-system.confirm-clear.description": "建立新專案將清除當前專案,任何未儲存的更動都會消失。您確定要繼續嗎?", + "file-system.confirm-clear.cancel": "取消", + "file-system.confirm-clear.continue": "繼續", + "file-system.confirm-clear.dont-show-again": "不再詢問", + "action.stop-following": "取消追蹤", + "people-menu.follow": "追蹤", + "style-panel.position": "位置" +} \ No newline at end of file diff --git a/config/CHANGELOG.md b/config/CHANGELOG.md new file mode 100644 index 000000000..140316890 --- /dev/null +++ b/config/CHANGELOG.md @@ -0,0 +1,85 @@ +# config + +## 2.0.0-alpha.8 + +### Patch Changes + +- Release day! + +## 2.0.0-alpha.7 + +### Patch Changes + +- Bug fixes. + +## 2.0.0-alpha.6 + +### Patch Changes + +- Add licenses. + +## 2.0.0-alpha.5 + +### Patch Changes + +- Add CSS files to tldraw/tldraw. + +## 2.0.0-alpha.4 + +### Patch Changes + +- Add children to tldraw/tldraw + +## 2.0.0-alpha.3 + +### Patch Changes + +- Change permissions. + +## 2.0.0-alpha.2 + +### Patch Changes + +- Add tldraw, editor + +## 0.1.0-alpha.11 + +### Patch Changes + +- Fix stale reactors. + +## 0.1.0-alpha.10 + +### Patch Changes + +- Fix type export bug. + +## 0.1.0-alpha.9 + +### Patch Changes + +- Fix import bugs. + +## 0.1.0-alpha.8 + +### Patch Changes + +- Changes validation requirements, exports validation helpers. + +## 0.1.0-alpha.7 + +### Patch Changes + +- - Pre-pre-release update + +## 0.0.2-alpha.1 + +### Patch Changes + +- Fix error with HMR + +## 0.0.2-alpha.0 + +### Patch Changes + +- Initial release diff --git a/config/LICENSE b/config/LICENSE new file mode 100644 index 000000000..4f227c380 --- /dev/null +++ b/config/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2023 tldraw GB Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/config/api-extractor.json b/config/api-extractor.json new file mode 100644 index 000000000..13795db52 --- /dev/null +++ b/config/api-extractor.json @@ -0,0 +1,403 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + "projectFolder": "..", + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "/packages//.tsbuild-api/index.d.ts", + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + "newlineKind": "lf", + /** + * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the + * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests. + * + * DEFAULT VALUE: "false" + */ + // "testMode": false, + /** + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output + * files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify + * "preserve". + * + * DEFAULT VALUE: "by-name" + */ + // "enumMemberOrder": "by-name", + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + "tsconfigFilePath": "/packages//tsconfig.json" + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true, + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + "reportFileName": "api-report.md", + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + "reportFolder": "/packages//", + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + "reportTempFolder": "/packages//api/temp" + /** + * Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false + }, + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true, + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + "apiJsonFilePath": "/packages//api/api.json" + /** + * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false, + /** + * The base URL where the project's source code can be viewed on a website such as GitHub or + * Azure DevOps. This URL path corresponds to the `` path on disk. + * + * This URL is concatenated with the file paths serialized to the doc model to produce URL file paths to individual API items. + * For example, if the `projectFolderUrl` is "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor" and an API + * item's file path is "api/ExtractorConfig.ts", the full URL file path would be + * "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js". + * + * Can be omitted if you don't need source code links in your API documentation reference. + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "projectFolderUrl": "http://github.com/path/to/your/projectFolder" + }, + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true, + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + "untrimmedFilePath": "/packages//api/internal.d.ts", + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. + * This file will include only declarations that are marked as "@public", "@beta", or "@alpha". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "alphaTrimmedFilePath": "0.0.0", + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + "publicTrimmedFilePath": "/packages//api/public.d.ts" + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + "enabled": false + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "error" + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "error" + }, + "ae-internal-missing-underscore": { + "logLevel": "none", + "addToApiReportFile": false + }, + "ae-forgotten-export": { + "logLevel": "none", + "addToApiReportFile": false + } + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "error" + // "addToApiReportFile": false + } + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/config/eslint-preset-react.js b/config/eslint-preset-react.js new file mode 100644 index 000000000..b6a76291d --- /dev/null +++ b/config/eslint-preset-react.js @@ -0,0 +1,30 @@ +/* eslint-disable */ + +module.exports = { + extends: ['prettier'], + settings: { + next: { + rootDir: ['apps/*/', 'packages/*/', 'bublic/apps/*/', 'bublic/packages/*/'], + }, + }, + rules: { + '@next/next/no-html-link-for-pages': 'off', + 'react/jsx-key': 'off', + 'no-non-null-assertion': 'off', + 'no-fallthrough': 'off', + '@typescript-eslint/no-fallthrough': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + 'react/display-name': 'off', + '@next/next/no-img-element': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + }, +} diff --git a/config/eslint-preset.js b/config/eslint-preset.js new file mode 100644 index 000000000..bd0cc958e --- /dev/null +++ b/config/eslint-preset.js @@ -0,0 +1,28 @@ +/* eslint-disable */ + +module.exports = { + extends: ['prettier'], + settings: { + next: { + rootDir: ['apps/*/', 'packages/*/', 'bublic/apps/*/', 'bublic/packages/*/'], + }, + }, + ignorePatterns: ['**/*.js'], + rules: { + 'no-non-null-assertion': 'off', + 'no-fallthrough': 'off', + '@typescript-eslint/no-fallthrough': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + }, +} diff --git a/config/jest/node/jest-preset.js b/config/jest/node/jest-preset.js new file mode 100644 index 000000000..27b1cc561 --- /dev/null +++ b/config/jest/node/jest-preset.js @@ -0,0 +1,36 @@ +module.exports = { + roots: ['/src'], + transform: { + '^.+\\.(tsx|jsx|ts|js|mjs)?$': [ + '@swc/jest', + { + jsc: { + parser: { + syntax: 'typescript', + dynamicImport: true, + decorators: true, + }, + transform: { + legacyDecorator: true, + decoratorMetadata: true, + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + }, + testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + modulePathIgnorePatterns: [ + '/test/__fixtures__', + '/node_modules', + '/dist', + '/.tsbuild', + '/.tsbuild-dev', + '/.tsbuild-pub', + ], + transformIgnorePatterns: ['node_modules/(?!(nanoid)/)'], + collectCoverageFrom: ['/src/**/*.{ts,tsx}'], +} diff --git a/config/package.json b/config/package.json new file mode 100644 index 000000000..1e5c14f8b --- /dev/null +++ b/config/package.json @@ -0,0 +1,17 @@ +{ + "name": "config", + "version": "2.0.0-alpha.8", + "main": "index.js", + "license": "MIT", + "private": true, + "files": [ + "eslint-preset.js" + ], + "dependencies": { + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-react": "7.28.0" + }, + "devDependencies": { + "lazyrepo": "0.0.0-alpha.20" + } +} diff --git a/config/setupJest.ts b/config/setupJest.ts new file mode 100644 index 000000000..81ee9c0fe --- /dev/null +++ b/config/setupJest.ts @@ -0,0 +1,76 @@ +import { equals, getObjectSubset, iterableEquality, subsetEquality } from '@jest/expect-utils' + +import { + matcherHint, + printDiffOrStringify, + printExpected, + printReceived, + stringify, +} from 'jest-matcher-utils' + +function convertNumbersInObject(obj: any, roundToNearest: number) { + if (!obj) return obj + if (Array.isArray(obj)) { + return obj.map((x) => convertNumbersInObject(x, roundToNearest)) + } + if (typeof obj === 'number') { + // || 0 to avoid -0 + return Math.round(obj / roundToNearest) * roundToNearest || 0 + } + if (typeof obj !== 'object') { + return obj + } + + const r: any = { + __converted: true, + } + + for (const k of Object.keys(obj)) { + r[k] = convertNumbersInObject(obj[k], roundToNearest) + } + + return r +} + +expect.extend({ + toCloselyMatchObject(actual: any, expected: any, roundToNearest = 0.0001) { + const matcherName = 'toCloselyMatchObject' + const options = { + isNot: this.isNot, + promise: this.promise, + } + + const EXPECTED_LABEL = 'Expected' + const RECEIVED_LABEL = 'Received' + const isExpand = (expand?: boolean): boolean => expand !== false + + const newActualObj = convertNumbersInObject(actual, roundToNearest) + + const newExpectedObj = convertNumbersInObject(expected, roundToNearest) + + const pass = equals(newActualObj, newExpectedObj, [iterableEquality, subsetEquality]) + + const message = pass + ? () => + // eslint-disable-next-line prefer-template + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + `Expected: not ${printExpected(expected)}` + + (stringify(expected) !== stringify(actual) + ? `\nReceived: ${printReceived(actual)}` + : '') + : () => + // eslint-disable-next-line prefer-template + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + printDiffOrStringify( + expected, + getObjectSubset(actual, expected), + EXPECTED_LABEL, + RECEIVED_LABEL, + isExpand(this.expand) + ) + + return { message, pass } + }, +}) diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json new file mode 100644 index 000000000..d3c47cda0 --- /dev/null +++ b/config/tsconfig.base.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "importHelpers": true, + "resolveJsonModule": true, + "incremental": true, + "jsx": "react-jsx", + "lib": ["dom", "DOM.Iterable", "esnext"], + "experimentalDecorators": true, + "module": "esnext", + "target": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "skipLibCheck": true, + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "useDefineForClassFields": true, + "noImplicitOverride": true, + "types": ["node", "@types/jest"] + }, + "exclude": ["node_modules"] +} diff --git a/lazy.config.ts b/lazy.config.ts new file mode 100644 index 000000000..7e9a9afd7 --- /dev/null +++ b/lazy.config.ts @@ -0,0 +1,126 @@ +import { LazyConfig } from 'lazyrepo' + +export function generateSharedTasks(bublic: '' | '/bublic') { + return { + build: { + runsAfter: { 'build:package': {}, prebuild: {} }, + }, + 'build:vscode-editor': { + runsAfter: { 'refresh-assets': {} }, + }, + dev: { + execution: 'independent', + runsAfter: { 'refresh-assets': {} }, + cache: 'none', + }, + 'dev:vscode': { + runsAfter: { 'build:vscode-editor': {} }, + }, + test: { + baseCommand: 'yarn run -T jest', + runsAfter: { 'refresh-assets': {} }, + }, + 'test:coverage': { + baseCommand: 'yarn run -T jest --coverage', + }, + lint: { + execution: 'independent', + runsAfter: { 'build:types': {} }, + }, + 'build:package': { + runsAfter: { 'build:api': {}, prebuild: {} }, + cache: { + inputs: ['api/**/*', 'src/**/*'], + }, + }, + 'pack-tarball': { + parallel: false, + }, + 'refresh-assets': { + execution: 'top-level', + baseCommand: `tsx ${bublic}/scripts/refresh-assets.ts`, + cache: { + inputs: ['package.json', `${bublic}/scripts/refresh-assets.ts`, `${bublic}/assets/**/*`], + }, + }, + 'build:types': { + execution: 'top-level', + baseCommand: `tsx ${bublic}/scripts/typecheck.ts`, + cache: { + inputs: { + include: [ + '{.,./bublic}/packages/*/src/**/*.{ts,tsx}', + '{.,./bublic}/{apps,scripts,e2e}/**/*.{ts,tsx}', + '{.,./bublic}/{apps,packages}/*/tsconfig.json', + '{.,./bublic}/{scripts,e2e}/tsconfig.json', + `${bublic}/config/tsconfig.base.json`, + ], + exclude: ['**/dist*/**/*.d.ts'], + }, + }, + runsAfter: { + 'refresh-assets': {}, + 'maybe-clean-tsbuildinfo': {}, + }, + }, + 'build:api': { + execution: 'independent', + cache: { + inputs: ['.tsbuild/**/*.d.ts', 'tsconfig.json'], + }, + runsAfter: { 'build:types': {} }, + }, + 'build:docs': { + runsAfter: { 'docs:content': {} }, + }, + 'dev:docs': { + runsAfter: { 'docs:content': {} }, + }, + 'app:build': { + runsAfter: { 'build:types': {} }, + }, + 'docs:content': { + runsAfter: { 'build:api': {} }, + cache: { + inputs: [ + 'content/**', + 'scripts/**', + `${bublic}/packages/*/api/api.json`, + `${bublic}/packages/*/package.json`, + ], + }, + }, + 'api:check': { + execution: 'top-level', + baseCommand: `tsx ${bublic}/scripts/api-check.ts`, + runsAfter: { 'build:api': {} }, + cache: { + inputs: ['**/api/bublic.d.ts'], + }, + }, + } satisfies LazyConfig['tasks'] +} + +const config = { + baseCacheConfig: { + include: [ + '/package.json', + '/yarn.lock', + '/lazy.config.mjs', + '/config/**/*', + '/scripts/**/*', + ], + exclude: [ + 'coverage/**/*', + 'dist*/**/*', + '**/*.tsbuildinfo', + '/apps/app/bublic/*.{js,map}', + '/apps/docs/content/gen/**/*', + ], + }, + tasks: { + ...generateSharedTasks(''), + }, +} satisfies LazyConfig + +export default config diff --git a/lerna.json b/lerna.json new file mode 100644 index 000000000..a7099725e --- /dev/null +++ b/lerna.json @@ -0,0 +1,5 @@ +{ + "$schema": "node_modules/lerna/schemas/lerna-schema.json", + "useWorkspaces": true, + "version": "2.0.0-alpha.12" +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..bb9819ea7 --- /dev/null +++ b/package.json @@ -0,0 +1,104 @@ +{ + "name": "@tldraw/monorepo", + "description": "A tiny little drawing app (monorepo).", + "version": "0.0.0", + "private": true, + "author": { + "name": "tldraw GB Ltd.", + "email": "hello@tldraw.com" + }, + "homepage": "https://tldraw.dev", + "repository": { + "type": "git", + "url": "https://github.com/tldraw/tldraw-lite" + }, + "bugs": { + "url": "https://github.com/tldraw/tldraw/issues" + }, + "keywords": [ + "tldraw", + "drawing", + "app", + "development", + "whiteboard", + "canvas", + "infinite" + ], + "workspaces": [ + "apps/*", + "packages/*", + "apps/vscode/*", + "config", + "scripts" + ], + "scripts": { + "clean": "scripts/clean.sh", + "postinstall": "yarn refresh-assets", + "refresh-assets": "lazy refresh-assets", + "build": "lazy build", + "build:docs": "lazy build:docs", + "dev": "lazy run dev --filter ./apps/examples --filter ./packages/tldraw", + "dev:docs": "lazy run dev:docs", + "dev:vscode": "code ./apps/vscode/extension && lazy run dev --filter './apps/vscode/{extension,editor}'", + "build:types": "lazy inherit", + "build:api": "lazy build:api", + "build:package": "lazy build:package", + "lint": "lazy lint", + "format": "prettier --write \"**/*.{ts,tsx}\"", + "typecheck": "yarn refresh-assets && tsx scripts/typecheck.ts", + "check-scripts": "tsx scripts/check-scripts.ts", + "api:check": "lazy api:check", + "test": "lazy test", + "prepare": "husky install" + }, + "engines": { + "npm": ">=7.0.0" + }, + "packageManager": "yarn@3.5.0", + "lint-staged": { + "*.{js,jsx,ts,tsx,json}": [ + "prettier --write" + ] + }, + "dependencies": { + "@next/eslint-plugin-next": "^13.3.0", + "@types/jest": "^28.1.2", + "@types/node": "18.7.3", + "@types/react": "^18.0.24", + "@types/react-dom": "^18.0.6", + "@typescript-eslint/eslint-plugin": "^5.57.0", + "@typescript-eslint/parser": "^5.57.0", + "cross-env": "^7.0.3", + "eslint": "^8.37.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-local": "^1.0.0", + "eslint-plugin-no-only-tests": "^3.1.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "husky": "^8.0.0", + "inquirer": "^9.1.4", + "jest": "^28.1.1", + "lint-staged": ">=10", + "open": "^8.4.0", + "prettier": "^2.8.6", + "prettier-plugin-organize-imports": "^3.2.2", + "typescript": "^5.0.2" + }, + "devDependencies": { + "@microsoft/api-extractor": "^7.34.1", + "@swc/core": "^1.3.41", + "@swc/jest": "^0.2.24", + "@types/glob": "^8.1.0", + "auto": "^10.44.0", + "fs-extra": "^11.1.0", + "json5": "^2.2.3", + "lazyrepo": "0.0.0-alpha.20", + "rimraf": "^4.4.0", + "tsx": "^3.12.2", + "vercel": "^28.16.15" + }, + "resolutions": { + "@microsoft/api-extractor@^7.34.1": "patch:@microsoft/api-extractor@npm%3A7.34.1#./.yarn/patches/@microsoft-api-extractor-npm-7.34.1-af268a32f8.patch" + } +} diff --git a/packages/assets/CHANGELOG.md b/packages/assets/CHANGELOG.md new file mode 100644 index 000000000..92bbf428c --- /dev/null +++ b/packages/assets/CHANGELOG.md @@ -0,0 +1,15 @@ +# v2.0.0-alpha.12 (Mon Apr 03 2023) + +#### 🐛 Bug Fix + +- add community translations [#1559](https://github.com/tldraw/tldraw-lite/pull/1559) ([@steveruizok](https://github.com/steveruizok) [@TodePond](https://github.com/TodePond)) +- Make sure all types and build stuff get run in CI [#1548](https://github.com/tldraw/tldraw-lite/pull/1548) ([@SomeHats](https://github.com/SomeHats)) +- add pre-commit api report generation [#1517](https://github.com/tldraw/tldraw-lite/pull/1517) ([@SomeHats](https://github.com/SomeHats)) +- [chore] restore api extractor [#1500](https://github.com/tldraw/tldraw-lite/pull/1500) ([@steveruizok](https://github.com/steveruizok)) +- Asset loading overhaul [#1457](https://github.com/tldraw/tldraw-lite/pull/1457) ([@SomeHats](https://github.com/SomeHats)) + +#### Authors: 3 + +- alex ([@SomeHats](https://github.com/SomeHats)) +- Lu[ke] Wilson ([@TodePond](https://github.com/TodePond)) +- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) diff --git a/packages/assets/LICENSE b/packages/assets/LICENSE new file mode 100644 index 000000000..4f227c380 --- /dev/null +++ b/packages/assets/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2023 tldraw GB Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/assets/api-extractor.json b/packages/assets/api-extractor.json new file mode 100644 index 000000000..f1ed80e93 --- /dev/null +++ b/packages/assets/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../config/api-extractor.json" +} diff --git a/packages/assets/api-report.md b/packages/assets/api-report.md new file mode 100644 index 000000000..6bef0b3fa --- /dev/null +++ b/packages/assets/api-report.md @@ -0,0 +1,231 @@ +## API Report File for "@tldraw/assets" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export function getBundlerAssetUrls(opts?: AssetUrlOptions): { + readonly fonts: { + readonly monospace: string; + readonly sansSerif: string; + readonly serif: string; + readonly draw: string; + }; + readonly icons: { + readonly 'align-bottom-center': string; + readonly 'align-bottom-left': string; + readonly 'align-bottom-right': string; + readonly 'align-bottom': string; + readonly 'align-center-center': string; + readonly 'align-center-horizontal': string; + readonly 'align-center-left': string; + readonly 'align-center-right': string; + readonly 'align-center-vertical': string; + readonly 'align-left': string; + readonly 'align-right': string; + readonly 'align-top-center': string; + readonly 'align-top-left': string; + readonly 'align-top-right': string; + readonly 'align-top': string; + readonly 'arrow-left': string; + readonly 'arrowhead-arrow': string; + readonly 'arrowhead-bar': string; + readonly 'arrowhead-diamond': string; + readonly 'arrowhead-dot': string; + readonly 'arrowhead-none': string; + readonly 'arrowhead-square': string; + readonly 'arrowhead-triangle-inverted': string; + readonly 'arrowhead-triangle': string; + readonly 'aspect-ratio': string; + readonly avatar: string; + readonly blob: string; + readonly 'bring-forward': string; + readonly 'bring-to-front': string; + readonly check: string; + readonly 'checkbox-checked': string; + readonly 'checkbox-empty': string; + readonly 'chevron-down': string; + readonly 'chevron-left': string; + readonly 'chevron-right': string; + readonly 'chevron-up': string; + readonly 'chevrons-ne': string; + readonly 'chevrons-sw': string; + readonly 'clipboard-copy': string; + readonly code: string; + readonly collab: string; + readonly color: string; + readonly comment: string; + readonly 'cross-2': string; + readonly cross: string; + readonly 'dash-dashed': string; + readonly 'dash-dotted': string; + readonly 'dash-draw': string; + readonly 'dash-solid': string; + readonly discord: string; + readonly 'distribute-horizontal': string; + readonly 'distribute-vertical': string; + readonly dot: string; + readonly 'dots-horizontal': string; + readonly 'dots-vertical': string; + readonly 'drag-handle-dots': string; + readonly duplicate: string; + readonly edit: string; + readonly 'external-link': string; + readonly file: string; + readonly 'fill-none': string; + readonly 'fill-pattern': string; + readonly 'fill-semi': string; + readonly 'fill-solid': string; + readonly follow: string; + readonly following: string; + readonly 'font-draw': string; + readonly 'font-mono': string; + readonly 'font-sans': string; + readonly 'font-serif': string; + readonly 'geo-arrow-down': string; + readonly 'geo-arrow-left': string; + readonly 'geo-arrow-right': string; + readonly 'geo-arrow-up': string; + readonly 'geo-diamond': string; + readonly 'geo-ellipse': string; + readonly 'geo-hexagon': string; + readonly 'geo-octagon': string; + readonly 'geo-oval': string; + readonly 'geo-pentagon': string; + readonly 'geo-rectangle': string; + readonly 'geo-rhombus-2': string; + readonly 'geo-rhombus': string; + readonly 'geo-star': string; + readonly 'geo-trapezoid': string; + readonly 'geo-triangle': string; + readonly 'geo-x-box': string; + readonly github: string; + readonly group: string; + readonly hidden: string; + readonly image: string; + readonly 'info-circle': string; + readonly leading: string; + readonly link: string; + readonly 'lock-small': string; + readonly lock: string; + readonly menu: string; + readonly minus: string; + readonly mixed: string; + readonly pack: string; + readonly page: string; + readonly plus: string; + readonly 'question-mark-circle': string; + readonly 'question-mark': string; + readonly redo: string; + readonly 'reset-zoom': string; + readonly 'rotate-ccw': string; + readonly 'rotate-cw': string; + readonly ruler: string; + readonly search: string; + readonly 'send-backward': string; + readonly 'send-to-back': string; + readonly 'settings-horizontal': string; + readonly 'settings-vertical-1': string; + readonly 'settings-vertical': string; + readonly 'share-1': string; + readonly 'share-2': string; + readonly 'size-extra-large': string; + readonly 'size-large': string; + readonly 'size-medium': string; + readonly 'size-small': string; + readonly 'spline-cubic': string; + readonly 'spline-line': string; + readonly 'stack-horizontal': string; + readonly 'stack-vertical': string; + readonly 'stretch-horizontal': string; + readonly 'stretch-vertical': string; + readonly 'text-align-center': string; + readonly 'text-align-justify': string; + readonly 'text-align-left': string; + readonly 'text-align-right': string; + readonly 'tool-arrow': string; + readonly 'tool-embed': string; + readonly 'tool-eraser': string; + readonly 'tool-frame': string; + readonly 'tool-hand': string; + readonly 'tool-highlighter': string; + readonly 'tool-line': string; + readonly 'tool-media': string; + readonly 'tool-note': string; + readonly 'tool-pencil': string; + readonly 'tool-pointer': string; + readonly 'tool-text': string; + readonly trash: string; + readonly 'triangle-down': string; + readonly 'triangle-up': string; + readonly twitter: string; + readonly undo: string; + readonly ungroup: string; + readonly 'unlock-small': string; + readonly unlock: string; + readonly visible: string; + readonly 'warning-triangle': string; + readonly 'zoom-in': string; + readonly 'zoom-out': string; + }; + readonly translations: { + readonly ar: string; + readonly ca: string; + readonly da: string; + readonly de: string; + readonly en: string; + readonly es: string; + readonly fa: string; + readonly fi: string; + readonly fr: string; + readonly gl: string; + readonly he: string; + readonly 'hi-in': string; + readonly hu: string; + readonly it: string; + readonly ja: string; + readonly 'ko-kr': string; + readonly ku: string; + readonly languages: string; + readonly main: string; + readonly my: string; + readonly ne: string; + readonly no: string; + readonly pl: string; + readonly 'pt-br': string; + readonly 'pt-pt': string; + readonly ro: string; + readonly ru: string; + readonly sv: string; + readonly te: string; + readonly th: string; + readonly tr: string; + readonly uk: string; + readonly vi: string; + readonly 'zh-cn': string; + readonly 'zh-tw': string; + }; + readonly embedIcons: { + readonly codepen: string; + readonly codesandbox: string; + readonly excalidraw: string; + readonly felt: string; + readonly figma: string; + readonly github_gist: string; + readonly google_calendar: string; + readonly google_maps: string; + readonly google_slides: string; + readonly observable: string; + readonly replit: string; + readonly scratch: string; + readonly spotify: string; + readonly tldraw: string; + readonly vimeo: string; + readonly youtube: string; + }; +}; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/assets/modules.d.ts b/packages/assets/modules.d.ts new file mode 100644 index 000000000..c204bbb7b --- /dev/null +++ b/packages/assets/modules.d.ts @@ -0,0 +1,16 @@ +declare module '*.svg' { + const url: string + export default url +} +declare module '*.png' { + const url: string + export default url +} +declare module '*.json' { + const url: string + export default url +} +declare module '*.woff2' { + const url: string + export default url +} diff --git a/packages/assets/package.json b/packages/assets/package.json new file mode 100644 index 000000000..cf4133c66 --- /dev/null +++ b/packages/assets/package.json @@ -0,0 +1,65 @@ +{ + "name": "@tldraw/assets", + "description": "A tiny little drawing app (assets).", + "version": "2.0.0-alpha.12", + "author": { + "name": "tldraw GB Ltd.", + "email": "hello@tldraw.com" + }, + "homepage": "https://tldraw.dev", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/tldraw/tldraw" + }, + "bugs": { + "url": "https://github.com/tldraw/tldraw/issues" + }, + "keywords": [ + "tldraw", + "drawing", + "app", + "development", + "whiteboard", + "canvas", + "infinite" + ], + "/* NOTE */": "These `main` and `types` fields are rewritten by the build script. They are not the actual values we publish", + "main": "./src/index.ts", + "types": "./.tsbuild/index.d.ts", + "/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here", + "files": [ + "embed-icons", + "icons", + "fonts", + "translations" + ], + "scripts": { + "test": "lazy inherit --passWithNoTests", + "test:coverage": "lazy inherit --passWithNoTests", + "build:package": "yarn run -T tsx ../../scripts/build-package.ts", + "build:api": "yarn run -T tsx ../../scripts/build-api.ts", + "prepack": "yarn run -T tsx ../../scripts/prepack.ts", + "postpack": "../../scripts/postpack.sh", + "pack-tarball": "yarn pack", + "lint": "yarn run -T tsx ../../scripts/lint.ts" + }, + "dependencies": { + "@tldraw/utils": "workspace:*" + }, + "devDependencies": { + "@swc/core": "^1.2.204", + "@swc/jest": "^0.2.21", + "lazyrepo": "0.0.0-alpha.20", + "ts-node-dev": "^1.1.8" + }, + "jest": { + "preset": "config/jest/node", + "setupFiles": [ + "raf/polyfill" + ], + "moduleNameMapper": { + "^~(.*)": "/src/$1" + } + } +} diff --git a/packages/assets/src/index.ts b/packages/assets/src/index.ts new file mode 100644 index 000000000..a448ef109 --- /dev/null +++ b/packages/assets/src/index.ts @@ -0,0 +1,440 @@ +// This file is automatically generated by scripts/refresh-assets.ts. +// Do not edit manually. + +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// +import codepenEmbedIconUrl from '../embed-icons/codepen.png' +import codesandboxEmbedIconUrl from '../embed-icons/codesandbox.png' +import excalidrawEmbedIconUrl from '../embed-icons/excalidraw.png' +import feltEmbedIconUrl from '../embed-icons/felt.png' +import figmaEmbedIconUrl from '../embed-icons/figma.png' +import githubGistEmbedIconUrl from '../embed-icons/github_gist.png' +import googleCalendarEmbedIconUrl from '../embed-icons/google_calendar.png' +import googleMapsEmbedIconUrl from '../embed-icons/google_maps.png' +import googleSlidesEmbedIconUrl from '../embed-icons/google_slides.png' +import observableEmbedIconUrl from '../embed-icons/observable.png' +import replitEmbedIconUrl from '../embed-icons/replit.png' +import scratchEmbedIconUrl from '../embed-icons/scratch.png' +import spotifyEmbedIconUrl from '../embed-icons/spotify.png' +import tldrawEmbedIconUrl from '../embed-icons/tldraw.png' +import vimeoEmbedIconUrl from '../embed-icons/vimeo.png' +import youtubeEmbedIconUrl from '../embed-icons/youtube.png' +import monospaceFontUrl from '../fonts/IBMPlexMono-Medium.woff2' +import sansSerifFontUrl from '../fonts/IBMPlexSans-Medium.woff2' +import serifFontUrl from '../fonts/IBMPlexSerif-Medium.woff2' +import drawFontUrl from '../fonts/Shantell_Sans-Normal-SemiBold.woff2' +import alignBottomCenterIconUrl from '../icons/icon/align-bottom-center.svg' +import alignBottomLeftIconUrl from '../icons/icon/align-bottom-left.svg' +import alignBottomRightIconUrl from '../icons/icon/align-bottom-right.svg' +import alignBottomIconUrl from '../icons/icon/align-bottom.svg' +import alignCenterCenterIconUrl from '../icons/icon/align-center-center.svg' +import alignCenterHorizontalIconUrl from '../icons/icon/align-center-horizontal.svg' +import alignCenterLeftIconUrl from '../icons/icon/align-center-left.svg' +import alignCenterRightIconUrl from '../icons/icon/align-center-right.svg' +import alignCenterVerticalIconUrl from '../icons/icon/align-center-vertical.svg' +import alignLeftIconUrl from '../icons/icon/align-left.svg' +import alignRightIconUrl from '../icons/icon/align-right.svg' +import alignTopCenterIconUrl from '../icons/icon/align-top-center.svg' +import alignTopLeftIconUrl from '../icons/icon/align-top-left.svg' +import alignTopRightIconUrl from '../icons/icon/align-top-right.svg' +import alignTopIconUrl from '../icons/icon/align-top.svg' +import arrowLeftIconUrl from '../icons/icon/arrow-left.svg' +import arrowheadArrowIconUrl from '../icons/icon/arrowhead-arrow.svg' +import arrowheadBarIconUrl from '../icons/icon/arrowhead-bar.svg' +import arrowheadDiamondIconUrl from '../icons/icon/arrowhead-diamond.svg' +import arrowheadDotIconUrl from '../icons/icon/arrowhead-dot.svg' +import arrowheadNoneIconUrl from '../icons/icon/arrowhead-none.svg' +import arrowheadSquareIconUrl from '../icons/icon/arrowhead-square.svg' +import arrowheadTriangleInvertedIconUrl from '../icons/icon/arrowhead-triangle-inverted.svg' +import arrowheadTriangleIconUrl from '../icons/icon/arrowhead-triangle.svg' +import aspectRatioIconUrl from '../icons/icon/aspect-ratio.svg' +import avatarIconUrl from '../icons/icon/avatar.svg' +import blobIconUrl from '../icons/icon/blob.svg' +import bringForwardIconUrl from '../icons/icon/bring-forward.svg' +import bringToFrontIconUrl from '../icons/icon/bring-to-front.svg' +import checkIconUrl from '../icons/icon/check.svg' +import checkboxCheckedIconUrl from '../icons/icon/checkbox-checked.svg' +import checkboxEmptyIconUrl from '../icons/icon/checkbox-empty.svg' +import chevronDownIconUrl from '../icons/icon/chevron-down.svg' +import chevronLeftIconUrl from '../icons/icon/chevron-left.svg' +import chevronRightIconUrl from '../icons/icon/chevron-right.svg' +import chevronUpIconUrl from '../icons/icon/chevron-up.svg' +import chevronsNeIconUrl from '../icons/icon/chevrons-ne.svg' +import chevronsSwIconUrl from '../icons/icon/chevrons-sw.svg' +import clipboardCopyIconUrl from '../icons/icon/clipboard-copy.svg' +import codeIconUrl from '../icons/icon/code.svg' +import collabIconUrl from '../icons/icon/collab.svg' +import colorIconUrl from '../icons/icon/color.svg' +import commentIconUrl from '../icons/icon/comment.svg' +import cross2IconUrl from '../icons/icon/cross-2.svg' +import crossIconUrl from '../icons/icon/cross.svg' +import dashDashedIconUrl from '../icons/icon/dash-dashed.svg' +import dashDottedIconUrl from '../icons/icon/dash-dotted.svg' +import dashDrawIconUrl from '../icons/icon/dash-draw.svg' +import dashSolidIconUrl from '../icons/icon/dash-solid.svg' +import discordIconUrl from '../icons/icon/discord.svg' +import distributeHorizontalIconUrl from '../icons/icon/distribute-horizontal.svg' +import distributeVerticalIconUrl from '../icons/icon/distribute-vertical.svg' +import dotIconUrl from '../icons/icon/dot.svg' +import dotsHorizontalIconUrl from '../icons/icon/dots-horizontal.svg' +import dotsVerticalIconUrl from '../icons/icon/dots-vertical.svg' +import dragHandleDotsIconUrl from '../icons/icon/drag-handle-dots.svg' +import duplicateIconUrl from '../icons/icon/duplicate.svg' +import editIconUrl from '../icons/icon/edit.svg' +import externalLinkIconUrl from '../icons/icon/external-link.svg' +import fileIconUrl from '../icons/icon/file.svg' +import fillNoneIconUrl from '../icons/icon/fill-none.svg' +import fillPatternIconUrl from '../icons/icon/fill-pattern.svg' +import fillSemiIconUrl from '../icons/icon/fill-semi.svg' +import fillSolidIconUrl from '../icons/icon/fill-solid.svg' +import followIconUrl from '../icons/icon/follow.svg' +import followingIconUrl from '../icons/icon/following.svg' +import fontDrawIconUrl from '../icons/icon/font-draw.svg' +import fontMonoIconUrl from '../icons/icon/font-mono.svg' +import fontSansIconUrl from '../icons/icon/font-sans.svg' +import fontSerifIconUrl from '../icons/icon/font-serif.svg' +import geoArrowDownIconUrl from '../icons/icon/geo-arrow-down.svg' +import geoArrowLeftIconUrl from '../icons/icon/geo-arrow-left.svg' +import geoArrowRightIconUrl from '../icons/icon/geo-arrow-right.svg' +import geoArrowUpIconUrl from '../icons/icon/geo-arrow-up.svg' +import geoDiamondIconUrl from '../icons/icon/geo-diamond.svg' +import geoEllipseIconUrl from '../icons/icon/geo-ellipse.svg' +import geoHexagonIconUrl from '../icons/icon/geo-hexagon.svg' +import geoOctagonIconUrl from '../icons/icon/geo-octagon.svg' +import geoOvalIconUrl from '../icons/icon/geo-oval.svg' +import geoPentagonIconUrl from '../icons/icon/geo-pentagon.svg' +import geoRectangleIconUrl from '../icons/icon/geo-rectangle.svg' +import geoRhombus2IconUrl from '../icons/icon/geo-rhombus-2.svg' +import geoRhombusIconUrl from '../icons/icon/geo-rhombus.svg' +import geoStarIconUrl from '../icons/icon/geo-star.svg' +import geoTrapezoidIconUrl from '../icons/icon/geo-trapezoid.svg' +import geoTriangleIconUrl from '../icons/icon/geo-triangle.svg' +import geoXBoxIconUrl from '../icons/icon/geo-x-box.svg' +import githubIconUrl from '../icons/icon/github.svg' +import groupIconUrl from '../icons/icon/group.svg' +import hiddenIconUrl from '../icons/icon/hidden.svg' +import imageIconUrl from '../icons/icon/image.svg' +import infoCircleIconUrl from '../icons/icon/info-circle.svg' +import leadingIconUrl from '../icons/icon/leading.svg' +import linkIconUrl from '../icons/icon/link.svg' +import lockSmallIconUrl from '../icons/icon/lock-small.svg' +import lockIconUrl from '../icons/icon/lock.svg' +import menuIconUrl from '../icons/icon/menu.svg' +import minusIconUrl from '../icons/icon/minus.svg' +import mixedIconUrl from '../icons/icon/mixed.svg' +import packIconUrl from '../icons/icon/pack.svg' +import pageIconUrl from '../icons/icon/page.svg' +import plusIconUrl from '../icons/icon/plus.svg' +import questionMarkCircleIconUrl from '../icons/icon/question-mark-circle.svg' +import questionMarkIconUrl from '../icons/icon/question-mark.svg' +import redoIconUrl from '../icons/icon/redo.svg' +import resetZoomIconUrl from '../icons/icon/reset-zoom.svg' +import rotateCcwIconUrl from '../icons/icon/rotate-ccw.svg' +import rotateCwIconUrl from '../icons/icon/rotate-cw.svg' +import rulerIconUrl from '../icons/icon/ruler.svg' +import searchIconUrl from '../icons/icon/search.svg' +import sendBackwardIconUrl from '../icons/icon/send-backward.svg' +import sendToBackIconUrl from '../icons/icon/send-to-back.svg' +import settingsHorizontalIconUrl from '../icons/icon/settings-horizontal.svg' +import settingsVertical1IconUrl from '../icons/icon/settings-vertical-1.svg' +import settingsVerticalIconUrl from '../icons/icon/settings-vertical.svg' +import share1IconUrl from '../icons/icon/share-1.svg' +import share2IconUrl from '../icons/icon/share-2.svg' +import sizeExtraLargeIconUrl from '../icons/icon/size-extra-large.svg' +import sizeLargeIconUrl from '../icons/icon/size-large.svg' +import sizeMediumIconUrl from '../icons/icon/size-medium.svg' +import sizeSmallIconUrl from '../icons/icon/size-small.svg' +import splineCubicIconUrl from '../icons/icon/spline-cubic.svg' +import splineLineIconUrl from '../icons/icon/spline-line.svg' +import stackHorizontalIconUrl from '../icons/icon/stack-horizontal.svg' +import stackVerticalIconUrl from '../icons/icon/stack-vertical.svg' +import stretchHorizontalIconUrl from '../icons/icon/stretch-horizontal.svg' +import stretchVerticalIconUrl from '../icons/icon/stretch-vertical.svg' +import textAlignCenterIconUrl from '../icons/icon/text-align-center.svg' +import textAlignJustifyIconUrl from '../icons/icon/text-align-justify.svg' +import textAlignLeftIconUrl from '../icons/icon/text-align-left.svg' +import textAlignRightIconUrl from '../icons/icon/text-align-right.svg' +import toolArrowIconUrl from '../icons/icon/tool-arrow.svg' +import toolEmbedIconUrl from '../icons/icon/tool-embed.svg' +import toolEraserIconUrl from '../icons/icon/tool-eraser.svg' +import toolFrameIconUrl from '../icons/icon/tool-frame.svg' +import toolHandIconUrl from '../icons/icon/tool-hand.svg' +import toolHighlighterIconUrl from '../icons/icon/tool-highlighter.svg' +import toolLineIconUrl from '../icons/icon/tool-line.svg' +import toolMediaIconUrl from '../icons/icon/tool-media.svg' +import toolNoteIconUrl from '../icons/icon/tool-note.svg' +import toolPencilIconUrl from '../icons/icon/tool-pencil.svg' +import toolPointerIconUrl from '../icons/icon/tool-pointer.svg' +import toolTextIconUrl from '../icons/icon/tool-text.svg' +import trashIconUrl from '../icons/icon/trash.svg' +import triangleDownIconUrl from '../icons/icon/triangle-down.svg' +import triangleUpIconUrl from '../icons/icon/triangle-up.svg' +import twitterIconUrl from '../icons/icon/twitter.svg' +import undoIconUrl from '../icons/icon/undo.svg' +import ungroupIconUrl from '../icons/icon/ungroup.svg' +import unlockSmallIconUrl from '../icons/icon/unlock-small.svg' +import unlockIconUrl from '../icons/icon/unlock.svg' +import visibleIconUrl from '../icons/icon/visible.svg' +import warningTriangleIconUrl from '../icons/icon/warning-triangle.svg' +import zoomInIconUrl from '../icons/icon/zoom-in.svg' +import zoomOutIconUrl from '../icons/icon/zoom-out.svg' +import arTranslationUrl from '../translations/ar.json' +import caTranslationUrl from '../translations/ca.json' +import daTranslationUrl from '../translations/da.json' +import deTranslationUrl from '../translations/de.json' +import enTranslationUrl from '../translations/en.json' +import esTranslationUrl from '../translations/es.json' +import faTranslationUrl from '../translations/fa.json' +import fiTranslationUrl from '../translations/fi.json' +import frTranslationUrl from '../translations/fr.json' +import glTranslationUrl from '../translations/gl.json' +import heTranslationUrl from '../translations/he.json' +import hiInTranslationUrl from '../translations/hi-in.json' +import huTranslationUrl from '../translations/hu.json' +import itTranslationUrl from '../translations/it.json' +import jaTranslationUrl from '../translations/ja.json' +import koKrTranslationUrl from '../translations/ko-kr.json' +import kuTranslationUrl from '../translations/ku.json' +import languagesTranslationUrl from '../translations/languages.json' +import mainTranslationUrl from '../translations/main.json' +import myTranslationUrl from '../translations/my.json' +import neTranslationUrl from '../translations/ne.json' +import noTranslationUrl from '../translations/no.json' +import plTranslationUrl from '../translations/pl.json' +import ptBrTranslationUrl from '../translations/pt-br.json' +import ptPtTranslationUrl from '../translations/pt-pt.json' +import roTranslationUrl from '../translations/ro.json' +import ruTranslationUrl from '../translations/ru.json' +import svTranslationUrl from '../translations/sv.json' +import teTranslationUrl from '../translations/te.json' +import thTranslationUrl from '../translations/th.json' +import trTranslationUrl from '../translations/tr.json' +import ukTranslationUrl from '../translations/uk.json' +import viTranslationUrl from '../translations/vi.json' +import zhCnTranslationUrl from '../translations/zh-cn.json' +import zhTwTranslationUrl from '../translations/zh-tw.json' +import { AssetUrlOptions, formatAssetUrl } from './utils' + +/** @public */ +export function getBundlerAssetUrls(opts?: AssetUrlOptions) { + return { + fonts: { + monospace: formatAssetUrl(monospaceFontUrl, opts), + sansSerif: formatAssetUrl(sansSerifFontUrl, opts), + serif: formatAssetUrl(serifFontUrl, opts), + draw: formatAssetUrl(drawFontUrl, opts), + }, + icons: { + 'align-bottom-center': formatAssetUrl(alignBottomCenterIconUrl, opts), + 'align-bottom-left': formatAssetUrl(alignBottomLeftIconUrl, opts), + 'align-bottom-right': formatAssetUrl(alignBottomRightIconUrl, opts), + 'align-bottom': formatAssetUrl(alignBottomIconUrl, opts), + 'align-center-center': formatAssetUrl(alignCenterCenterIconUrl, opts), + 'align-center-horizontal': formatAssetUrl(alignCenterHorizontalIconUrl, opts), + 'align-center-left': formatAssetUrl(alignCenterLeftIconUrl, opts), + 'align-center-right': formatAssetUrl(alignCenterRightIconUrl, opts), + 'align-center-vertical': formatAssetUrl(alignCenterVerticalIconUrl, opts), + 'align-left': formatAssetUrl(alignLeftIconUrl, opts), + 'align-right': formatAssetUrl(alignRightIconUrl, opts), + 'align-top-center': formatAssetUrl(alignTopCenterIconUrl, opts), + 'align-top-left': formatAssetUrl(alignTopLeftIconUrl, opts), + 'align-top-right': formatAssetUrl(alignTopRightIconUrl, opts), + 'align-top': formatAssetUrl(alignTopIconUrl, opts), + 'arrow-left': formatAssetUrl(arrowLeftIconUrl, opts), + 'arrowhead-arrow': formatAssetUrl(arrowheadArrowIconUrl, opts), + 'arrowhead-bar': formatAssetUrl(arrowheadBarIconUrl, opts), + 'arrowhead-diamond': formatAssetUrl(arrowheadDiamondIconUrl, opts), + 'arrowhead-dot': formatAssetUrl(arrowheadDotIconUrl, opts), + 'arrowhead-none': formatAssetUrl(arrowheadNoneIconUrl, opts), + 'arrowhead-square': formatAssetUrl(arrowheadSquareIconUrl, opts), + 'arrowhead-triangle-inverted': formatAssetUrl(arrowheadTriangleInvertedIconUrl, opts), + 'arrowhead-triangle': formatAssetUrl(arrowheadTriangleIconUrl, opts), + 'aspect-ratio': formatAssetUrl(aspectRatioIconUrl, opts), + avatar: formatAssetUrl(avatarIconUrl, opts), + blob: formatAssetUrl(blobIconUrl, opts), + 'bring-forward': formatAssetUrl(bringForwardIconUrl, opts), + 'bring-to-front': formatAssetUrl(bringToFrontIconUrl, opts), + check: formatAssetUrl(checkIconUrl, opts), + 'checkbox-checked': formatAssetUrl(checkboxCheckedIconUrl, opts), + 'checkbox-empty': formatAssetUrl(checkboxEmptyIconUrl, opts), + 'chevron-down': formatAssetUrl(chevronDownIconUrl, opts), + 'chevron-left': formatAssetUrl(chevronLeftIconUrl, opts), + 'chevron-right': formatAssetUrl(chevronRightIconUrl, opts), + 'chevron-up': formatAssetUrl(chevronUpIconUrl, opts), + 'chevrons-ne': formatAssetUrl(chevronsNeIconUrl, opts), + 'chevrons-sw': formatAssetUrl(chevronsSwIconUrl, opts), + 'clipboard-copy': formatAssetUrl(clipboardCopyIconUrl, opts), + code: formatAssetUrl(codeIconUrl, opts), + collab: formatAssetUrl(collabIconUrl, opts), + color: formatAssetUrl(colorIconUrl, opts), + comment: formatAssetUrl(commentIconUrl, opts), + 'cross-2': formatAssetUrl(cross2IconUrl, opts), + cross: formatAssetUrl(crossIconUrl, opts), + 'dash-dashed': formatAssetUrl(dashDashedIconUrl, opts), + 'dash-dotted': formatAssetUrl(dashDottedIconUrl, opts), + 'dash-draw': formatAssetUrl(dashDrawIconUrl, opts), + 'dash-solid': formatAssetUrl(dashSolidIconUrl, opts), + discord: formatAssetUrl(discordIconUrl, opts), + 'distribute-horizontal': formatAssetUrl(distributeHorizontalIconUrl, opts), + 'distribute-vertical': formatAssetUrl(distributeVerticalIconUrl, opts), + dot: formatAssetUrl(dotIconUrl, opts), + 'dots-horizontal': formatAssetUrl(dotsHorizontalIconUrl, opts), + 'dots-vertical': formatAssetUrl(dotsVerticalIconUrl, opts), + 'drag-handle-dots': formatAssetUrl(dragHandleDotsIconUrl, opts), + duplicate: formatAssetUrl(duplicateIconUrl, opts), + edit: formatAssetUrl(editIconUrl, opts), + 'external-link': formatAssetUrl(externalLinkIconUrl, opts), + file: formatAssetUrl(fileIconUrl, opts), + 'fill-none': formatAssetUrl(fillNoneIconUrl, opts), + 'fill-pattern': formatAssetUrl(fillPatternIconUrl, opts), + 'fill-semi': formatAssetUrl(fillSemiIconUrl, opts), + 'fill-solid': formatAssetUrl(fillSolidIconUrl, opts), + follow: formatAssetUrl(followIconUrl, opts), + following: formatAssetUrl(followingIconUrl, opts), + 'font-draw': formatAssetUrl(fontDrawIconUrl, opts), + 'font-mono': formatAssetUrl(fontMonoIconUrl, opts), + 'font-sans': formatAssetUrl(fontSansIconUrl, opts), + 'font-serif': formatAssetUrl(fontSerifIconUrl, opts), + 'geo-arrow-down': formatAssetUrl(geoArrowDownIconUrl, opts), + 'geo-arrow-left': formatAssetUrl(geoArrowLeftIconUrl, opts), + 'geo-arrow-right': formatAssetUrl(geoArrowRightIconUrl, opts), + 'geo-arrow-up': formatAssetUrl(geoArrowUpIconUrl, opts), + 'geo-diamond': formatAssetUrl(geoDiamondIconUrl, opts), + 'geo-ellipse': formatAssetUrl(geoEllipseIconUrl, opts), + 'geo-hexagon': formatAssetUrl(geoHexagonIconUrl, opts), + 'geo-octagon': formatAssetUrl(geoOctagonIconUrl, opts), + 'geo-oval': formatAssetUrl(geoOvalIconUrl, opts), + 'geo-pentagon': formatAssetUrl(geoPentagonIconUrl, opts), + 'geo-rectangle': formatAssetUrl(geoRectangleIconUrl, opts), + 'geo-rhombus-2': formatAssetUrl(geoRhombus2IconUrl, opts), + 'geo-rhombus': formatAssetUrl(geoRhombusIconUrl, opts), + 'geo-star': formatAssetUrl(geoStarIconUrl, opts), + 'geo-trapezoid': formatAssetUrl(geoTrapezoidIconUrl, opts), + 'geo-triangle': formatAssetUrl(geoTriangleIconUrl, opts), + 'geo-x-box': formatAssetUrl(geoXBoxIconUrl, opts), + github: formatAssetUrl(githubIconUrl, opts), + group: formatAssetUrl(groupIconUrl, opts), + hidden: formatAssetUrl(hiddenIconUrl, opts), + image: formatAssetUrl(imageIconUrl, opts), + 'info-circle': formatAssetUrl(infoCircleIconUrl, opts), + leading: formatAssetUrl(leadingIconUrl, opts), + link: formatAssetUrl(linkIconUrl, opts), + 'lock-small': formatAssetUrl(lockSmallIconUrl, opts), + lock: formatAssetUrl(lockIconUrl, opts), + menu: formatAssetUrl(menuIconUrl, opts), + minus: formatAssetUrl(minusIconUrl, opts), + mixed: formatAssetUrl(mixedIconUrl, opts), + pack: formatAssetUrl(packIconUrl, opts), + page: formatAssetUrl(pageIconUrl, opts), + plus: formatAssetUrl(plusIconUrl, opts), + 'question-mark-circle': formatAssetUrl(questionMarkCircleIconUrl, opts), + 'question-mark': formatAssetUrl(questionMarkIconUrl, opts), + redo: formatAssetUrl(redoIconUrl, opts), + 'reset-zoom': formatAssetUrl(resetZoomIconUrl, opts), + 'rotate-ccw': formatAssetUrl(rotateCcwIconUrl, opts), + 'rotate-cw': formatAssetUrl(rotateCwIconUrl, opts), + ruler: formatAssetUrl(rulerIconUrl, opts), + search: formatAssetUrl(searchIconUrl, opts), + 'send-backward': formatAssetUrl(sendBackwardIconUrl, opts), + 'send-to-back': formatAssetUrl(sendToBackIconUrl, opts), + 'settings-horizontal': formatAssetUrl(settingsHorizontalIconUrl, opts), + 'settings-vertical-1': formatAssetUrl(settingsVertical1IconUrl, opts), + 'settings-vertical': formatAssetUrl(settingsVerticalIconUrl, opts), + 'share-1': formatAssetUrl(share1IconUrl, opts), + 'share-2': formatAssetUrl(share2IconUrl, opts), + 'size-extra-large': formatAssetUrl(sizeExtraLargeIconUrl, opts), + 'size-large': formatAssetUrl(sizeLargeIconUrl, opts), + 'size-medium': formatAssetUrl(sizeMediumIconUrl, opts), + 'size-small': formatAssetUrl(sizeSmallIconUrl, opts), + 'spline-cubic': formatAssetUrl(splineCubicIconUrl, opts), + 'spline-line': formatAssetUrl(splineLineIconUrl, opts), + 'stack-horizontal': formatAssetUrl(stackHorizontalIconUrl, opts), + 'stack-vertical': formatAssetUrl(stackVerticalIconUrl, opts), + 'stretch-horizontal': formatAssetUrl(stretchHorizontalIconUrl, opts), + 'stretch-vertical': formatAssetUrl(stretchVerticalIconUrl, opts), + 'text-align-center': formatAssetUrl(textAlignCenterIconUrl, opts), + 'text-align-justify': formatAssetUrl(textAlignJustifyIconUrl, opts), + 'text-align-left': formatAssetUrl(textAlignLeftIconUrl, opts), + 'text-align-right': formatAssetUrl(textAlignRightIconUrl, opts), + 'tool-arrow': formatAssetUrl(toolArrowIconUrl, opts), + 'tool-embed': formatAssetUrl(toolEmbedIconUrl, opts), + 'tool-eraser': formatAssetUrl(toolEraserIconUrl, opts), + 'tool-frame': formatAssetUrl(toolFrameIconUrl, opts), + 'tool-hand': formatAssetUrl(toolHandIconUrl, opts), + 'tool-highlighter': formatAssetUrl(toolHighlighterIconUrl, opts), + 'tool-line': formatAssetUrl(toolLineIconUrl, opts), + 'tool-media': formatAssetUrl(toolMediaIconUrl, opts), + 'tool-note': formatAssetUrl(toolNoteIconUrl, opts), + 'tool-pencil': formatAssetUrl(toolPencilIconUrl, opts), + 'tool-pointer': formatAssetUrl(toolPointerIconUrl, opts), + 'tool-text': formatAssetUrl(toolTextIconUrl, opts), + trash: formatAssetUrl(trashIconUrl, opts), + 'triangle-down': formatAssetUrl(triangleDownIconUrl, opts), + 'triangle-up': formatAssetUrl(triangleUpIconUrl, opts), + twitter: formatAssetUrl(twitterIconUrl, opts), + undo: formatAssetUrl(undoIconUrl, opts), + ungroup: formatAssetUrl(ungroupIconUrl, opts), + 'unlock-small': formatAssetUrl(unlockSmallIconUrl, opts), + unlock: formatAssetUrl(unlockIconUrl, opts), + visible: formatAssetUrl(visibleIconUrl, opts), + 'warning-triangle': formatAssetUrl(warningTriangleIconUrl, opts), + 'zoom-in': formatAssetUrl(zoomInIconUrl, opts), + 'zoom-out': formatAssetUrl(zoomOutIconUrl, opts), + }, + translations: { + ar: formatAssetUrl(arTranslationUrl, opts), + ca: formatAssetUrl(caTranslationUrl, opts), + da: formatAssetUrl(daTranslationUrl, opts), + de: formatAssetUrl(deTranslationUrl, opts), + en: formatAssetUrl(enTranslationUrl, opts), + es: formatAssetUrl(esTranslationUrl, opts), + fa: formatAssetUrl(faTranslationUrl, opts), + fi: formatAssetUrl(fiTranslationUrl, opts), + fr: formatAssetUrl(frTranslationUrl, opts), + gl: formatAssetUrl(glTranslationUrl, opts), + he: formatAssetUrl(heTranslationUrl, opts), + 'hi-in': formatAssetUrl(hiInTranslationUrl, opts), + hu: formatAssetUrl(huTranslationUrl, opts), + it: formatAssetUrl(itTranslationUrl, opts), + ja: formatAssetUrl(jaTranslationUrl, opts), + 'ko-kr': formatAssetUrl(koKrTranslationUrl, opts), + ku: formatAssetUrl(kuTranslationUrl, opts), + languages: formatAssetUrl(languagesTranslationUrl, opts), + main: formatAssetUrl(mainTranslationUrl, opts), + my: formatAssetUrl(myTranslationUrl, opts), + ne: formatAssetUrl(neTranslationUrl, opts), + no: formatAssetUrl(noTranslationUrl, opts), + pl: formatAssetUrl(plTranslationUrl, opts), + 'pt-br': formatAssetUrl(ptBrTranslationUrl, opts), + 'pt-pt': formatAssetUrl(ptPtTranslationUrl, opts), + ro: formatAssetUrl(roTranslationUrl, opts), + ru: formatAssetUrl(ruTranslationUrl, opts), + sv: formatAssetUrl(svTranslationUrl, opts), + te: formatAssetUrl(teTranslationUrl, opts), + th: formatAssetUrl(thTranslationUrl, opts), + tr: formatAssetUrl(trTranslationUrl, opts), + uk: formatAssetUrl(ukTranslationUrl, opts), + vi: formatAssetUrl(viTranslationUrl, opts), + 'zh-cn': formatAssetUrl(zhCnTranslationUrl, opts), + 'zh-tw': formatAssetUrl(zhTwTranslationUrl, opts), + }, + embedIcons: { + codepen: formatAssetUrl(codepenEmbedIconUrl, opts), + codesandbox: formatAssetUrl(codesandboxEmbedIconUrl, opts), + excalidraw: formatAssetUrl(excalidrawEmbedIconUrl, opts), + felt: formatAssetUrl(feltEmbedIconUrl, opts), + figma: formatAssetUrl(figmaEmbedIconUrl, opts), + github_gist: formatAssetUrl(githubGistEmbedIconUrl, opts), + google_calendar: formatAssetUrl(googleCalendarEmbedIconUrl, opts), + google_maps: formatAssetUrl(googleMapsEmbedIconUrl, opts), + google_slides: formatAssetUrl(googleSlidesEmbedIconUrl, opts), + observable: formatAssetUrl(observableEmbedIconUrl, opts), + replit: formatAssetUrl(replitEmbedIconUrl, opts), + scratch: formatAssetUrl(scratchEmbedIconUrl, opts), + spotify: formatAssetUrl(spotifyEmbedIconUrl, opts), + tldraw: formatAssetUrl(tldrawEmbedIconUrl, opts), + vimeo: formatAssetUrl(vimeoEmbedIconUrl, opts), + youtube: formatAssetUrl(youtubeEmbedIconUrl, opts), + }, + } as const +} diff --git a/packages/assets/src/utils.ts b/packages/assets/src/utils.ts new file mode 100644 index 000000000..565e37d56 --- /dev/null +++ b/packages/assets/src/utils.ts @@ -0,0 +1,16 @@ +/** @public */ +export type AssetUrl = string | { src: string } + +/** @public */ +export type AssetUrlOptions = { + baseUrl?: string +} + +/** @public */ +export function formatAssetUrl(assetUrl: AssetUrl, { baseUrl = '' }: AssetUrlOptions = {}): string { + const assetUrlString = typeof assetUrl === 'string' ? assetUrl : assetUrl.src + + if (assetUrlString.startsWith('data:')) return assetUrlString + + return `${baseUrl.replace(/\/$/, '')}/${assetUrlString.replace(/^\.?\//, '')}` +} diff --git a/packages/assets/tsconfig.json b/packages/assets/tsconfig.json new file mode 100644 index 000000000..e650c33c9 --- /dev/null +++ b/packages/assets/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../config/tsconfig.base.json", + "include": ["src", "modules.d.ts"], + "exclude": ["node_modules", "dist", ".tsbuild*"], + "compilerOptions": { + "outDir": "./.tsbuild", + "rootDir": "src", + "resolveJsonModule": false + }, + "references": [{ "path": "../utils" }] +} diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md new file mode 100644 index 000000000..f5e138346 --- /dev/null +++ b/packages/editor/CHANGELOG.md @@ -0,0 +1,288 @@ +# v2.0.0-alpha.12 (Mon Apr 03 2023) + +#### 🐛 Bug Fix + +- [fix] Start on page 1 when importing from v1 [#1589](https://github.com/tldraw/tldraw-lite/pull/1589) ([@steveruizok](https://github.com/steveruizok)) +- [fix] Arrow rebinding in v1 imports [#1588](https://github.com/tldraw/tldraw-lite/pull/1588) ([@steveruizok](https://github.com/steveruizok)) +- Move resizing to the correct place. [#1579](https://github.com/tldraw/tldraw-lite/pull/1579) ([@MitjaBezensek](https://github.com/MitjaBezensek)) +- [fix] use masked page bounds for finding drop parent [#1564](https://github.com/tldraw/tldraw-lite/pull/1564) ([@steveruizok](https://github.com/steveruizok)) +- Revert "[fix] text jump bug" [#1566](https://github.com/tldraw/tldraw-lite/pull/1566) ([@ds300](https://github.com/ds300)) +- [improvement] select shapes on paste [#1565](https://github.com/tldraw/tldraw-lite/pull/1565) ([@steveruizok](https://github.com/steveruizok)) +- Fix to `setPenMode` to `false` when `this._touchEventsRemainingBeforeExitingPenMode` reaches zero [#1541](https://github.com/tldraw/tldraw-lite/pull/1541) ([@orangemug](https://github.com/orangemug)) +- [fix] text jump bug [#1555](https://github.com/tldraw/tldraw-lite/pull/1555) ([@ds300](https://github.com/ds300)) +- Add proper messaging & import flows for migration from local & multiplayer rooms [#1506](https://github.com/tldraw/tldraw-lite/pull/1506) ([@SomeHats](https://github.com/SomeHats) [@steveruizok](https://github.com/steveruizok)) +- fix errors when migrating extremely large v1 rooms or rooms with funky data [#1553](https://github.com/tldraw/tldraw-lite/pull/1553) ([@SomeHats](https://github.com/SomeHats)) +- Fix an error when we have an empty group. [#1549](https://github.com/tldraw/tldraw-lite/pull/1549) ([@MitjaBezensek](https://github.com/MitjaBezensek)) +- Make sure all types and build stuff get run in CI [#1548](https://github.com/tldraw/tldraw-lite/pull/1548) ([@SomeHats](https://github.com/SomeHats)) +- make sure error annotations can't throw [#1550](https://github.com/tldraw/tldraw-lite/pull/1550) ([@SomeHats](https://github.com/SomeHats)) +- [fix] Prevent unwanted offsets when embedding tldraw in scrollable page [#1551](https://github.com/tldraw/tldraw-lite/pull/1551) ([@ds300](https://github.com/ds300)) +- Fix an error with importing certain files. [#1547](https://github.com/tldraw/tldraw-lite/pull/1547) ([@MitjaBezensek](https://github.com/MitjaBezensek)) +- [fix] simplify draw shape's outline [#1537](https://github.com/tldraw/tldraw-lite/pull/1537) ([@steveruizok](https://github.com/steveruizok)) +- [fix] simplify line shape's outline [#1536](https://github.com/tldraw/tldraw-lite/pull/1536) ([@steveruizok](https://github.com/steveruizok)) +- [feature] `App.canMoveCamera` [#1543](https://github.com/tldraw/tldraw-lite/pull/1543) ([@steveruizok](https://github.com/steveruizok)) +- Fix the migration of ovals, size was not correct. [#1544](https://github.com/tldraw/tldraw-lite/pull/1544) ([@MitjaBezensek](https://github.com/MitjaBezensek)) +- An attempt to fix text selection on chrome/android [#1452](https://github.com/tldraw/tldraw-lite/pull/1452) ([@orangemug](https://github.com/orangemug) [@steveruizok](https://github.com/steveruizok)) +- run v1 migrations when rebuilding v1 doc [#1534](https://github.com/tldraw/tldraw-lite/pull/1534) ([@SomeHats](https://github.com/SomeHats)) +- add pre-commit api report generation [#1517](https://github.com/tldraw/tldraw-lite/pull/1517) ([@SomeHats](https://github.com/SomeHats)) +- Migrate assets to v2 storage [#1520](https://github.com/tldraw/tldraw-lite/pull/1520) ([@SomeHats](https://github.com/SomeHats)) +- [improvement] restore snap to center [#1529](https://github.com/tldraw/tldraw-lite/pull/1529) ([@steveruizok](https://github.com/steveruizok)) +- Rename some methods [#1528](https://github.com/tldraw/tldraw-lite/pull/1528) ([@steveruizok](https://github.com/steveruizok)) +- [ux] Don't select draw shapes when you use the draw tool [#1527](https://github.com/tldraw/tldraw-lite/pull/1527) ([@steveruizok](https://github.com/steveruizok)) +- [fix] brush while pinch zooming [#1526](https://github.com/tldraw/tldraw-lite/pull/1526) ([@steveruizok](https://github.com/steveruizok)) +- [fix] Don't let changing screen bounds be undoable [#1525](https://github.com/tldraw/tldraw-lite/pull/1525) ([@steveruizok](https://github.com/steveruizok)) +- [tweak] Center camera on shape in new page [#1522](https://github.com/tldraw/tldraw-lite/pull/1522) ([@steveruizok](https://github.com/steveruizok)) +- [fix] clear editing shape id when window loses focus [#1523](https://github.com/tldraw/tldraw-lite/pull/1523) ([@steveruizok](https://github.com/steveruizok)) +- Fix splitting of chars for wide UTF-8 characters [#1501](https://github.com/tldraw/tldraw-lite/pull/1501) ([@orangemug](https://github.com/orangemug)) +- Don't use previous opacity for new `bookmark`/`embed` shapes [#1510](https://github.com/tldraw/tldraw-lite/pull/1510) ([@orangemug](https://github.com/orangemug)) +- Fix back to content button. [#1519](https://github.com/tldraw/tldraw-lite/pull/1519) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok)) +- Allow migration of readonly rooms. [#1498](https://github.com/tldraw/tldraw-lite/pull/1498) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok)) +- [chore] restore api extractor [#1500](https://github.com/tldraw/tldraw-lite/pull/1500) ([@steveruizok](https://github.com/steveruizok)) +- Asset loading overhaul [#1457](https://github.com/tldraw/tldraw-lite/pull/1457) ([@SomeHats](https://github.com/SomeHats)) +- [improvement] docs / api cleanup [#1491](https://github.com/tldraw/tldraw-lite/pull/1491) ([@steveruizok](https://github.com/steveruizok)) +- David/publish good [#1488](https://github.com/tldraw/tldraw-lite/pull/1488) ([@ds300](https://github.com/ds300)) +- [improvement] mobile docs [#1487](https://github.com/tldraw/tldraw-lite/pull/1487) ([@steveruizok](https://github.com/steveruizok)) +- [chore] alpha 10 [#1486](https://github.com/tldraw/tldraw-lite/pull/1486) ([@ds300](https://github.com/ds300)) +- [chore] package build improvements [#1484](https://github.com/tldraw/tldraw-lite/pull/1484) ([@ds300](https://github.com/ds300)) +- [chore] bump for alpha 8 [#1485](https://github.com/tldraw/tldraw-lite/pull/1485) ([@steveruizok](https://github.com/steveruizok)) +- [fix] page point offset [#1483](https://github.com/tldraw/tldraw-lite/pull/1483) ([@steveruizok](https://github.com/steveruizok)) +- [improvement] API Reference docs [#1478](https://github.com/tldraw/tldraw-lite/pull/1478) ([@steveruizok](https://github.com/steveruizok)) +- stop using broken-af turbo for publishing [#1476](https://github.com/tldraw/tldraw-lite/pull/1476) ([@ds300](https://github.com/ds300)) +- [chore] add canary release script [#1423](https://github.com/tldraw/tldraw-lite/pull/1423) ([@ds300](https://github.com/ds300) [@steveruizok](https://github.com/steveruizok)) +- [fix] missing fonts in exports [#1468](https://github.com/tldraw/tldraw-lite/pull/1468) ([@steveruizok](https://github.com/steveruizok)) +- [temp] no preload icons [#1466](https://github.com/tldraw/tldraw-lite/pull/1466) ([@steveruizok](https://github.com/steveruizok)) +- [fix] crash with frames [#1465](https://github.com/tldraw/tldraw-lite/pull/1465) ([@steveruizok](https://github.com/steveruizok)) +- Removed incorrect width recalc in text label for geo shapes [#1396](https://github.com/tldraw/tldraw-lite/pull/1396) ([@orangemug](https://github.com/orangemug) [@steveruizok](https://github.com/steveruizok)) +- derive currentToolId from app.root [#1459](https://github.com/tldraw/tldraw-lite/pull/1459) ([@ds300](https://github.com/ds300) [@steveruizok](https://github.com/steveruizok)) +- Convert multiple spaces in export by converting to nbsp [#1419](https://github.com/tldraw/tldraw-lite/pull/1419) ([@orangemug](https://github.com/orangemug) [@TodePond](https://github.com/TodePond) [@steveruizok](https://github.com/steveruizok)) +- Always file->print with light-mode enabled [#1315](https://github.com/tldraw/tldraw-lite/pull/1315) ([@orangemug](https://github.com/orangemug) [@steveruizok](https://github.com/steveruizok)) +- [chore] export frameutil [#1461](https://github.com/tldraw/tldraw-lite/pull/1461) ([@steveruizok](https://github.com/steveruizok)) +- [chore] upgrade yarn [#1430](https://github.com/tldraw/tldraw-lite/pull/1430) ([@ds300](https://github.com/ds300)) +- Added `preserveAspectRatio` to print for overflow of content [#1453](https://github.com/tldraw/tldraw-lite/pull/1453) ([@orangemug](https://github.com/orangemug)) +- Fixed throttle of `updateBounds` in `useScreenBounds` [#1442](https://github.com/tldraw/tldraw-lite/pull/1442) ([@orangemug](https://github.com/orangemug) [@steveruizok](https://github.com/steveruizok)) +- [update] docs [#1448](https://github.com/tldraw/tldraw-lite/pull/1448) ([@steveruizok](https://github.com/steveruizok)) +- Always paste images with opactiy=1 [#1444](https://github.com/tldraw/tldraw-lite/pull/1444) ([@orangemug](https://github.com/orangemug) [@steveruizok](https://github.com/steveruizok)) +- [improvement] Wrap `buildFromV1Document` in transact [#1435](https://github.com/tldraw/tldraw-lite/pull/1435) ([@steveruizok](https://github.com/steveruizok)) +- Hack around the outline cache for rendering x-box shapes [#1438](https://github.com/tldraw/tldraw-lite/pull/1438) ([@orangemug](https://github.com/orangemug) [@steveruizok](https://github.com/steveruizok)) +- [fix] dev version number for tldraw/tldraw [#1434](https://github.com/tldraw/tldraw-lite/pull/1434) ([@steveruizok](https://github.com/steveruizok)) +- repo cleanup [#1426](https://github.com/tldraw/tldraw-lite/pull/1426) ([@steveruizok](https://github.com/steveruizok)) +- Vscode extension [#1253](https://github.com/tldraw/tldraw-lite/pull/1253) ([@steveruizok](https://github.com/steveruizok) [@MitjaBezensek](https://github.com/MitjaBezensek) [@orangemug](https://github.com/orangemug)) +- [fix] use polyfill for `structuredClone` [#1408](https://github.com/tldraw/tldraw-lite/pull/1408) ([@TodePond](https://github.com/TodePond) [@steveruizok](https://github.com/steveruizok)) +- Run all the tests. Fix linting for tests. [#1389](https://github.com/tldraw/tldraw-lite/pull/1389) ([@MitjaBezensek](https://github.com/MitjaBezensek)) +- Fix an issue with loading v1 draw shapes that don't have any points. [#1404](https://github.com/tldraw/tldraw-lite/pull/1404) ([@MitjaBezensek](https://github.com/MitjaBezensek) [@steveruizok](https://github.com/steveruizok)) + +#### ⚠️ Pushed to `main` + +- Revert "update tldraw's bounds" ([@steveruizok](https://github.com/steveruizok)) +- update tldraw's bounds ([@steveruizok](https://github.com/steveruizok)) + +#### Authors: 6 + +- alex ([@SomeHats](https://github.com/SomeHats)) +- David Sheldrick ([@ds300](https://github.com/ds300)) +- Lu[ke] Wilson ([@TodePond](https://github.com/TodePond)) +- Mitja Bezenšek ([@MitjaBezensek](https://github.com/MitjaBezensek)) +- Orange Mug ([@orangemug](https://github.com/orangemug)) +- Steve Ruiz ([@steveruizok](https://github.com/steveruizok)) + +--- + +# @tldraw/tldraw-beta + +## 2.0.0-alpha.11 + +### Patch Changes + +- fix some package build scripting +- Updated dependencies + - @tldraw/primitives@2.0.0-alpha.11 + - @tldraw/tlschema@2.0.0-alpha.11 + - @tldraw/tlstore@2.0.0-alpha.11 + - @tldraw/tlvalidate@2.0.0-alpha.10 + - @tldraw/utils@2.0.0-alpha.10 + +## 2.0.0-alpha.10 + +### Patch Changes + +- Updated dependencies [4b4399b6e] + - @tldraw/primitives@2.0.0-alpha.10 + - @tldraw/tlschema@2.0.0-alpha.10 + - @tldraw/tlstore@2.0.0-alpha.10 + - @tldraw/tlvalidate@2.0.0-alpha.9 + - @tldraw/utils@2.0.0-alpha.9 + +## 2.0.0-alpha.9 + +### Patch Changes + +- Release day! +- Updated dependencies + - @tldraw/primitives@2.0.0-alpha.9 + - @tldraw/tlschema@2.0.0-alpha.9 + - @tldraw/tlstore@2.0.0-alpha.9 + - @tldraw/tlvalidate@2.0.0-alpha.8 + - @tldraw/utils@2.0.0-alpha.8 + +## 2.0.0-alpha.8 + +### Patch Changes + +- 23dd81cfe: Make signia a peer dependency +- Updated dependencies [23dd81cfe] + - @tldraw/tlstore@2.0.0-alpha.8 + - @tldraw/tlschema@2.0.0-alpha.8 + - @tldraw/primitives@2.0.0-alpha.8 + +## 2.0.0-alpha.7 + +### Patch Changes + +- Bug fixes. +- Updated dependencies + - @tldraw/primitives@2.0.0-alpha.7 + - @tldraw/tlschema@2.0.0-alpha.7 + - @tldraw/tlstore@2.0.0-alpha.7 + - @tldraw/tlvalidate@2.0.0-alpha.7 + - @tldraw/utils@2.0.0-alpha.7 + +## 2.0.0-alpha.6 + +### Patch Changes + +- Add licenses. +- Updated dependencies + - @tldraw/primitives@2.0.0-alpha.6 + - @tldraw/tlschema@2.0.0-alpha.6 + - @tldraw/tlstore@2.0.0-alpha.6 + - @tldraw/tlvalidate@2.0.0-alpha.6 + - @tldraw/utils@2.0.0-alpha.6 + +## 2.0.0-alpha.5 + +### Patch Changes + +- Add CSS files to tldraw/tldraw. +- Updated dependencies + - @tldraw/primitives@2.0.0-alpha.5 + - @tldraw/tlschema@2.0.0-alpha.5 + - @tldraw/tlstore@2.0.0-alpha.5 + - @tldraw/tlvalidate@2.0.0-alpha.5 + - @tldraw/utils@2.0.0-alpha.5 + +## 2.0.0-alpha.4 + +### Patch Changes + +- Add children to tldraw/tldraw +- Updated dependencies + - @tldraw/primitives@2.0.0-alpha.4 + - @tldraw/tlschema@2.0.0-alpha.4 + - @tldraw/tlstore@2.0.0-alpha.4 + - @tldraw/tlvalidate@2.0.0-alpha.4 + - @tldraw/utils@2.0.0-alpha.4 + +## 2.0.0-alpha.3 + +### Patch Changes + +- Change permissions. +- Updated dependencies + - @tldraw/primitives@2.0.0-alpha.3 + - @tldraw/tlschema@2.0.0-alpha.3 + - @tldraw/tlstore@2.0.0-alpha.3 + - @tldraw/tlvalidate@2.0.0-alpha.3 + - @tldraw/utils@2.0.0-alpha.3 + +## 2.0.0-alpha.2 + +### Patch Changes + +- Add tldraw, editor +- Updated dependencies + - @tldraw/primitives@2.0.0-alpha.2 + - @tldraw/tlschema@2.0.0-alpha.2 + - @tldraw/tlstore@2.0.0-alpha.2 + - @tldraw/tlvalidate@2.0.0-alpha.2 + - @tldraw/utils@2.0.0-alpha.2 + +## 0.1.0-alpha.11 + +### Patch Changes + +- Fix stale reactors. +- Updated dependencies + - @tldraw/primitives@0.1.0-alpha.11 + - @tldraw/tlschema@0.1.0-alpha.11 + - @tldraw/tlstore@0.1.0-alpha.11 + - @tldraw/tlvalidate@0.1.0-alpha.11 + - @tldraw/utils@0.1.0-alpha.11 + +## 0.1.0-alpha.10 + +### Patch Changes + +- Fix type export bug. +- Updated dependencies + - @tldraw/primitives@0.1.0-alpha.10 + - @tldraw/tlschema@0.1.0-alpha.10 + - @tldraw/tlstore@0.1.0-alpha.10 + - @tldraw/tlvalidate@0.1.0-alpha.10 + - @tldraw/utils@0.1.0-alpha.10 + +## 0.1.0-alpha.9 + +### Patch Changes + +- Fix import bugs. +- Updated dependencies + - @tldraw/primitives@0.1.0-alpha.9 + - @tldraw/tlschema@0.1.0-alpha.9 + - @tldraw/tlstore@0.1.0-alpha.9 + - @tldraw/tlvalidate@0.1.0-alpha.9 + - @tldraw/utils@0.1.0-alpha.9 + +## 0.1.0-alpha.8 + +### Patch Changes + +- Changes validation requirements, exports validation helpers. +- Updated dependencies + - @tldraw/primitives@0.1.0-alpha.8 + - @tldraw/tlschema@0.1.0-alpha.8 + - @tldraw/tlstore@0.1.0-alpha.8 + - @tldraw/tlvalidate@0.1.0-alpha.8 + - @tldraw/utils@0.1.0-alpha.8 + +## 0.1.0-alpha.7 + +### Patch Changes + +- - Pre-pre-release update +- Updated dependencies + - @tldraw/primitives@0.1.0-alpha.7 + - @tldraw/tlschema@0.1.0-alpha.7 + - @tldraw/tlstore@0.1.0-alpha.7 + - @tldraw/tlvalidate@0.1.0-alpha.7 + - @tldraw/utils@0.1.0-alpha.7 + +## 0.0.2-alpha.1 + +### Patch Changes + +- Fix error with HMR +- Updated dependencies + - @tldraw/primitives@0.0.2-alpha.1 + - @tldraw/tlschema@0.0.2-alpha.1 + - @tldraw/tlstore@0.0.2-alpha.1 + - @tldraw/utils@0.0.2-alpha.1 + +## 0.0.2-alpha.0 + +### Patch Changes + +- Initial release +- Updated dependencies + - @tldraw/primitives@0.0.2-alpha.0 + - @tldraw/tlschema@0.0.2-alpha.0 + - @tldraw/tlstore@0.0.2-alpha.0 + - @tldraw/utils@0.0.2-alpha.0 diff --git a/packages/editor/LICENSE b/packages/editor/LICENSE new file mode 100644 index 000000000..4f227c380 --- /dev/null +++ b/packages/editor/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2023 tldraw GB Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/editor/README.md b/packages/editor/README.md new file mode 100644 index 000000000..d5c9dae2b --- /dev/null +++ b/packages/editor/README.md @@ -0,0 +1,15 @@ +# tldraw/tldraw + +## Benchmark + +To run the benchmarks + +``` +yarn workspace @tldraw/tldraw benchmark +``` + +Or + +``` +yarn workspace @tldraw/tldraw benchmark "file_search_string" +``` diff --git a/packages/editor/api-extractor.json b/packages/editor/api-extractor.json new file mode 100644 index 000000000..f1ed80e93 --- /dev/null +++ b/packages/editor/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../config/api-extractor.json" +} diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md new file mode 100644 index 000000000..a55f93ff0 --- /dev/null +++ b/packages/editor/api-report.md @@ -0,0 +1,2691 @@ +## API Report File for "@tldraw/editor" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Atom } from 'signia'; +import { Box2d } from '@tldraw/primitives'; +import { Box2dModel } from '@tldraw/tlschema'; +import { Computed } from 'signia'; +import { ComputedCache } from '@tldraw/tlstore'; +import { CubicSpline2d } from '@tldraw/primitives'; +import { EASINGS } from '@tldraw/primitives'; +import { EmbedDefinition } from '@tldraw/tlschema'; +import { EventEmitter } from 'eventemitter3'; +import { getHashForString } from '@tldraw/utils'; +import { HistoryEntry } from '@tldraw/tlstore'; +import { ID } from '@tldraw/tlstore'; +import { MatLike } from '@tldraw/primitives'; +import { Matrix2d } from '@tldraw/primitives'; +import { Matrix2dModel } from '@tldraw/primitives'; +import { Migrations } from '@tldraw/tlstore'; +import { Polyline2d } from '@tldraw/primitives'; +import * as React_2 from 'react'; +import { default as React_3 } from 'react'; +import { RecordType } from '@tldraw/tlstore'; +import { RotateCorner } from '@tldraw/primitives'; +import { SelectionCorner } from '@tldraw/primitives'; +import { SelectionEdge } from '@tldraw/primitives'; +import { SelectionHandle } from '@tldraw/primitives'; +import { SerializedSchema } from '@tldraw/tlstore'; +import { StoreSchema } from '@tldraw/tlstore'; +import { StoreSnapshot } from '@tldraw/tlstore'; +import { StoreValidator } from '@tldraw/tlstore'; +import { StrokePoint } from '@tldraw/primitives'; +import { TLAlignType } from '@tldraw/tlschema'; +import { TLArrowheadType } from '@tldraw/tlschema'; +import { TLArrowShape } from '@tldraw/tlschema'; +import { TLAsset } from '@tldraw/tlschema'; +import { TLAssetId } from '@tldraw/tlschema'; +import { TLAssetPartial } from '@tldraw/tlschema'; +import { TLBaseShape } from '@tldraw/tlschema'; +import { TLBookmarkAsset } from '@tldraw/tlschema'; +import { TLBookmarkShape } from '@tldraw/tlschema'; +import { TLCamera } from '@tldraw/tlschema'; +import { TLColorStyle } from '@tldraw/tlschema'; +import { TLColorType } from '@tldraw/tlschema'; +import { TLCursor } from '@tldraw/tlschema'; +import { TLDocument } from '@tldraw/tlschema'; +import { TLDrawShape } from '@tldraw/tlschema'; +import { TLDrawShapeSegment } from '@tldraw/tlschema'; +import { TLEmbedShape } from '@tldraw/tlschema'; +import { TLFontType } from '@tldraw/tlschema'; +import { TLFrameShape } from '@tldraw/tlschema'; +import { TLGeoShape } from '@tldraw/tlschema'; +import { TLGroupShape } from '@tldraw/tlschema'; +import { TLHandle } from '@tldraw/tlschema'; +import { TLImageAsset } from '@tldraw/tlschema'; +import { TLImageShape } from '@tldraw/tlschema'; +import { TLInstance } from '@tldraw/tlschema'; +import { TLInstanceId } from '@tldraw/tlschema'; +import { TLInstancePageState } from '@tldraw/tlschema'; +import { TLInstancePropsForNextShape } from '@tldraw/tlschema'; +import { TLLineShape } from '@tldraw/tlschema'; +import { TLNoteShape } from '@tldraw/tlschema'; +import { TLNullableShapeProps } from '@tldraw/tlschema'; +import { TLPage } from '@tldraw/tlschema'; +import { TLPageId } from '@tldraw/tlschema'; +import { TLParentId } from '@tldraw/tlschema'; +import { TLRecord } from '@tldraw/tlschema'; +import { TLScribble } from '@tldraw/tlschema'; +import { TLShape } from '@tldraw/tlschema'; +import { TLShapeId } from '@tldraw/tlschema'; +import { TLShapePartial } from '@tldraw/tlschema'; +import { TLShapeProp } from '@tldraw/tlschema'; +import { TLShapeProps } from '@tldraw/tlschema'; +import { TLShapeType } from '@tldraw/tlschema'; +import { TLSizeStyle } from '@tldraw/tlschema'; +import { TLSizeType } from '@tldraw/tlschema'; +import { TLStore } from '@tldraw/tlschema'; +import { TLStoreProps } from '@tldraw/tlschema'; +import { TLStyleCollections } from '@tldraw/tlschema'; +import { TLStyleType } from '@tldraw/tlschema'; +import { TLTextShape } from '@tldraw/tlschema'; +import { TLTextShapeProps } from '@tldraw/tlschema'; +import { TLUnknownShape } from '@tldraw/tlschema'; +import { TLUser } from '@tldraw/tlschema'; +import { TLUserDocument } from '@tldraw/tlschema'; +import { TLUserId } from '@tldraw/tlschema'; +import { TLUserPresence } from '@tldraw/tlschema'; +import { TLVideoAsset } from '@tldraw/tlschema'; +import { TLVideoShape } from '@tldraw/tlschema'; +import { Vec2d } from '@tldraw/primitives'; +import { Vec2dModel } from '@tldraw/tlschema'; +import { VecLike } from '@tldraw/primitives'; + +// @public (undocumented) +export const ACCEPTED_ASSET_TYPE: string; + +// @public (undocumented) +export const ACCEPTED_IMG_TYPE: string[]; + +// @public (undocumented) +export const ACCEPTED_VID_TYPE: string[]; + +// @internal (undocumented) +export const ANIMATION_MEDIUM_MS = 320; + +// @internal (undocumented) +export const ANIMATION_SHORT_MS = 80; + +// @public (undocumented) +export type AnimationOptions = Partial<{ + duration: number; + easing: typeof EASINGS.easeInOutCubic; +}>; + +// @public (undocumented) +export class App extends EventEmitter { + constructor({ config, store, getContainer }: AppOptions); + alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this; + get allShapesCommonBounds(): Box2d | null; + animateCamera(x: number, y: number, z?: number, opts?: AnimationOptions): this; + animateShapes(partials: (null | TLShapePartial | undefined)[], options?: { + duration?: number; + ease?: (t: number) => number; + }): this; + // (undocumented) + animateToShape(shapeId: TLShapeId, opts?: AnimationOptions): this; + // @internal (undocumented) + annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: { + origin: string; + willCrashApp: boolean; + tags?: Record; + extras?: Record; + }): void; + get assets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[]; + bail(): this; + bailToMark(id: string): this; + batch(fn: () => void): this; + blur(): this; + bringForward(ids?: TLShapeId[]): this; + bringToFront(ids?: TLShapeId[]): this; + // (undocumented) + get brush(): Box2dModel | null; + get camera(): TLCamera; + cancel(): this; + cancelDoubleClick(): void; + get canMoveCamera(): boolean; + set canMoveCamera(canMove: boolean); + get canRedo(): boolean; + get canUndo(): boolean; + centerOnPoint(x: number, y: number, opts?: AnimationOptions): this; + // @internal + protected _clickManager: ClickManager; + complete(): this; + readonly config: TldrawEditorConfig; + // @internal (undocumented) + crash(error: unknown): void; + // @internal + get crashingError(): unknown; + createAssets(assets: TLAsset[]): this; + // @internal (undocumented) + createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): { + tags: { + origin: string; + willCrashApp: 'unknown' | boolean; + }; + extras: { + activeStateNode?: string; + selectedShapes?: TLUnknownShape[]; + editingShape?: TLUnknownShape; + inputs?: Record; + }; + }; + createPage(title: string, id?: TLPageId, belowPageIndex?: string): this; + createShapeId(id?: string): TLShapeId; + createShapes(partials: TLShapePartial[], select?: boolean): this; + get croppingId(): null | TLShapeId; + get cullingBounds(): Box2d; + // @internal (undocumented) + readonly _cullingBounds: Atom; + get cullingBoundsExpanded(): Box2d; + // @internal (undocumented) + readonly _cullingBoundsExpanded: Atom; + get currentPage(): TLPage; + get currentPageId(): TLPageId; + get currentToolId(): string; + // (undocumented) + get cursor(): TLCursor; + deleteAssets(ids: TLAssetId[]): this; + deletePage(id: TLPageId): void; + deleteShapes(ids?: TLShapeId[]): this; + deselect(...ids: TLShapeId[]): this; + // (undocumented) + get devicePixelRatio(): number; + dispatch: (info: TLEventInfo) => this; + readonly disposables: Set<() => void>; + dispose(): void; + distributeShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this; + get documentSettings(): TLDocument; + // (undocumented) + duplicatePage(id?: TLPageId, createId?: TLPageId): void; + duplicateShapes(ids?: TLShapeId[], offset?: VecLike): this; + get editingId(): null | TLShapeId; + // (undocumented) + get editingShape(): null | TLUnknownShape; + get erasingIds(): TLShapeId[]; + get erasingIdsSet(): Set; + findAncestor(shape: TLShape, predicate: (parent: TLShape) => boolean): TLShape | undefined; + findCommonAncestor(shapes: TLShape[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined; + flipShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this; + focus(): this; + // (undocumented) + get focusLayerId(): TLPageId | TLShapeId; + // (undocumented) + get focusLayerShape(): TLShape | undefined; + getAncestors(shape: TLShape, acc?: TLShape[]): TLShape[]; + getAncestorsById(id: TLShapeId, acc?: TLShape[]): TLShape[]; + getArrowsBoundTo(shapeId: TLShapeId): { + arrowId: TLShapeId; + handleId: "end" | "start"; + }[]; + getAssetById(id: TLAssetId): TLAsset | undefined; + getAssetBySrc(src: string): TLBookmarkAsset | TLImageAsset | TLVideoAsset | undefined; + getBounds(shape: TLShape): Box2d; + getBoundsById(id: TLShapeId): Box2d | undefined; + getClipPathById(id: TLShapeId): string | undefined; + getContainer: () => HTMLElement; + // (undocumented) + getContent(ids?: TLShapeId[]): TLClipboardModel | undefined; + getCssColor(id: TLColorStyle['id']): string; + getDeltaInParentSpace(shape: TLShape, delta: VecLike): Vec2d; + getDeltaInShapeSpace(shape: TLShape, delta: VecLike): Vec2d; + // (undocumented) + getDroppingShape(point: VecLike, droppingShapes?: TLShape[]): TLUnknownShape | undefined; + // (undocumented) + getHighestIndexForParent(parentId: TLPageId | TLShapeId): string; + getMaskedPageBounds(shape: TLShape): Box2d | undefined; + getMaskedPageBoundsById(id: TLShapeId): Box2d | undefined; + // (undocumented) + getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape; + getOutline(shape: TLShape): Vec2dModel[]; + getOutlineById(id: TLShapeId): Vec2dModel[]; + getPageBounds(shape: TLShape): Box2d | undefined; + getPageBoundsById(id: TLShapeId): Box2d | undefined; + getPageById(id: TLPage['id']): TLPage | undefined; + getPageCenter(shape: TLShape): null | Vec2d; + getPageCenterById(id: TLShapeId): null | Vec2d; + getPageCorners(shape: TLShape): Vec2d[]; + getPageInfoById(id: TLPage['id']): TLPage | undefined; + getPageMaskById(id: TLShapeId): undefined | VecLike[]; + getPagePointById(id: TLShapeId): undefined | Vec2d; + getPageRotation(shape: TLShape): number; + getPageRotationById(id: TLShapeId): number; + getPageStateByPageId(id: TLPageId): TLInstancePageState | undefined; + getPageTransform(shape: TLShape): Matrix2d | undefined; + getPageTransformById(id: TLShapeId): Matrix2d | undefined; + // (undocumented) + getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShapeType): TLPageId | TLShapeId; + getParentPageId(shape?: TLShape): TLPageId | undefined; + getParentShape(shape?: TLShape): TLShape | undefined; + getParentsMappedToChildren(ids: TLShapeId[]): Map>; + getParentTransform(shape: TLShape): Matrix2d; + getPointInParentSpace(shapeId: TLShapeId, point: VecLike): Vec2d; + getPointInShapeSpace(shape: TLShape, point: VecLike): Vec2d; + getShapeById(id: TLParentId): T | undefined; + // (undocumented) + getShapesAndDescendantsInOrder(ids: TLShapeId[]): TLShape[]; + getShapesAtPoint(point: VecLike): TLShape[]; + getShapesInPage(pageId: TLPageId): TLShape[]; + getShapeUtil(shape: T): TLShapeUtil; + getShapeUtilByDef>(def: Def): ReturnType; + getSortedChildIds(parentId: TLParentId): TLShapeId[]; + getStateDescendant(path: string): StateNode | undefined; + getStrokeWidth(id: TLSizeStyle['id']): number; + // (undocumented) + getSvg(ids?: TLShapeId[], opts?: Partial<{ + scale: number; + background: boolean; + padding: number; + darkMode?: boolean | undefined; + preserveAspectRatio: React.SVGAttributes['preserveAspectRatio']; + }>): Promise; + getTransform(shape: TLShape): Matrix2d; + // (undocumented) + get gridSize(): number; + // (undocumented) + groupShapes(ids?: TLShapeId[], groupId?: TLShapeId): this; + hasAncestor(shape: TLShape | undefined, ancestorId: TLShapeId): boolean; + get hintingIds(): TLShapeId[]; + readonly history: HistoryManager; + // (undocumented) + get hoveredId(): null | TLShapeId; + // (undocumented) + get hoveredShape(): null | TLUnknownShape; + inputs: { + originPagePoint: Vec2d; + originScreenPoint: Vec2d; + previousPagePoint: Vec2d; + previousScreenPoint: Vec2d; + currentPagePoint: Vec2d; + currentScreenPoint: Vec2d; + keys: Set; + buttons: Set; + isPen: boolean; + shiftKey: boolean; + ctrlKey: boolean; + altKey: boolean; + isDragging: boolean; + isPointing: boolean; + isPinching: boolean; + isEditing: boolean; + isPanning: boolean; + pointerVelocity: Vec2d; + }; + get instanceId(): TLInstanceId; + get instanceState(): TLInstance; + interrupt(): this; + get isChangingStyle(): boolean; + set isChangingStyle(v: boolean); + readonly isChromeForIos: boolean; + get isCoarsePointer(): boolean; + set isCoarsePointer(v: boolean); + get isFocused(): boolean; + // (undocumented) + get isGridMode(): boolean; + isIn(path: string): boolean; + isInAny(...paths: string[]): boolean; + readonly isIos: boolean; + get isMenuOpen(): boolean; + // (undocumented) + get isPenMode(): boolean; + isPointInShape(point: VecLike, shape: TLShape): boolean; + // (undocumented) + get isReadOnly(): boolean; + readonly isSafari: boolean; + isSelected(id: TLShapeId): boolean; + isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean; + isShapeInViewport(id: TLShapeId): boolean; + isWithinSelection(id: TLShapeId): boolean; + // (undocumented) + lockShapes(_ids?: TLShapeId[]): this; + mark(reason?: string, onUndo?: boolean, onRedo?: boolean): string; + moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this; + nudgeShapes(ids: TLShapeId[], direction: Vec2dModel, major?: boolean, ephemeral?: boolean): this; + onCreateAssetFromFile(file: File): Promise; + onCreateBookmarkFromUrl(url: string): Promise<{ + image: string; + title: string; + description: string; + }>; + get onlySelectedShape(): TLBaseShape | null; + openMenus: Set; + packShapes(ids?: TLShapeId[], padding?: number): this; + get pages(): TLPage[]; + get pageState(): TLInstancePageState; + pageToScreen(x: number, y: number, z?: number, camera?: Vec2dModel): { + x: number; + y: number; + z: number; + }; + pan(dx: number, dy: number, opts?: AnimationOptions): this; + panZoomIntoView(ids: TLShapeId[], opts?: AnimationOptions): this; + // (undocumented) + popFocusLayer(): this; + // @internal + get props(): null | TLNullableShapeProps; + // (undocumented) + putContent(content: TLClipboardModel, options?: { + point?: VecLike; + select?: boolean; + preservePosition?: boolean; + preserveIds?: boolean; + }): this; + redo(): this; + renamePage(id: TLPageId, name: string, squashing?: boolean): this; + get renderingShapes(): { + id: TLShapeId; + index: number; + opacity: number; + isCulled: boolean; + isInViewport: boolean; + }[]; + reorderShapes(operation: TLReorderOperation, ids: TLShapeId[]): this; + reparentShapesById(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this; + // (undocumented) + replaceStoreContentsWithRecordsForOtherDocument(records: TLRecord[]): void; + resetZoom(point?: Vec2d, opts?: AnimationOptions): this; + // (undocumented) + resizeShape(id: TLShapeId, scale: VecLike, options?: { + initialBounds?: Box2d; + scaleOrigin?: VecLike; + scaleAxisRotation?: number; + initialShape?: TLShape; + initialPageTransform?: MatLike; + dragHandle?: TLResizeHandle; + mode?: TLResizeMode; + }): this; + readonly root: RootState; + rotateShapesBy(ids: TLShapeId[], delta: number): this; + screenToPage(x: number, y: number, z?: number, camera?: Vec2dModel): { + x: number; + y: number; + z: number; + }; + // (undocumented) + get scribble(): null | TLScribble; + select(...ids: TLShapeId[]): this; + selectAll(): this; + get selectedIds(): TLShapeId[]; + get selectedIdsSet(): ReadonlySet; + get selectedPageBounds(): Box2d | null; + get selectedShapes(): TLBaseShape[]; + // (undocumented) + get selectionBounds(): Box2d | undefined; + // (undocumented) + get selectionPageCenter(): null | Vec2d; + get selectionRotation(): number; + selectNone(): this; + sendBackward(ids?: TLShapeId[]): this; + sendToBack(ids?: TLShapeId[]): this; + setBrush(brush?: Box2dModel | null): this; + setCamera(x: number, y: number, z?: number, { stopFollowing }?: ViewportOptions): this; + // (undocumented) + setCroppingId(id: null | TLShapeId): this; + setCurrentPageId(pageId: TLPageId, { stopFollowing }?: ViewportOptions): this; + setCursor(cursor: Partial): this; + // (undocumented) + setDarkMode(isDarkMode: boolean): void; + setEditingId(id: null | TLShapeId): this; + setErasingIds(ids?: TLShapeId[]): this; + setFocusLayer(next: null | TLShapeId): this; + // (undocumented) + setGridMode(isGridMode: boolean): void; + setHintingIds(ids: TLShapeId[]): this; + setHoveredId(id?: null | TLShapeId): this; + setInstancePageState(partial: Partial, ephemeral?: boolean): void; + // (undocumented) + setPenMode(isPenMode: boolean): void; + setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this; + // (undocumented) + setReadOnly(isReadOnly: boolean): void; + setScribble(scribble?: null | TLScribble): this; + setSelectedIds(ids: TLShapeId[], squashing?: boolean): this; + setSelectedTool(id: string, info?: {}): this; + setZoomBrush(zoomBrush?: Box2dModel | null): this; + get shapeIds(): Set; + get shapesArray(): TLShape[]; + shapeUtils: { + readonly [K in string]?: TLShapeUtil; + }; + // (undocumented) + slideCamera(opts?: { + speed: number; + direction: Vec2d; + friction: number; + speedThreshold?: number | undefined; + }): this; + readonly snaps: SnapManager; + get sortedShapesArray(): TLShape[]; + stackShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[], gap?: number): this; + startFollowingUser: (userId: TLUserId) => this | undefined; + stopCameraAnimation(): this; + stopFollowingUser: () => this; + readonly store: TLStore; + stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this; + static styles: TLStyleCollections; + textMeasure: TextManager; + undo(): HistoryManager; + // (undocumented) + ungroupShapes(ids?: TLShapeId[]): this; + updateAssets(assets: TLAssetPartial[]): this; + // @internal + updateCullingBounds(): this; + updateInstanceState(partial: Partial>, ephemeral?: boolean, squashing?: boolean): this; + updatePage(partial: RequiredKeys, squashing?: boolean): this; + updateShapes(partials: (null | TLShapePartial | undefined)[], squashing?: boolean): this; + updateUser(partial: Partial): void; + updateUserDocumentSettings(partial: Partial, ephemeral?: boolean): this; + // (undocumented) + updateUserPresence: ({ cursor, color, viewportPageBounds, }?: { + cursor?: undefined | Vec2dModel; + color?: string | undefined; + viewportPageBounds?: Box2dModel | undefined; + }) => void; + updateViewportScreenBounds(center?: boolean): this; + get user(): TLUser; + // (undocumented) + get userDocumentSettings(): TLUserDocument; + get userId(): TLUserId; + // (undocumented) + get userPresence(): TLUserPresence | undefined; + get userSettings(): TLUser; + get viewportPageBounds(): Box2d; + get viewportPageCenter(): Vec2d; + get viewportScreenBounds(): Box2d; + get viewportScreenCenter(): Vec2d; + visitDescendants(parentId: TLParentId, visitor: (id: TLShapeId) => false | void): void; + // (undocumented) + get zoomBrush(): Box2dModel | null; + zoomIn(point?: Vec2d, opts?: AnimationOptions): this; + get zoomLevel(): number; + zoomOut(point?: Vec2d, opts?: AnimationOptions): this; + zoomToBounds(x: number, y: number, width: number, height: number, targetZoom?: number, opts?: AnimationOptions): this; + zoomToFit(opts?: AnimationOptions): this; + zoomToSelection(opts?: AnimationOptions): this; +} + +// @internal (undocumented) +export function applyRotationToSnapshotShapes({ delta, app, snapshot, stage, }: { + delta: number; + snapshot: RotationSnapshot; + app: App; + stage: 'end' | 'one-off' | 'start' | 'update'; +}): void; + +// @public (undocumented) +export interface AppOptions { + config?: TldrawEditorConfig; + getContainer: () => HTMLElement; + instanceId?: TLInstanceId; + store: TLStore; + userId?: TLUserId; +} + +// @public (undocumented) +export const ARROW_LABEL_FONT_SIZES: Record; + +// @public (undocumented) +export function blobAsString(blob: Blob): Promise; + +// @internal (undocumented) +export const BOUND_ARROW_OFFSET = 10; + +// @internal (undocumented) +export function buildFromV1Document(app: App, document: LegacyTldrawDocument): void; + +// @public (undocumented) +export const Canvas: React_2.MemoExoticComponent<({ onDropOverride, }: { + onDropOverride?: ((defaultOnDrop: (e: React_2.DragEvent) => Promise) => (e: React_2.DragEvent) => Promise) | undefined; +}) => JSX.Element>; + +// @public (undocumented) +export const checkFlag: (flag: (() => boolean) | boolean | undefined) => boolean | undefined; + +// @public (undocumented) +export type ClipboardPayload = { + data: string; + kind: 'file'; + type: 'application/tldraw'; +} | { + data: string; + kind: 'text'; + type: 'application/tldraw'; +} | { + data: TLClipboardModel; + kind: 'content'; + type: 'application/tldraw'; +}; + +// @public +export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight; + +// @public (undocumented) +export function correctSpacesToNbsp(input: string): string; + +// @public (undocumented) +export function createAssetShapeAtPoint(app: App, svgString: string, point: Vec2dModel): Promise; + +// @public +export function createBookmarkShapeAtPoint(app: App, url: string, point: Vec2dModel): Promise; + +// @public (undocumented) +export function createEmbedShapeAtPoint(app: App, url: string, point: Vec2dModel, props: { + width?: number; + height?: number; + doesResize?: boolean; +}): void; + +// @public (undocumented) +export function createShapesFromFiles(app: App, files: File[], position: VecLike, _ignoreParent?: boolean): Promise; + +// @public (undocumented) +export function dataTransferItemAsString(item: DataTransferItem): Promise; + +// @public (undocumented) +export function dataUrlToFile(url: string, filename: string, mimeType: string): Promise; + +// @internal (undocumented) +export const debugFlags: { + preventDefaultLogging: Atom; + pointerCaptureLogging: Atom; + pointerCaptureTracking: Atom; + pointerCaptureTrackingObject: Atom, unknown>; + elementRemovalLogging: Atom; + debugSvg: Atom; + throwToBlob: Atom; + peopleMenu: Atom; + logMessages: Atom; + resetConnectionEveryPing: Atom; +}; + +// @internal (undocumented) +export const DEFAULT_ANIMATION_OPTIONS: { + duration: number; + easing: (t: number) => number; +}; + +// @internal (undocumented) +export const DEFAULT_BOOKMARK_HEIGHT = 320; + +// @internal (undocumented) +export const DEFAULT_BOOKMARK_WIDTH = 300; + +// @public (undocumented) +export const defaultEditorAssetUrls: EditorAssetUrls; + +// @public (undocumented) +export function defaultEmptyAs(str: string, dflt: string): string; + +// @internal (undocumented) +export const DefaultErrorFallback: TLErrorFallback; + +// @public (undocumented) +export function defineShape = TLShapeUtil>({ type, getShapeUtil, validator, migrations, }: { + type: ShapeType['type']; + getShapeUtil: () => TLShapeUtilConstructor; + validator?: StoreValidator; + migrations?: Migrations; +}): TLShapeDef; + +// @internal (undocumented) +export const DOUBLE_CLICK_DURATION = 450; + +// @public (undocumented) +export function downloadDataURLAsFile(dataUrl: string, filename: string): void; + +// @internal (undocumented) +export const DRAG_DISTANCE = 4; + +// @public (undocumented) +export type EditorAssetUrls = { + fonts: { + monospace: string; + serif: string; + sansSerif: string; + draw: string; + }; +}; + +// @public (undocumented) +export type EmbedResult = { + definition: EmbedDefinition; + url: string; + embedUrl: string; +} | undefined; + +// @public (undocumented) +export class ErrorBoundary extends React_2.Component>, ErrorBoundaryState> { + // (undocumented) + componentDidCatch(error: unknown): void; + // (undocumented) + static getDerivedStateFromError(error: Error): { + error: Error; + }; + // (undocumented) + render(): React_2.ReactNode; + // (undocumented) + state: ErrorBoundaryState; +} + +// @public (undocumented) +export interface ErrorBoundaryProps { + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + fallback: (error: unknown) => React_2.ReactNode; + // (undocumented) + onError?: ((error: unknown) => void) | null; +} + +// @public (undocumented) +export function ErrorScreen({ children }: { + children: any; +}): JSX.Element; + +// @public (undocumented) +export interface ErrorSyncedStore { + // (undocumented) + readonly error: Error; + // (undocumented) + readonly status: 'error'; + // (undocumented) + readonly store?: undefined; +} + +// @public (undocumented) +export const EVENT_NAME_MAP: Record, keyof TLEventHandlers>; + +// @public +export function fileToBase64(file: Blob): Promise; + +// @public (undocumented) +export const FONT_ALIGNMENT: Record; + +// @public (undocumented) +export const FONT_FAMILIES: Record; + +// @public (undocumented) +export const FONT_SIZES: Record; + +// @public +export function getEmbedInfo(inputUrl: string): EmbedResult; + +// @public +export function getEmbedInfoUnsafely(inputUrl: string): EmbedResult; + +// @public +export function getFileMetaData(file: File): Promise<{ + isAnimated: boolean; +}>; + +export { getHashForString } + +// @public +export function getImageSizeFromSrc(dataURL: string): Promise<{ + w: number; + h: number; +}>; + +// @public +export function getIncrementedName(name: string, others: string[]): string; + +// @public (undocumented) +export function getIndexAbove(below: string): string; + +// @public (undocumented) +export function getIndexBelow(above: string): string; + +// @public (undocumented) +export function getIndexBetween(below: string, above?: string): string; + +// @public (undocumented) +export function getIndexGenerator(): () => string; + +// @public (undocumented) +export function getIndices(n: number): string[]; + +// @public (undocumented) +export function getIndicesAbove(below: string, n: number): string[]; + +// @public (undocumented) +export function getIndicesBelow(above: string, n: number): string[]; + +// @public (undocumented) +export function getIndicesBetween(below: string | undefined, above: string | undefined, n: number): string[]; + +// @public (undocumented) +export function getMaxIndex(...indices: (string | undefined)[]): string; + +// @public +export function getMediaAssetFromFile(file: File): Promise; + +// @internal (undocumented) +export function getPointerInfo(e: PointerEvent | React.PointerEvent, container: HTMLElement): { + point: { + x: number; + y: number; + z: number; + }; + shiftKey: boolean; + altKey: boolean; + ctrlKey: boolean; + pointerId: number; + button: number; + isPen: boolean; +}; + +// @public +export function getResizedImageDataUrl(dataURLForImage: string, width: number, height: number): Promise; + +// @internal (undocumented) +export function getRotationSnapshot({ app }: { + app: App; +}): { + selectionPageCenter: Vec2d; + initialCursorAngle: number; + initialSelectionRotation: number; + shapeSnapshots: { + shape: TLBaseShape; + initialPagePoint: Vec2d; + }[]; +}; + +// @public (undocumented) +export function getSplineForLineShape(shape: TLLineShape): NonNullable; + +// @public (undocumented) +export function getSvgAsDataUrl(svg: SVGElement): Promise; + +// @public (undocumented) +export function getSvgAsDataUrlSync(node: SVGElement): string; + +// @public (undocumented) +export function getSvgAsImage(svg: SVGElement, options: { + type: TLCopyType | TLExportType; + quality: number; + scale: number; +}): Promise; + +// @public (undocumented) +export function getSvgAsString(svg: SVGElement): string; + +// @public +export function getSvgPathFromStroke(points: Vec2d[], closed?: boolean): string; + +// @public +export function getSvgPathFromStrokePoints(points: StrokePoint[], closed?: boolean): string; + +// @public (undocumented) +export function getTextBoundingBox(text: SVGTextElement): DOMRect; + +// @public (undocumented) +export const getValidHttpURLList: (url: string) => string[] | undefined; + +// @public +export function getVideoSizeFromSrc(src: string): Promise<{ + w: number; + h: number; +}>; + +// @internal (undocumented) +export const GRID_INCREMENT = 5; + +// @public (undocumented) +export const GRID_STEPS: { + min: number; + mid: number; + step: number; +}[]; + +// @internal (undocumented) +export const HAND_TOOL_FRICTION = 0.09; + +// @public (undocumented) +export function hardResetApp(): void; + +// @internal (undocumented) +export const HASH_PATERN_ZOOM_NAMES: Record; + +// @public (undocumented) +export function HTMLContainer({ children, className, ...rest }: HTMLContainerProps): JSX.Element; + +// @public (undocumented) +export type HTMLContainerProps = React_2.HTMLAttributes; + +// @public (undocumented) +export const ICON_SIZES: Record; + +// @public (undocumented) +export function indexGenerator(n?: number): Generator; + +// @public (undocumented) +export interface InitializingSyncedStore { + // (undocumented) + readonly error?: undefined; + // (undocumented) + readonly status: 'loading'; + // (undocumented) + readonly store?: undefined; +} + +// @public +export function isAnimated(buffer: ArrayBuffer): boolean; + +// @public (undocumented) +export function isGeoShape(shape: TLShape): shape is TLGeoShape; + +// @public +export function isGIF(buffer: ArrayBuffer): boolean; + +// @public (undocumented) +export const isImage: (ext: string) => boolean; + +// @public (undocumented) +export function isNoteShape(shape: TLShape): shape is TLNoteShape; + +// @public +export function isSerializable(value: any): boolean; + +// @public (undocumented) +export function isShapeWithHandles(shape: TLShape): boolean; + +// @public (undocumented) +export const isSvgText: (text: string) => boolean; + +// @public (undocumented) +export const isValidHttpURL: (url: string) => boolean; + +// @public (undocumented) +export const LABEL_FONT_SIZES: Record; + +// @internal (undocumented) +export interface LegacyTldrawDocument { + // (undocumented) + assets: TDAssets; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + pages: Record; + // (undocumented) + pageStates: Record; + // (undocumented) + version: number; +} + +// @public (undocumented) +export function LoadingScreen({ children }: { + children: any; +}): JSX.Element; + +// @public (undocumented) +export function loopToHtmlElement(elm: Element): HTMLElement; + +// @internal (undocumented) +export const MAJOR_NUDGE_FACTOR = 10; + +// @public (undocumented) +export function matchEmbedUrl(url: string): { + definition: { + readonly type: "codepen"; + readonly title: "Codepen"; + readonly hostnames: readonly ["codepen.io"]; + readonly minWidth: 300; + readonly minHeight: 300; + readonly width: 520; + readonly height: 400; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "codesandbox"; + readonly title: "CodeSandbox"; + readonly hostnames: readonly ["codesandbox.io"]; + readonly minWidth: 300; + readonly minHeight: 300; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "excalidraw"; + readonly title: "Excalidraw"; + readonly hostnames: readonly ["excalidraw.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly isAspectRatioLocked: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "felt"; + readonly title: "Felt"; + readonly hostnames: readonly ["felt.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "figma"; + readonly title: "Figma"; + readonly hostnames: readonly ["figma.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "github_gist"; + readonly title: "GitHub Gist"; + readonly hostnames: readonly ["gist.github.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "google_calendar"; + readonly title: "Google Calendar"; + readonly hostnames: readonly ["calendar.google.*"]; + readonly width: 720; + readonly height: 500; + readonly minWidth: 460; + readonly minHeight: 360; + readonly doesResize: true; + readonly instructionLink: "https://support.google.com/calendar/answer/41207?hl=en"; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "google_maps"; + readonly title: "Google Maps"; + readonly hostnames: readonly ["google.*"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "google_slides"; + readonly title: "Google Slides"; + readonly hostnames: readonly ["docs.google.*"]; + readonly width: 720; + readonly height: 500; + readonly minWidth: 460; + readonly minHeight: 360; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "observable"; + readonly title: "Observable"; + readonly hostnames: readonly ["observablehq.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly isAspectRatioLocked: false; + readonly backgroundColor: "#fff"; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "replit"; + readonly title: "Replit"; + readonly hostnames: readonly ["replit.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "scratch"; + readonly title: "Scratch"; + readonly hostnames: readonly ["scratch.mit.edu"]; + readonly width: 520; + readonly height: 400; + readonly doesResize: false; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "spotify"; + readonly title: "Spotify"; + readonly hostnames: readonly ["open.spotify.com"]; + readonly width: 720; + readonly height: 500; + readonly minHeight: 500; + readonly overrideOutlineRadius: 12; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "tldraw"; + readonly title: "tldraw"; + readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"]; + readonly minWidth: 300; + readonly minHeight: 300; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "vimeo"; + readonly title: "Vimeo"; + readonly hostnames: readonly ["vimeo.com", "player.vimeo.com"]; + readonly width: 640; + readonly height: 360; + readonly doesResize: true; + readonly isAspectRatioLocked: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "youtube"; + readonly title: "YouTube"; + readonly hostnames: readonly ["*.youtube.com", "youtube.com", "youtu.be"]; + readonly width: 800; + readonly height: 450; + readonly doesResize: true; + readonly overridePermissions: { + readonly 'allow-presentation': true; + }; + readonly isAspectRatioLocked: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + }; + url: string; + embedUrl: string; +} | undefined; + +// @public (undocumented) +export function matchUrl(url: string): { + definition: { + readonly type: "codepen"; + readonly title: "Codepen"; + readonly hostnames: readonly ["codepen.io"]; + readonly minWidth: 300; + readonly minHeight: 300; + readonly width: 520; + readonly height: 400; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "codesandbox"; + readonly title: "CodeSandbox"; + readonly hostnames: readonly ["codesandbox.io"]; + readonly minWidth: 300; + readonly minHeight: 300; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "excalidraw"; + readonly title: "Excalidraw"; + readonly hostnames: readonly ["excalidraw.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly isAspectRatioLocked: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "felt"; + readonly title: "Felt"; + readonly hostnames: readonly ["felt.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "figma"; + readonly title: "Figma"; + readonly hostnames: readonly ["figma.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "github_gist"; + readonly title: "GitHub Gist"; + readonly hostnames: readonly ["gist.github.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "google_calendar"; + readonly title: "Google Calendar"; + readonly hostnames: readonly ["calendar.google.*"]; + readonly width: 720; + readonly height: 500; + readonly minWidth: 460; + readonly minHeight: 360; + readonly doesResize: true; + readonly instructionLink: "https://support.google.com/calendar/answer/41207?hl=en"; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "google_maps"; + readonly title: "Google Maps"; + readonly hostnames: readonly ["google.*"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "google_slides"; + readonly title: "Google Slides"; + readonly hostnames: readonly ["docs.google.*"]; + readonly width: 720; + readonly height: 500; + readonly minWidth: 460; + readonly minHeight: 360; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "observable"; + readonly title: "Observable"; + readonly hostnames: readonly ["observablehq.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly isAspectRatioLocked: false; + readonly backgroundColor: "#fff"; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "replit"; + readonly title: "Replit"; + readonly hostnames: readonly ["replit.com"]; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "scratch"; + readonly title: "Scratch"; + readonly hostnames: readonly ["scratch.mit.edu"]; + readonly width: 520; + readonly height: 400; + readonly doesResize: false; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "spotify"; + readonly title: "Spotify"; + readonly hostnames: readonly ["open.spotify.com"]; + readonly width: 720; + readonly height: 500; + readonly minHeight: 500; + readonly overrideOutlineRadius: 12; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "tldraw"; + readonly title: "tldraw"; + readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"]; + readonly minWidth: 300; + readonly minHeight: 300; + readonly width: 720; + readonly height: 500; + readonly doesResize: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "vimeo"; + readonly title: "Vimeo"; + readonly hostnames: readonly ["vimeo.com", "player.vimeo.com"]; + readonly width: 640; + readonly height: 360; + readonly doesResize: true; + readonly isAspectRatioLocked: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + } | { + readonly type: "youtube"; + readonly title: "YouTube"; + readonly hostnames: readonly ["*.youtube.com", "youtube.com", "youtu.be"]; + readonly width: 800; + readonly height: 450; + readonly doesResize: true; + readonly overridePermissions: { + readonly 'allow-presentation': true; + }; + readonly isAspectRatioLocked: true; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly fromEmbedUrl: (url: string) => string | undefined; + }; + embedUrl: string; + url: string; +} | undefined; + +// @internal (undocumented) +export const MAX_ASSET_HEIGHT = 1000; + +// @internal (undocumented) +export const MAX_ASSET_WIDTH = 1000; + +// @internal (undocumented) +export const MAX_PAGES = 40; + +// @internal (undocumented) +export const MAX_SHAPES_PER_PAGE = 2000; + +// @internal (undocumented) +export const MAX_ZOOM = 8; + +// @internal (undocumented) +export const MIN_ARROW_LENGTH = 48; + +// @internal (undocumented) +export const MIN_ZOOM = 0.1; + +// @internal (undocumented) +export const MINOR_NUDGE_FACTOR = 1; + +// @internal (undocumented) +export const MULTI_CLICK_DURATION = 200; + +// @public (undocumented) +export function normalizeWheel(event: React.WheelEvent | WheelEvent): { + x: number; + y: number; + z: number; +}; + +// @public (undocumented) +export type OnBeforeCreateHandler = (next: T) => T | void; + +// @public (undocumented) +export type OnBeforeUpdateHandler = (prev: T, next: T) => T | void; + +// @public (undocumented) +export type OnBindingChangeHandler = (shape: T) => TLShapePartial | void; + +// @public (undocumented) +export type OnChildrenChangeHandler = (shape: T) => TLShapePartial[] | void; + +// @public (undocumented) +export type OnClickHandler = (shape: T) => TLShapePartial | void; + +// @public (undocumented) +export type OnDoubleClickHandleHandler = (shape: T, handle: TLHandle) => TLShapePartial | void; + +// @public (undocumented) +export type OnDoubleClickHandler = (shape: T) => TLShapePartial | void; + +// @public (undocumented) +export type OnDragHandler = (shape: T, shapes: TLShape[]) => R; + +// @public (undocumented) +export type OnEditEndHandler = (shape: T) => void; + +// @public (undocumented) +export type OnHandleChangeHandler = (shape: T, info: { + handle: TLHandle; + isPrecise: boolean; +}) => TLShapePartial | void; + +// @public (undocumented) +export type OnResizeEndHandler = EventChangeHandler; + +// @public (undocumented) +export type OnResizeHandler = (shape: T, info: { + newPoint: Vec2dModel; + handle: TLResizeHandle; + mode: TLResizeMode; + scaleX: number; + scaleY: number; + initialBounds: Box2d; + initialShape: T; +}) => Partial> | undefined | void; + +// @public (undocumented) +export type OnResizeStartHandler = EventStartHandler; + +// @public (undocumented) +export type OnRotateEndHandler = EventChangeHandler; + +// @public (undocumented) +export type OnRotateHandler = EventChangeHandler; + +// @public (undocumented) +export type OnRotateStartHandler = EventStartHandler; + +// @public (undocumented) +export type OnTranslateEndHandler = EventChangeHandler; + +// @public (undocumented) +export type OnTranslateHandler = EventChangeHandler; + +// @public (undocumented) +export type OnTranslateStartHandler = EventStartHandler; + +// @public (undocumented) +export function openWindow(url: string, target?: string): void; + +// @internal (undocumented) +export function OptionalErrorBoundary({ children, fallback, ...props }: Omit & { + fallback: ((error: unknown) => React_2.ReactNode) | null; +}): JSX.Element; + +// @public +export function preventDefault(event: Event | React_3.BaseSyntheticEvent): void; + +// @public (undocumented) +export interface ReadySyncedStore { + // (undocumented) + readonly error?: undefined; + // (undocumented) + readonly status: 'synced'; + // (undocumented) + readonly store: TLStore; +} + +// @public (undocumented) +export function refreshPage(): void; + +// @public (undocumented) +export function releasePointerCapture(element: Element, event: PointerEvent | React_3.PointerEvent): void; + +// @internal (undocumented) +export const REMOVE_SYMBOL: unique symbol; + +// @public (undocumented) +export type RequiredKeys = Pick & Partial; + +// @internal (undocumented) +export const RICH_TYPES: Record; + +// @public (undocumented) +export function rotateBoxShadow(rotation: number, shadows: { + offsetX: number; + offsetY: number; + blur: number; + spread: number; + color: string; +}[]): string; + +// @public (undocumented) +export const ROTATING_SHADOWS: { + offsetX: number; + offsetY: number; + blur: number; + spread: number; + color: string; +}[]; + +// @internal (undocumented) +export type RotationSnapshot = ReturnType; + +// @public (undocumented) +export const runtime: { + openWindow: (url: string, target: string) => void; + refreshPage: () => void; + hardReset: () => void; +}; + +// @public (undocumented) +export function setPointerCapture(element: Element, event: PointerEvent | React_3.PointerEvent): void; + +// @public (undocumented) +export function setPropsForNextShape(previousProps: TLInstancePropsForNextShape, newProps: Partial): TLInstancePropsForNextShape; + +// @public (undocumented) +export function setRuntimeOverrides(input: Partial): void; + +// @public (undocumented) +export function snapToGrid(n: number, gridSize: number): number; + +// @public (undocumented) +export function sortById(a: T, b: T): -1 | 0 | 1; + +// @public (undocumented) +export function sortByIndex(a: T, b: T): -1 | 0 | 1; + +// @public (undocumented) +export abstract class StateNode implements Partial { + constructor(app: App, parent?: StateNode); + // (undocumented) + app: App; + // (undocumented) + static children?: () => StateNodeConstructor[]; + // (undocumented) + children?: Record; + // (undocumented) + current: Atom; + // (undocumented) + enter(info: any, from: string): void; + // (undocumented) + exit(info: any, from: string): void; + // (undocumented) + handleEvent(info: Exclude): void; + // (undocumented) + static id: string; + // (undocumented) + id: string; + // (undocumented) + static initial?: string; + // (undocumented) + initial?: string; + // (undocumented) + isActive: boolean; + // (undocumented) + onCancel?: TLEventHandlers['onCancel']; + // (undocumented) + onComplete?: TLEventHandlers['onComplete']; + // (undocumented) + onDoubleClick?: TLEventHandlers['onDoubleClick']; + // (undocumented) + onEnter?: UiEnterHandler; + // (undocumented) + onExit?: UiExitHandler; + // (undocumented) + onInterrupt?: TLEventHandlers['onInterrupt']; + // (undocumented) + onKeyDown?: TLEventHandlers['onKeyDown']; + // (undocumented) + onKeyRepeat?: TLEventHandlers['onKeyRepeat']; + // (undocumented) + onKeyUp?: TLEventHandlers['onKeyUp']; + // (undocumented) + onMiddleClick?: TLEventHandlers['onMiddleClick']; + // (undocumented) + onPointerDown?: TLEventHandlers['onPointerDown']; + // (undocumented) + onPointerEnter?: TLEventHandlers['onPointerEnter']; + // (undocumented) + onPointerLeave?: TLEventHandlers['onPointerLeave']; + // (undocumented) + onPointerMove?: TLEventHandlers['onPointerMove']; + // (undocumented) + onPointerUp?: TLEventHandlers['onPointerUp']; + // (undocumented) + onQuadrupleClick?: TLEventHandlers['onQuadrupleClick']; + // (undocumented) + onRightClick?: TLEventHandlers['onRightClick']; + // (undocumented) + onTripleClick?: TLEventHandlers['onTripleClick']; + // (undocumented) + onWheel?: TLEventHandlers['onWheel']; + // (undocumented) + parent: StateNode; + // (undocumented) + path: Computed; + // (undocumented) + readonly styles: TLStyleType[]; + // (undocumented) + transition(id: string, info: any): this; + // (undocumented) + type: StateNodeType; +} + +// @public (undocumented) +export interface StateNodeConstructor { + // (undocumented) + new (app: App, parent?: StateNode): StateNode; + // (undocumented) + children?: () => StateNodeConstructor[]; + // (undocumented) + id: string; + // (undocumented) + initial?: string; + // (undocumented) + styles?: TLStyleType[]; +} + +// @public (undocumented) +export const STYLES: TLStyleCollections; + +// @internal (undocumented) +export const SVG_PADDING = 32; + +// @public (undocumented) +export function SVGContainer({ children, className, ...rest }: SVGContainerProps): JSX.Element; + +// @public (undocumented) +export type SVGContainerProps = React_2.HTMLAttributes; + +// @public (undocumented) +export type SyncedStore = ErrorSyncedStore | InitializingSyncedStore | ReadySyncedStore; + +// @public (undocumented) +export const TEXT_PROPS: { + lineHeight: number; + fontWeight: string; + fontVariant: string; + fontStyle: string; + padding: string; + maxWidth: string; +}; + +// @public (undocumented) +export const TLArrowShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLArrowUtil extends TLShapeUtil { + // (undocumented) + canBind: () => boolean; + // (undocumented) + canEdit: () => boolean; + // (undocumented) + defaultProps(): TLArrowShape['props']; + // (undocumented) + getArrowInfo(shape: TLArrowShape): ArrowInfo | undefined; + // (undocumented) + getBounds(shape: TLArrowShape): Box2d; + // (undocumented) + getCenter(shape: TLArrowShape): Vec2d; + // (undocumented) + getEditingBounds: (shape: TLArrowShape) => Box2d; + // (undocumented) + getHandles(shape: TLArrowShape): TLHandle[]; + // (undocumented) + getLabelBounds(shape: TLArrowShape): Box2d | null; + // (undocumented) + getOutline(shape: TLArrowShape): Vec2dModel[]; + // (undocumented) + getOutlineWithoutLabel(shape: TLArrowShape): VecLike[]; + // (undocumented) + hideResizeHandles: TLShapeUtilFlag; + // (undocumented) + hideRotateHandle: TLShapeUtilFlag; + // (undocumented) + hideSelectionBoundsBg: TLShapeUtilFlag; + // (undocumented) + hideSelectionBoundsFg: TLShapeUtilFlag; + // (undocumented) + hitTestLineSegment(shape: TLArrowShape, A: VecLike, B: VecLike): boolean; + // (undocumented) + hitTestPoint(shape: TLArrowShape, point: VecLike): boolean; + // (undocumented) + indicator(shape: TLArrowShape): JSX.Element | null; + // (undocumented) + isClosed: () => boolean; + // (undocumented) + get labelBoundsCache(): ComputedCache; + // (undocumented) + onDoubleClickHandle: (shape: TLArrowShape, handle: TLHandle) => TLShapePartial | void; + // (undocumented) + onEditEnd: OnEditEndHandler; + // (undocumented) + onHandleChange: OnHandleChangeHandler; + // (undocumented) + onResize: OnResizeHandler; + // (undocumented) + onTranslateStart: OnTranslateStartHandler; + // (undocumented) + render(shape: TLArrowShape): JSX.Element | null; + // (undocumented) + snapPoints(_shape: TLArrowShape): Vec2d[]; + // (undocumented) + toSvg(shape: TLArrowShape, font: string, colors: TLExportColors): SVGGElement; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export interface TLBaseEventInfo { + // (undocumented) + altKey: boolean; + // (undocumented) + ctrlKey: boolean; + // (undocumented) + shiftKey: boolean; + // (undocumented) + type: UiEventType; +} + +// @public (undocumented) +export const TLBookmarkShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLBookmarkUtil extends TLBoxUtil { + // (undocumented) + canResize: () => boolean; + // (undocumented) + defaultProps(): TLBookmarkShape['props']; + // (undocumented) + getHumanReadableAddress(shape: TLBookmarkShape): string; + // (undocumented) + hideSelectionBoundsBg: () => boolean; + // (undocumented) + hideSelectionBoundsFg: () => boolean; + // (undocumented) + indicator(shape: TLBookmarkShape): JSX.Element; + // (undocumented) + onBeforeCreate?: OnBeforeCreateHandler; + // (undocumented) + onBeforeUpdate?: OnBeforeUpdateHandler; + // (undocumented) + render(shape: TLBookmarkShape): JSX.Element; + // (undocumented) + static type: string; + // (undocumented) + protected updateBookmarkAsset: { + (shape: TLBookmarkShape): Promise; + cancel(): void; + }; +} + +// @public (undocumented) +export type TLBoxLike = TLBaseShape; + +// @public (undocumented) +export abstract class TLBoxTool extends StateNode { + // (undocumented) + static children: () => (typeof Idle_4 | typeof Pointing_3)[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + abstract shapeType: string; + // (undocumented) + styles: ("align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "opacity" | "size" | "spline")[]; +} + +// @public (undocumented) +export abstract class TLBoxUtil extends TLShapeUtil { + // (undocumented) + getBounds(shape: Shape): Box2d; + // (undocumented) + getCenter(shape: Shape): Vec2d; + // (undocumented) + getOutline(shape: Shape): Vec2d[]; + // (undocumented) + hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean; + // (undocumented) + hitTestPoint(shape: Shape, point: VecLike): boolean; + // (undocumented) + onResize: OnResizeHandler; +} + +// @public (undocumented) +export type TLCancelEvent = (info: TLCancelEventInfo) => void; + +// @public (undocumented) +export type TLCancelEventInfo = { + type: 'misc'; + name: 'cancel'; +}; + +// @public (undocumented) +export type TLChange = HistoryEntry; + +// @public (undocumented) +export type TLClickEvent = (info: TLClickEventInfo) => void; + +// @public (undocumented) +export type TLClickEventInfo = TLBaseEventInfo & { + type: 'click'; + name: TLCLickEventName; + point: VecLike; + pointerId: number; + button: number; + phase: 'down' | 'settle' | 'up'; +} & TLPointerEventTarget; + +// @public (undocumented) +export type TLCLickEventName = 'double_click' | 'quadruple_click' | 'triple_click'; + +// @public (undocumented) +export interface TLClipboardModel { + // (undocumented) + assets: TLAsset[]; + // (undocumented) + rootShapeIds: TLShapeId[]; + // (undocumented) + schema: SerializedSchema; + // (undocumented) + shapes: TLShape[]; +} + +// @public (undocumented) +export type TLCommand = { + type: 'command'; + id: string; + data: Data; + name: Name; + preservesRedoStack?: boolean; +}; + +// @public (undocumented) +export type TLCommandHandler = { + do: (data: Data) => void; + undo: (data: Data) => void; + redo?: (data: Data) => void; + squash?: (prevData: Data, nextData: Data) => Data; +}; + +// @public (undocumented) +export type TLCompleteEvent = (info: TLCompleteEventInfo) => void; + +// @public (undocumented) +export type TLCompleteEventInfo = { + type: 'misc'; + name: 'complete'; +}; + +// @public (undocumented) +export type TLCopyType = 'jpeg' | 'json' | 'png' | 'svg'; + +// @public (undocumented) +export function TldrawEditor(props: TldrawEditorProps): JSX.Element; + +// @public (undocumented) +export class TldrawEditorConfig { + constructor({ shapes, tools, allowUnknownShapes, }: { + shapes?: readonly TLShapeDef[]; + tools?: readonly StateNodeConstructor[]; + allowUnknownShapes?: boolean; + }); + // (undocumented) + createStore(config: { + initialData?: StoreSnapshot; + userId: TLUserId; + instanceId: TLInstanceId; + }): TLStore; + // (undocumented) + static readonly default: TldrawEditorConfig; + // (undocumented) + readonly shapes: readonly TLUnknownShapeDef[]; + // (undocumented) + readonly storeSchema: StoreSchema; + // (undocumented) + readonly TLShape: RecordType; + // (undocumented) + readonly tools: readonly StateNodeConstructor[]; +} + +// @public (undocumented) +export interface TldrawEditorProps { + assetUrls?: EditorAssetUrls; + autoFocus?: boolean; + // (undocumented) + children?: any; + components?: Partial; + config?: TldrawEditorConfig; + instanceId?: TLInstanceId; + isDarkMode?: boolean; + onCreateAssetFromFile?: (file: File) => Promise; + onCreateBookmarkFromUrl?: (url: string) => Promise<{ + image: string; + title: string; + description: string; + }>; + onMount?: (app: App) => void; + store?: SyncedStore | TLStore; + userId?: TLUserId; +} + +// @public (undocumented) +export const TLDrawShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLDrawUtil extends TLShapeUtil { + // (undocumented) + defaultProps(): TLDrawShape['props']; + // (undocumented) + getBounds(shape: TLDrawShape): Box2d; + // (undocumented) + getCenter(shape: TLDrawShape): Vec2d; + // (undocumented) + getOutline(shape: TLDrawShape): Vec2d[]; + // (undocumented) + hideResizeHandles: (shape: TLDrawShape) => boolean; + // (undocumented) + hideRotateHandle: (shape: TLDrawShape) => boolean; + // (undocumented) + hideSelectionBoundsBg: (shape: TLDrawShape) => boolean; + // (undocumented) + hideSelectionBoundsFg: (shape: TLDrawShape) => boolean; + // (undocumented) + hitTestLineSegment(shape: TLDrawShape, A: VecLike, B: VecLike): boolean; + // (undocumented) + hitTestPoint(shape: TLDrawShape, point: VecLike): boolean; + // (undocumented) + indicator(shape: TLDrawShape): JSX.Element; + // (undocumented) + isClosed: (shape: TLDrawShape) => boolean; + // (undocumented) + onResize: OnResizeHandler; + // (undocumented) + render(shape: TLDrawShape): JSX.Element; + // (undocumented) + toSvg(shape: TLDrawShape, _font: string | undefined, colors: TLExportColors): SVGGElement; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export type TLEasingType = 'easeInCubic' | 'easeInExpo' | 'easeInOutCubic' | 'easeInOutExpo' | 'easeInOutQuad' | 'easeInOutQuart' | 'easeInOutQuint' | 'easeInOutSine' | 'easeInQuad' | 'easeInQuart' | 'easeInQuint' | 'easeInSine' | 'easeOutCubic' | 'easeOutExpo' | 'easeOutQuad' | 'easeOutQuart' | 'easeOutQuint' | 'easeOutSine' | 'linear'; + +// @public (undocumented) +export interface TLEditorComponents { + // (undocumented) + Background: null | TLBackgroundComponent; + // (undocumented) + Brush: null | TLBrushComponent; + // (undocumented) + CollaboratorBrush: null | TLBrushComponent; + // (undocumented) + CollaboratorCursor: null | TLCursorComponent; + // (undocumented) + CollaboratorHint: null | TLCollaboratorHintComponent; + // (undocumented) + CollaboratorScribble: null | TLScribbleComponent; + // (undocumented) + CollaboratorShapeIndicator: null | TLShapeIndicatorComponent; + // (undocumented) + Cursor: null | TLCursorComponent; + // (undocumented) + ErrorFallback: null | TLErrorFallback; + // (undocumented) + Grid: null | TLGridComponent; + // (undocumented) + Handle: null | TLHandleComponent; + // (undocumented) + Scribble: null | TLScribbleComponent; + // (undocumented) + ShapeErrorFallback: null | TLShapeErrorFallback; + // (undocumented) + ShapeIndicatorErrorFallback: null | TLShapeIndicatorErrorFallback; + // (undocumented) + SnapLine: null | TLSnapLineComponent; + // (undocumented) + Spinner: null | TLSpinnerComponent; + // (undocumented) + SvgDefs: null | TLSvgDefsComponent; + // (undocumented) + ZoomBrush: null | TLBrushComponent; +} + +// @public (undocumented) +export const TLEmbedShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLEmbedUtil extends TLBoxUtil { + // (undocumented) + canEdit: TLShapeUtilFlag; + // (undocumented) + canResize: (shape: TLEmbedShape) => boolean; + // (undocumented) + canUnmount: TLShapeUtilFlag; + // (undocumented) + defaultProps(): TLEmbedShape['props']; + // (undocumented) + hideSelectionBoundsBg: TLShapeUtilFlag; + // (undocumented) + hideSelectionBoundsFg: TLShapeUtilFlag; + // (undocumented) + indicator(shape: TLEmbedShape): JSX.Element; + // (undocumented) + isAspectRatioLocked: TLShapeUtilFlag; + // (undocumented) + onResize: OnResizeHandler; + // (undocumented) + render(shape: TLEmbedShape): JSX.Element; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export interface TLEventHandlers { + // (undocumented) + onCancel: TLCancelEvent; + // (undocumented) + onComplete: TLCompleteEvent; + // (undocumented) + onDoubleClick: TLClickEvent; + // (undocumented) + onInterrupt: TLInterruptEvent; + // (undocumented) + onKeyDown: TLKeyboardEvent; + // (undocumented) + onKeyRepeat: TLKeyboardEvent; + // (undocumented) + onKeyUp: TLKeyboardEvent; + // (undocumented) + onMiddleClick: TLPointerEvent; + // (undocumented) + onPointerDown: TLPointerEvent; + // (undocumented) + onPointerEnter: TLPointerEvent; + // (undocumented) + onPointerLeave: TLPointerEvent; + // (undocumented) + onPointerMove: TLPointerEvent; + // (undocumented) + onPointerUp: TLPointerEvent; + // (undocumented) + onQuadrupleClick: TLClickEvent; + // (undocumented) + onRightClick: TLPointerEvent; + // (undocumented) + onTripleClick: TLClickEvent; + // (undocumented) + onWheel: TLWheelEvent; +} + +// @public (undocumented) +export type TLEventInfo = TLCancelEventInfo | TLClickEventInfo | TLCompleteEventInfo | TLInterruptEventInfo | TLKeyboardEventInfo | TLPinchEventInfo | TLPointerEventInfo | TLWheelEventInfo; + +// @public (undocumented) +export type TLEventName = 'cancel' | 'complete' | 'interrupt' | 'wheel' | TLCLickEventName | TLKeyboardEventName | TLPinchEventName | TLPointerEventName; + +// @public (undocumented) +export type TLExportType = 'jpeg' | 'json' | 'png' | 'svg' | 'webp'; + +// @public (undocumented) +export const TLFrameShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLFrameUtil extends TLBoxUtil { + // (undocumented) + canBind: () => boolean; + // (undocumented) + canDropShapes: (_shape: TLFrameShape, _shapes: TLShape[]) => boolean; + // (undocumented) + canEdit: () => boolean; + // (undocumented) + canReceiveNewChildrenOfType: (_type: TLShapeType) => boolean; + // (undocumented) + defaultProps(): TLFrameShape['props']; + // (undocumented) + indicator(shape: TLFrameShape): JSX.Element; + // (undocumented) + onDragShapesOut: (_shape: TLFrameShape, shapes: TLShape[]) => void; + // (undocumented) + onDragShapesOver: (frame: TLFrameShape, shapes: TLShape[]) => { + shouldHint: boolean; + }; + // (undocumented) + onResizeEnd: OnResizeEndHandler; + // (undocumented) + render(shape: TLFrameShape): JSX.Element; + // (undocumented) + toSvg(shape: TLFrameShape, font: string, colors: TLExportColors): Promise | SVGElement; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export const TLGeoShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLGeoUtil extends TLBoxUtil { + // (undocumented) + canEdit: () => boolean; + // (undocumented) + defaultProps(): TLGeoShape['props']; + // (undocumented) + getBounds(shape: TLGeoShape): Box2d; + // (undocumented) + getCenter(shape: TLGeoShape): Vec2d; + // (undocumented) + getOutline(shape: TLGeoShape): Vec2d[]; + // (undocumented) + hitTestLineSegment(shape: TLGeoShape, A: VecLike, B: VecLike): boolean; + // (undocumented) + indicator(shape: TLGeoShape): JSX.Element; + // (undocumented) + onBeforeCreate: (shape: TLGeoShape) => { + props: { + growY: number; + geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box"; + labelColor: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"; + color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"; + fill: "none" | "pattern" | "semi" | "solid"; + dash: "dashed" | "dotted" | "draw" | "solid"; + size: "l" | "m" | "s" | "xl"; + opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1"; + font: "draw" | "mono" | "sans" | "serif"; + align: "end" | "middle" | "start"; + url: string; + w: number; + h: number; + text: string; + }; + type: "geo"; + x: number; + y: number; + rotation: number; + index: string; + parentId: TLParentId; + isLocked: boolean; + id: ID; + typeName: "shape"; + } | undefined; + // (undocumented) + onBeforeUpdate: (prev: TLGeoShape, next: TLGeoShape) => { + props: { + growY: number; + geo: "arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box"; + labelColor: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"; + color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"; + fill: "none" | "pattern" | "semi" | "solid"; + dash: "dashed" | "dotted" | "draw" | "solid"; + size: "l" | "m" | "s" | "xl"; + opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1"; + font: "draw" | "mono" | "sans" | "serif"; + align: "end" | "middle" | "start"; + url: string; + w: number; + h: number; + text: string; + }; + type: "geo"; + x: number; + y: number; + rotation: number; + index: string; + parentId: TLParentId; + isLocked: boolean; + id: ID; + typeName: "shape"; + } | undefined; + // (undocumented) + onEditEnd: OnEditEndHandler; + // (undocumented) + onResize: OnResizeHandler; + // (undocumented) + render(shape: TLGeoShape): JSX.Element; + // (undocumented) + toSvg(shape: TLGeoShape, font: string, colors: TLExportColors): SVGElement; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export const TLGroupShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLGroupUtil extends TLShapeUtil { + // (undocumented) + canBind: () => boolean; + // (undocumented) + defaultProps(): TLGroupShape['props']; + // (undocumented) + getBounds(shape: TLGroupShape): Box2d; + // (undocumented) + getCenter(shape: TLGroupShape): Vec2dModel; + // (undocumented) + getOutline(shape: TLGroupShape): Vec2dModel[]; + // (undocumented) + hideSelectionBoundsBg: () => boolean; + // (undocumented) + hideSelectionBoundsFg: () => boolean; + // (undocumented) + indicator(shape: TLGroupShape): JSX.Element; + // (undocumented) + onChildrenChange: OnChildrenChangeHandler; + // (undocumented) + render(shape: TLGroupShape): JSX.Element | null; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export type TLHistoryEntry = TLCommand | TLMark; + +// @public (undocumented) +export const TLImageShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLImageUtil extends TLBoxUtil { + // (undocumented) + canCrop: () => boolean; + // (undocumented) + defaultProps(): TLImageShape['props']; + // (undocumented) + indicator(shape: TLImageShape): JSX.Element | null; + // (undocumented) + isAspectRatioLocked: () => boolean; + // (undocumented) + onDoubleClick: (shape: TLImageShape) => void; + // (undocumented) + onDoubleClickEdge: OnDoubleClickHandler; + // (undocumented) + render(shape: TLImageShape): JSX.Element; + // (undocumented) + toSvg(shape: TLImageShape): Promise; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export type TLInterruptEvent = (info: TLInterruptEventInfo) => void; + +// @public (undocumented) +export type TLInterruptEventInfo = { + type: 'misc'; + name: 'interrupt'; +}; + +// @public (undocumented) +export type TLKeyboardEvent = (info: TLKeyboardEventInfo) => void; + +// @public (undocumented) +export type TLKeyboardEventInfo = TLBaseEventInfo & { + type: 'keyboard'; + name: TLKeyboardEventName; + key: string; + code: string; +}; + +// @public (undocumented) +export type TLKeyboardEventName = 'key_down' | 'key_repeat' | 'key_up'; + +// @public (undocumented) +export const TLLineShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLLineUtil extends TLShapeUtil { + // (undocumented) + defaultProps(): TLLineShape['props']; + // (undocumented) + getBounds(shape: TLLineShape): Box2d; + // (undocumented) + getCenter(shape: TLLineShape): Vec2d; + // (undocumented) + getHandles(shape: TLLineShape): TLHandle[]; + // (undocumented) + getOutline(shape: TLLineShape): Vec2d[]; + // (undocumented) + hideResizeHandles: () => boolean; + // (undocumented) + hideRotateHandle: () => boolean; + // (undocumented) + hideSelectionBoundsBg: () => boolean; + // (undocumented) + hideSelectionBoundsFg: () => boolean; + // (undocumented) + hitTestLineSegment(shape: TLLineShape, A: VecLike, B: VecLike): boolean; + // (undocumented) + hitTestPoint(shape: TLLineShape, point: Vec2d): boolean; + // (undocumented) + indicator(shape: TLLineShape): JSX.Element; + // (undocumented) + isClosed: () => boolean; + // (undocumented) + onHandleChange: OnHandleChangeHandler; + // (undocumented) + onResize: OnResizeHandler; + // (undocumented) + render(shape: TLLineShape): JSX.Element | undefined; + // (undocumented) + toSvg(shape: TLLineShape, _font: string, colors: TLExportColors): SVGGElement; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export type TLMark = { + type: 'STOP'; + id: string; + onUndo: boolean; + onRedo: boolean; +}; + +// @public (undocumented) +export const TLNoteShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLNoteUtil extends TLShapeUtil { + // (undocumented) + canEdit: () => boolean; + // (undocumented) + defaultProps(): TLNoteShape['props']; + // (undocumented) + getBounds(shape: TLNoteShape): Box2d; + // (undocumented) + getCenter(_shape: TLNoteShape): Vec2d; + // (undocumented) + getHeight(shape: TLNoteShape): number; + // (undocumented) + getOutline(shape: TLNoteShape): Vec2d[]; + // (undocumented) + hideResizeHandles: () => boolean; + // (undocumented) + hideSelectionBoundsBg: () => boolean; + // (undocumented) + hideSelectionBoundsFg: () => boolean; + // (undocumented) + indicator(shape: TLNoteShape): JSX.Element; + // (undocumented) + onBeforeCreate: (next: TLNoteShape) => { + props: { + growY: number; + color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"; + size: "l" | "m" | "s" | "xl"; + font: "draw" | "mono" | "sans" | "serif"; + align: "end" | "middle" | "start"; + opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1"; + url: string; + text: string; + }; + type: "note"; + x: number; + y: number; + rotation: number; + index: string; + parentId: TLParentId; + isLocked: boolean; + id: ID; + typeName: "shape"; + } | undefined; + // (undocumented) + onBeforeUpdate: (prev: TLNoteShape, next: TLNoteShape) => { + props: { + growY: number; + color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"; + size: "l" | "m" | "s" | "xl"; + font: "draw" | "mono" | "sans" | "serif"; + align: "end" | "middle" | "start"; + opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1"; + url: string; + text: string; + }; + type: "note"; + x: number; + y: number; + rotation: number; + index: string; + parentId: TLParentId; + isLocked: boolean; + id: ID; + typeName: "shape"; + } | undefined; + // (undocumented) + onEditEnd: OnEditEndHandler; + // (undocumented) + render(shape: TLNoteShape): JSX.Element; + // (undocumented) + toSvg(shape: TLNoteShape, font: string, colors: TLExportColors): SVGGElement; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export type TLPinchEvent = (info: TLPinchEventInfo) => void; + +// @public (undocumented) +export type TLPinchEventInfo = TLBaseEventInfo & { + type: 'pinch'; + name: TLPinchEventName; + point: Vec2dModel; + delta: Vec2dModel; +}; + +// @public (undocumented) +export type TLPinchEventName = 'pinch_end' | 'pinch_start' | 'pinch'; + +// @public (undocumented) +export type TLPointerEvent = (info: TLPointerEventInfo) => void; + +// @public (undocumented) +export type TLPointerEventInfo = TLBaseEventInfo & { + type: 'pointer'; + name: TLPointerEventName; + point: VecLike; + pointerId: number; + button: number; + isPen: boolean; +} & TLPointerEventTarget; + +// @public (undocumented) +export type TLPointerEventName = 'middle_click' | 'pointer_down' | 'pointer_enter' | 'pointer_leave' | 'pointer_move' | 'pointer_up' | 'right_click'; + +// @public (undocumented) +export type TLPointerEventTarget = { + target: 'canvas'; + shape?: undefined; +} | { + target: 'handle'; + shape: TLShape; + handle: TLHandle; +} | { + target: 'selection'; + handle?: TLSelectionHandle; + shape?: undefined; +} | { + target: 'shape'; + shape: TLShape; +}; + +// @public (undocumented) +export type TLReorderOperation = 'backward' | 'forward' | 'toBack' | 'toFront'; + +// @public (undocumented) +export type TLResizeHandle = SelectionCorner | SelectionEdge; + +// @public +export type TLResizeMode = 'resize_bounds' | 'scale_shape'; + +// @public (undocumented) +export type TLSelectionHandle = RotateCorner | SelectionCorner | SelectionEdge; + +// @public (undocumented) +export interface TLShapeDef = TLShapeUtil> { + // (undocumented) + readonly createShapeUtils: (app: App) => ShapeUtil; + // (undocumented) + readonly is: (shape: TLUnknownShape) => shape is ShapeType; + // (undocumented) + readonly migrations: Migrations; + // (undocumented) + readonly type: ShapeType['type']; + // (undocumented) + readonly validator?: StoreValidator; +} + +// @public (undocumented) +export abstract class TLShapeUtil { + constructor(app: App, type: T['type']); + // (undocumented) + app: App; + bounds(shape: T): Box2d; + canBind: (_shape: T, _otherShape?: K | undefined) => boolean; + canCrop: TLShapeUtilFlag; + canDropShapes(shape: T, shapes: TLShape[]): boolean; + canEdit: TLShapeUtilFlag; + canReceiveNewChildrenOfType(type: TLShapeType): boolean; + canResize: TLShapeUtilFlag; + canScroll: TLShapeUtilFlag; + canUnmount: TLShapeUtilFlag; + center(shape: T): Vec2dModel; + abstract defaultProps(): T['props']; + protected abstract getBounds(shape: T): Box2d; + abstract getCenter(shape: T): Vec2dModel; + getEditingBounds: (shape: T) => Box2d; + protected getHandles?(shape: T): TLHandle[]; + protected abstract getOutline(shape: T): Vec2dModel[]; + handles(shape: T): TLHandle[]; + hideResizeHandles: TLShapeUtilFlag; + hideRotateHandle: TLShapeUtilFlag; + hideSelectionBoundsBg: TLShapeUtilFlag; + hideSelectionBoundsFg: TLShapeUtilFlag; + hitTestLineSegment(shape: T, A: VecLike, B: VecLike): boolean; + hitTestPoint(shape: T, point: VecLike): boolean; + abstract indicator(shape: T): any; + // (undocumented) + is(shape: TLBaseShape): shape is T; + isAspectRatioLocked: TLShapeUtilFlag; + isClosed: TLShapeUtilFlag; + onBeforeCreate?: OnBeforeCreateHandler; + onBeforeUpdate?: OnBeforeUpdateHandler; + // (undocumented) + onBindingChange?: OnBindingChangeHandler; + // (undocumented) + onChildrenChange?: OnChildrenChangeHandler; + // (undocumented) + onClick?: OnClickHandler; + // (undocumented) + onDoubleClick?: OnDoubleClickHandler; + // (undocumented) + onDoubleClickEdge?: OnDoubleClickHandler; + // (undocumented) + onDoubleClickHandle?: OnDoubleClickHandleHandler; + // (undocumented) + onDragShapesOut?: OnDragHandler; + // (undocumented) + onDragShapesOver?: OnDragHandler; + // (undocumented) + onDropShapesOver?: OnDragHandler; + // (undocumented) + onEditEnd?: OnEditEndHandler; + // (undocumented) + onHandleChange?: OnHandleChangeHandler; + // (undocumented) + onResize?: OnResizeHandler; + // (undocumented) + onResizeEnd?: OnResizeEndHandler; + // (undocumented) + onResizeStart?: OnResizeStartHandler; + // (undocumented) + onRotate?: OnRotateHandler; + // (undocumented) + onRotateEnd?: OnRotateEndHandler; + // (undocumented) + onRotateStart?: OnRotateStartHandler; + // (undocumented) + onTranslate?: OnTranslateHandler; + // (undocumented) + onTranslateEnd?: OnTranslateEndHandler; + // (undocumented) + onTranslateStart?: OnTranslateStartHandler; + outline(shape: T): Vec2dModel[]; + point(shape: T): Vec2dModel; + abstract render(shape: T): any; + snapPoints(shape: T): Vec2d[]; + toSvg?(shape: T, font: string | undefined, colors: TLExportColors): Promise | SVGElement; + transform(shape: T): Matrix2d; + // (undocumented) + readonly type: T['type']; +} + +// @public (undocumented) +export interface TLShapeUtilConstructor = TLShapeUtil> { + // (undocumented) + new (app: App, type: T['type']): ShapeUtil; +} + +// @public (undocumented) +export type TLShapeUtilFlag = (shape: T) => boolean; + +// @public (undocumented) +export const TLTextShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLTextUtil extends TLShapeUtil { + // (undocumented) + canEdit: () => boolean; + // (undocumented) + defaultProps(): TLTextShape['props']; + // (undocumented) + getBounds(shape: TLTextShape): Box2d; + // (undocumented) + getCenter(shape: TLTextShape): Vec2d; + // (undocumented) + getMinDimensions(shape: TLTextShape): { + height: number; + width: number; + }; + // (undocumented) + getOutline(shape: TLTextShape): Vec2d[]; + // (undocumented) + indicator(shape: TLTextShape): JSX.Element; + // (undocumented) + isAspectRatioLocked: TLShapeUtilFlag; + // (undocumented) + onBeforeCreate: (shape: TLTextShape) => { + x: number; + y: number; + type: "text"; + rotation: number; + index: string; + parentId: TLParentId; + isLocked: boolean; + props: TLTextShapeProps; + id: ID; + typeName: "shape"; + } | undefined; + // (undocumented) + onBeforeUpdate: (prev: TLTextShape, next: TLTextShape) => { + x: number; + y: number; + props: { + w: number; + color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"; + size: "l" | "m" | "s" | "xl"; + font: "draw" | "mono" | "sans" | "serif"; + align: "end" | "middle" | "start"; + opacity: "0.1" | "0.25" | "0.5" | "0.75" | "1"; + text: string; + scale: number; + autoSize: boolean; + }; + type: "text"; + rotation: number; + index: string; + parentId: TLParentId; + isLocked: boolean; + id: ID; + typeName: "shape"; + } | undefined; + // (undocumented) + onDoubleClickEdge: (shape: TLTextShape) => { + id: ID; + type: "text"; + props: { + autoSize: boolean; + scale?: undefined; + }; + } | { + id: ID; + type: "text"; + props: { + scale: number; + autoSize?: undefined; + }; + } | undefined; + // (undocumented) + onEditEnd: OnEditEndHandler; + // (undocumented) + onResize: OnResizeHandler; + // (undocumented) + render(shape: TLTextShape): JSX.Element; + // (undocumented) + toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors): SVGGElement; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export type TLTickEvent = (elapsed: number) => void; + +// @public (undocumented) +export type TLUnknownShapeDef = TLShapeDef>; + +// @public (undocumented) +export const TLVideoShapeDef: TLShapeDef; + +// @public (undocumented) +export class TLVideoUtil extends TLBoxUtil { + // (undocumented) + canEdit: () => boolean; + // (undocumented) + defaultProps(): TLVideoShape['props']; + // (undocumented) + indicator(shape: TLVideoShape): JSX.Element; + // (undocumented) + isAspectRatioLocked: () => boolean; + // (undocumented) + render(shape: TLVideoShape): JSX.Element; + // (undocumented) + toSvg(shape: TLVideoShape): SVGGElement; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export type TLWheelEvent = (info: TLWheelEventInfo) => void; + +// @public (undocumented) +export type TLWheelEventInfo = TLBaseEventInfo & { + type: 'wheel'; + name: 'wheel'; + delta: Vec2dModel; +}; + +// @public (undocumented) +export const truncateStringWithEllipsis: (str: string, maxLength: number) => string; + +// @public (undocumented) +export type UiEnterHandler = (info: any, from: string) => void; + +// @public (undocumented) +export type UiEvent = TLCancelEvent | TLClickEvent | TLCompleteEvent | TLKeyboardEvent | TLPinchEvent | TLPointerEvent; + +// @public (undocumented) +export type UiEventType = 'click' | 'keyboard' | 'pinch' | 'pointer' | 'wheel' | 'zoom'; + +// @public (undocumented) +export type UiExitHandler = (info: any, to: string) => void; + +// @public +export function uniqueId(): string; + +// @public (undocumented) +export const useApp: () => App; + +// @public (undocumented) +export function useContainer(): HTMLDivElement; + +// @public (undocumented) +export function usePrefersReducedMotion(): boolean; + +// @public (undocumented) +export function useQuickReactor(name: string, reactFn: () => void, deps?: any[]): void; + +// @public (undocumented) +export function useReactor(name: string, reactFn: () => void, deps?: any[] | undefined): void; + +// @public (undocumented) +export function useUrlState(changeUrl: (params: Params) => void): void; + +// @internal (undocumented) +export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10; + +// @public (undocumented) +export class WeakMapCache { + // (undocumented) + access(item: T): K | undefined; + // (undocumented) + bust(): void; + // (undocumented) + get

(item: P, cb: (item: P) => K): NonNullable; + // (undocumented) + has(item: T): boolean; + // (undocumented) + invalidate(item: T): void; + // (undocumented) + items: WeakMap; + // (undocumented) + set(item: T, value: K): void; +} + +// @internal (undocumented) +export const ZOOMS: number[]; + + +export * from "@tldraw/tlschema"; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/editor/editor.css b/packages/editor/editor.css new file mode 100644 index 000000000..e5b35cd8a --- /dev/null +++ b/packages/editor/editor.css @@ -0,0 +1,1568 @@ +.rs-container { + width: 100%; + height: 100%; + font-size: 12px; + /* Spacing */ + --space-1: 2px; + --space-2: 4px; + --space-3: 8px; + --space-4: 12px; + --space-5: 16px; + --space-6: 20px; + --space-7: 28px; + --space-8: 32px; + --space-9: 64px; + --space-10: 72px; + /* Radius */ + --radius-0: 2px; + --radius-1: 4px; + --radius-2: 7px; + --radius-3: 9px; + --radius-4: 12px; + --radius-5: 16px; + --layer-grid: 150; + --layer-canvas: 200; + /* Misc */ + --rs-zoom: 1; + --rs-cursor: default; + --rs-scale: calc(1 / var(--rs-zoom)); + --rs-font-draw: 'tldraw_draw', sans-serif; + --rs-font-sans: 'tldraw_sans', sans-serif; + --rs-font-serif: 'tldraw_serif', serif; + --rs-font-mono: 'tldraw_mono', monospace; + --a: calc(min(0.5, 1 / var(--rs-zoom)) * 2px); + --b: calc(min(0.5, 1 / var(--rs-zoom)) * -2px); + --rs-text-outline: 0 var(--b) 0 var(--color-background), 0 var(--a) 0 var(--color-background), + var(--b) var(--b) 0 var(--color-background), var(--a) var(--b) 0 var(--color-background), + var(--a) var(--a) 0 var(--color-background), var(--b) var(--a) 0 var(--color-background); + /* Own properties */ + position: relative; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + overflow: hidden; +} + +.rs-theme__light { + --color-accent: #e64a4a; + --color-background: rgb(249, 250, 251); + --color-brush-fill: rgba(144, 144, 144, 0.102); + --color-brush-stroke: rgba(144, 144, 144, 0.251); + --color-grid: rgba(144, 144, 144, 0.902); + --color-low: rgb(237, 240, 242); + --color-culled: rgb(235, 238, 240); + --color-muted-0: rgba(0, 0, 0, 0.02); + --color-muted-1: rgba(0, 0, 0, 0.1); + --color-muted-2: rgba(0, 0, 0, 0.035); + --color-hint: rgba(0, 0, 0, 0.055); + --color-overlay: rgba(0, 0, 0, 0.2); + --color-divider: #e8e8e8; + --color-panel-contrast: #ffffff; + --color-panel-overlay: rgba(255, 255, 255, 0.82); + --color-panel: #fdfdfd; + --color-focus: #004094; + --color-selected: #2f80ed; + --color-selected-contrast: #ffffff; + --color-selection-fill: #1e90ff06; + --color-selection-stroke: #2f80ed; + --color-text-0: #1d1d1d; + --color-text-1: #2d2d2d; + --color-text-2: #5f6369; + --color-text-3: #b6b7ba; + --color-primary: #2f80ed; + --color-warn: #d10b0b; + --color-text: #000000; + --palette-black: #1d1d1d; + --palette-blue: #4263eb; + --palette-green: #099268; + --palette-grey: #adb5bd; + --palette-light-blue: #4dabf7; + --palette-light-green: #40c057; + --palette-light-red: #ff8787; + --palette-light-violet: #e599f7; + --palette-orange: #f76707; + --palette-red: #e03131; + --palette-violet: #ae3ec9; + --palette-white: #ffffff; + --palette-yellow: #ffc078; + /* TODO: fill style colors should be generated at runtime (later task) */ + /* for fill style 'semi' */ + --palette-solid: #fcfffe; + --palette-black-semi: #e8e8e8; + --palette-blue-semi: #dce1f8; + --palette-green-semi: #d3e9e3; + --palette-grey-semi: #eceef0; + --palette-light-blue-semi: #ddedfa; + --palette-light-green-semi: #dbf0e0; + --palette-light-red-semi: #f4dadb; + --palette-light-violet-semi: #f5eafa; + --palette-orange-semi: #f8e2d4; + --palette-red-semi: #f4dadb; + --palette-violet-semi: #ecdcf2; + --palette-white-semi: #ffffff; + --palette-yellow-semi: #f9f0e6; + /* for fill style 'pattern' */ + --palette-black-pattern: #494949; + --palette-blue-pattern: #6681ee; + --palette-green-pattern: #39a785; + --palette-grey-pattern: #bcc3c9; + --palette-light-blue-pattern: #6fbbf8; + --palette-light-green-pattern: #65cb78; + --palette-light-red-pattern: #fe9e9e; + --palette-light-violet-pattern: #e9acf8; + --palette-orange-pattern: #f78438; + --palette-red-pattern: #e55959; + --palette-violet-pattern: #bd63d3; + --palette-white-pattern: #ffffff; + --palette-yellow-pattern: #fecb92; + --shadow-1: 0px 1px 2px rgba(0, 0, 0, 0.22), 0px 1px 3px rgba(0, 0, 0, 0.09); + --shadow-2: 0px 0px 2px rgba(0, 0, 0, 0.12), 0px 2px 3px rgba(0, 0, 0, 0.24), + 0px 2px 6px rgba(0, 0, 0, 0.1), inset 0px 0px 0px 1px var(--color-panel-contrast); + --shadow-3: 0px 1px 2px rgba(0, 0, 0, 0.25), 0px 2px 6px rgba(0, 0, 0, 0.14), + inset 0px 0px 0px 1px var(--color-panel-contrast); + --shadow-4: 0px 0px 3px rgba(0, 0, 0, 0.16), 0px 5px 4px rgba(0, 0, 0, 0.16), + 0px 2px 16px rgba(0, 0, 0, 0.06), inset 0px 0px 0px 1px var(--color-panel-contrast); +} + +.rs-theme__dark { + --color-accent: #e64a4a; + --color-background: #212529; + --color-brush-fill: rgba(180, 180, 180, 0.05); + --color-brush-stroke: rgba(180, 180, 180, 0.25); + --color-grid: #909090e6; + --color-low: #2c3136; + --color-culled: rgb(47, 52, 57); + --color-muted-0: rgba(255, 255, 255, 0.02); + --color-muted-1: rgba(255, 255, 255, 0.1); + --color-muted-2: rgba(255, 255, 255, 0.05); + --color-hint: rgba(255, 255, 255, 0.1); + --color-overlay: rgba(0, 0, 0, 0.35); + --color-divider: #49555f; + --color-panel-contrast: #49555f; + --color-panel: #363d44; + --color-panel-overlay: rgba(54, 61, 68, 0.82); + --color-focus: #a5c3f3; + --color-selected: #4285f4; + --color-selected-contrast: #ffffff; + --color-selection-fill: rgba(38, 150, 255, 0.05); + --color-selection-stroke: #2f80ed; + --color-text-0: #f0eded; + --color-text-1: #d9d9d9; + --color-text-2: #8e9094; + --color-text-3: #515a62; + --color-primary: #2f80ed; + --color-warn: #d10b0b; + --color-text: #f8f9fa; + --palette-black: #e1e1e1; + --palette-blue: #4156be; + --palette-green: #3b7b5e; + --palette-grey: #93989f; + --palette-light-blue: #588fc9; + --palette-light-green: #599f57; + --palette-light-red: #c67877; + --palette-light-violet: #b583c9; + --palette-orange: #bf612e; + --palette-red: #aa3c37; + --palette-violet: #873fa3; + --palette-white: #1d1d1d; + --palette-yellow: #cba371; + /* TODO: fill style colors should be generated at runtime (later task) */ + /* for fill style 'semi' */ + --palette-solid: #28292e; + --palette-black-semi: #2c3036; + --palette-blue-semi: #262d40; + --palette-green-semi: #253231; + --palette-grey-semi: #33373c; + --palette-light-blue-semi: #2a3642; + --palette-light-green-semi: #2a3830; + --palette-light-red-semi: #3b3235; + --palette-light-violet-semi: #383442; + --palette-orange-semi: #3a2e2a; + --palette-red-semi: #36292b; + --palette-violet-semi: #31293c; + --palette-white-semi: #ffffff; + --palette-yellow-semi: #3c3934; + /* for fill style 'pattern' */ + --palette-black-pattern: #989898; + --palette-blue-pattern: #3a4b9e; + --palette-green-pattern: #366a53; + --palette-grey-pattern: #7c8187; + --palette-light-blue-pattern: #4d7aa9; + --palette-light-green-pattern: #4e874e; + --palette-light-red-pattern: #a56767; + --palette-light-violet-pattern: #9770a9; + --palette-orange-pattern: #9f552d; + --palette-red-pattern: #8f3734; + --palette-violet-pattern: #763a8b; + --palette-white-pattern: #ffffff; + --palette-yellow-pattern: #fecb92; + --shadow-1: 0px 1px 2px #00000029, 0px 1px 3px #00000038, + inset 0px 0px 0px 1px var(--color-panel-contrast); + --shadow-2: 0px 1px 3px #00000077, 0px 2px 6px #00000055, + inset 0px 0px 0px 1px var(--color-panel-contrast); + --shadow-3: 0px 1px 3px #00000077, 0px 2px 12px rgba(0, 0, 0, 0.22), + inset 0px 0px 0px 1px var(--color-panel-contrast); +} + +.rs-counter-scaled { + transform: scale(var(--rs-scale)); + transform-origin: top left; + width: calc(100% * var(--rs-zoom)); + height: calc(100% * var(--rs-zoom)); +} + +.rs-pointer-events { + pointer-events: all; +} + +.rs-container, +.rs-container * { + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; + scrollbar-highlight-color: transparent; + -webkit-user-select: none; + user-select: none; + outline: none; +} + +.rs-container a { + -webkit-touch-callout: initial; +} + +input, +*[contenteditable], +*[contenteditable] * { + -webkit-user-select: text; +} + +.rs-background { + position: absolute; + inset: 0px; + background-color: var(--color-background); +} + +.rs-font-preload { + height: 1px; + width: 1px; + opacity: 0; + position: absolute; + top: -1000px; + left: -1000px; +} + +/* Portals (menus, modals, popovers, etc) */ + +.rs-panels { + z-index: var(--layer-panels); +} + +.rs-portal { + z-index: var(--layer-portals); +} + +.rs-overlay { + z-index: var(--layer-overlays); +} + +.rs-modal { + z-index: var(--layer-dialogs); +} + +.tlui-spacer { + flex-grow: 2; + min-height: 0px; + margin-left: calc(-1 * var(--space-4)); + margin-top: calc(-1 * var(--space-4)); + pointer-events: none; +} + +/* -------------------------------------------------- */ +/* Canvas */ +/* -------------------------------------------------- */ + +.rs-canvas { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + background-color: var(--color-background); + color: var(--color-text); + z-index: var(--layer-canvas); + cursor: var(--rs-cursor); + overflow: clip; + content-visibility: auto; + touch-action: none; + contain: strict; +} + +/* --------------------- Grid Layer --------------------- */ + +.tl-grid { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + touch-action: none; + pointer-events: none; + z-index: 2; +} + +.tl-grid-dot { + fill: var(--color-grid); +} + +/* --------------------- Layers --------------------- */ + +.rs-html-layer { + position: absolute; + top: 0px; + left: 0px; + width: 1px; + height: 1px; + contain: layout style size; + z-index: 3; +} + +.rs-svg-layer { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + overflow: clip; + pointer-events: none; + contain: strict; + z-index: 4; +} + +/* ---------------------- Brush --------------------- */ + +.rs-brush { + stroke-width: calc(var(--rs-scale) * 1px); +} + +.rs-brush__default { + stroke: var(--color-brush-stroke); + fill: var(--color-brush-fill); +} + +/* -------------------- Scribble -------------------- */ + +.rs-scribble { + stroke-linejoin: round; + stroke-linecap: round; + pointer-events: none; +} + +/* ---------------------- Shape --------------------- */ + +.rs-shape { + position: absolute; + pointer-events: none; + overflow: visible; + transform-origin: top left; +} + +.rs-shape__culled { + position: relative; + background-color: var(--color-culled); +} + +.rs-hitarea-stroke { + fill: none; + stroke: transparent; + pointer-events: stroke; + stroke-width: min(100px, calc(24px * var(--rs-scale))); +} + +.rs-hitarea-fill { + fill: var(--color-background); + stroke: transparent; + pointer-events: all; + stroke-width: min(100px, calc(24px * var(--rs-scale))); +} + +.rs-hitarea-fill-solid { + stroke: transparent; + pointer-events: all; + stroke-width: min(100px, calc(24px * var(--rs-scale))); +} + +/* ---------------- Shape Containers ---------------- */ + +.rs-svg-container { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + pointer-events: none; + stroke-linecap: round; + stroke-linejoin: round; + transform-origin: top left; + overflow: visible; +} + +.rs-html-container { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + pointer-events: none; + stroke-linecap: round; + stroke-linejoin: round; + /* content-visibility: auto; */ + transform-origin: top left; + color: inherit; +} + +/* -------------------- Indicator ------------------- */ + +.rs-shape-indicator { + transform-origin: top left; + fill: none; + stroke-width: calc(1.5px * var(--rs-scale)); +} + +.rs-shape-indicator__hinting { + stroke-width: calc(2.5px * var(--rs-scale)); +} + +/* ------------------ SelectionBox ------------------ */ + +.tlui-selection__bg { + position: absolute; + top: 0px; + left: 0px; + transform-origin: top left; + background-color: transparent; + pointer-events: all; +} + +.tlui-selection__fg { + pointer-events: none; +} + +.tlui-selection__fg-outline { + fill: none; + pointer-events: none; + stroke: var(--color-selection-stroke); + stroke-width: calc(1.5px * var(--rs-scale)); +} + +.rs-corner-handle { + pointer-events: none; + stroke: var(--color-selection-stroke); + fill: var(--color-background); + stroke-width: calc(1.5px * var(--rs-scale)); +} + +.rs-text-handle { + pointer-events: none; + fill: var(--color-selection-stroke); +} + +.rs-corner-crop-handle { + pointer-events: none; + fill: none; + stroke: var(--color-selection-stroke); +} + +.rs-corner-crop-edge-handle { + pointer-events: none; + fill: none; + stroke: var(--color-selection-stroke); +} + +.rs-rotate-handle { + stroke: var(--color-selection-stroke); + fill: var(--color-background); + stroke-width: calc(1.5px * var(--rs-scale)); + pointer-events: all; +} + +.rs-mobile-rotate__bg { + pointer-events: all; + r: calc(max(calc(14px * var(--rs-scale)), 20px / max(1, var(--rs-zoom)))); + cursor: grab; +} + +.rs-mobile-rotate__fg { + pointer-events: none; + stroke: var(--color-selection-stroke); + fill: var(--color-background); + stroke-width: calc(1.5px * var(--rs-scale)); +} + +.rs-transparent { + fill: transparent; + stroke: transparent; +} + +/* --------------------- Handles -------------------- */ + +.rs-handle { + pointer-events: all; +} + +.rs-handle-bg { + fill: transparent; + stroke: transparent; + pointer-events: all; + cursor: grab; + r: calc(12px / var(--rs-zoom)); +} + +.rs-handle-fg { + fill: var(--color-background); + stroke: var(--color-selection-stroke); + stroke-width: calc(1.5px * var(--rs-scale)); + r: calc(4px * var(--rs-scale)); + pointer-events: none; +} + +.rs-handle-bg:active { + fill: none; +} + +.rs-handle-bg:hover { + cursor: grab; + fill: var(--color-selection-fill); +} + +.rs-handle-hint { + opacity: 0; +} + +.rs-handle-hint:hover { + opacity: 1; +} + +@media (pointer: coarse) { + .rs-handle-bg:active { + fill: var(--color-selection-fill); + } + + .rs-handle-hint { + opacity: 1; + } + + .rs-handle-hint > .rs-handle-fg { + r: calc(3px * var(--rs-scale)); + } +} + +/* ------------------ Bounds Detail ----------------- */ + +.rs-image, +.rs-video { + object-fit: cover; + background-size: cover; + width: 100%; + height: 100%; +} + +.rs-image-container, +.rs-video-container, +.rs-embed-container { + width: 100%; + height: 100%; + pointer-events: all; + /* background-color: var(--color-background); */ + + display: flex; + justify-content: center; + align-items: center; +} + +.rs-image__meda-tag { + --scale: calc(min(2, var(--rs-scale))); + position: absolute; + top: calc(var(--scale) * 8px); + right: calc(var(--scale) * 8px); + font-size: 10px; + scale: var(--scale); + transform-origin: top right; + background-color: var(--color-background); + padding: 2px 4px; + border-radius: 4px; +} + +/* --------------------- Assets --------------------- */ + +.rs-asset-loading { + position: absolute; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-muted); + z-index: 1; + pointer-events: all; +} + +/* --------------------- Nametag -------------------- */ + +/* Rounded corners */ +.rs-nametag { + color: var(--color-selected-contrast); + white-space: nowrap; + position: absolute; + border-radius: 10px; + padding: 2px 6px; + font-size: 12px; + font-family: var(--font-family); +} + +/* -------------------------------------------------- */ +/* Spinner */ +/* -------------------------------------------------- */ + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} + +.rs-spinner::after { + content: ''; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + border-radius: 50%; + border: 2px solid #ccc; + border-top-color: #000; + animation: spinner 0.6s linear infinite; + pointer-events: none; +} + +/* -------------------- IconShape ------------------- */ + +.rs-iconshape__icon { + pointer-events: all; + width: 100%; + height: 100%; +} + +.rs-icon-preview { + width: 14px; + height: 14px; +} + +/* ------------------- Text Shape ------------------- */ + +.rs-text-shape__wrapper { + position: relative; + font-weight: normal; + min-width: 1px; + padding: 0px; + margin: 0px; + border: none; + height: 100%; + font-variant: normal; + font-style: normal; + pointer-events: all; + white-space: pre-wrap; + overflow-wrap: break-word; + text-shadow: var(--rs-text-outline); +} + +.rs-text-shape__wrapper[data-align='start'] { + text-align: left; +} + +.rs-text-shape__wrapper[data-align='middle'] { + text-align: center; +} + +.rs-text-shape__wrapper[data-align='end'] { + text-align: right; +} + +.rs-text-shape__wrapper[data-font='draw'] { + font-family: var(--rs-font-draw); +} + +.rs-text-shape__wrapper[data-font='sans'] { + font-family: var(--rs-font-sans); +} + +.rs-text-shape__wrapper[data-font='serif'] { + font-family: var(--rs-font-serif); +} + +.rs-text-shape__wrapper[data-font='mono'] { + font-family: var(--rs-font-mono); +} + +.rs-text-shape__wrapper[data-isediting='true'] .rs-text-content { + opacity: 0; +} + +.rs-text { + /* remove overflow from textarea on windows */ + margin: 0px; + padding: 0px; + border: 0px; + color: inherit; + caret-color: var(--color-text); + background: none; + border-image: none; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit; + font-variant: inherit; + font-style: inherit; + text-align: inherit; + letter-spacing: inherit; + text-shadow: inherit; + outline: none; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + pointer-events: all; + text-rendering: auto; + text-transform: none; + text-indent: 0px; + display: inline-block; + appearance: auto; + column-count: initial !important; + writing-mode: horizontal-tb !important; + word-spacing: 0px; +} + +.rs-text-measure { + position: absolute; + z-index: 999999; + top: -9999px; + right: -9999px; + opacity: 0; + width: fit-content; + box-sizing: border-box; + pointer-events: none; + line-break: normal; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + resize: none; + border: none; + user-select: none; + -webkit-user-select: none; +} + +.rs-text-edit-container { + position: relative; + width: 100%; + height: 100%; +} + +.rs-text-input, +.rs-text-content { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + min-width: 1px; + min-height: 1px; + overflow: visible; +} + +.rs-text-content { + pointer-events: none; +} + +.rs-text-input { + resize: none; + user-select: all; + -webkit-user-select: text; + overflow: hidden; +} + +.rs-text-input::selection { + background: var(--color-selected); + color: var(--color-selected-contrast); + text-shadow: none; +} + +/* ------------------- Snap Lines ------------------- */ + +.rs-snap-line { + stroke: var(--color-accent); + stroke-width: calc(1px * var(--rs-scale)); + fill: none; +} + +.rs-snap-point { + stroke: var(--color-accent); + stroke-width: calc(1px * var(--rs-scale)); + fill: none; +} + +/* -------------------- Groups ------------------ */ + +.rs-group { + stroke: var(--color-text); + stroke-width: calc(1px * var(--rs-scale)); + opacity: 0.5; +} + +/* ------------------- Bookmark Shape ------------------- */ + +.rs-bookmark__container { + width: 100%; + height: 100%; + position: relative; + border: 1px solid var(--color-panel-contrast); + background-color: var(--color-panel); + border-radius: var(--radius-2); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.rs-bookmark__image_container { + flex: 1; + overflow: hidden; + border-top-left-radius: var(--radius-1); + border-top-right-radius: var(--radius-1); + width: 100%; + height: 100%; + display: flex; + justify-content: flex-end; + align-items: flex-start; +} + +.rs-bookmark__image_container > .rs-hyperlink-button::after { + background-color: var(--color-panel); +} + +.rs-bookmark__placeholder { + width: 100%; + height: 100%; + background-color: var(--color-muted-2); + border-bottom: 1px solid var(--color-muted-2); +} + +.rs-bookmark__image { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; +} + +.rs-bookmark__copy_container { + background-color: var(--color-muted); + padding: var(--space-4); + pointer-events: all; +} + +.rs-bookmark__heading, +.rs-bookmark__description, +.rs-bookmark__link { + margin: 0; + width: 100%; + font-family: inherit; +} + +.rs-bookmark__heading { + font-size: 16px; + font-weight: bold; + padding-bottom: var(--space-2); + margin: 4px 0; +} + +.rs-bookmark__description { + font-size: 12px; + padding-bottom: var(--space-4); +} + +.rs-bookmark__link { + font-size: 14px; + pointer-events: all; + z-index: 999; + overflow: hidden; + cursor: pointer; + display: block; + color: var(--color-text); + text-overflow: ellipsis; + text-decoration: none; + color: var(--color-text-2); +} + +.rs-bookmark__link:hover { + color: var(--color-selected); +} + +/* ---------------- Hyperlink Button ---------------- */ + +.rs-hyperlink-button { + background: none; + margin: 0px; + position: absolute; + top: 0px; + right: 0px; + height: 44px; + width: 44px; + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + font-size: 12px; + font-weight: 400; + color: var(--color-text-1); + padding: 13px; + cursor: pointer; + border: none; + outline: none; + pointer-events: all; +} + +.rs-hyperlink-button::after { + content: ''; + z-index: -1; + position: absolute; + right: 6px; + bottom: 6px; + display: block; + width: calc(100% - 12px); + height: calc(100% - 12px); + border-radius: var(--radius-1); + background-color: var(--color-background); + pointer-events: none; +} + +.rs-hyperlink-button:hover { + color: var(--color-selected); +} + +.rs-hyperlink-button:focus-visible { + color: var(--color-selected); +} + +.rs-hyperlink-button__icon { + width: 18px; + height: 18px; + background-color: currentColor; + pointer-events: none; +} + +/* ---------------- Geo shape ---------------- */ + +.rs-text-label { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + color: var(--color-text); + text-shadow: var(--rs-text-outline); + overflow: hidden; + line-height: inherit; +} + +.rs-text-label[data-isediting='true'] .rs-text-content { + opacity: 0; +} + +.rs-text-label[data-hastext='false'][data-isediting='false'] > .rs-text-label__inner { + width: 40px; + height: 40px; +} + +.rs-text-label__inner { + position: relative; + width: fit-content; + height: fit-content; + display: flex; + align-items: center; + justify-content: center; + pointer-events: all; + min-height: auto; +} + +.rs-text-label__inner > .rs-text { + position: relative; + top: 0px; + left: 0px; + padding: 16px; + height: fit-content; + width: fit-content; + border-radius: var(--radius-1); + max-width: 100%; + z-index: 3; +} + +.rs-text-label__inner > .rs-text-input { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + padding: 16px; + z-index: 4; +} + +.rs-text-label[data-textwrap='true'] > .rs-text-label__inner { + max-width: 100%; +} + +.rs-text-label[data-isediting='true'] { + background-color: none; + min-height: auto; +} + +.rs-text-label[data-isediting='true'] p { + opacity: 0; +} + +.rs-text-label[data-align='start'] { + text-align: left; +} + +.rs-text-label[data-align='middle'] { + text-align: center; +} + +.rs-text-label[data-align='end'] { + text-align: right; +} + +.rs-arrow-hint { + stroke: var(--color-text-1); + fill: none; + stroke-linecap: round; + overflow: visible; +} + +.rs-arrow-label[data-font='draw'], +.rs-text-label[data-font='draw'] { + font-family: var(--rs-font-draw); +} + +.rs-arrow-label[data-font='sans'], +.rs-text-label[data-font='sans'] { + font-family: var(--rs-font-sans); +} + +.rs-arrow-label[data-font='serif'], +.rs-text-label[data-font='serif'] { + font-family: var(--rs-font-serif); +} + +.rs-arrow-label[data-font='mono'], +.rs-text-label[data-font='mono'] { + font-family: var(--rs-font-mono); +} + +/* ------------------- Arrow Shape ------------------ */ + +.rs-arrow-label { + position: absolute; + top: -1px; + left: -1px; + width: 2px; + height: 2px; + padding: 0px; + display: flex; + justify-content: center; + align-items: center; + color: var(--color-text); + text-shadow: var(--rs-text-outline); +} + +.rs-arrow-label[data-isediting='true'] p { + opacity: 0; +} + +.rs-arrow-label[data-isediting='true'] > .rs-arrow-label__inner { + background-color: var(--color-background); + border: calc(var(--rs-scale) * 1.5px) solid var(--color-selected); +} + +.rs-arrow-label__inner { + border-radius: var(--radius-1); + box-sizing: content-box; + position: relative; + height: max-content; + width: max-content; + pointer-events: all; + display: flex; + justify-content: center; + align-items: center; +} + +.rs-arrow-label p, +.rs-arrow-label textarea { + margin: 0px; + padding: 0px; + border: 0px; + color: inherit; + caret-color: var(--color-text); + background: none; + border-image: none; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit; + font-variant: inherit; + font-style: inherit; + text-align: inherit; + letter-spacing: inherit; + text-shadow: inherit; + outline: none; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + pointer-events: all; + text-rendering: auto; + text-transform: none; + text-indent: 0px; + display: inline-block; + appearance: auto; + column-count: initial !important; + writing-mode: horizontal-tb !important; + word-spacing: 0px; +} + +.rs-arrow-label p { + position: relative; + height: max-content; + z-index: 2; + padding: 4px; + overflow: visible; +} + +.rs-arrow-label textarea { + z-index: 3; + margin: 0px; + padding: 4px; + height: 100%; + width: 100%; + position: absolute; + resize: none; + border: 0px; + user-select: all; + -webkit-user-select: text; + caret-color: var(--color-text); + border-image: none; + /* Don't allow textarea to be zero width */ + min-width: 4px; +} + +/* -------------------- NoteShape ------------------- */ + +.rs-note__container { + position: relative; + width: 100%; + height: 100%; + border-radius: var(--radius-2); + box-shadow: var(--shadow-1); + overflow: hidden; + border-color: currentColor; + border-style: solid; + border-width: 1px; +} + +.rs-note__container .rs-text-label { + text-shadow: none; +} + +.rs-note__scrim { + position: absolute; + z-index: 1; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + background-color: var(--color-background); + opacity: 0.28; +} + +.rs-loading { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: var(--space-2); + font-size: 14px; + font-weight: 500; + opacity: 0; + animation: fade-in 0.2s ease-in-out forwards; + animation-delay: 0.2s; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +/* -------------------- FrameShape ------------------- */ + +.rs-frame__body { + fill: var(--palette-solid); + stroke: var(--color-text); + stroke-width: calc(1px * var(--rs-scale)); +} + +.rs-frame__background { + border-style: solid; + border-width: calc(1px * var(--rs-scale)); + border-color: currentColor; + background-color: var(--palette-solid); + border-radius: calc(var(--radius-1) * var(--rs-scale)); + width: 100%; + height: 100%; + z-index: 2; + position: absolute; + pointer-events: none; +} + +.rs-frame__hitarea { + border-style: solid; + border-width: calc(8px * var(--rs-scale)); + border-color: transparent; + background: none; + pointer-events: stroke; + box-sizing: border-box; + top: calc(-8px * var(--rs-scale)); + left: calc(-8px * var(--rs-scale)); + width: calc(100% + calc(16px * var(--rs-scale))); + height: calc(100% + calc(16px * var(--rs-scale))); + z-index: 1; + position: absolute; +} + +.rs-frame-heading { + display: flex; + align-items: center; + position: absolute; + transform-origin: 0% 100%; + overflow: hidden; + max-width: 100%; + min-width: 32px; + height: auto; + font-size: 12px; + padding-bottom: 4px; + pointer-events: all; +} + +.rs-frame-heading-hit-area { + pointer-events: all; + /* scale from bottom left corner so we can pin it to the top left corner of the frame */ + transform-origin: 0% 100%; + display: flex; + height: 100%; + width: 100%; + align-items: center; + border-radius: var(--radius-1); + background-color: var(--color-background); +} + +.rs-frame-label { + pointer-events: all; + overflow: hidden; + text-overflow: ellipsis; + padding: var(--space-3) var(--space-3); + position: relative; + font-size: inherit; + white-space: pre; +} + +.rs-frame-label__editing { + color: transparent; + outline: 1.5px solid var(--color-selection-stroke); + white-space: pre; + width: auto; + overflow: visible; + background-color: var(--color-panel); + border-radius: var(--radius-1); +} + +.rs-frame-name-input { + position: absolute; + top: 0px; + left: 0px; + border: none; + background: none; + outline: none; + padding: var(--space-3) var(--space-3); + inset: 0px; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + width: 100%; + color: var(--color-text-1); + border-radius: var(--radius-1); + user-select: all; + -webkit-user-select: text; + white-space: pre; +} + +/* If mobile use 16px as font size */ +/* On iOS, font size under 16px in an input will make the page zoom into the input 🤦‍♂️ */ +/* https://css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom/ */ +@media (max-width: 600px) { + .rs-frame-heading { + font-size: 16px; + } +} + +/* ------------------ iFrames Detail ----------------- */ + +.rs-embed { + border: none; + border-radius: var(--radius-2); +} + +/* ------------------- Code Editor ------------------ */ + +.rs-image__button { + padding: 4px 8px; + color: var(--color-text); + background-color: var(--color-panel); + border-radius: var(--radius-2); + box-shadow: var(--shadow-1); + pointer-events: all; + cursor: pointer; + outline: none; + display: flex; +} + +.rs-image__button:disabled { + opacity: 0.5; + pointer-events: none; +} + +.rs-image__toolbox { + position: absolute; + top: 0px; + left: 0px; + display: flex; + justify-content: flex-end; + align-items: flex-end; + padding: 10px; +} + +.rs-image__toolbox__hidden { + display: none; +} + +/* -------------- Shape Error Boundary -------------- */ + +.rs-shape-error-boundary { + width: 100%; + height: 100%; + background-color: var(--color-muted-1); + border-width: calc(1px * var(--rs-scale)); + border-color: var(--color-muted-1); + border-style: solid; + border-radius: calc(var(--radius-1) * var(--rs-scale)); + display: flex; + align-items: center; + justify-content: center; + position: relative; + pointer-events: all; + overflow: hidden; + padding: var(--space-2); +} + +.rs-shape-error-boundary::after { + transform: scale(var(--rs-scale)); + content: 'Error'; + font-size: 12px; + font-family: inherit; + color: var(--color-text-0); +} + +/* ----------------- Error Boundary ----------------- */ + +.rs-error-boundary { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-4); + background-color: var(--color-background); + color: var(--color-text-1); + position: absolute; + z-index: var(--layer-dialogs); +} + +.rs-error-boundary__overlay { + position: absolute; + inset: 0px; + background-color: var(--color-overlay); +} + +.rs-error-boundary__content * { + user-select: all; + -webkit-user-select: text; + pointer-events: all; +} + +.rs-error-boundary__canvas { + pointer-events: none; + position: absolute; + inset: 0; + z-index: -1; +} +/* some browsers seem to have some weird interactions between stacking contexts +and pointer-events. this ::after pseudo element covers the canvas and prevents +it from receiving any pointer events or affecting the cursor. */ +.rs-error-boundary__canvas::after { + content: ' '; + display: block; + position: absolute; + inset: 0; + z-index: var(--layer-dialogs); + pointer-events: auto; +} + +.rs-error-boundary__content { + width: fit-content; + height: fit-content; + max-width: 100%; + width: 400px; + max-height: 100%; + background-color: var(--color-panel); + padding: var(--space-6); + border-radius: var(--radius-4); + box-shadow: var(--shadow-2); + font-size: 14px; + font-weight: 400; + display: flex; + flex-direction: column; + gap: var(--space-5); + overflow: auto; +} +.rs-error-boundary__content__expanded { + width: 600px; +} + +.rs-error-boundary__content h2 { + font-size: 16px; + margin: 0px; + font-weight: 500; +} + +.rs-error-boundary__content p { + line-height: 1.5; + margin: 0; +} + +.rs-error-boundary__content pre { + background-color: var(--color-muted-2); + padding: var(--space-5); + border-radius: var(--radius-2); + overflow: auto; + font-size: 12px; + max-height: 320px; + margin: 0; +} + +.rs-error-boundary__content button { + background: none; + border: none; + font-family: inherit; + font-size: 14px; + font-weight: 500; + padding: var(--space-4); + border-radius: var(--radius-3); + cursor: pointer; + color: inherit; + background-color: transparent; +} +.rs-error-boundary__content button:hover { + background-color: var(--color-low); +} + +.rs-error-boundary__content a { + color: var(--color-text-1); + font-weight: 500; + text-decoration: none; +} +.rs-error-boundary__content a:hover { + color: var(--color-text-2); +} + +.rs-error-boundary__content__error { + position: relative; +} +.rs-error-boundary__content__error button { + position: absolute; + top: var(--space-2); + right: var(--space-2); + font-size: 12px; + padding: var(--space-2) var(--space-3); + background-color: var(--color-panel); + border-radius: var(--radius-1); +} + +.rs-error-boundary__content__actions { + display: flex; + justify-content: space-between; + gap: var(--space-4); + margin: calc(var(--space-4) * -1); + margin-top: 0; +} +.rs-error-boundary__content__actions__group { + display: flex; + gap: var(--space-4); +} +.rs-error-boundary__content .rs-error-boundary__reset { + color: var(--color-warn); +} +.rs-error-boundary__content .rs-error-boundary__refresh { + background-color: var(--color-primary); + color: var(--color-selected-contrast); +} +.rs-error-boundary__content .rs-error-boundary__refresh:hover { + background-color: var(--color-primary); + opacity: 0.9; +} + +/* --------------------- Coarse --------------------- */ + +@media screen and (pointer: coarse) { + /* If mobile always show handle-hint as there is no hover state */ + .rs-canvas__mobile .rs-handle-hint { + opacity: 1; + } + + .rs-canvas__mobile .rs-handle-bg { + r: calc(20px / var(--rs-zoom)); + } +} + +.rs-hidden { + opacity: 0; + pointer-events: none; +} + +.debug__ui-logger { + position: absolute; + top: 62px; + left: 16px; + color: #555; + font-size: 12px; + font-family: monospace; +} diff --git a/packages/editor/package.json b/packages/editor/package.json new file mode 100644 index 000000000..fdbefc633 --- /dev/null +++ b/packages/editor/package.json @@ -0,0 +1,111 @@ +{ + "name": "@tldraw/editor", + "description": "A tiny little drawing app (editor).", + "version": "2.0.0-alpha.12", + "packageManager": "yarn@3.5.0", + "author": { + "name": "tldraw GB Ltd.", + "email": "hello@tldraw.com" + }, + "homepage": "https://tldraw.dev", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/tldraw/tldraw" + }, + "bugs": { + "url": "https://github.com/tldraw/tldraw/issues" + }, + "keywords": [ + "tldraw", + "drawing", + "app", + "development", + "whiteboard", + "canvas", + "infinite" + ], + "/* NOTE */": "These `main` and `types` fields are rewritten by the build script. They are not the actual values we publish", + "main": "./src/index.ts", + "types": "./.tsbuild/index.d.ts", + "style": "./editor.css", + "/* GOTCHA */": "files will include ./dist and index.d.ts by default, add any others you want to include in here", + "files": [ + "editor.css" + ], + "scripts": { + "test": "lazy inherit", + "benchmark": "yarn run -T tsx ./scripts/benchmark.ts", + "test:coverage": "lazy inherit", + "build": "lazy build:package", + "build:package": "yarn run -T tsx ../../scripts/build-package.ts", + "build:api": "yarn run -T tsx ../../scripts/build-api.ts", + "prepack": "yarn run -T tsx ../../scripts/prepack.ts", + "postpack": "../../scripts/postpack.sh", + "pack-tarball": "yarn pack", + "lint": "yarn run -T tsx ../../scripts/lint.ts" + }, + "dependencies": { + "@tldraw/primitives": "workspace:*", + "@tldraw/tlschema": "workspace:*", + "@tldraw/tlstore": "workspace:*", + "@tldraw/tlvalidate": "workspace:*", + "@tldraw/utils": "workspace:*", + "@use-gesture/react": "^10.2.24", + "classnames": "^2.3.2", + "escape-string-regexp": "^5.0.0", + "eventemitter3": "^4.0.7", + "is-plain-object": "^5.0.0", + "lodash.throttle": "^4.1.1", + "lodash.uniq": "^4.5.0", + "nanoid": "^3.0.0" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18", + "signia": "*", + "signia-react": "*" + }, + "devDependencies": { + "@peculiar/webcrypto": "^1.4.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^14.0.0", + "@types/benchmark": "^2.1.2", + "@types/lodash.throttle": "^4.1.7", + "@types/lodash.uniq": "^4.5.7", + "@types/react-test-renderer": "^18.0.0", + "@types/wicg-file-system-access": "^2020.9.5", + "benchmark": "^2.1.4", + "fake-indexeddb": "^4.0.0", + "gzip-size": "^7.0.0", + "jest-canvas-mock": "^2.4.0", + "jest-environment-jsdom": "^29.4.3", + "lazyrepo": "0.0.0-alpha.20", + "react-test-renderer": "^18.2.0", + "resize-observer-polyfill": "^1.5.1" + }, + "jest": { + "preset": "config/jest/node", + "testEnvironment": "jsdom", + "fakeTimers": { + "enableGlobally": true + }, + "testPathIgnorePatterns": [ + "^.+\\.*.css$" + ], + "transformIgnorePatterns": [ + "node_modules/(?!(nanoid|escape-string-regexp)/)" + ], + "moduleNameMapper": { + "^~(.*)": "/src/$1", + "\\.(css|less|scss|sass)$": "identity-obj-proxy" + }, + "setupFiles": [ + "raf/polyfill", + "/setupTests.js" + ], + "setupFilesAfterEnv": [ + "../../config/setupJest.ts" + ] + } +} diff --git a/packages/editor/setupTests.js b/packages/editor/setupTests.js new file mode 100644 index 000000000..23a09c1c2 --- /dev/null +++ b/packages/editor/setupTests.js @@ -0,0 +1,13 @@ +require('fake-indexeddb/auto') +global.ResizeObserver = require('resize-observer-polyfill') +global.crypto = new (require('@peculiar/webcrypto').Crypto)() +global.FontFace = class FontFace { + load() { + return Promise.resolve() + } +} +document.fonts = { + add: () => {}, + delete: () => {}, + forEach: () => {}, +} diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts new file mode 100644 index 000000000..08b24dcf5 --- /dev/null +++ b/packages/editor/src/index.ts @@ -0,0 +1,267 @@ +// Important! don't move this tlschema re-export to lib/index.ts, doing so causes esbuild to produce +// incorrect output. https://github.com/evanw/esbuild/issues/1737 + +// eslint-disable-next-line local/no-export-star +export * from '@tldraw/tlschema' +export { getHashForString } from '@tldraw/utils' +export { + ErrorScreen, + LoadingScreen, + TldrawEditor, + type TldrawEditorProps, +} from './lib/TldrawEditor' +export { + App, + isShapeWithHandles, + type AnimationOptions, + type AppOptions, + type TLChange, +} from './lib/app/App' +export { TLArrowShapeDef, TLArrowUtil } from './lib/app/shapeutils/TLArrowUtil/TLArrowUtil' +export { + TLBookmarkShapeDef, + TLBookmarkUtil, +} from './lib/app/shapeutils/TLBookmarkUtil/TLBookmarkUtil' +export { TLBoxUtil } from './lib/app/shapeutils/TLBoxUtil' +export { TLDrawShapeDef, TLDrawUtil } from './lib/app/shapeutils/TLDrawUtil/TLDrawUtil' +export { TLEmbedShapeDef, TLEmbedUtil } from './lib/app/shapeutils/TLEmbedUtil/TLEmbedUtil' +export { TLFrameShapeDef, TLFrameUtil } from './lib/app/shapeutils/TLFrameUtil/TLFrameUtil' +export { TLGeoShapeDef, TLGeoUtil } from './lib/app/shapeutils/TLGeoUtil/TLGeoUtil' +export { TLGroupShapeDef, TLGroupUtil } from './lib/app/shapeutils/TLGroupUtil/TLGroupUtil' +export { TLImageShapeDef, TLImageUtil } from './lib/app/shapeutils/TLImageUtil/TLImageUtil' +export { + TLLineShapeDef, + TLLineUtil, + getSplineForLineShape, +} from './lib/app/shapeutils/TLLineUtil/TLLineUtil' +export { TLNoteShapeDef, TLNoteUtil } from './lib/app/shapeutils/TLNoteUtil/TLNoteUtil' +export { + TLShapeUtil, + type OnBeforeCreateHandler, + type OnBeforeUpdateHandler, + type OnBindingChangeHandler, + type OnChildrenChangeHandler, + type OnClickHandler, + type OnDoubleClickHandleHandler, + type OnDoubleClickHandler, + type OnDragHandler, + type OnEditEndHandler, + type OnHandleChangeHandler, + type OnResizeEndHandler, + type OnResizeHandler, + type OnResizeStartHandler, + type OnRotateEndHandler, + type OnRotateHandler, + type OnRotateStartHandler, + type OnTranslateEndHandler, + type OnTranslateHandler, + type OnTranslateStartHandler, + type TLResizeMode, + type TLShapeUtilConstructor, + type TLShapeUtilFlag, +} from './lib/app/shapeutils/TLShapeUtil' +export { TLTextShapeDef, TLTextUtil } from './lib/app/shapeutils/TLTextUtil/TLTextUtil' +export { TLVideoShapeDef, TLVideoUtil } from './lib/app/shapeutils/TLVideoUtil/TLVideoUtil' +export { StateNode, type StateNodeConstructor } from './lib/app/statechart/StateNode' +export { TLBoxTool, type TLBoxLike } from './lib/app/statechart/TLBoxTool/TLBoxTool' +export { type ClipboardPayload, type TLClipboardModel } from './lib/app/types/clipboard-types' +export { + EVENT_NAME_MAP, + type TLBaseEventInfo, + type TLCLickEventName, + type TLCancelEvent, + type TLCancelEventInfo, + type TLClickEvent, + type TLClickEventInfo, + type TLCompleteEvent, + type TLCompleteEventInfo, + type TLEventHandlers, + type TLEventInfo, + type TLEventName, + type TLInterruptEvent, + type TLInterruptEventInfo, + type TLKeyboardEvent, + type TLKeyboardEventInfo, + type TLKeyboardEventName, + type TLPinchEvent, + type TLPinchEventInfo, + type TLPinchEventName, + type TLPointerEvent, + type TLPointerEventInfo, + type TLPointerEventName, + type TLPointerEventTarget, + type TLTickEvent, + type TLWheelEvent, + type TLWheelEventInfo, + type UiEnterHandler, + type UiEvent, + type UiEventType, + type UiExitHandler, +} from './lib/app/types/event-types' +export { + type TLCommand, + type TLCommandHandler, + type TLHistoryEntry, + type TLMark, +} from './lib/app/types/history-types' +export { type RequiredKeys, type TLEasingType } from './lib/app/types/misc-types' +export { type TLReorderOperation } from './lib/app/types/reorder-types' +export { type TLResizeHandle, type TLSelectionHandle } from './lib/app/types/selection-types' +export { defaultEditorAssetUrls, type EditorAssetUrls } from './lib/assetUrls' +export { Canvas } from './lib/components/Canvas' +export { DefaultErrorFallback } from './lib/components/DefaultErrorFallback' +export { + ErrorBoundary, + OptionalErrorBoundary, + type ErrorBoundaryProps, +} from './lib/components/ErrorBoundary' +export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer' +export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer' +export { + type ErrorSyncedStore, + type InitializingSyncedStore, + type ReadySyncedStore, + type SyncedStore, +} from './lib/config/SyncedStore' +export { + defineShape, + type TLShapeDef, + type TLUnknownShapeDef, +} from './lib/config/TLShapeDefinition' +export { TldrawEditorConfig } from './lib/config/TldrawEditorConfig' +export { + ANIMATION_MEDIUM_MS, + ANIMATION_SHORT_MS, + ARROW_LABEL_FONT_SIZES, + BOUND_ARROW_OFFSET, + DEFAULT_ANIMATION_OPTIONS, + DEFAULT_BOOKMARK_HEIGHT, + DEFAULT_BOOKMARK_WIDTH, + DOUBLE_CLICK_DURATION, + DRAG_DISTANCE, + FONT_ALIGNMENT, + FONT_FAMILIES, + FONT_SIZES, + GRID_INCREMENT, + GRID_STEPS, + HAND_TOOL_FRICTION, + HASH_PATERN_ZOOM_NAMES, + ICON_SIZES, + LABEL_FONT_SIZES, + MAJOR_NUDGE_FACTOR, + MAX_ASSET_HEIGHT, + MAX_ASSET_WIDTH, + MAX_PAGES, + MAX_SHAPES_PER_PAGE, + MAX_ZOOM, + MINOR_NUDGE_FACTOR, + MIN_ARROW_LENGTH, + MIN_ZOOM, + MULTI_CLICK_DURATION, + REMOVE_SYMBOL, + RICH_TYPES, + ROTATING_SHADOWS, + STYLES, + SVG_PADDING, + TEXT_PROPS, + WAY_TOO_BIG_ARROW_BEND_FACTOR, + ZOOMS, +} from './lib/constants' +export { normalizeWheel } from './lib/hooks/shared' +export { useApp } from './lib/hooks/useApp' +export { useContainer } from './lib/hooks/useContainer' +export type { TLEditorComponents } from './lib/hooks/useEditorComponents' +export { useQuickReactor } from './lib/hooks/useQuickReactor' +export { useReactor } from './lib/hooks/useReactor' +export { useUrlState } from './lib/hooks/useUrlState' +export { WeakMapCache } from './lib/utils/WeakMapCache' +export { + ACCEPTED_ASSET_TYPE, + ACCEPTED_IMG_TYPE, + ACCEPTED_VID_TYPE, + containBoxSize, + createAssetShapeAtPoint, + createBookmarkShapeAtPoint, + createEmbedShapeAtPoint, + createShapesFromFiles, + dataUrlToFile, + getFileMetaData, + getImageSizeFromSrc, + getMediaAssetFromFile, + getResizedImageDataUrl, + getValidHttpURLList, + getVideoSizeFromSrc, + isImage, + isSvgText, + isValidHttpURL, +} from './lib/utils/assets' +export { buildFromV1Document, type LegacyTldrawDocument } from './lib/utils/buildFromV1Document' +export { + checkFlag, + fileToBase64, + getIncrementedName, + isSerializable, + snapToGrid, + uniqueId, +} from './lib/utils/data' +export { debugFlags } from './lib/utils/debug-flags' +export { + loopToHtmlElement, + preventDefault, + releasePointerCapture, + rotateBoxShadow, + setPointerCapture, + truncateStringWithEllipsis, + usePrefersReducedMotion, +} from './lib/utils/dom' +export { + getEmbedInfo, + getEmbedInfoUnsafely, + matchEmbedUrl, + matchUrl, + type EmbedResult, +} from './lib/utils/embeds' +export { + downloadDataURLAsFile, + getSvgAsDataUrl, + getSvgAsDataUrlSync, + getSvgAsImage, + getSvgAsString, + getTextBoundingBox, + isGeoShape, + isNoteShape, + type TLCopyType, + type TLExportType, +} from './lib/utils/export' +export { hardResetApp } from './lib/utils/hard-reset' +export { isAnimated, isGIF } from './lib/utils/is-gif-animated' +export { setPropsForNextShape } from './lib/utils/props-for-next-shape' +export { refreshPage } from './lib/utils/refresh-page' +export { + getIndexAbove, + getIndexBelow, + getIndexBetween, + getIndexGenerator, + getIndices, + getIndicesAbove, + getIndicesBelow, + getIndicesBetween, + getMaxIndex, + indexGenerator, + sortById, + sortByIndex, +} from './lib/utils/reordering/reordering' +export { + applyRotationToSnapshotShapes, + getRotationSnapshot, + type RotationSnapshot, +} from './lib/utils/rotation' +export { runtime, setRuntimeOverrides } from './lib/utils/runtime' +export { + blobAsString, + correctSpacesToNbsp, + dataTransferItemAsString, + defaultEmptyAs, +} from './lib/utils/string' +export { getPointerInfo, getSvgPathFromStroke, getSvgPathFromStrokePoints } from './lib/utils/svg' +export { openWindow } from './lib/utils/window-open' diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx new file mode 100644 index 000000000..cafb9f827 --- /dev/null +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -0,0 +1,325 @@ +import { TLAsset, TLInstance, TLInstanceId, TLStore, TLUser, TLUserId } from '@tldraw/tlschema' +import { Store } from '@tldraw/tlstore' +import { annotateError } from '@tldraw/utils' +import React, { useCallback, useSyncExternalStore } from 'react' +import { App } from './app/App' +import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls' +import { OptionalErrorBoundary } from './components/ErrorBoundary' + +import { SyncedStore } from './config/SyncedStore' +import { TldrawEditorConfig } from './config/TldrawEditorConfig' + +import { DefaultErrorFallback } from './components/DefaultErrorFallback' +import { AppContext } from './hooks/useApp' +import { ContainerProvider, useContainer } from './hooks/useContainer' +import { useCursor } from './hooks/useCursor' +import { useDarkMode } from './hooks/useDarkMode' +import { + EditorComponentsProvider, + TLEditorComponents, + useEditorComponents, +} from './hooks/useEditorComponents' +import { useEvent } from './hooks/useEvent' +import { useForceUpdate } from './hooks/useForceUpdate' +import { usePreloadAssets } from './hooks/usePreloadAssets' +import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix' +import { useZoomCss } from './hooks/useZoomCss' + +/** @public */ +export interface TldrawEditorProps { + children?: any + /** Overrides for the tldraw components */ + components?: Partial + /** Whether to display the dark mode. */ + isDarkMode?: boolean + /** A configuration defining major customizations to the app, such as custom shapes and new tools */ + config?: TldrawEditorConfig + /** + * Called when the app has mounted. + * + * @example + * + * ```ts + * function TldrawEditor() { + * return app.selectAll()} /> + * } + * ``` + * + * @param app - The app instance. + */ + onMount?: (app: App) => void + /** + * Called when the app generates a new asset from a file, such as when an image is dropped into + * the canvas. + * + * @example + * + * ```ts + * const app = new App({ + * onCreateAssetFromFile: (file) => uploadFileAndCreateAsset(file), + * }) + * ``` + * + * @param file - The file to generate an asset from. + * @param id - The id to be assigned to the resulting asset. + */ + onCreateAssetFromFile?: (file: File) => Promise + + /** + * Called when a URL is converted to a bookmark. This callback should return the metadata for the + * bookmark. + * + * @example + * + * ```ts + * app.onCreateBookmarkFromUrl(url, id) + * ``` + * + * @param url - The url that was created. + * @public + */ + onCreateBookmarkFromUrl?: ( + url: string + ) => Promise<{ image: string; title: string; description: string }> + + /** + * The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading + * from a server or database. + */ + store?: TLStore | SyncedStore + /** The id of the current user. If not given, one will be generated. */ + userId?: TLUserId + /** + * The id of the app instance (e.g. a browser tab if the app will have only one tldraw app per + * tab). If not given, one will be generated. + */ + instanceId?: TLInstanceId + /** Asset URLs */ + assetUrls?: EditorAssetUrls + /** Whether to automatically focus the editor when it mounts. */ + autoFocus?: boolean +} + +declare global { + interface Window { + tldrawReady: boolean + } +} + +/** @public */ +export function TldrawEditor(props: TldrawEditorProps) { + const [container, setContainer] = React.useState(null) + const { components, ...rest } = props + + const ErrorFallback = + components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback + + return ( +

+ : null} + onError={(error) => annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })} + > + {container && ( + + + + + + )} + +
+ ) +} + +function TldrawEditorBeforeLoading({ + config = TldrawEditorConfig.default, + userId, + instanceId, + store, + ...props +}: TldrawEditorProps) { + const { done: preloadingComplete, error: preloadingError } = usePreloadAssets( + props.assetUrls ?? defaultEditorAssetUrls + ) + + store ??= config.createStore({ + userId: userId ?? TLUser.createId(), + instanceId: instanceId ?? TLInstance.createId(), + }) + + let loadedStore + if (!(store instanceof Store)) { + if (store.error) { + // for error handling, we fall back to the default error boundary. + // if users want to handle this error differently, they can render + // their own error screen before the TldrawEditor component + throw store.error + } + if (!store.store) { + return Connecting... + } + + loadedStore = store.store + } else { + loadedStore = store + } + + if (instanceId && loadedStore.props.instanceId !== instanceId) { + console.error( + `The store's instanceId (${loadedStore.props.instanceId}) does not match the instanceId prop (${instanceId}). This may cause unexpected behavior.` + ) + } + + if (userId && loadedStore.props.userId !== userId) { + console.error( + `The store's userId (${loadedStore.props.userId}) does not match the userId prop (${userId}). This may cause unexpected behavior.` + ) + } + + if (preloadingError) { + return Could not load assets. Please refresh the page. + } + + if (!preloadingComplete) { + return Loading assets... + } + + return +} + +function TldrawEditorAfterLoading({ + onMount, + config, + isDarkMode, + children, + onCreateAssetFromFile, + onCreateBookmarkFromUrl, + store, + autoFocus, +}: Omit & { + config: TldrawEditorConfig + store: TLStore +}) { + const container = useContainer() + + const [app, setApp] = React.useState(null) + const { ErrorFallback } = useEditorComponents() + + React.useLayoutEffect(() => { + const app = new App({ + store, + getContainer: () => container, + config, + }) + setApp(app) + + if (autoFocus) { + app.focus() + } + ;(window as any).app = app + return () => { + app.dispose() + setApp((prevApp) => (prevApp === app ? null : prevApp)) + } + }, [container, config, store, autoFocus]) + + React.useEffect(() => { + if (app) { + // Overwrite the default onCreateAssetFromFile handler. + if (onCreateAssetFromFile) { + app.onCreateAssetFromFile = onCreateAssetFromFile + } + + if (onCreateBookmarkFromUrl) { + app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl + } + } + }, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl]) + + const onMountEvent = useEvent((app: App) => onMount?.(app)) + React.useEffect(() => { + if (app) { + // Set the initial theme state. + if (isDarkMode !== undefined) { + app.updateUserDocumentSettings({ isDarkMode }) + } + + // Run onMount + window.tldrawReady = true + onMountEvent(app) + } + }, [app, onMountEvent, isDarkMode]) + + const crashingError = useSyncExternalStore( + useCallback( + (onStoreChange) => { + if (app) { + app.on('crash', onStoreChange) + return () => app.off('crash', onStoreChange) + } + return () => { + // noop + } + }, + [app] + ), + () => app?.crashingError ?? null + ) + + if (!app) { + return null + } + + return ( + // the top-level tldraw component also renders an error boundary almost + // identical to this one. the reason we have two is because this one has + // access to `App`, which means that here we can enrich errors with data + // from app for reporting, and also still attempt to render the user's + // document in the event of an error to reassure them that their work is + // not lost. + : null} + onError={(error) => app.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })} + > + {crashingError ? ( + + ) : ( + + {children} + + )} + + ) +} + +function Layout({ children }: { children: any }) { + useZoomCss() + useCursor() + useDarkMode() + useSafariFocusOutFix() + useForceUpdate() + + return children +} + +function Crash({ crashingError }: { crashingError: unknown }): null { + throw crashingError +} + +/** @public */ +export function LoadingScreen({ children }: { children: any }) { + const { Spinner } = useEditorComponents() + + return ( +
+ {Spinner ? : null} + {children} +
+ ) +} + +/** @public */ +export function ErrorScreen({ children }: { children: any }) { + return
{children}
+} diff --git a/packages/editor/src/lib/app/App.ts b/packages/editor/src/lib/app/App.ts new file mode 100644 index 000000000..b53fa7c66 --- /dev/null +++ b/packages/editor/src/lib/app/App.ts @@ -0,0 +1,8723 @@ +import { + approximately, + areAnglesCompatible, + Box2d, + clamp, + EASINGS, + intersectPolygonPolygon, + MatLike, + Matrix2d, + Matrix2dModel, + PI2, + pointInPolygon, + Vec2d, + VecLike, +} from '@tldraw/primitives' +import { + Box2dModel, + createCustomShapeId, + createShapeId, + isShape, + isShapeId, + TLArrowShape, + TLAsset, + TLAssetId, + TLAssetPartial, + TLCamera, + TLColorStyle, + TLColorType, + TLCursor, + TLCursorType, + TLDOCUMENT_ID, + TLFrameShape, + TLGroupShape, + TLImageAsset, + TLInstance, + TLInstanceId, + TLInstancePageState, + TLNullableShapeProps, + TLPage, + TLPageId, + TLParentId, + TLRecord, + TLScribble, + TLShape, + TLShapeId, + TLShapePartial, + TLShapeProp, + TLShapeType, + TLSizeStyle, + TLStore, + TLUnknownShape, + TLUser, + TLUserDocument, + TLUserId, + TLVideoAsset, + Vec2dModel, +} from '@tldraw/tlschema' +import { ComputedCache, HistoryEntry } from '@tldraw/tlstore' +import { annotateError, compact, dedupe, deepCopy, partition, structuredClone } from '@tldraw/utils' +import { EventEmitter } from 'eventemitter3' +import { nanoid } from 'nanoid' +import { atom, computed, EMPTY_ARRAY, transact } from 'signia' +import { TldrawEditorConfig } from '../config/TldrawEditorConfig' +import { TLShapeDef } from '../config/TLShapeDefinition' +import { + ANIMATION_MEDIUM_MS, + BLACKLISTED_PROPS, + DEFAULT_ANIMATION_OPTIONS, + DRAG_DISTANCE, + FOLLOW_CHASE_PAN_SNAP, + FOLLOW_CHASE_PAN_UNSNAP, + FOLLOW_CHASE_PROPORTION, + FOLLOW_CHASE_ZOOM_SNAP, + FOLLOW_CHASE_ZOOM_UNSNAP, + GRID_INCREMENT, + HAND_TOOL_FRICTION, + MAJOR_NUDGE_FACTOR, + MAX_PAGES, + MAX_SHAPES_PER_PAGE, + MAX_ZOOM, + MIN_ZOOM, + MINOR_NUDGE_FACTOR, + STYLES, + SVG_PADDING, + ZOOMS, +} from '../constants' +import { exportPatternSvgDefs } from '../hooks/usePattern' +import { dataUrlToFile, getMediaAssetFromFile } from '../utils/assets' +import { getIncrementedName, uniqueId } from '../utils/data' +import { setPropsForNextShape } from '../utils/props-for-next-shape' +import { + getIndexAbove, + getIndexBetween, + getIndices, + getIndicesAbove, + getIndicesBetween, + sortById, + sortByIndex, +} from '../utils/reordering/reordering' +import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation' +import { WeakMapCache } from '../utils/WeakMapCache' +import { arrowBindingsIndex } from './derivations/arrowBindingsIndex' +import { parentsToChildrenWithIndexes } from './derivations/parentsToChildrenWithIndexes' +import { shapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage' +import { ActiveAreaManager, getActiveAreaScreenSpace } from './managers/ActiveAreaManager' +import { CameraManager } from './managers/CameraManager' +import { ClickManager } from './managers/ClickManager' +import { DprManager } from './managers/DprManager' +import { HistoryManager } from './managers/HistoryManager' +import { SnapManager } from './managers/SnapManager' +import { TextManager } from './managers/TextManager' +import { TickManager } from './managers/TickManager' +import { TLExportColors } from './shapeutils/shared/TLExportColors' +import { getCurvedArrowInfo } from './shapeutils/TLArrowUtil/arrow/curved-arrow' +import { + getArrowTerminalsInArrowSpace, + getIsArrowStraight, +} from './shapeutils/TLArrowUtil/arrow/shared' +import { getStraightArrowInfo } from './shapeutils/TLArrowUtil/arrow/straight-arrow' +import { TLArrowShapeDef } from './shapeutils/TLArrowUtil/TLArrowUtil' +import { TLFrameShapeDef } from './shapeutils/TLFrameUtil/TLFrameUtil' +import { TLGroupShapeDef } from './shapeutils/TLGroupUtil/TLGroupUtil' +import { TLResizeMode, TLShapeUtil } from './shapeutils/TLShapeUtil' +import { TLTextShapeDef } from './shapeutils/TLTextUtil/TLTextUtil' +import { RootState } from './statechart/RootState' +import { StateNode } from './statechart/StateNode' +import { TLClipboardModel } from './types/clipboard-types' +import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types' +import { RequiredKeys } from './types/misc-types' +import { TLReorderOperation } from './types/reorder-types' +import { TLResizeHandle } from './types/selection-types' + +/** @public */ +export type TLChange = HistoryEntry + +/** @public */ +export type AnimationOptions = Partial<{ + duration: number + easing: typeof EASINGS.easeInOutCubic +}> + +/** @public */ +export type ViewportOptions = Partial<{ + /** Whether to animate the viewport change or not. Defaults to true. */ + stopFollowing: boolean +}> + +/** @public */ +export interface AppOptions { + /** + * The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading + * from a server or database. + */ + store: TLStore + /** A configuration defining major customizations to the app, such as custom shapes and new tools */ + config?: TldrawEditorConfig + /** + * Should return a containing html element which has all the styles applied to the app. If not + * given, the body element will be used. + */ + getContainer: () => HTMLElement + + /** The id of the current user. If not given, one will be generated. */ + userId?: TLUserId + /** + * The id of the app instance (e.g. a browser tab if the app will have only one tldraw app per + * tab). If not given, one will be generated. + */ + instanceId?: TLInstanceId +} + +/** @public */ +export function isShapeWithHandles(shape: TLShape) { + return shape.type === 'arrow' || shape.type === 'line' || shape.type === 'draw' +} + +/** @public */ +export class App extends EventEmitter { + constructor({ config = TldrawEditorConfig.default, store, getContainer }: AppOptions) { + super() + + if (store.schema !== config.storeSchema) { + throw new Error('Store schema does not match schema given to App') + } + + this.config = config + this.store = store + + this.getContainer = getContainer ?? (() => document.body) + + this.textMeasure = new TextManager(this) + + // Set the shape utils + this.shapeUtils = Object.fromEntries( + config.shapes.map((def) => [ + def.type, + def.createShapeUtils(this) as TLShapeUtil, + ]) + ) + + if (typeof window !== 'undefined' && 'navigator' in window) { + this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) + this.isIos = !!navigator.userAgent.match(/iPad/i) || !!navigator.userAgent.match(/iPhone/i) + this.isChromeForIos = /crios.*safari/i.test(navigator.userAgent) + } else { + this.isSafari = false + this.isIos = false + this.isChromeForIos = false + } + + // Set styles + this.colors = new Map(App.styles.color.map((c) => [c.id, `var(--palette-${c.id})`])) + + this.root = new RootState(this) + if (this.root.children) { + config.tools.forEach((Ctor) => { + this.root.children![Ctor.id] = new Ctor(this) + }) + } + + this.store.onBeforeDelete = (record) => { + if (record.typeName === 'shape') { + this._shapeWillBeDeleted(record) + } else if (record.typeName === 'page') { + this._pageWillBeDeleted(record) + } + } + + this.store.onAfterChange = (prev, next) => { + this._updateDepth++ + if (this._updateDepth > 1000) { + console.error('[onAfterChange] Maximum update depth exceeded, bailing out.') + } + if (prev.typeName === 'shape' && next.typeName === 'shape') { + this._shapeDidChange(prev, next) + } else if ( + prev.typeName === 'instance_page_state' && + next.typeName === 'instance_page_state' + ) { + this._tabStateDidChange(prev, next) + } + + this._updateDepth-- + } + this.store.onAfterCreate = (record) => { + if (record.typeName === 'shape' && TLArrowShapeDef.is(record)) { + this._arrowDidUpdate(record) + } + } + + this._shapeIds = shapeIdsInCurrentPage(this.store, () => this.currentPageId) + this._parentIdsToChildIds = parentsToChildrenWithIndexes(this.store) + + this.disposables.add( + this.store.listen((changes) => { + this.emit('change', changes as TLChange) + }) + ) + + const container = this.getContainer() + const focusin = () => { + this._isFocused.set(true) + } + const focusout = () => { + this._isFocused.set(false) + } + + container.addEventListener('focusin', focusin) + container.addEventListener('focus', focusin) + container.addEventListener('focusout', focusout) + container.addEventListener('blur', focusout) + + this.disposables.add(() => { + container.removeEventListener('focusin', focusin) + container.removeEventListener('focus', focusin) + container.removeEventListener('focusout', focusout) + container.removeEventListener('blur', focusout) + }) + + this.store.ensureStoreIsUsable() + + // clear ephemeral state + this.setInstancePageState( + { + editingId: null, + hoveredId: null, + erasingIds: [], + }, + true + ) + + this.root.enter(undefined, 'initial') + + if (this.instanceState.followingUserId) { + this.stopFollowingUser() + } + + this.updateCullingBounds() + + requestAnimationFrame(() => { + this._tickManager.start() + }) + } + + /** + * The editor's store + * + * @public + */ + readonly store: TLStore + + /** + * The editor's config + * + * @public + */ + readonly config: TldrawEditorConfig + + /** + * The root state of the statechart. + * + * @public + */ + readonly root: RootState + + /** + * A cache of shape ids in the current page. + * + * @internal + */ + private readonly _shapeIds: ReturnType + + /** + * A set of functions to call when the app is disposed. + * + * @public + */ + readonly disposables = new Set<() => void>() + + /** @internal */ + private _dprManager = new DprManager(this) + + /** @internal */ + private _cameraManager = new CameraManager(this) + + /** @internal */ + private _activeAreaManager = new ActiveAreaManager(this) + + /** @internal */ + private _tickManager = new TickManager(this) + + /** @internal */ + private _updateDepth = 0 + + /** + * A manager for the app's snapping feature. + * + * @public + */ + readonly snaps = new SnapManager(this) + + /** + * Whether the editor is running in Safari. + * + * @public + */ + readonly isSafari: boolean + + /** + * Whether the editor is running on iOS. + * + * @public + */ + readonly isIos: boolean + + /** + * Whether the editor is running on iOS. + * + * @public + */ + readonly isChromeForIos: boolean + + // Flags + + private _canMoveCamera = atom('can move camera', true) + + /** + * Set whether the editor's camera can move. + * + * @example + * + * ```ts + * app.canMoveCamera = false + * ``` + * + * @param canMove - Whether the camera can move. + * @public + */ + get canMoveCamera() { + return this._canMoveCamera.value + } + + set canMoveCamera(canMove: boolean) { + this._canMoveCamera.set(canMove) + } + + private _isFocused = atom('_isFocused', false) + + /** + * Whether or not the editor is focused. + * + * @public + */ + get isFocused() { + return this._isFocused.value + } + + /** + * The current HTML element containing the editor. + * + * @example + * + * ```ts + * const container = app.getContainer() + * ``` + * + * @public + */ + getContainer: () => HTMLElement + + /** + * The editor's userId (defined in its store.props). + * + * @example + * + * ```ts + * const userId = app.userId + * ``` + * + * @public + */ + get userId(): TLUserId { + return this.store.props.userId + } + + /** + * The editor's instanceId (defined in its store.props). + * + * @example + * + * ```ts + * const instanceId = app.instanceId + * ``` + * + * @public + */ + get instanceId(): TLInstanceId { + return this.store.props.instanceId + } + + /** @internal */ + annotateError( + error: unknown, + { + origin, + willCrashApp, + tags, + extras, + }: { + origin: string + willCrashApp: boolean + tags?: Record + extras?: Record + } + ) { + const defaultAnnotations = this.createErrorAnnotations(origin, willCrashApp) + annotateError(error, { + tags: { ...defaultAnnotations.tags, ...tags }, + extras: { ...defaultAnnotations.extras, ...extras }, + }) + if (willCrashApp) { + this.store.markAsPossiblyCorrupted() + } + } + + /** @internal */ + createErrorAnnotations( + origin: string, + willCrashApp: boolean | 'unknown' + ): { + tags: { origin: string; willCrashApp: boolean | 'unknown' } + extras: { + activeStateNode?: string + selectedShapes?: TLUnknownShape[] + editingShape?: TLUnknownShape + inputs?: Record + } + } { + try { + return { + tags: { + origin: origin, + willCrashApp, + }, + extras: { + activeStateNode: this.root.path.value, + selectedShapes: this.selectedShapes, + editingShape: this.editingId ? this.getShapeById(this.editingId) : undefined, + inputs: this.inputs, + }, + } + } catch { + return { + tags: { + origin: origin, + willCrashApp, + }, + extras: {}, + } + } + } + + /** @internal */ + private _crashingError: unknown | null = null + + /** + * We can't use an `atom` here because there's a chance that when `crashAndReportError` is called, + * we're in a transaction that's about to be rolled back due to the same error we're currently + * reporting. + * + * Instead, to listen to changes to this value, you need to listen to app's `crash` event. + * + * @internal + */ + get crashingError() { + return this._crashingError + } + + /** @internal */ + crash(error: unknown) { + this._crashingError = error + this.store.markAsPossiblyCorrupted() + this.emit('crash') + } + + get devicePixelRatio() { + return this._dprManager.dpr.value + } + + /** + * A set of strings representing any open menus or modals. + * + * @public + */ + openMenus = new Set() + + /** + * Get whether any menus are open. + * + * @public + */ + get isMenuOpen() { + return this.openMenus.size > 0 + } + + /** @internal */ + private _isCoarsePointer = atom('isCoarsePointer', false as any) + + /** + * Whether the user is using a "coarse" pointer, such as on a touch screen. + * + * @public + */ + get isCoarsePointer() { + return this._isCoarsePointer.value + } + + set isCoarsePointer(v) { + this._isCoarsePointer.set(v) + } + + /** @internal */ + private _isChangingStyle = atom('isChangingStyle', false as any) + + /** @internal */ + private _isChangingStyleTimeout = -1 as any + + /** + * Whether the user is currently changing the style of a shape. This may cause the UI to change. + * + * @example + * + * ```ts + * app.isChangingStyle = true + * ``` + * + * @public + */ + get isChangingStyle() { + return this._isChangingStyle.value + } + + set isChangingStyle(v) { + this._isChangingStyle.set(v) + // Clear any reset timeout + clearTimeout(this._isChangingStyleTimeout) + if (v) { + // If we've set to true, set a new reset timeout to change the value back to false after 2 seonds + this._isChangingStyleTimeout = setTimeout(() => (this.isChangingStyle = false), 2000) + } + } + + /** + * A cache of page transforms. + * + * @internal + */ + @computed private get _pageTransformCache(): ComputedCache { + return this.store.createComputedCache('pageTransformCache', (shape) => { + if (TLPage.isId(shape.parentId)) { + return this.getTransform(shape) + } + // some weird circular type thing here that I had to work wround with (as any) + const parent = (this._pageTransformCache as any).get(shape.parentId) + + return Matrix2d.Compose(parent, this.getTransform(shape)) + }) + } + + /** + * A cache of axis aligned page bounding boxes. + * + * @internal + */ + @computed private get _pageBoundsCache(): ComputedCache { + return this.store.createComputedCache('pageBoundsCache', (shape) => { + const pageTransform = this._pageTransformCache.get(shape.id) + + if (!pageTransform) return new Box2d() + + const result = Box2d.FromPoints( + Matrix2d.applyToPoints(pageTransform, this.getShapeUtil(shape).outline(shape)) + ) + + return result + }) + } + + /** + * A cache of page masks used for clipping. + * + * @internal + */ + @computed private get _pageMaskCache(): ComputedCache { + return this.store.createComputedCache('pageMaskCache', (shape) => { + if (TLPage.isId(shape.parentId)) { + return undefined + } + + const frameAncestors = this.getAncestorsById(shape.id).filter((s) => s.type === 'frame') + + if (frameAncestors.length === 0) return undefined + + const pageMask = frameAncestors + .map((s) => + // Apply the frame transform to the frame outline to get the frame outline in page space + Matrix2d.applyToPoints(this._pageTransformCache.get(s.id)!, this.getOutline(s)) + ) + .reduce((acc, b) => (b && acc ? intersectPolygonPolygon(acc, b) ?? undefined : undefined)) + + return pageMask + }) + } + + /** + * Get the page mask for a shape. + * + * @example + * + * ```ts + * const pageMask = app.getPageMaskById(shape.id) + * ``` + * + * @param id - The id of the shape to get the page mask for. + * @returns The page mask for the shape. + * @public + */ + getPageMaskById(id: TLShapeId) { + return this._pageMaskCache.get(id) + } + + /** + * A cache of clip paths used for clipping. + * + * @internal + */ + @computed private get _clipPathCache(): ComputedCache { + return this.store.createComputedCache('clipPathCache', (shape) => { + const pageMask = this._pageMaskCache.get(shape.id) + if (!pageMask) return undefined + const pageTransform = this._pageTransformCache.get(shape.id) + if (!pageTransform) return undefined + + if (pageMask.length === 0) { + return `polygon(0px 0px, 0px 0px, 0px 0px)` + } + + const localMask = Matrix2d.applyToPoints(Matrix2d.Inverse(pageTransform), pageMask) + + return `polygon(${localMask.map((p) => `${p.x}px ${p.y}px`).join(',')})` + }) + } + + /** + * Get the clip path for a shape. + * + * @example + * + * ```ts + * const clipPath = app.getClipPathById(shape.id) + * ``` + * + * @param id - The shape id. + * @returns The clip path or undefined. + * @public + */ + getClipPathById(id: TLShapeId) { + return this._clipPathCache.get(id) + } + + /** + * A cache of parents to children. + * + * @internal + */ + private readonly _parentIdsToChildIds: ReturnType + + /** + * Dispose the app. + * + * @public + */ + dispose() { + this.disposables.forEach((dispose) => dispose()) + this.disposables.clear() + } + + /** + * A manager for the app's history. + * + * @readonly + */ + readonly history = new HistoryManager( + this, + () => this._complete(), + (error) => { + this.annotateError(error, { origin: 'history.batch', willCrashApp: true }) + this.crash(error) + } + ) + + /** + * Undo to the last mark. + * + * @example + * + * ```ts + * app.undo() + * ``` + * + * @public + */ + undo() { + return this.history.undo() + } + + /** + * Whether the app can undo. + * + * @public + */ + @computed get canUndo() { + return this.history.numUndos > 0 + } + + /** + * Redo to the next mark. + * + * @example + * + * ```ts + * app.redo() + * ``` + * + * @public + */ + redo() { + this.history.redo() + return this + } + + /** + * Whether the app can redo. + * + * @public + */ + @computed get canRedo() { + return this.history.numRedos > 0 + } + + /** + * Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear + * any redos. + * + * @example + * + * ```ts + * app.mark() + * app.mark('flip shapes') + * ``` + * + * @param reason - The reason for the mark. + * @param onUndo - Whether to stop at the mark when undoing. + * @param onRedo - Whether to stop at the mark when redoing. + * @public + */ + mark(reason?: string, onUndo?: boolean, onRedo?: boolean) { + return this.history.mark(reason, onUndo, onRedo) + } + + /** + * Clear all marks in the undo stack back to the next mark. + * + * @example + * + * ```ts + * app.bail() + * ``` + * + * @public + */ + bail() { + this.history.bail() + return this + } + + /** + * Clear all marks in the undo stack back to the mark with the provided mark id. + * + * @example + * + * ```ts + * app.bailToMark('creating') + * ``` + * + * @public + */ + bailToMark(id: string) { + this.history.bailToMark(id) + return this + } + + /** + * Run a function in a batch, which will be undone/redone as a single action. + * + * @example + * + * ```ts + * app.batch(() => { + * app.selectAll() + * app.deleteShapes() + * app.createShapes(myShapes) + * app.selectNone() + * }) + * + * app.undo() // will undo all of the above + * ``` + * + * @public + */ + batch(fn: () => void) { + this.history.batch(fn) + return this + } + + /** + * A map of shape utility classes (TLShapeUtils) by shape type. + * + * @public + */ + shapeUtils: { readonly [K in string]?: TLShapeUtil } + + /** + * Get a shape util for a given shape or shape type. + * + * @example + * + * ```ts + * app.getShapeUtil(myBoxShape) + * ``` + * + * @param type - The shape type. + * @public + */ + getShapeUtil(shape: T): TLShapeUtil { + return this.shapeUtils[shape.type] as any as TLShapeUtil + } + + /** + * Get a shape util by its definition. + * + * @example + * + * ```ts + * app.getShapeUtilByDef(TLDrawShapeDef) + * ``` + * + * @param def - The shape definition. + * @public + */ + getShapeUtilByDef>( + def: Def + ): ReturnType { + return this.shapeUtils[def.type] as ReturnType + } + + /** + * A cache of children for each parent. + * + * @internal + */ + private _childIdsCache = new WeakMapCache() + + /** + * Get an array of all the children of a shape. + * + * @example + * + * ```ts + * app.getSortedChildIds('frame1') + * ``` + * + * @param parentId - The id of the parent shape. + * @public + */ + getSortedChildIds(parentId: TLParentId): TLShapeId[] { + const withIndices = this._parentIdsToChildIds.value[parentId] + if (!withIndices) return EMPTY_ARRAY + return this._childIdsCache.get(withIndices, () => withIndices.map(([id]) => id)) + } + + /** + * Run a visitor function for all descendants of a shape. + * + * @example + * + * ```ts + * app.visitDescendants('frame1', myCallback) + * ``` + * + * @param parentId - The id of the parent shape. + * @param visitor - The visitor function. + * @public + */ + visitDescendants(parentId: TLParentId, visitor: (id: TLShapeId) => void | false) { + const children = this.getSortedChildIds(parentId) + for (const id of children) { + if (visitor(id) === false) continue + this.visitDescendants(id, visitor) + } + } + + /** + * The editor's current erasing ids. + * + * @public + */ + @computed get erasingIds() { + return this.pageState.erasingIds + } + + /** + * The editor's current hinting ids. + * + * @public + */ + @computed get hintingIds() { + return this.pageState.hintingIds + } + + /** + * A derived set containing the current erasing ids. + * + * @public + */ + @computed get erasingIdsSet() { + // todo: Make incremental derivation, so that this only gets updated when erasingIds changes: we're creating this too often! + return new Set(this.erasingIds) + } + + /** + * Get all the current props among the users selected shapes + * + * @internal + */ + private _extractSharedProps(shape: TLShape, sharedProps: TLNullableShapeProps) { + if (shape.type === 'group') { + // For groups, ignore the props of the group shape and instead include + // the props of the group's children. These are the shapes that would have + // their props changed if the user called `setProp` on the current selection. + const childIds = this._parentIdsToChildIds.value[shape.id] + if (!childIds) return + + for (let i = 0, n = childIds.length; i < n; i++) { + this._extractSharedProps(this.getShapeById(childIds[i][0])!, sharedProps) + } + } else { + const props = Object.entries(shape.props) + let prop: [TLShapeProp, any] + for (let i = 0, n = props.length; i < n; i++) { + prop = props[i] as [TLShapeProp, any] + + // We should probably white list rather than black list here + if (BLACKLISTED_PROPS.has(prop[0])) continue + + // Check the value of this prop on the shared props object. + switch (sharedProps[prop[0]]) { + case undefined: { + // If this key hasn't been defined yet in the shared props object, + // we can set it to the value from the shape's props object. + sharedProps[prop[0]] = prop[1] + break + } + case null: + case prop[1]: { + // If the value in the shared props object matches the value from + // the shape's props object exactly—or if there is already a mixed + // value (null) in the shared props object—then this is a noop. We + // want to leave the value as it is in the shared props object. + continue + } + default: { + // If there's a value in the shared props object that isn't null AND + // that isn't undefined AND that doesn't match the shape's props object, + // then we've got a conflict, mixed props, so set the value to null. + sharedProps[prop[0]] = null + } + } + } + } + } + + /** + * A derived object containing all current props among the user's selected shapes. + * + * @internal + */ + private _selectionSharedProps = computed('_selectionSharedProps', () => { + const { selectedShapes } = this + + const sharedProps = {} as TLNullableShapeProps + + for (let i = 0, n = selectedShapes.length; i < n; i++) { + this._extractSharedProps(selectedShapes[i], sharedProps) + } + + return sharedProps as TLNullableShapeProps + }) + + /** @internal */ + private _prevProps: any = {} + + /** + * A derived object containing either all current props among the user's selected shapes, or else + * the user's most recent prop choices that correspond to the current active state (i.e. the + * selected tool). + * + * @internal + */ + @computed get props(): TLNullableShapeProps | null { + let next: TLNullableShapeProps | null + + // If we're in selecting and if we have a selection, + // return the shared props from the current selection + if (this.isIn('select') && this.selectedIds.length > 0) { + next = this._selectionSharedProps.value + } else { + // Otherwise, pull the style props from the app state + // (the most recent choices made by the user) that are + // exposed by the current state (i.e. the active tool). + const currentState = this.root.current.value! + if (currentState.styles.length === 0) { + next = null + } else { + const { propsForNextShape } = this.instanceState + next = Object.fromEntries( + currentState.styles.map((k) => { + return [k, propsForNextShape[k]] + }) + ) + } + } + + // todo: any way to improve this? still faster than rendering the style panel every frame + if (JSON.stringify(this._prevProps) === JSON.stringify(next)) { + return this._prevProps + } + + this._prevProps = next + + return next + } + + /** + * An array of all of the shapes on the current page. + * + * @public + */ + get shapeIds() { + return this._shapeIds.value + } + + /** + * _invalidParents is used to trigger the 'onChildrenChange' callback that shapes can have. + * + * @internal + */ + private readonly _invalidParents = new Set() + + /** @internal */ + private _complete() { + const { lastUpdatedPageId, lastUsedTabId } = this.userDocumentSettings + if (lastUsedTabId !== this.instanceId || lastUpdatedPageId !== this.currentPageId) { + this.store.put([ + { + ...this.userDocumentSettings, + lastUsedTabId: this.instanceId, + lastUpdatedPageId: this.currentPageId, + }, + ]) + } + for (const parentId of this._invalidParents) { + this._invalidParents.delete(parentId) + const parent = this.getShapeById(parentId) + if (!parent) continue + + const util = this.getShapeUtil(parent) + const changes = util.onChildrenChange?.(parent) + + if (changes?.length) { + this.updateShapes(changes, true) + } + } + + this.updateUserPresence() + this.emit('update') + } + + /** @internal */ + @computed + private get _arrowBindingsIndex() { + return arrowBindingsIndex(this.store) + } + + /** GetArrowsBoundTo */ + getArrowsBoundTo(shapeId: TLShapeId) { + return this._arrowBindingsIndex.value[shapeId] || EMPTY_ARRAY + } + + /** @internal */ + private _reparentArrow(arrowId: TLShapeId) { + const arrow = this.getShapeById(arrowId) as TLArrowShape | undefined + if (!arrow) return + const { start, end } = arrow.props + const startShape = start.type === 'binding' ? this.getShapeById(start.boundShapeId) : undefined + const endShape = end.type === 'binding' ? this.getShapeById(end.boundShapeId) : undefined + + const parentPageId = this.getParentPageId(arrow) + if (!parentPageId) return + + let nextParentId: TLParentId + if (startShape && endShape) { + // if arrow has two bindings, always parent arrow to closest common ancestor of the bindings + nextParentId = this.findCommonAncestor([startShape, endShape]) ?? parentPageId + } else if (startShape || endShape) { + // if arrow has one binding, keep arrow on its own page + nextParentId = parentPageId + } else { + return + } + + if (nextParentId && nextParentId !== arrow.parentId) { + this.reparentShapesById([arrowId], nextParentId) + } + + const reparentedArrow = this.getShapeById(arrowId) as TLArrowShape + + const startSibling = this.getNearestSiblingShape(reparentedArrow, startShape) + const endSibling = this.getNearestSiblingShape(reparentedArrow, endShape) + + let highestSibling: TLShape | undefined + + if (startSibling && endSibling) { + highestSibling = startSibling.index > endSibling.index ? startSibling : endSibling + } else if (startSibling && !endSibling) { + highestSibling = startSibling + } else if (endSibling && !startSibling) { + highestSibling = endSibling + } else { + return + } + + let finalIndex: string + + const higherSiblings = this.getSortedChildIds(highestSibling.parentId) + .map((id) => this.getShapeById(id)!) + .filter((sibling) => sibling.index > highestSibling!.index) + + if (higherSiblings.length) { + // there are siblings above the highest bound sibling, we need to + // insert between them. + + // if the next sibling is also a bound arrow though, we can end up + // all fighting for the same indexes. so lets find the next + // non-arrow sibling... + const nextHighestNonArrowSibling = higherSiblings.find((sibling) => sibling.type !== 'arrow') + + if ( + // ...then, if we're above the last shape we want to be above... + reparentedArrow.index > highestSibling.index && + // ...but below the next non-arrow sibling... + (!nextHighestNonArrowSibling || reparentedArrow.index < nextHighestNonArrowSibling.index) + ) { + // ...then we're already in the right place. no need to update! + return + } + + // otherwise, we need to find the index between the highest sibling + // we want to be above, and the next highest sibling we want to be + // below: + finalIndex = getIndexBetween(highestSibling.index, higherSiblings[0].index) + } else { + // if there are no siblings above us, we can just get the next index: + finalIndex = getIndexAbove(highestSibling.index) + } + + if (finalIndex !== reparentedArrow.index) { + this.updateShapes([{ id: arrowId, type: 'arrow', index: finalIndex }]) + } + } + + /** @internal */ + private _unbindArrowTerminal(arrow: TLArrowShape, handleId: 'start' | 'end') { + const { x, y } = getArrowTerminalsInArrowSpace(this, arrow)[handleId] + this.store.put([{ ...arrow, props: { ...arrow.props, [handleId]: { type: 'point', x, y } } }]) + } + + // private _shapeWillUpdate = (prev: TLShape, next: TLShape) => { + // const update = this.getShapeUtil(next).onUpdate?.(prev, next) + // return update ?? next + // } + + /** @internal */ + private _shapeWillBeDeleted(deletedShape: TLShape) { + // if the deleted shape has a parent shape make sure we call it's onChildrenChange callback + if (deletedShape.parentId && isShapeId(deletedShape.parentId)) { + this._invalidParents.add(deletedShape.parentId) + } + // clean up any arrows bound to this shape + const bindings = this._arrowBindingsIndex.value[deletedShape.id] + if (bindings?.length) { + for (const { arrowId, handleId } of bindings) { + const arrow = this.getShapeById(arrowId) + if (!arrow) continue + this._unbindArrowTerminal(arrow, handleId) + } + } + + const pageStates = this.store.query.records('instance_page_state').value + const deletedIds = new Set([deletedShape.id]) + const updates = compact( + pageStates.map((pageState) => { + return this._cleanupInstancePageState(pageState, deletedIds) + }) + ) + + if (updates.length) { + this.store.put(updates) + } + } + + /** @internal */ + private _arrowDidUpdate(arrow: TLArrowShape) { + // if the shape is an arrow and its bound shape is on another page + // or was deleted, unbind it + for (const handle of ['start', 'end'] as const) { + const terminal = arrow.props[handle] + if (terminal.type !== 'binding') continue + const boundShape = this.getShapeById(terminal.boundShapeId) + const isShapeInSamePageAsArrow = + this.getParentPageId(arrow) === this.getParentPageId(boundShape) + if (!boundShape || !isShapeInSamePageAsArrow) { + this._unbindArrowTerminal(arrow, handle) + } + } + + // always check the arrow parents + this._reparentArrow(arrow.id) + } + + /** @internal */ + private _cleanupInstancePageState( + prevPageState: TLInstancePageState, + shapesNoLongerInPage: Set + ) { + let nextPageState = null as null | TLInstancePageState + + const selectedIds = prevPageState.selectedIds.filter((id) => !shapesNoLongerInPage.has(id)) + if (selectedIds.length !== prevPageState.selectedIds.length) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.selectedIds = selectedIds + } + + const erasingIds = prevPageState.erasingIds.filter((id) => !shapesNoLongerInPage.has(id)) + if (erasingIds.length !== prevPageState.erasingIds.length) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.erasingIds = erasingIds + } + + if (prevPageState.hoveredId && shapesNoLongerInPage.has(prevPageState.hoveredId)) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.hoveredId = null + } + + if (prevPageState.editingId && shapesNoLongerInPage.has(prevPageState.editingId)) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.editingId = null + } + + const hintingIds = prevPageState.hintingIds.filter((id) => !shapesNoLongerInPage.has(id)) + if (hintingIds.length !== prevPageState.hintingIds.length) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.hintingIds = hintingIds + } + + if (prevPageState.focusLayerId && shapesNoLongerInPage.has(prevPageState.focusLayerId)) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.focusLayerId = null + } + return nextPageState + } + + /** @internal */ + private _shapeDidChange(prev: TLShape, next: TLShape) { + if (TLArrowShapeDef.is(next)) { + this._arrowDidUpdate(next) + } + + // if the shape's parent changed and it is bound to an arrow, update the arrow's parent + if (prev.parentId !== next.parentId) { + const reparentBoundArrows = (id: TLShapeId) => { + const boundArrows = this._arrowBindingsIndex.value[id] + if (boundArrows?.length) { + for (const arrow of boundArrows) { + this._reparentArrow(arrow.arrowId) + } + } + } + reparentBoundArrows(next.id) + this.visitDescendants(next.id, reparentBoundArrows) + } + + // if this shape moved to a new page, clean up any previous page's instance state + if (prev.parentId !== next.parentId && TLPage.isId(next.parentId)) { + const allMovingIds = new Set([prev.id]) + this.visitDescendants(prev.id, (id) => { + allMovingIds.add(id) + }) + + for (const instancePageState of this.store.query.records('instance_page_state').value) { + if (instancePageState.pageId === next.parentId) continue + const nextPageState = this._cleanupInstancePageState(instancePageState, allMovingIds) + + if (nextPageState) { + this.store.put([nextPageState]) + } + } + } + + if (prev.parentId && isShapeId(prev.parentId)) { + this._invalidParents.add(prev.parentId) + } + + if (next.parentId !== prev.parentId && isShapeId(next.parentId)) { + this._invalidParents.add(next.parentId) + } + } + + /** @internal */ + private _tabStateDidChange(prev: TLInstancePageState, next: TLInstancePageState) { + if (prev?.selectedIds !== next?.selectedIds) { + // ensure that descendants and ascenants are not selected at the same time + const filtered = next.selectedIds.filter((id) => { + let parentId = this.getShapeById(id)?.parentId + while (isShapeId(parentId)) { + if (next.selectedIds.includes(parentId)) { + return false + } + parentId = this.getShapeById(parentId)?.parentId + } + return true + }) + + const nextFocusLayerId = + filtered.length === 0 + ? next?.focusLayerId + : this.findCommonAncestor( + compact(filtered.map((id) => this.getShapeById(id))), + (shape) => shape.type === 'group' + ) + + if (filtered.length !== next.selectedIds.length || nextFocusLayerId != next.focusLayerId) { + this.store.put([{ ...next, selectedIds: filtered, focusLayerId: nextFocusLayerId ?? null }]) + } + } + } + + /** @internal */ + private _pageWillBeDeleted(page: TLPage) { + // page was deleted, need to check whether it's the current page and select another one if so + const instanceStates = this.store.query.exec('instance', { currentPageId: { eq: page.id } }) + + if (!instanceStates.length) return + const backupPageId = this.pages.find((p) => p.id !== page.id)?.id + + if (!backupPageId) return + + this.store.put(instanceStates.map((state) => ({ ...state, currentPageId: backupPageId }))) + } + + /* -------------------- Shortcuts ------------------- */ + + /** The global document settings that applies to all users */ + @computed get documentSettings() { + return this.store.get(TLDOCUMENT_ID)! + } + + get gridSize() { + return this.documentSettings.gridSize + } + + /** + * The user's global settings. + * + * @public + * @readonly + */ + get userSettings(): TLUser { + return this.store.get(this.userId)! + } + + /** @internal */ + @computed private get _userDocumentSettings() { + return this.store.query.record('user_document', () => ({ userId: { eq: this.userId } })) + } + + get userDocumentSettings(): TLUserDocument { + return this._userDocumentSettings.value! + } + + get isReadOnly() { + return this.userDocumentSettings.isReadOnly + } + + get isGridMode() { + return this.userDocumentSettings.isGridMode + } + + setGridMode(isGridMode: boolean) { + this.updateUserDocumentSettings({ isGridMode }, true) + } + + setDarkMode(isDarkMode: boolean) { + this.updateUserDocumentSettings({ isDarkMode }, true) + } + + setReadOnly(isReadOnly: boolean) { + this.updateUserDocumentSettings({ isReadOnly }, true) + if (isReadOnly) { + this.setSelectedTool('hand') + } + } + + /** @internal */ + private _isPenMode = atom('isPenMode', false as any) + + /** @internal */ + private _touchEventsRemainingBeforeExitingPenMode = 0 + + get isPenMode() { + return this._isPenMode.value + } + + setPenMode(isPenMode: boolean) { + if (isPenMode) this._touchEventsRemainingBeforeExitingPenMode = 3 + + this._isPenMode.set(isPenMode) + } + + // User / User App State + + /** + * The current user state. + * + * @public + */ + get user(): TLUser { + return this.store.get(this.userId)! + } + + /** The current tab state */ + get instanceState(): TLInstance { + return this.store.get(this.instanceId)! + } + + get cursor() { + return this.instanceState.cursor + } + + get brush() { + return this.instanceState.brush + } + + get zoomBrush() { + return this.instanceState.zoomBrush + } + + get scribble() { + return this.instanceState.scribble + } + + /** @internal */ + @computed private get _pageState() { + return this.store.query.record( + 'instance_page_state', + () => { + return { + pageId: { eq: this.currentPageId }, + instanceId: { eq: this.instanceId }, + } + }, + 'app._pageState' + ) + } + + /** + * The current page state. + * + * @public + */ + get pageState(): TLInstancePageState { + return this._pageState.value! + } + + /** The current camera. */ + @computed get camera() { + return this.store.get(this.pageState.cameraId)! + } + + /** The current camera zoom level. */ + @computed get zoomLevel() { + return this.camera.z + } + + /** + * The current selected ids. + * + * @public + */ + @computed get selectedIds() { + return this.pageState.selectedIds + } + + /** + * The current selected ids as a set + * + * @public + */ + @computed get selectedIdsSet(): ReadonlySet { + return new Set(this.selectedIds) + } + + // Pages + + /** @internal */ + @computed private get _pages() { + return this.store.query.records('page') + } + + /** + * Info about the project's current pages. + * + * @public + * @readonly + */ + @computed get pages() { + return this._pages.value.sort(sortByIndex) + } + + /** + * The current page. + * + * @public + */ + get currentPage() { + return this.getPageById(this.currentPageId)! + } + + /** + * The current page id. + * + * @public + */ + get currentPageId() { + return this.instanceState.currentPageId + } + + /** + * Get a page by its ID. + * + * @example + * + * ```ts + * app.getPageById(myPage.id) + * ``` + * + * @public + */ + getPageById(id: TLPage['id']) { + return this.store.get(id) + } + + /** @internal */ + @computed private get _pageStates() { + return this.store.query.records('instance_page_state', () => ({ + instanceId: { eq: this.instanceId }, + })) + } + + /** + * Get a page state by its id. + * + * @example + * + * ```ts + * app.getPageStateByPageId('page1') + * ``` + * + * @public + */ + getPageStateByPageId(id: TLPageId) { + return this._pageStates.value.find((p) => p.pageId === id) + } + + /** + * Get a page by its ID. + * + * @example + * + * ```ts + * app.getPageById(myPage.id) + * ``` + * + * @public + */ + getPageInfoById(id: TLPage['id']) { + return this.store.get(id) + } + + /** Get shapes on a page. */ + getShapesInPage(pageId: TLPageId) { + const result = this.store.query.exec('shape', { parentId: { eq: pageId } }) + return this.getShapesAndDescendantsInOrder(result.map((s) => s.id)) + } + + /* --------------------- Shapes --------------------- */ + + /** + * Get the local transform for a shape as a matrix model. This transform reflects both its + * translation (x, y) from from either its parent's top left corner, if the shape's parent is + * another shape, or else from the 0,0 of the page, if the shape's parent is the page; and the + * shape's rotation. + * + * @example + * + * ```ts + * app.getTransform(myShape) + * ``` + * + * @param shape - The shape to get the local transform for. + * @public + */ + getTransform(shape: TLShape) { + const util = this.getShapeUtil(shape) + return util.transform(shape) + } + + /** + * Get the local transform of a shape's parent as a matrix model. + * + * @example + * + * ```ts + * app.getParentTransform(myShape) + * ``` + * + * @param shape - The shape to get the parent transform for. + * @public + */ + getParentTransform(shape: TLShape) { + if (TLPage.isId(shape.parentId)) { + return Matrix2d.Identity() + } + return this._pageTransformCache.get(shape.parentId) ?? Matrix2d.Identity() + } + + /** + * Get the page transform (or absolute transform) of a shape. + * + * @example + * + * ```ts + * app.getPageTransform(myShape) + * ``` + * + * @param shape - The shape to get the page transform for. + * @public + */ + getPageTransform(shape: TLShape) { + return this.getPageTransformById(shape.id) + } + + /** + * Get the page transform (or absolute transform) of a shape by its id. + * + * @example + * + * ```ts + * app.getPageTransformById(myShape) + * ``` + * + * @param id - The if of the shape to get the page transform for. + * @public + */ + getPageTransformById(id: TLShapeId) { + return this._pageTransformCache.get(id) + } + + /** + * Get the page point (or absolute point) of a shape. + * + * @example + * + * ```ts + * app.getPagePoint(myShape) + * ``` + * + * @param shape - The shape to get the page point for. + * @public + */ + getPagePointById(id: TLShapeId) { + const pageTransform = this.getPageTransformById(id) + if (!pageTransform) return + return Matrix2d.applyToPoint(pageTransform, new Vec2d()) + } + + /** + * Get the page point (or absolute point) of a shape. + * + * @example + * + * ```ts + * app.getPagePoint(myShape) + * ``` + * + * @param shape - The shape to get the page point for. + * @public + */ + getPageCenter(shape: TLShape) { + const pageTransform = this.getPageTransformById(shape.id) + if (!pageTransform) return null + const util = this.getShapeUtil(shape) + const center = util.center(shape) + return Matrix2d.applyToPoint(pageTransform, center) + } + + /** + * Get the page point (or absolute point) of a shape by its id. + * + * @example + * + * ```ts + * app.getPagePoint(myShape) + * ``` + * + * @param id - The shape id to get the page point for. + * @public + */ + getPageCenterById(id: TLShapeId) { + const shape = this.getShapeById(id)! + return this.getPageCenter(shape) + } + + /** + * Get the page rotation (or absolute rotation) of a shape. + * + * @example + * + * ```ts + * app.getPageRotation(myShape) + * ``` + * + * @param shape - The shape to get the page rotation for. + * @public + */ + getPageRotation(shape: TLShape): number { + return this.getPageRotationById(shape.id) + } + + /** + * Get the page rotation (or absolute rotation) of a shape by its id. + * + * @param id - The id of the shape to get the page rotation for. + */ + getPageRotationById(id: TLShapeId): number { + const pageTransform = this.getPageTransformById(id) + if (pageTransform) { + return Matrix2d.Decompose(pageTransform).rotation + } + return 0 + } + + /** + * Get the local bounds of a shape. + * + * @example + * + * ```ts + * app.getBounds(myShape) + * ``` + * + * @param shape - The shape to get the bounds for. + * @public + */ + getBounds(shape: TLShape): Box2d { + return this.getShapeUtil(shape).bounds(shape) + } + + /** + * Get the local bounds of a shape by its id. + * + * @example + * + * ```ts + * app.getBoundsById(myShape) + * ``` + * + * @param id - The id of the shape to get the bounds for. + * @public + */ + getBoundsById(id: TLShapeId): Box2d | undefined { + const shape = this.getShapeById(id) + if (!shape) return undefined + return this.getBounds(shape) + } + + /** + * Get the page (or absolute) bounds of a shape. + * + * @example + * + * ```ts + * app.getPageBounds(myShape) + * ``` + * + * @param shape - The shape to get the bounds for. + * @public + */ + getPageBounds(shape: TLShape): Box2d | undefined { + return this.getPageBoundsById(shape.id) + } + + /** + * Get the page (or absolute) bounds of a shape by its id. + * + * @example + * + * ```ts + * app.getPageBoundsById(myShape) + * ``` + * + * @param id - The id of the shape to get the page bounds for. + * @public + */ + getPageBoundsById(id: TLShapeId): Box2d | undefined { + return this._pageBoundsCache.get(id) + } + + /** + * Get the page (or absolute) bounds of a shape, incorporating any masks. For example, if the + * shape were the child of a frame and was half way out of the frame, the bounds would be the half + * of the shape that was in the frame. + * + * @example + * + * ```ts + * app.getMaskedPageBounds(myShape) + * ``` + * + * @param shape - The shape to get the masked bounds for. + * @public + */ + getMaskedPageBounds(shape: TLShape): Box2d | undefined { + return this.getMaskedPageBoundsById(shape.id) + } + + /** + * Get the page (or absolute) bounds of a shape by its id, incorporating any masks. For example, + * if the shape were the child of a frame and was half way out of the frame, the bounds would be + * the half of the shape that was in the frame. + * + * @example + * + * ```ts + * app.getMaskedPageBoundsById(myShape) + * ``` + * + * @param id - The id of the shape to get the masked page bounds for. + * @public + */ + getMaskedPageBoundsById(id: TLShapeId): Box2d | undefined { + const pageBounds = this._pageBoundsCache.get(id) + if (!pageBounds) return + const pageMask = this._pageMaskCache.get(id) + if (pageMask) { + const intersection = intersectPolygonPolygon(pageMask, pageBounds.corners) + if (!intersection) return + return Box2d.FromPoints(intersection) + } + return pageBounds + } + + /** + * Get the local outline of a shape. + * + * @example + * + * ```ts + * app.getOutline(myShape) + * ``` + * + * @param shape - The shape to get the outline for. + * @public + */ + getOutline(shape: TLShape) { + return this.getShapeUtil(shape).outline(shape) + } + + /** + * Get the local outline of a shape. + * + * @example + * + * ```ts + * app.getOutlineById(myShape) + * ``` + * + * @param id - The shape id to get the outline for. + * @public + */ + getOutlineById(id: TLShapeId) { + return this.getOutline(this.getShapeById(id)!) + } + + /** + * Get the ancestors of a shape. + * + * @example + * + * ```ts + * const ancestors = app.getAncestors(myShape) + * ``` + * + * @param shape - The shape to get the ancestors for. + * @public + */ + getAncestors(shape: TLShape, acc: TLShape[] = []): TLShape[] { + const parentId = shape.parentId + if (TLPage.isId(parentId)) { + acc.reverse() + return acc + } + + const parent = this.store.get(parentId)! + acc.push(parent) + return this.getAncestors(parent, acc) + } + + /** + * Get the ancestors of a shape by its id. + * + * @example + * + * ```ts + * const ancestors = app.getAncestorsById(myShape) + * ``` + * + * @param id - The id of the shape to get the ancestors for. + * @public + */ + getAncestorsById(id: TLShapeId, acc: TLShape[] = []): TLShape[] { + const shape = this.getShapeById(id)! + return this.getAncestors(shape, acc) + } + + /** + * Find the first ancestor matching the given predicate + * + * @example + * + * ```ts + * const ancestor = app.findAncestor(myShape) + * ``` + * + * @param shape - The shape to check the ancestors for. + * @public + */ + findAncestor(shape: TLShape, predicate: (parent: TLShape) => boolean): TLShape | undefined { + const parentId = shape.parentId + + if (TLPage.isId(parentId)) { + return undefined + } + + const parent = this.getShapeById(parentId) + + if (parent) { + if (predicate(parent)) { + return parent + } + return this.findAncestor(parent, predicate) + } + + return undefined + } + + /** Returns true if the the given shape has the given ancestor */ + hasAncestor(shape: TLShape | undefined, ancestorId: TLShapeId): boolean { + if (!shape) return false + if (shape.parentId === ancestorId) return true + return this.hasAncestor(this.getParentShape(shape), ancestorId) + } + + /** + * Get the common ancestor of two or more shapes that matches a predicate. + * + * @param shapes - The shapes to check. + * @param predicate - The predicate to match. + */ + findCommonAncestor( + shapes: TLShape[], + predicate?: (shape: TLShape) => boolean + ): TLShapeId | undefined { + if (shapes.length === 0) { + return + } + if (shapes.length === 1) { + const parentId = shapes[0].parentId + if (TLPage.isId(parentId)) { + return + } + return predicate ? this.findAncestor(shapes[0], predicate)?.id : parentId + } + + const [nodeA, ...others] = shapes + let ancestor = this.getParentShape(nodeA) + while (ancestor) { + // TODO: this is not ideal, optimize + if (predicate && !predicate(ancestor)) { + ancestor = this.getParentShape(ancestor) + continue + } + if (others.every((shape) => this.hasAncestor(shape, ancestor!.id))) { + return ancestor!.id + } + ancestor = this.getParentShape(ancestor) + } + return undefined + } + + /** + * Check whether a shape is within the bounds of the current viewport. + * + * @param id - The id of the shape to check. + * @public + */ + isShapeInViewport(id: TLShapeId) { + const pageBounds = this.getPageBoundsById(id) + if (!pageBounds) return false + return this.viewportPageBounds.includes(pageBounds) + } + + /** + * Get the shapes that should be displayed in the current viewport. + * + * @public + */ + @computed get renderingShapes() { + // Here we get the shape as well as any of its children, as well as their + // opacities. If the shape is beign erased, and none of its ancestors are + // being erased, then we reduce the opacity of the shape and all of its + // ancestors; but we don't apply this effect more than once among a set + // of descendants so that it does not compound. + + // This is designed to keep all the shapes in a single list which + // allows the DOM nodes to be reused even when they become children + // of other nodes. + + // Its IMPORTANT that the result be sorted by id AND include the index + // that the shape should be displayed at. Steve, this is the past you + // telling the present you not to change this. + + // We want to sort by id because moving elements about in the DOM will + // cause the element to get removed by react as it moves the DOM node. This + // causes to re-render which is hella annoying and a perf + // drain. By always sorting by 'id' we keep the shapes always in the + // same order; but we later use index to set the element's 'z-index' + // to change the "rendered" position in z-space. + + const { currentPageId, cullingBounds, cullingBoundsExpanded, erasingIdsSet, editingId } = this + + const renderingShapes: { + id: TLShapeId + index: number + opacity: number + isCulled: boolean + isInViewport: boolean + }[] = [] + + const getShapeToDisplay = ( + id: TLShapeId, + parentOpacity: number, + isAncestorErasing: boolean + ) => { + const shape = this.getShapeById(id) + + if (!shape) return + + // todo: move opacity to a property of shape, rather than a property of props + let opacity = (+(shape.props as { opacity: string }).opacity ?? 1) * parentOpacity + let isShapeErasing = false + + if (!isAncestorErasing && erasingIdsSet.has(id)) { + isShapeErasing = true + opacity *= 0.32 + } + + // If a child is outside of its parent's clipping bounds, then bounds will be undefined. + const bounds = this.getMaskedPageBoundsById(id) + + // Whether the shape is on screen. Use the "strict" viewport here. + const isInViewport = bounds ? cullingBounds.includes(bounds) : false + + // Whether the shape should actually be culled / unmounted. + // - Use the "expanded" culling viewport to include shapes that are just off-screen. + // - Editing shapes should never be culled. + const isCulled = bounds ? editingId !== id && !cullingBoundsExpanded.includes(bounds) : true + + renderingShapes.push({ id, index: renderingShapes.length, opacity, isCulled, isInViewport }) + + this.getSortedChildIds(id).forEach((id) => { + getShapeToDisplay(id, opacity, isAncestorErasing || isShapeErasing) + }) + } + + this.getSortedChildIds(currentPageId).forEach((shapeId) => getShapeToDisplay(shapeId, 1, false)) + + return renderingShapes.sort(sortById) + } + + /** + * The common bounds of all of the shapes on the page. + * + * @public + */ + @computed get allShapesCommonBounds(): Box2d | null { + let commonBounds = null as Box2d | null + + this.shapeIds.forEach((shapeId) => { + const bounds = this.getMaskedPageBoundsById(shapeId) + if (bounds) { + if (commonBounds) { + commonBounds.expand(bounds) + } else { + commonBounds = bounds.clone() + } + } + }) + + return commonBounds + } + + /** + * Get the corners of a shape in page space. + * + * @example + * + * ```ts + * const corners = app.getPageCorners(myShape) + * ``` + * + * @param shape - The shape to get the corners for. + * @public + */ + getPageCorners(shape: TLShape): Vec2d[] { + const ancestors = this.getAncestors(shape) + const corners = this.getBounds(shape).corners + + const transform = Matrix2d.Compose( + ...ancestors.flatMap((s) => [Matrix2d.Translate(s.x, s.y), Matrix2d.Rotate(s.rotation)]), + Matrix2d.Translate(shape.x, shape.y), + Matrix2d.Rotate(shape.rotation, 0, 0) + ) + + return Matrix2d.applyToPoints(transform, corners) + } + + /** + * Test whether a point (in page space) will will a shape. This method takes into account masks, + * such as when a shape is the child of a frame and is partially clipped by the frame. + * + * @example + * + * ```ts + * app.isPointInShape({ x: 100, y: 100 }, myShape) + * ``` + * + * @param point - The page point to test. + * @param shape - The shape to test against. + * @public + */ + isPointInShape(point: VecLike, shape: TLShape): boolean { + const util = this.getShapeUtil(shape) + + const pageMask = this._pageMaskCache.get(shape.id) + + if (pageMask) { + const hit = pointInPolygon(point, pageMask) + if (!hit) return false + } + + return util.hitTestPoint(shape, this.getPointInShapeSpace(shape, point)) + } + + /** + * Get the shapes, if any, at a given page point. + * + * @example + * + * ```ts + * app.getShapesAtPoint({ x: 100, y: 100 }) + * ``` + * + * @param point - The page point to test. + * @public + */ + getShapesAtPoint(point: VecLike): TLShape[] { + return this.shapesArray.filter((shape) => { + const pageMask = this._pageMaskCache.get(shape.id) + if (pageMask) { + const hasHit = pointInPolygon(point, pageMask) + if (!hasHit) return false + } + + return this.getShapeUtil(shape).hitTestPoint(shape, this.getPointInShapeSpace(shape, point)) + }) + } + + /** + * Convert a point in page space to a point in the local space of a shape. For example, if a + * shape's page point were `{ x: 100, y: 100 }`, a page point at `{ x: 110, y: 110 }` would be at + * `{ x: 10, y: 10 }` in the shape's local space. + * + * @example + * + * ```ts + * app.getPointInShapeSpace(myShape, { x: 100, y: 100 }) + * ``` + * + * @param shape - The shape to get the point in the local space of. + * @param point - The page point to get in the local space of the shape. + * @public + */ + getPointInShapeSpace(shape: TLShape, point: VecLike): Vec2d { + return Matrix2d.applyToPoint(Matrix2d.Inverse(this.getPageTransform(shape)!), point) + } + + /** + * Convert a delta in page space to a point in the local space of a shape. For example, if a + * shape's page point were `{ x: 100, y: 100 }`, a page point at `{ x: 110, y: 110 }` would be at + * `{ x: 10, y: 10 }` in the shape's local space. + * + * @example + * + * ```ts + * app.getPointInShapeSpace(myShape.id, { x: 100, y: 100 }) + * ``` + * + * @param shape - The shape to get the point in the local space of. + * @param point - The page point to get in the local space of the shape. + * @public + */ + getPointInParentSpace(shapeId: TLShapeId, point: VecLike): Vec2d { + const shape = this.getShapeById(shapeId)! + if (!shape) { + return new Vec2d(0, 0) + } + if (TLPage.isId(shape.parentId)) return Vec2d.From(point) + + const parentTransform = this.getPageTransformById(shape.parentId) + if (!parentTransform) return Vec2d.From(point) + + return Matrix2d.applyToPoint(Matrix2d.Inverse(parentTransform), point) + } + + /** + * Convert a delta in page space to a delta in the local space of a shape. + * + * @example + * + * ```ts + * app.getDeltaInShapeSpace(myShape, { x: 100, y: 100 }) + * ``` + * + * @param shape - The shape to get the delta in the local space of. + * @param delta - The page delta to convert. + * @public + */ + getDeltaInShapeSpace(shape: TLShape, delta: VecLike): Vec2d { + const pageTransform = this.getPageTransform(shape) + if (!pageTransform) return Vec2d.From(delta) + return Vec2d.Rot(delta, -Matrix2d.Decompose(pageTransform).rotation) + } + + /** + * Convert a delta in page space to a delta in the parent space of a shape. + * + * @example + * + * ```ts + * app.getDeltaInParentSpace(myShape, { x: 100, y: 100 }) + * ``` + * + * @param shape - The shape to get the delta in the parent space of. + * @param delta - The page delta to convert. + * @public + */ + getDeltaInParentSpace(shape: TLShape, delta: VecLike): Vec2d { + if (TLPage.isId(shape.parentId)) return Vec2d.From(delta) + + const parent = this.getShapeById(shape.parentId) + if (!parent) return Vec2d.From(delta) + + return this.getDeltaInShapeSpace(parent, delta) + } + + /** + * For a given set of ids, get a map containing the ids of their parents and the children of those + * parents. + * + * @example + * + * ```ts + * app.getParentsMappedToChildren(['id1', 'id2', 'id3']) + * ``` + * + * @param ids - The ids to get the parents and children of. + * @public + */ + getParentsMappedToChildren(ids: TLShapeId[]) { + const shapes = ids.map((id) => this.store.get(id)!) + const parents = new Map>() + shapes.forEach((shape) => { + if (!parents.has(shape.parentId)) { + parents.set(shape.parentId, new Set()) + } + parents.get(shape.parentId)?.add(shape) + }) + return parents + } + + /* -------------------- Viewport -------------------- */ + + /** + * Update the viewport. The viewport will measure the size and screen position of its container + * element. This should be done whenever the container's position on the screen changes. + * + * @example + * + * ```ts + * app.updateViewportScreenBounds() + * ``` + * + * @param center - Whether to preserve the viewport page center as the viewport changes. + * (optional) + * @public + */ + updateViewportScreenBounds(center = false) { + const container = this.getContainer() + + if (!container) return this + const rect = container.getBoundingClientRect() + const screenBounds = new Box2d(0, 0, Math.max(rect.width, 1), Math.max(rect.height, 1)) + + const boundsAreEqual = screenBounds.equals(this.viewportScreenBounds) + + // Get the current value + const { _willSetInitialBounds } = this + + if (boundsAreEqual) { + this._willSetInitialBounds = false + } else { + if (_willSetInitialBounds) { + // If we have just received the initial bounds, don't center the camera. + this._willSetInitialBounds = false + this.updateInstanceState({ screenBounds: screenBounds.toJson() }, true, true) + } else { + const { zoomLevel } = this + if (center) { + const before = this.viewportPageCenter + this.updateInstanceState({ screenBounds: screenBounds.toJson() }, true, true) + const after = this.viewportPageCenter + if (!this.instanceState.followingUserId) { + this.pan((after.x - before.x) * zoomLevel, (after.y - before.y) * zoomLevel) + } + } else { + const before = this.screenToPage(0, 0) + this.updateInstanceState({ screenBounds: screenBounds.toJson() }, true, true) + const after = this.screenToPage(0, 0) + if (!this.instanceState.followingUserId) { + this.pan((after.x - before.x) * zoomLevel, (after.y - before.y) * zoomLevel) + } + } + } + } + + this._cameraManager.tick() + this.updateCullingBounds() + + const { editingId } = this + + if (editingId) { + this.panZoomIntoView([editingId]) + } + + return this + } + + /** + * The bounds of the editor's viewport in screen space. + * + * @public + */ + @computed get viewportScreenBounds() { + const { x, y, w, h } = this.instanceState.screenBounds + return new Box2d(x, y, w, h) + } + + /** + * The center of the editor's viewport in screen space. + * + * @public + */ + @computed get viewportScreenCenter() { + return this.viewportScreenBounds.center + } + + /** + * The current viewport in page space. + * + * @public + */ + @computed get viewportPageBounds() { + const { x, y, w, h } = this.viewportScreenBounds + const tl = this.screenToPage(x, y) + const br = this.screenToPage(x + w, y + h) + return new Box2d(tl.x, tl.y, br.x - tl.x, br.y - tl.y) + } + + /** + * The current culling bounds in page space, used for checking which shapes are "on screen". + * + * @public + */ + @computed get cullingBounds() { + return this._cullingBounds.value + } + + /** @internal */ + readonly _cullingBounds = atom('culling viewport', new Box2d()) + + /** + * The current culling bounds in page space, expanded slightly. Used for determining which shapes + * to render and which to "cull". + * + * @public + */ + @computed get cullingBoundsExpanded() { + return this._cullingBoundsExpanded.value + } + + /** @internal */ + readonly _cullingBoundsExpanded = atom('culling viewport expanded', new Box2d()) + + /** + * Update the culling bounds. This should be called when the viewport has stopped changing, such + * as at the end of a pan, zoom, or animation. + * + * @example + * + * ```ts + * app.updateCullingBounds() + * ``` + * + * @internal + */ + updateCullingBounds(): this { + const { viewportPageBounds } = this + if (viewportPageBounds.equals(this._cullingBounds.__unsafe__getWithoutCapture())) return this + this._cullingBounds.set(viewportPageBounds.clone()) + this._cullingBoundsExpanded.set(viewportPageBounds.clone().expandBy(100 / this.zoomLevel)) + return this + } + + /** + * The center of the viewport in page space. + * + * @public + */ + @computed get viewportPageCenter() { + return this.viewportPageBounds.center + } + + /** + * Convert a point in screen space to a point in page space. + * + * @example + * + * ```ts + * app.screenToPage(100, 100) + * ``` + * + * @param x - The x coordinate of the point in screen space. + * @param y - The y coordinate of the point in screen space. + * @param camera - The camera to use. Defaults to the current camera. + * @public + */ + screenToPage(x: number, y: number, z = 0.5, camera: Vec2dModel = this.camera) { + const { screenBounds } = this.store.unsafeGetWithoutCapture(this.instanceId)! + const { x: cx, y: cy, z: cz = 1 } = camera + return { + x: (x - screenBounds.x) / cz - cx, + y: (y - screenBounds.y) / cz - cy, + z, + } + } + + /** + * Convert a point in page space to a point in screen space. + * + * @example + * + * ```ts + * app.pageToScreen(100, 100) + * ``` + * + * @param x - The x coordinate of the point in screen space. + * @param y - The y coordinate of the point in screen space. + * @param camera - The camera to use. Defaults to the current camera. + * @public + */ + pageToScreen(x: number, y: number, z = 0.5, camera: Vec2dModel = this.camera) { + const { x: cx, y: cy, z: cz = 1 } = camera + return { + x: x + cx * cz, + y: y + cy * cz, + z, + } + } + + /* Focus Layers */ + + get focusLayerId() { + return this.pageState.focusLayerId ?? this.currentPageId + } + + get focusLayerShape(): TLShape | undefined { + const id = this.pageState.focusLayerId + if (!id) { + return + } + return this.getShapeById(id) + } + + popFocusLayer() { + const current = this.pageState.focusLayerId + const focusedShape = current && this.getShapeById(current) + + if (focusedShape) { + // If we have a focused layer, look for an ancestor of the focused shape that is a group + const match = this.findAncestor(focusedShape, (s) => s.type === 'group') + // If we have an ancestor that can become a focused layer, set it as the focused layer + this.setFocusLayer(match?.id ?? null) + this.select(focusedShape.id) + } else { + // If there's no focused shape, then clear the focus layer and clear selection + this.setFocusLayer(null) + this.selectNone() + } + + return this + } + + /** + * Set the focus layer to the given shape id. + * + * @param next - The next focus layer id or null to reset the focus layer to the page + * @public + */ + setFocusLayer(next: null | TLShapeId) { + this._setFocusLayer(next) + return this + } + + /** @internal */ + private _setFocusLayer = this.history.createCommand( + 'setFocusLayer', + (next: null | TLShapeId) => { + // When we first click an empty canvas we don't want this to show up in the undo stack + if (next === null && !this.canUndo) { + return + } + const prev = this.pageState.focusLayerId + return { data: { prev, next }, preservesRedoStack: true, squashing: true } + }, + { + do: ({ next }) => { + this.store.update(this.pageState.id, (s) => ({ ...s, focusLayerId: next })) + }, + undo: ({ prev }) => { + this.store.update(this.pageState.id, (s) => ({ ...s, focusLayerId: prev })) + }, + squash({ prev }, { next }) { + return { prev, next } + }, + } + ) + + /** + * Set the hinted shape ids. + * + * @param ids - The ids to set as hinted. + * @public + */ + setHintingIds(ids: TLShapeId[]): this { + // always ephemeral + this.store.update(this.pageState.id, (s) => ({ ...s, hintingIds: dedupe(ids) })) + return this + } + + /** + * The current editing shape's id. + * + * @public + */ + get editingId() { + return this.pageState.editingId + } + + /** + * The current cropping shape's id. + * + * @public + */ + get croppingId() { + return this.pageState.croppingId + } + + @computed get editingShape() { + if (!this.editingId) return null + return this.getShapeById(this.editingId) ?? null + } + + /** + * Set the current editing id. + * + * @param id - The id of the shape to edit or null to clear the editing id. + * @public + */ + setEditingId(id: TLShapeId | null): this { + if (!id) { + this.setInstancePageState({ editingId: null }) + } else { + if (id !== this.editingId) { + const shape = this.getShapeById(id)! + const util = this.getShapeUtil(shape) + if (shape && util.canEdit(shape)) { + this.setInstancePageState({ editingId: id, hoveredId: null }, false) + const { viewportPageBounds } = this + const localEditingBounds = util.getEditingBounds(shape)! + const pageTransform = this.getPageTransformById(id)! + const pageEditingBounds = Box2d.FromPoints( + Matrix2d.applyToPoints(pageTransform, localEditingBounds.corners) + ) + + if (!viewportPageBounds.contains(pageEditingBounds)) { + if ( + pageEditingBounds.width > viewportPageBounds.width || + pageEditingBounds.height > viewportPageBounds.height + ) { + this.zoomToBounds( + pageEditingBounds.minX, + pageEditingBounds.minY, + pageEditingBounds.width, + pageEditingBounds.height + ) + } else { + this.centerOnPoint(pageEditingBounds.midX, pageEditingBounds.midY) + } + } + } + } + } + + return this + } + + setCroppingId(id: TLShapeId | null): this { + if (id !== this.croppingId) { + if (!id) { + this.setInstancePageState({ croppingId: null }) + if (this.isInAny('select.crop', 'select.pointing_crop_handle', 'select.cropping')) { + this.setSelectedTool('select.idle') + } + } else { + const shape = this.getShapeById(id)! + const util = this.getShapeUtil(shape) + if (shape && util.canCrop(shape)) { + this.setInstancePageState({ croppingId: id, hoveredId: null }) + } + } + } + return this + } + + getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShapeType) { + const shapes = this.sortedShapesArray + + for (let i = shapes.length - 1; i >= 0; i--) { + const shape = shapes[i] + const util = this.getShapeUtil(shape) + if (!util.canReceiveNewChildrenOfType(shapeType)) continue + const maskedPageBounds = this.getMaskedPageBoundsById(shape.id) + if ( + maskedPageBounds && + maskedPageBounds.containsPoint(point) && + util.hitTestPoint(shape, this.getPointInShapeSpace(shape, point)) + ) { + return shape.id + } + } + + return this.focusLayerId + } + + getDroppingShape(point: VecLike, droppingShapes: TLShape[] = []) { + const shapes = this.sortedShapesArray + + for (let i = shapes.length - 1; i >= 0; i--) { + const shape = shapes[i] + // don't allow dropping a shape on itself or one of it's children + if (droppingShapes.find((s) => s.id === shape.id || this.hasAncestor(shape, s.id))) continue + const util = this.getShapeUtil(shape) + if (!util.canDropShapes(shape, droppingShapes)) continue + const maskedPageBounds = this.getMaskedPageBoundsById(shape.id) + if ( + maskedPageBounds && + maskedPageBounds.containsPoint(point) && + util.hitTestPoint(shape, this.getPointInShapeSpace(shape, point)) + ) { + return shape + } + } + + return undefined + } + + // This returns the node that should be selected when you click on this one, assuming there is nothing + // already selected. It will not return anything higher than or including the current focus layer. + getOutermostSelectableShape(shape: TLShape, filter?: (shape: TLShape) => boolean): TLShape { + let match = shape + let node = shape as TLShape | undefined + while (node) { + if ( + node.type === 'group' && + this.focusLayerId !== node.id && + !this.hasAncestor(this.focusLayerShape, node.id) && + (filter?.(node) ?? true) + ) { + match = node + } else if (this.focusLayerId === node.id) { + break + } + node = this.getParentShape(node) + } + + return match + } + + /* --------------------- Shapes --------------------- */ + + /** + * The app's set of styles. + * + * @public + */ + static styles = STYLES + + /** + * The current page bounds of all the selected shapes (Not the same thing as the page bounds of + * the selection bounding box when the selection has been rotated) + * + * @readonly + * @public + */ + @computed get selectedPageBounds(): Box2d | null { + const { + pageState: { selectedIds }, + } = this + + if (selectedIds.length === 0) return null + + return Box2d.Common(compact(selectedIds.map((id) => this.getPageBoundsById(id)))) + } + + /** The rotation of the selection bounding box. */ + @computed get selectionRotation(): number { + const { selectedIds } = this + if (selectedIds.length === 0) { + return 0 + } + if (selectedIds.length === 1) { + return this.getPageRotationById(this.selectedIds[0]) + } + + const allRotations = selectedIds.map((id) => this.getPageRotationById(id) % (Math.PI / 2)) + // if the rotations are all compatible with each other, return the rotation of any one of them + if (allRotations.every((rotation) => Math.abs(rotation - allRotations[0]) < Math.PI / 180)) { + return this.getPageRotationById(selectedIds[0]) + } + return 0 + } + + @computed get selectionBounds(): Box2d | undefined { + const { selectedIds } = this + + if (selectedIds.length === 0) { + return undefined + } + + const { selectionRotation } = this + if (selectionRotation === 0) { + return this.selectedPageBounds! + } + + if (selectedIds.length === 1) { + const bounds = this.getBounds(this.getShapeById(selectedIds[0])!).clone() + bounds.point = Matrix2d.applyToPoint(this.getPageTransformById(selectedIds[0])!, bounds.point) + return bounds + } + + // need to 'un-rotate' all the outlines of the existing nodes so we can fit them inside a box + const allPoints = this.selectedIds + .flatMap((id) => { + const pageTransform = this.getPageTransformById(id) + if (!pageTransform) return [] + return this.getOutlineById(id).map((point) => Matrix2d.applyToPoint(pageTransform, point)) + }) + .map((p) => Vec2d.Rot(p, -selectionRotation)) + const box = Box2d.FromPoints(allPoints) + // now position box so that it's top-left corner is in the right place + box.point = box.point.rot(selectionRotation) + return box + } + + @computed get selectionPageCenter() { + const { selectionBounds, selectionRotation } = this + if (!selectionBounds) return null + return Vec2d.RotWith(selectionBounds.center, selectionBounds.point, selectionRotation) + } + + /** + * An array containing all of the shapes in the current page. + * + * @example + * + * ```ts + * app.shapesArray + * ``` + * + * @readonly + * @public + */ + @computed get shapesArray() { + return Array.from(this.shapeIds).map((id) => this.store.get(id)! as TLShape) + } + + /** + * An array containing all of the shapes in the current page, sorted in z-index order (accounting + * for nested shapes): e.g. A, B, BA, BB, C. + * + * @example + * + * ```ts + * app.sortedShapesArray + * ``` + * + * @readonly + * @public + */ + @computed get sortedShapesArray(): TLShape[] { + const shapes = new Set(this.shapesArray.sort(sortByIndex)) + + const results: TLShape[] = [] + + function pushShapeWithDescendants(shape: TLShape): void { + results.push(shape) + shapes.delete(shape) + + shapes.forEach((otherShape) => { + if (otherShape.parentId === shape.id) { + pushShapeWithDescendants(otherShape) + } + }) + } + + shapes.forEach((shape) => { + const parent = this.getShapeById(shape.parentId) + if (!isShape(parent)) { + pushShapeWithDescendants(shape) + } + }) + + return results + } + + /** + * An array containing all of the currently selected shapes. + * + * @example + * + * ```ts + * app.selectedShapes + * ``` + * + * @public + * @readonly + */ + @computed get selectedShapes() { + const { selectedIds } = this.pageState + return compact(selectedIds.map((id) => this.store.get(id))) + } + + /** + * The app's only selected shape. + * + * @example + * + * ```ts + * app.onlySelectedShape + * ``` + * + * @returns Null if there is no shape or more than one selected shape, otherwise the selected + * shape. + * @public + * @readonly + */ + @computed get onlySelectedShape() { + const { selectedShapes } = this + return selectedShapes.length === 1 ? selectedShapes[0] : null + } + + /** + * Get a shape by its id. + * + * @example + * + * ```ts + * app.getShapeById('box1') + * ``` + * + * @param id - The id of the shape to get. + * @public + */ + getShapeById(id: TLParentId): T | undefined { + if (!isShapeId(id)) return undefined + return this.store.get(id) as T + } + + /** + * Get the parent shape for a given shape. Returns undefined if the shape is the direct child of + * the page. + * + * @example + * + * ```ts + * app.getParentShape(myShape) + * ``` + * + * @public + */ + getParentShape(shape?: TLShape): TLShape | undefined { + if (shape === undefined || !isShapeId(shape.parentId)) return undefined + return this.store.get(shape.parentId) + } + + /** + * If siblingShape and targetShape are siblings, this returns targetShape. If targetShape has an + * ancestor who is a sibling of siblingShape, this returns that ancestor. Otherwise, this returns + * undefined + */ + private getNearestSiblingShape( + siblingShape: TLShape, + targetShape: TLShape | undefined + ): TLShape | undefined { + if (!targetShape) { + return undefined + } + if (targetShape.parentId === siblingShape.parentId) { + return targetShape + } + + const ancestor = this.findAncestor( + targetShape, + (ancestor) => ancestor.parentId === siblingShape.parentId + ) + + return ancestor + } + + /** Get the id of the containing page for a given shape. */ + getParentPageId(shape?: TLShape): TLPageId | undefined { + if (shape === undefined) return undefined + if (TLPage.isId(shape.parentId)) { + return shape.parentId + } else { + return this.getParentPageId(this.getShapeById(shape.parentId)) + } + } + + /** + * Get whether the given shape is the descendant of the given page. + * + * @example + * + * ```ts + * app.isShapeInPage(myShape) + * app.isShapeInPage(myShape, 'page1') + * ``` + * + * @param shape - The shape to check. + * @param pageId - The id of the page to check against. Defaults to the current page. + * @public + */ + isShapeInPage(shape: TLShape, pageId = this.currentPageId): boolean { + let shapeIsInPage = false + + if (shape.parentId === pageId) { + shapeIsInPage = true + } else { + let parent = this.getShapeById(shape.parentId) + isInPageSearch: while (parent) { + if (parent.parentId === pageId) { + shapeIsInPage = true + break isInPageSearch + } + parent = this.getShapeById(parent.parentId) + } + } + + return shapeIsInPage + } + + /* --------------------- Styles --------------------- */ + + /** + * A mapping of color ids to CSS color values. + * + * @internal + */ + private colors: Map + + /** + * A mapping of size ids to size values. + * + * @internal + */ + private sizes = { + s: 2, + m: 3.5, + l: 5, + xl: 10, + } + + /** + * Get the CSS color value for a given color id. + * + * @example + * + * ```ts + * app.getCssColor('red') + * ``` + * + * @param id - The id of the color to get. + * @public + */ + getCssColor(id: TLColorStyle['id']): string { + return this.colors.get(id)! + } + + /** + * Get the stroke width value for a given size id. + * + * @example + * + * ```ts + * app.getStrokeWidth('m') + * ``` + * + * @param id - The id of the size to get. + * @public + */ + getStrokeWidth(id: TLSizeStyle['id']): number { + return this.sizes[id] + } + + /* ------------------- Statechart ------------------- */ + + /** + * The id of the current selected tool. + * + * @public + */ + get currentToolId(): string { + const activeTool = this.root.current.value + let activeToolId = activeTool?.id + + // Often a tool will transition into one of the following select states after the initial pointerdown: 'translating', 'resizing', 'dragging_handle' + // It should then supply the tool id to the `onInteractionEnd` property to tell us which tool initially triggered the interaction. + // If tool lock mode is on then tldraw will switch to the given tool id. + // If tool lock mode is off then tldraw will switch back to the select tool when the interaction ends. + + if (activeToolId === 'select' || activeToolId === 'zoom') { + const currentChildState = activeTool?.current.value as any + activeToolId = currentChildState?.info?.onInteractionEnd ?? 'select' + } + + return activeToolId ?? 'select' + } + + /** + * Set the selected tool. + * + * @example + * + * ```ts + * app.setSelectedTool('hand') + * app.setSelectedTool('hand', { date: Date.now() }) + * ``` + * + * @param id - The id of the tool to select. + * @param info - Arbitrary data to pass along into the transition. + * @public + */ + setSelectedTool(id: string, info = {}) { + this.root.transition(id, info) + return this + } + + /** + * Get a descendant by its path. + * + * @example + * + * ```ts + * state.getStateDescendant('select') + * state.getStateDescendant('select.brushing') + * ``` + * + * @param path - The descendant's path of state ids, separated by periods. + * @public + */ + getStateDescendant(path: string): StateNode | undefined { + const ids = path.split('.').reverse() + let state = this.root as StateNode + while (ids.length > 0) { + const id = ids.pop() + if (!id) return state + const childState = state.children?.[id] + if (!childState) return undefined + state = childState + } + return state + } + + /** + * Get whether a certain tool (or other state node) is currently active. + * + * @example + * + * ```ts + * app.isIn('select') + * app.isIn('select.brushing') + * ``` + * + * @param path - The path of active states, separated by periods. + * @public + */ + isIn(path: string): boolean { + const ids = path.split('.').reverse() + let state = this.root as StateNode + while (ids.length > 0) { + const id = ids.pop() + if (!id) return true + const current = state.current.value + if (current?.id === id) { + if (ids.length === 0) return true + state = current + continue + } else return false + } + return false + } + + /** + * Get whether the state node is in any of the given active paths. + * + * @example + * + * ```ts + * state.isInAny('select', 'erase') + * state.isInAny('select.brushing', 'erase.idle') + * ``` + * + * @public + */ + isInAny(...paths: string[]): boolean { + return paths.some((path) => this.isIn(path)) + } + + /* --------------------- Inputs --------------------- */ + + /** + * The app's current input state. + * + * @public + */ + inputs = { + /** The most recent pointer down's position in page space. */ + originPagePoint: new Vec2d(), + /** The most recent pointer down's position in screen space. */ + originScreenPoint: new Vec2d(), + /** The previous pointer position in page space. */ + previousPagePoint: new Vec2d(), + /** The previous pointer position in screen space. */ + previousScreenPoint: new Vec2d(), + /** The most recent pointer position in page space. */ + currentPagePoint: new Vec2d(), + /** The most recent pointer position in screen space. */ + currentScreenPoint: new Vec2d(), + /** A set containing the currently pressed keys. */ + keys: new Set(), + /** A set containing the currently pressed buttons. */ + buttons: new Set(), + /** Whether the input is from a pe. */ + isPen: false, + /** Whether the shift key is currently pressed. */ + shiftKey: false, + /** Whether the control or command key is currently pressed. */ + ctrlKey: false, + /** Whether the alt or option key is currently pressed. */ + altKey: false, + /** Whether the user is dragging. */ + isDragging: false, + /** Whether the user is pointing. */ + isPointing: false, + /** Whether the user is pinching. */ + isPinching: false, + /** Whether the user is editing. */ + isEditing: false, + /** Whether the user is panning. */ + isPanning: false, + /** Veclocity of mouse pointer, in pixels per millisecond */ + pointerVelocity: new Vec2d(), + } + + /** + * Update the input points from a pointer or pinch event. + * + * @param info - The event info. + * @internal + */ + private _updateInputsFromEvent(info: TLPointerEventInfo | TLPinchEventInfo) { + const { previousScreenPoint, previousPagePoint, currentScreenPoint, currentPagePoint } = + this.inputs + + const { screenBounds } = this.store.unsafeGetWithoutCapture(this.instanceId)! + const { x: sx, y: sy, z: sz } = info.point + const { x: cx, y: cy, z: cz } = this.camera + + previousScreenPoint.setTo(currentScreenPoint) + previousPagePoint.setTo(currentPagePoint) + + const px = (sx - screenBounds.x) / cz - cx + const py = (sy - screenBounds.y) / cz - cy + + currentScreenPoint.set(sx, sy) + currentPagePoint.set(px, py, sz ?? 0.5) + + this.inputs.isPen = info.type === 'pointer' && info.isPen + + // Reset velocity on pointer down + if (info.name === 'pointer_down') { + this.inputs.pointerVelocity = new Vec2d() + } + + // todo: We only have to do this if there are multiple users in the document + this.updateUserPresence({ cursor: currentPagePoint.toJson() }) + } + + /* --------------------- Events --------------------- */ + + /** + * A manager for recording multiple click events. + * + * @internal + */ + protected _clickManager = new ClickManager(this) + + /** + * Prevent a double click event from firing the next time the user clicks + * + * @public + */ + cancelDoubleClick() { + this._clickManager.cancelDoubleClickTimeout() + } + + /** + * The previous cursor. Used for restoring the cursor after pan events. + * + * @internal + */ + private _prevCursor: TLCursorType = 'default' + + /** @internal */ + private _shiftKeyTimeout = -1 as any + + /** @internal */ + private _setShiftKeyTimeout = () => { + this.inputs.shiftKey = false + this.dispatch({ + type: 'keyboard', + name: 'key_up', + key: 'Shift', + shiftKey: this.inputs.shiftKey, + ctrlKey: this.inputs.ctrlKey, + altKey: this.inputs.altKey, + code: 'ShiftLeft', + }) + } + + /** @internal */ + private _altKeyTimeout = -1 as any + + /** @internal */ + private _setAltKeyTimeout = () => { + this.inputs.altKey = false + this.dispatch({ + type: 'keyboard', + name: 'key_up', + key: 'Alt', + shiftKey: this.inputs.shiftKey, + ctrlKey: this.inputs.ctrlKey, + altKey: this.inputs.altKey, + code: 'AltLeft', + }) + } + + /** @internal */ + private _ctrlKeyTimeout = -1 as any + + /** @internal */ + private _setCtrlKeyTimeout = () => { + this.inputs.ctrlKey = false + this.dispatch({ + type: 'keyboard', + name: 'key_up', + key: 'Ctrl', + shiftKey: this.inputs.shiftKey, + ctrlKey: this.inputs.ctrlKey, + altKey: this.inputs.altKey, + code: 'CtrlLeft', + }) + } + + /** @internal */ + private _restoreToolId = 'select' + + /** @internal */ + private _pinchStart = 1 + + /** @internal */ + private _didPinch = false + + /** @internal */ + private _selectedIdsAtPointerDown: TLShapeId[] = [] + + /** + * Dispatch an event to the app. + * + * @example + * + * ```ts + * app.dispatch(myPointerEvent) + * ``` + * + * @param info - The event info. + * @public + */ + dispatch = (info: TLEventInfo): this => { + // prevent us from spamming similar event errors if we're crashed. + // todo: replace with new readonly mode? + if (this.crashingError) return this + + const { inputs } = this + const { type } = info + + this.batch(() => { + if (info.type === 'misc') { + // stop panning if the interaction is cancelled or completed + if (info.name === 'cancel' || info.name === 'complete') { + this.inputs.isDragging = false + + if (this.inputs.isPanning) { + this.inputs.isPanning = false + this.setCursor({ + type: this._prevCursor, + }) + } + } + + this.root.handleEvent(info) + return + } + + if (info.shiftKey) { + clearInterval(this._shiftKeyTimeout) + this._shiftKeyTimeout = -1 + inputs.shiftKey = true + } else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) { + this._shiftKeyTimeout = setTimeout(this._setShiftKeyTimeout, 150) + } + + if (info.altKey) { + clearInterval(this._altKeyTimeout) + this._altKeyTimeout = -1 + inputs.altKey = true + } else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) { + this._altKeyTimeout = setTimeout(this._setAltKeyTimeout, 150) + } + + if (info.ctrlKey) { + clearInterval(this._ctrlKeyTimeout) + this._ctrlKeyTimeout = -1 + inputs.ctrlKey = true /** @internal */ /** @internal */ /** @internal */ + } else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) { + this._ctrlKeyTimeout = setTimeout(this._setCtrlKeyTimeout, 150) + } + + const { originPagePoint, originScreenPoint, currentPagePoint, currentScreenPoint } = inputs + + if (!inputs.isPointing) { + inputs.isDragging = false + } + + switch (type) { + case 'pinch': { + if (!this.canMoveCamera) return + this._updateInputsFromEvent(info) + + switch (info.name) { + case 'pinch_start': { + if (inputs.isPinching) return + + if (!inputs.isEditing) { + this._pinchStart = this.camera.z + if (!this._selectedIdsAtPointerDown.length) { + this._selectedIdsAtPointerDown = this.selectedIds.slice() + } + + this._didPinch = true + + inputs.isPinching = true + + this.interrupt() + } + + return // Stop here! + } + case 'pinch': { + if (!inputs.isPinching) return + + const { + point: { x, y, z = 1 }, + delta: { x: dx, y: dy }, + } = info + + const { + camera: { x: cx, y: cy, z: cz }, + } = this + + const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z)) + + this.setCamera( + cx + dx / cz - x / cz + x / zoom, + cy + dy / cz - y / cz + y / zoom, + zoom + ) + + return // Stop here! + } + case 'pinch_end': { + if (!inputs.isPinching) return this + + inputs.isPinching = false + const { _selectedIdsAtPointerDown } = this + this.setSelectedIds(this._selectedIdsAtPointerDown, true) + this._selectedIdsAtPointerDown = [] + + const { + camera: { x: cx, y: cy, z: cz }, + } = this + + let zoom: number | undefined + + if (cz > 0.9 && cz < 1.05) { + zoom = 1 + } else if (cz > 0.49 && cz < 0.505) { + zoom = 0.5 + } + + if (cz > this._pinchStart - 0.1 && cz < this._pinchStart + 0.05) { + zoom = this._pinchStart + } + + if (zoom !== undefined) { + const { x, y } = this.viewportScreenCenter + this.animateCamera( + cx + (x / zoom - x) - (x / cz - x), + cy + (y / zoom - y) - (y / cz - y), + zoom, + { duration: 100 } + ) + } + + if (this._didPinch) { + this._didPinch = false + requestAnimationFrame(() => { + if (!this._didPinch) { + this.setSelectedIds(_selectedIdsAtPointerDown, true) + } + }) + } + + return // Stop here! + } + } + } + case 'wheel': { + if (!this.canMoveCamera) return + + if (this.isMenuOpen) { + // noop + } else { + if (inputs.ctrlKey) { + // todo: Start or update the zoom end interval + + // If the alt or ctrl keys are pressed, + // zoom or pan the camera and then return. + const { x, y } = this.inputs.currentScreenPoint + const { x: cx, y: cy, z: cz } = this.camera + + const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, cz + (info.delta.z ?? 0) * cz)) + + this.setCamera( + cx + (x / zoom - x) - (x / cz - x), + cy + (y / zoom - y) - (y / cz - y), + zoom + ) + + // We want to return here because none of the states in our + // statechart should respond to this event (a camera zoom) + return + } + + // Update the camera here, which will dispatch a pointer move... + // this will also update the pointer position, etc + this.pan(info.delta.x, info.delta.y) + + if ( + !inputs.isDragging && + inputs.isPointing && + originPagePoint.dist(currentPagePoint) > DRAG_DISTANCE / this.zoomLevel + ) { + inputs.isDragging = true + } + } + break + } + case 'pointer': { + // If we're pinching, return + if (inputs.isPinching) return + + this._updateInputsFromEvent(info) + + const { isPen } = info + + switch (info.name) { + case 'pointer_down': { + this._selectedIdsAtPointerDown = this.selectedIds.slice() + + // Add the button from the buttons set + inputs.buttons.add(info.button) + + inputs.isPointing = true + inputs.isDragging = false + + if (this.isPenMode) { + if (!isPen) { + // decrement the remaining taps before exiting pen mode + this._touchEventsRemainingBeforeExitingPenMode-- + if (this._touchEventsRemainingBeforeExitingPenMode === 0) { + this.setPenMode(false) + } else { + return + } + } else { + // reset the remaining taps before exiting pen mode + this._touchEventsRemainingBeforeExitingPenMode = 3 + } + } else { + if (isPen) { + this.setPenMode(true) + } + } + + if (info.button === 5) { + // Eraser button activates eraser + this._restoreToolId = this.currentToolId + this.complete() + this.setSelectedTool('eraser') + } else if (info.button === 1) { + // Middle mouse pan activates panning + if (!this.inputs.isPanning) { + this._prevCursor = this.instanceState.cursor.type + } + + this.inputs.isPanning = true + } + + if (this.inputs.isPanning) { + this.stopCameraAnimation() + this.setCursor({ + type: 'grabbing', + }) + return this + } + + originScreenPoint.setTo(currentScreenPoint) + originPagePoint.setTo(currentPagePoint) + break + } + case 'pointer_move': { + // If the user is in pen mode, but the pointer is not a pen, stop here. + if (!isPen && this.isPenMode) { + return + } + + if (this.inputs.isPanning && this.inputs.isPointing) { + // Handle panning + const { currentScreenPoint, previousScreenPoint } = this.inputs + const delta = Vec2d.Sub(currentScreenPoint, previousScreenPoint) + this.pan(delta.x, delta.y) + return + } + + if ( + !inputs.isDragging && + inputs.isPointing && + originPagePoint.dist(currentPagePoint) > DRAG_DISTANCE / this.zoomLevel + ) { + inputs.isDragging = true + } + break + } + case 'pointer_up': { + // Remove the button from the buttons set + inputs.buttons.delete(info.button) + + inputs.isPointing = false + inputs.isDragging = false + + if (this.isMenuOpen) { + // Surpressing pointerup here as doesn't seem to do what we what here. + return + } + + if (!isPen && this.isPenMode) { + return + } + + if (inputs.isPanning) { + if (info.button === 1) { + if (!this.inputs.keys.has(' ')) { + inputs.isPanning = false + + this.slideCamera({ + speed: Math.min(2, this.inputs.pointerVelocity.len()), + direction: this.inputs.pointerVelocity, + friction: HAND_TOOL_FRICTION, + }) + this.setCursor({ + type: this._prevCursor, + }) + } else { + this.slideCamera({ + speed: Math.min(2, this.inputs.pointerVelocity.len()), + direction: this.inputs.pointerVelocity, + friction: HAND_TOOL_FRICTION, + }) + this.setCursor({ + type: 'grab', + }) + } + } else if (info.button === 0) { + this.slideCamera({ + speed: Math.min(2, this.inputs.pointerVelocity.len()), + direction: this.inputs.pointerVelocity, + friction: HAND_TOOL_FRICTION, + }) + this.setCursor({ + type: 'grab', + }) + } + } else { + if (info.button === 5) { + // Eraser button activates eraser + this.complete() + this.setSelectedTool(this._restoreToolId) + } + } + + break + } + } + + break + } + case 'keyboard': { + switch (info.name) { + case 'key_down': { + // Add the key from the keys set + inputs.keys.add(info.code) + + // If the space key is pressed (but meta / control isn't!) activate panning + if (!info.ctrlKey && info.code === 'Space') { + if (!this.inputs.isPanning) { + this._prevCursor = this.instanceState.cursor.type + } + + this.inputs.isPanning = true + this.setCursor({ + type: this.inputs.isPointing ? 'grabbing' : 'grab', + }) + } + + break + } + case 'key_up': { + // Remove the key from the keys set + inputs.keys.delete(info.code) + + if (info.code === 'Space' && !this.inputs.buttons.has(1)) { + this.inputs.isPanning = false + this.setCursor({ + type: this._prevCursor, + }) + } + + break + } + case 'key_repeat': { + // nooop + break + } + } + break + } + } + + // Correct the info name for right / middle clicks + if (info.type === 'pointer') { + if (info.button === 1) { + info.name = 'middle_click' + } else if (info.button === 2) { + info.name = 'right_click' + } + + // If a pointer event, send the event to the click manager. + if (info.isPen === this.isPenMode) { + switch (info.name) { + case 'pointer_down': { + const otherEvent = this._clickManager.transformPointerDownEvent(info) + if (info.name !== otherEvent.name) { + this.root.handleEvent(info) + this.emit('event', info) + this.root.handleEvent(otherEvent) + this.emit('event', otherEvent) + return + } + + break + } + case 'pointer_up': { + const otherEvent = this._clickManager.transformPointerUpEvent(info) + if (info.name !== otherEvent.name) { + this.root.handleEvent(info) + this.emit('event', info) + this.root.handleEvent(otherEvent) + this.emit('event', otherEvent) + return + } + + break + } + case 'pointer_move': { + this._clickManager.handleMove() + break + } + } + } + } + + // Send the event to the statechart. It will be handled by all + // active states, starting at the root. + this.root.handleEvent(info) + this.emit('event', info) + }) + + return this + } + + replaceStoreContentsWithRecordsForOtherDocument(records: TLRecord[]) { + transact(() => { + this.store.clear() + const [shapes, nonShapes] = partition(records, (record) => record.typeName === 'shape') + this.store.put(nonShapes, 'initialize') + this.store.ensureStoreIsUsable() + this.store.put(shapes, 'initialize') + this.history.clear() + this.updateViewportScreenBounds() + this.updateCullingBounds() + + const bounds = this.allShapesCommonBounds + if (bounds) { + this.zoomToBounds(bounds.minX, bounds.minY, bounds.width, bounds.height, 1) + } + }) + } + + getContent(ids: TLShapeId[] = this.selectedIds): TLClipboardModel | undefined { + if (!ids) return + if (ids.length === 0) return + + const pageTransforms: Record = {} + + let shapes = dedupe( + ids + .map((id) => this.getShapeById(id) as TLShape) + .sort(sortByIndex) + .flatMap((shape) => { + const allShapes = [shape] + this.visitDescendants(shape.id, (descendant) => { + allShapes.push(this.getShapeById(descendant) as TLShape) + }) + return allShapes + }) + ) + + shapes = shapes.map((shape) => { + pageTransforms[shape.id] = this.getPageTransformById(shape.id)! + + shape = structuredClone(shape) as typeof shape + + if (TLArrowShapeDef.is(shape)) { + const startBindingId = + shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : undefined + + const endBindingId = + shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : undefined + + const info = this.getShapeUtilByDef(TLArrowShapeDef).getArrowInfo(shape) + + if (shape.props.start.type === 'binding') { + if (!shapes.some((s) => s.id === startBindingId)) { + // Uh oh, the arrow's bound-to shape isn't among the shapes + // that we're getting the content for. We should try to adjust + // the arrow so that it appears in the place it would be + if (info?.isValid) { + const { x, y } = info.start.point + shape.props.start = { + type: 'point', + x, + y, + } + } else { + const { start } = getArrowTerminalsInArrowSpace(this, shape) + shape.props.start = { + type: 'point', + x: start.x, + y: start.y, + } + } + } + } + + if (shape.props.end.type === 'binding') { + if (!shapes.some((s) => s.id === endBindingId)) { + if (info?.isValid) { + const { x, y } = info.end.point + shape.props.end = { + type: 'point', + x, + y, + } + } else { + const { end } = getArrowTerminalsInArrowSpace(this, shape) + shape.props.end = { + type: 'point', + x: end.x, + y: end.y, + } + } + } + } + + const infoAfter = getIsArrowStraight(shape) + ? getStraightArrowInfo(this, shape) + : getCurvedArrowInfo(this, shape) + + if (info?.isValid && infoAfter?.isValid && !getIsArrowStraight(shape)) { + const mpA = Vec2d.Med(info.start.handle, info.end.handle) + const distA = Vec2d.Dist(info.middle, mpA) + const distB = Vec2d.Dist(infoAfter.middle, mpA) + if (shape.props.bend < 0) { + shape.props.bend += distB - distA + } else { + shape.props.bend -= distB - distA + } + } + + return shape + } + + return shape + }) + + const rootShapeIds: TLShapeId[] = [] + + shapes.forEach((shape) => { + if (shapes.find((s) => s.id === shape.parentId) === undefined) { + // Need to get page point and rotation of the shape because shapes in + // groups use local position/rotation + + const pagePoint = this.getPagePointById(shape.id)! + const pageRotation = this.getPageRotationById(shape.id)! + shape.x = pagePoint.x + shape.y = pagePoint.y + shape.rotation = pageRotation + shape.parentId = this.currentPageId + + rootShapeIds.push(shape.id) + } + }) + + const assetsSet = new Set() + + shapes.forEach((shape) => { + if ('assetId' in shape.props) { + if (shape.props.assetId !== null) { + assetsSet.add(shape.props.assetId) + } + } + }) + + return { + shapes, + rootShapeIds, + schema: this.store.schema.serialize(), + assets: compact(Array.from(assetsSet).map((id) => this.getAssetById(id))), + } + } + + /* --------------------- Commands --------------------- */ + + putContent( + content: TLClipboardModel, + options: { + point?: VecLike + select?: boolean + preservePosition?: boolean + preserveIds?: boolean + } = {} + ): this { + if (this.isReadOnly) return this + + if (!content.schema) { + throw Error('Could not put content: content is missing a schema.') + } + + const { select = false, preserveIds = false, preservePosition = false } = options + let { point = undefined } = options + + // decide on a parent for the put shapes; if the parent is among the put shapes(?) then use its parent + + const { currentPageId } = this + const { assets, shapes, rootShapeIds } = content + + const idMap = new Map(shapes.map((shape) => [shape.id, createShapeId()])) + + // By default, the paste parent will be the current page. + let pasteParentId = this.currentPageId as TLPageId | TLShapeId + let lowestDepth = Infinity + let lowestAncestors: TLShape[] = [] + + // Among the selected shapes, find the shape with the fewest ancestors and use its first ancestor. + for (const shape of this.selectedShapes) { + if (lowestDepth === 0) break + + const ancestors = this.getAncestors(shape) + if (shape.type === 'frame') ancestors.push(shape) + + const depth = shape.type === 'frame' ? ancestors.length + 1 : ancestors.length + + if (depth < lowestDepth) { + lowestDepth = depth + lowestAncestors = ancestors + pasteParentId = shape.type === 'frame' ? shape.id : shape.parentId + } else if (depth === lowestDepth) { + if (lowestAncestors.length !== ancestors.length) { + throw Error(`Ancestors: ${lowestAncestors.length} !== ${ancestors.length}`) + } + + if (lowestAncestors.length === 0) { + pasteParentId = currentPageId + break + } else { + pasteParentId = currentPageId + for (let i = 0; i < lowestAncestors.length; i++) { + if (ancestors[i] !== lowestAncestors[i]) break + pasteParentId = ancestors[i].id + } + } + } + } + + let isDuplicating = false + + if (!TLPage.isId(pasteParentId)) { + const parent = this.getShapeById(pasteParentId) + if (parent) { + if (!this.viewportPageBounds.includes(this.getPageBounds(parent)!)) { + pasteParentId = currentPageId + } else { + if (rootShapeIds.length === 1) { + const rootShape = shapes.find((s) => s.id === rootShapeIds[0])! + if ( + TLFrameShapeDef.is(parent) && + TLFrameShapeDef.is(rootShape) && + rootShape.props.w === parent?.props.w && + rootShape.props.h === parent?.props.h + ) { + isDuplicating = true + } + } + } + } else { + pasteParentId = currentPageId + } + } + + if (!isDuplicating) { + isDuplicating = idMap.has(pasteParentId) + } + + if (isDuplicating) { + pasteParentId = this.getShapeById(pasteParentId)!.parentId + } + + let index = this.getHighestIndexForParent(pasteParentId) + + const rootShapes: TLShape[] = [] + + const newShapes: TLShapePartial[] = shapes.map((shape): TLShape => { + let newShape: TLShape + + if (preserveIds) { + newShape = deepCopy(shape) + idMap.set(shape.id, shape.id) + } else { + const id = idMap.get(shape.id)! + + // Create the new shape (new except for the id) + newShape = deepCopy({ ...shape, id }) + } + + if (rootShapeIds.includes(shape.id)) { + newShape.parentId = currentPageId + rootShapes.push(newShape) + } + + // Assign the child to its new parent. + + // If the child's parent is among the putting shapes, then assign + // it to the new parent's id. + if (idMap.has(newShape.parentId)) { + newShape.parentId = idMap.get(shape.parentId)! + } else { + rootShapeIds.push(newShape.id) + // newShape.parentId = pasteParentId + newShape.index = index + index = getIndexAbove(index) + } + + if (TLArrowShapeDef.is(newShape)) { + if (newShape.props.start.type === 'binding') { + const mappedId = idMap.get(newShape.props.start.boundShapeId) + newShape.props.start = mappedId + ? { ...newShape.props.start, boundShapeId: mappedId } + : // this shouldn't happen, if you copy an arrow but not it's bound shape it should + // convert the binding to a point at the time of copying + { type: 'point', x: 0, y: 0 } + } + if (newShape.props.end.type === 'binding') { + const mappedId = idMap.get(newShape.props.end.boundShapeId) + newShape.props.end = mappedId + ? { ...newShape.props.end, boundShapeId: mappedId } + : // this shouldn't happen, if you copy an arrow but not it's bound shape it should + // convert the binding to a point at the time of copying + { type: 'point', x: 0, y: 0 } + } + } + + return newShape + }) + + if (newShapes.length + this.shapeIds.size > MAX_SHAPES_PER_PAGE) { + // There's some complexity here involving children + // that might be created without their parents, so + // if we're going over the limit then just don't paste. + alertMaxShapes(this) + return this + } + + // Migrate the new shapes + + let assetsToCreate: TLAsset[] = [] + + if (assets) { + for (let i = 0; i < assets.length; i++) { + const asset = assets[i] + const result = this.store.schema.migratePersistedRecord(asset, content.schema) + if (result.type === 'success') { + assets[i] = result.value as TLAsset + } else { + throw Error( + `Could not put content: could not migrate content for asset:\n${JSON.stringify( + asset, + null, + 2 + )}` + ) + } + } + + const assetsToUpdate: (TLImageAsset | TLVideoAsset)[] = [] + + assetsToCreate = assets + .filter((asset) => !this.store.has(asset.id)) + .map((asset) => { + if (asset.type === 'image' || asset.type === 'video') { + if (asset.props.src && asset.props.src?.startsWith('data:image')) { + assetsToUpdate.push(structuredClone(asset)) + asset.props.src = null + } else { + assetsToUpdate.push(structuredClone(asset)) + } + } + + return asset + }) + + Promise.allSettled( + assetsToUpdate.map(async (asset) => { + const file = await dataUrlToFile( + asset.props.src!, + asset.props.name, + asset.props.mimeType ?? 'image/png' + ) + + const newAsset = await this.onCreateAssetFromFile(file) + + return [asset, newAsset] as const + }) + ).then((assets) => { + this.updateAssets( + compact( + assets.map((result) => + result.status === 'fulfilled' + ? { ...result.value[1], id: result.value[0].id } + : undefined + ) + ) + ) + }) + } + + for (let i = 0; i < newShapes.length; i++) { + const shape = newShapes[i] as TLShape + const result = this.store.schema.migratePersistedRecord(shape, content.schema) + if (result.type === 'success') { + newShapes[i] = result.value as TLShape + } else { + throw Error( + `Could not put content: could not migrate content for shape:\n${JSON.stringify( + shape, + null, + 2 + )}` + ) + } + } + + this.batch(() => { + // Create any assets that need to be created + if (assetsToCreate.length > 0) { + this.createAssets(assetsToCreate) + } + + // Create the shapes with root shapes as children of the page + this.createShapes(newShapes, select) + + // And then, if needed, reparent the root shapes to the paste parent + if (pasteParentId !== currentPageId) { + this.reparentShapesById( + rootShapes.map((s) => s.id), + pasteParentId + ) + } + + const newCreatedShapes = newShapes.map((s) => this.getShapeById(s.id)!) + const bounds = Box2d.Common(newCreatedShapes.map((s) => this.getPageBounds(s)!)) + + if (point === undefined) { + if (!TLPage.isId(pasteParentId)) { + // Put the shapes in the middle of the (on screen) parent + const shape = this.getShapeById(pasteParentId)! + const util = this.getShapeUtil(shape) + point = util.center(shape) + } else { + const { viewportPageBounds } = this + if (preservePosition || viewportPageBounds.includes(Box2d.From(bounds))) { + // Otherwise, put shapes where they used to be + point = bounds.center + } else { + // If the old bounds are outside of the viewport... + // put the shapes in the middle of the viewport + point = viewportPageBounds.center + } + } + } + + if (rootShapes.length === 1) { + const onlyRoot = rootShapes[0] as TLFrameShape + // If the old bounds are in the viewport... + if (onlyRoot.type === 'frame') { + while ( + this.getShapesAtPoint(point).some( + (shape) => + TLFrameShapeDef.is(shape) && + shape.props.w === onlyRoot.props.w && + shape.props.h === onlyRoot.props.h + ) + ) { + point.x += bounds.w + 16 + } + } + } + + this.updateShapes( + rootShapes.map((s) => { + const delta = { + x: (s.x ?? 0) - (bounds.x + bounds.w / 2), + y: (s.y ?? 0) - (bounds.y + bounds.h / 2), + } + + return { id: s.id, type: s.type, x: point!.x + delta.x, y: point!.y + delta.y } + }) + ) + }) + + return this + } + + /* --------------------- Shapes --------------------- */ + + /** + * Get a unique id for a shape. + * + * @example + * + * ```ts + * app.createShapeId() + * app.createShapeId('box1') + * ``` + * + * @param id - The id to use. + * @public + */ + createShapeId(id?: string) { + return id ? createCustomShapeId(id) : createShapeId() + } + + getHighestIndexForParent(parentId: TLShapeId | TLPageId) { + const children = this._parentIdsToChildIds.value[parentId] + + if (!children || children.length === 0) { + return 'a1' + } + return getIndexAbove(children[children.length - 1][1]) + } + + /** + * Create shapes. + * + * @example + * + * ```ts + * app.createShapes([{ id: 'box1', type: 'box' }]) + * ``` + * + * @param partials - The shape partials to create. + * @param select - Whether to select the created shapes. Defaults to false. + * @public + */ + createShapes(partials: TLShapePartial[], select = false) { + this._createShapes(partials, select) + return this + } + + /** @internal */ + private _createShapes = this.history.createCommand( + 'createShapes', + (partials: TLShapePartial[], select = false) => { + if (this.isReadOnly) return null + if (partials.length <= 0) return null + + const { shapeIds, selectedIds } = this + + const prevSelectedIds = select ? selectedIds : undefined + + const maxShapesReached = partials.length + shapeIds.size > MAX_SHAPES_PER_PAGE + + if (maxShapesReached) { + alertMaxShapes(this) + } + + const partialsToCreate = maxShapesReached + ? partials.slice(0, MAX_SHAPES_PER_PAGE - shapeIds.size) + : partials + + if (partialsToCreate.length === 0) return null + + return { + data: { + prevSelectedIds, + partials: partialsToCreate, + select, + }, + } + }, + { + do: ({ partials, select }) => { + const { focusLayerId } = this + + // 1. Parents + + // Make sure that each partial will become the child of either the + // page or another shape that exists (or that will exist) in this page. + + partials = partials.map((partial) => { + if ( + // No parentId provided + !partial.parentId || + // A parentId is proved but the parent is neither a) in the store + // or b) among the other creating shape partials + (!this.store.get(partial.parentId) && !partials.find((p) => p.id === partial.parentId)) + ) { + partial = { ...partial } + const parentId = this.getParentIdForNewShapeAtPoint( + { x: partial.x ?? 0, y: partial.y ?? 0 }, + partial.type + ) + partial.parentId = parentId + // If the parent is a shape (rather than a page) then insert the + // shapes into the shape's children. Ajust the point and page rotation to be + // preserved relative to the parent. + if (isShapeId(parentId)) { + const point = this.getPointInShapeSpace(this.getShapeById(parentId)!, { + x: partial.x ?? 0, + y: partial.y ?? 0, + }) + partial.x = point.x + partial.y = point.y + partial.rotation = -this.getPageRotationById(parentId) + (partial.rotation ?? 0) + } + return partial + } + return partial + }) + + // 2. Indices + + // Get the highest index among the parents of each of the + // the shapes being created; we'll increment from there. + + const parentIndices = new Map() + + const shapeRecordsTocreate: TLShape[] = [] + + for (const partial of partials) { + const util = this.getShapeUtil(partial as TLShape) + + // If an index is not explicitly provided, then add the + // shapes to the top of their parents' children; using the + // value in parentsMappedToIndex, get the index above, use it, + // and set it back to parentsMappedToIndex for next time. + let index = partial.index + + if (!index) { + const parentId = partial.parentId ?? focusLayerId + if (!parentIndices.has(parentId)) { + parentIndices.set(parentId, this.getHighestIndexForParent(parentId)) + } + index = parentIndices.get(parentId)! + parentIndices.set(parentId, getIndexAbove(index)) + } + + // The initial props starts as the shape utility's default props + const initialProps = util.defaultProps() + + // We then look up each key in the tab state's props; and if it's there, + // we use the value from the tab state's props instead of the default. + // Note that props will never include opacity. + const { propsForNextShape } = this.instanceState + for (const key in initialProps) { + if (key in propsForNextShape) { + if (key === 'url') continue + ;(initialProps as any)[key] = (propsForNextShape as any)[key] + } + } + + // When we create the shape, take in the partial (the props coming into the + // function) and merge it with the default props. + let shapeRecordToCreate = this.config.TLShape.create({ + ...partial, + index, + parentId: partial.parentId ?? focusLayerId, + props: 'props' in partial ? { ...initialProps, ...partial.props } : initialProps, + }) + + if (shapeRecordToCreate.index === undefined) { + throw Error('no index!') + } + + const next = this.getShapeUtil(shapeRecordToCreate).onBeforeCreate?.(shapeRecordToCreate) + + if (next) { + shapeRecordToCreate = next + } + + shapeRecordsTocreate.push(shapeRecordToCreate) + } + + this.store.put(shapeRecordsTocreate) + + // If we're also selecting the newly created shapes, attempt to select all of them; + // the engine will filter out any shapes that are descendants of other new shapes. + if (select) { + const selectedIds = partials.map((partial) => partial.id) + this.store.update(this.pageState.id, (state) => ({ ...state, selectedIds })) + } + }, + undo: ({ partials, prevSelectedIds }) => { + this.store.remove(partials.map((p) => p.id)) + + if (prevSelectedIds) { + this.store.update(this.pageState.id, (state) => ({ + ...state, + selectedIds: prevSelectedIds, + })) + } + }, + } + ) + + private animatingShapes = new Map() + + /** + * Animate shapes. + * + * @example + * + * ```ts + * app.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }]) + * ``` + * + * @param partials - The shape partials to update. + * @public + */ + animateShapes( + partials: (TLShapePartial | null | undefined)[], + options: { + /** The animation's duration in milliseconds. */ + duration?: number + /** The animation's easing function. */ + ease?: (t: number) => number + } = {} + ) { + const { duration = 500, ease = EASINGS.linear } = options + + const animationId = uniqueId() + + let remaining = duration + let t: number + + type FromTo = { prop: string; from: number; to: number } + type ShapeAnimation = { partial: TLShapePartial; values: FromTo[] } + + const animations: ShapeAnimation[] = [] + + partials.forEach((partial) => { + if (!partial) return + + const result: ShapeAnimation = { + partial, + values: [], + } + + const shape = this.getShapeById(partial.id)! + + if (!shape) return + + for (const key of ['x', 'y', 'rotation'] as const) { + if (partial[key] !== undefined && shape[key] !== partial[key]) { + result.values.push({ prop: key, from: shape[key], to: partial[key] as number }) + } + } + + animations.push(result) + this.animatingShapes.set(shape.id, animationId) + }) + + let value: ShapeAnimation + + const handleTick = (elapsed: number) => { + remaining -= elapsed + + if (remaining < 0) { + const { animatingShapes } = this + const partialsToUpdate = partials.filter( + (p) => p && animatingShapes.get(p.id) === animationId + ) + if (partialsToUpdate.length) { + this.updateShapes(partialsToUpdate, false) + // update shapes also removes the shape from animating shapes + } + + this.removeListener('tick', handleTick) + return + } + + t = ease(1 - remaining / duration) + + const { animatingShapes } = this + + try { + const tPartials: TLShapePartial[] = [] + + for (let i = 0; i < animations.length; i++) { + value = animations[i] + + if (animatingShapes.get(value.partial.id) === animationId) { + tPartials.push({ + id: value.partial.id, + type: value.partial.type, + ...value.values.reduce((acc, { prop, from, to }) => { + acc[prop] = from + (to - from) * t + return acc + }, {} as any), + }) + } + } + + this._updateShapes(tPartials, true) + } catch (e) { + // noop + } + } + + this.addListener('tick', handleTick) + + return this + } + + /** + * Update shapes. + * + * @example + * + * ```ts + * app.updateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }]) + * ``` + * + * @param partials - The shape partials to update. + * @param squashing - Whether the change is ephemeral. + * @public + */ + updateShapes(partials: (TLShapePartial | null | undefined)[], squashing = false) { + if (this.animatingShapes.size > 0) { + let partial: TLShapePartial | null | undefined + for (let i = 0; i < partials.length; i++) { + partial = partials[i] + if (partial) { + this.animatingShapes.delete(partial.id) + } + } + } + + this._updateShapes(partials, squashing) + return this + } + + /** @internal */ + private _updateShapes = this.history.createCommand( + 'updateShapes', + (_partials: (TLShapePartial | null | undefined)[], squashing = false) => { + if (this.isReadOnly) return null + + const partials = compact(_partials) + + const snapshots = Object.fromEntries( + compact(partials.map(({ id }) => this.getShapeById(id))).map((shape) => { + return [shape.id, shape] + }) + ) + + if (partials.length <= 0) return null + + const updated = compact( + partials.map((partial) => { + const prev = snapshots[partial.id] + if (!prev) return null + let newRecord = null as null | TLShape + for (const [k, v] of Object.entries(partial)) { + switch (k) { + case 'id': + case 'type': + case 'typeName': { + continue + } + default: { + if (v !== (prev as any)[k]) { + if (!newRecord) { + newRecord = { ...prev } + } + + if (k === 'props') { + newRecord!.props = { ...prev.props, ...(v as any) } + } else { + ;(newRecord as any)[k] = v + } + } + } + } + } + + return newRecord ?? prev + }) + ) as TLShape[] + + const updates = Object.fromEntries(updated.map((shape) => [shape.id, shape])) + + return { data: { snapshots, updates }, squashing } + }, + { + do: ({ updates }) => { + const arr = Object.values(updates) + + // Iterate through array; if any shape has an onUpdate handler, call it + // and, if the handler returns a new shape, replace the old shape with + // the new one. This is used for example when repositioning a text shape + // based on its new text content. + let shape: TLShape + let next: TLShape | void + for (let i = 0, n = arr.length; i < n; i++) { + shape = arr[i] + next = this.getShapeUtil(shape).onBeforeUpdate?.(this.store.get(shape.id)!, shape) + if (next) { + arr[i] = next + } + } + this.store.put(arr) + }, + undo: ({ snapshots }) => { + this.store.put(Object.values(snapshots)) + }, + squash(prevData, nextData) { + return { + // keep the oldest snapshots + snapshots: { ...nextData.snapshots, ...prevData.snapshots }, + // keep the newest updates + updates: { ...prevData.updates, ...nextData.updates }, + } + }, + } + ) + + /** + * Delete shapes. + * + * @example + * + * ```ts + * app.deleteShapes() + * app.deleteShapes(['box1', 'box2']) + * ``` + * + * @param ids - The ids of the shapes to delete. Defaults to the selected shapes. + * @public + */ + deleteShapes(ids: TLShapeId[] = this.selectedIds) { + this._deleteShapes(ids) + return this + } + + /** @internal */ + private _deleteShapes = this.history.createCommand( + 'delete_shapes', + (ids: TLShapeId[]) => { + if (this.isReadOnly) return null + if (ids.length === 0) return null + const prevSelectedIds = [...this.pageState.selectedIds] + + const allIds = new Set(ids) + + for (const id of ids) { + this.visitDescendants(id, (childId) => { + allIds.add(childId) + }) + } + + const deletedIds = [...allIds] + const arrowBindings = this._arrowBindingsIndex.value + const snapshots = compact( + deletedIds.flatMap((id) => { + const shape = this.getShapeById(id) + + // Add any bound arrows to the snapshots, so that we can restore the bindings on undo + const bindings = arrowBindings[id] + if (bindings && bindings.length > 0) { + return bindings.map(({ arrowId }) => this.getShapeById(arrowId)).concat(shape) + } + return shape + }) + ) + + const postSelectedIds = prevSelectedIds.filter((id) => !allIds.has(id)) + + return { data: { deletedIds, snapshots, prevSelectedIds, postSelectedIds } } + }, + { + do: ({ deletedIds, postSelectedIds }) => { + this.store.remove(deletedIds) + this.store.update(this.pageState.id, (state) => ({ + ...state, + selectedIds: postSelectedIds, + })) + }, + undo: ({ snapshots, prevSelectedIds }) => { + this.store.put(snapshots) + this.store.update(this.pageState.id, (state) => ({ + ...state, + selectedIds: prevSelectedIds, + })) + }, + } + ) + + /** + * Update user document settings + * + * @example + * + * ```ts + * app.updateUserDocumentSettings({ isGridMode: true }) + * ``` + * + * @public + */ + updateUserDocumentSettings(partial: Partial, ephemeral = false) { + this._updateUserDocumentSettings(partial, ephemeral) + return this + } + + /** @internal */ + private _updateUserDocumentSettings = this.history.createCommand( + 'updateUserDocumentSettings', + (partial: Partial, ephemeral = false) => { + const prev = this.userDocumentSettings + const next = { ...prev, ...partial } + return { data: { prev, next }, ephemeral } + }, + { + do: ({ next }) => { + this.store.put([next]) + }, + undo: ({ prev }) => { + this.store.put([prev]) + }, + } + ) + + /** + * Update a page. + * + * @example + * + * ```ts + * app.updatePage({ id: 'page2', name: 'Page 2' }) + * ``` + * + * @param partial - The partial of the shape to update. + * @public + */ + updatePage(partial: RequiredKeys, squashing = false) { + this._updatePage(partial, squashing) + return this + } + + /** @internal */ + private _updatePage = this.history.createCommand( + 'updatePage', + (partial: RequiredKeys, squashing = false) => { + if (this.isReadOnly) return null + + const prev = this.getPageById(partial.id) + + if (!prev) return null + + return { data: { prev, partial }, squashing } + }, + { + do: ({ partial }) => { + this.store.update(partial.id, (page) => ({ ...page, ...partial })) + }, + undo: ({ prev, partial }) => { + this.store.update(partial.id, () => prev) + }, + squash(prevData, nextData) { + return { + prev: { ...prevData.prev, ...nextData.prev }, + partial: nextData.partial, + } + }, + } + ) + + /** + * Create a page. + * + * @example + * + * ```ts + * app.createPage('New Page') + * app.createPage('New Page', 'page1') + * ``` + * + * @param id - The new page's id. + * @param title - The new page's title. + * @public + */ + createPage(title: string, id: TLPageId = TLPage.createId(), belowPageIndex?: string) { + this._createPage(title, id, belowPageIndex) + return this + } + + /** @internal */ + private _createPage = this.history.createCommand( + 'createPage', + (title: string, id: TLPageId = TLPage.createId(), belowPageIndex?: string) => { + if (this.isReadOnly) return null + if (this.pages.length >= MAX_PAGES) return null + const pageInfo = this.pages + const topIndex = belowPageIndex ?? pageInfo[pageInfo.length - 1]?.index ?? 'a1' + const bottomIndex = pageInfo[pageInfo.findIndex((p) => p.index === topIndex) + 1]?.index + + const prevPageState = { ...this.pageState } + const prevInstanceState = { ...this.instanceState } + + title = getIncrementedName( + title, + pageInfo.map((p) => p.name) + ) + + const newPage = TLPage.create({ + id, + name: title, + index: bottomIndex ? getIndexBetween(topIndex, bottomIndex) : getIndexAbove(topIndex), + }) + + const newCamera = TLCamera.create({}) + + const newTabPageState = TLInstancePageState.create({ + pageId: newPage.id, + instanceId: this.instanceId, + cameraId: newCamera.id, + }) + + return { + data: { + prevPageState, + prevTabState: prevInstanceState, + newPage, + newTabPageState, + newCamera, + }, + } + }, + { + do: ({ newPage, newTabPageState, newCamera }) => { + this.store.put([ + newPage, + newCamera, + newTabPageState, + { ...this.instanceState, currentPageId: newPage.id }, + ]) + this.updateCullingBounds() + }, + undo: ({ newPage, prevPageState, prevTabState, newTabPageState }) => { + this.store.put([prevPageState, prevTabState]) + this.store.remove([newTabPageState.id, newPage.id, newTabPageState.cameraId]) + this.updateCullingBounds() + }, + } + ) + + duplicatePage(id: TLPageId = this.currentPageId, createId: TLPageId = TLPage.createId()) { + if (this.pages.length >= MAX_PAGES) return + const page = this.getPageById(id) + if (!page) return + + const camera = { ...this.camera } + const content = this.getContent(this.getSortedChildIds(page.id)) + + this.batch(() => { + this.createPage(page.name + ' Copy', createId, page.index) + this.setCurrentPageId(createId) + this.setCamera(camera.x, camera.y, camera.z) + + // will change page automatically + if (content) { + return this.putContent(content) + } + }) + } + + /** + * Delete a page. + * + * @example + * + * ```ts + * app.deletePage('page1') + * ``` + * + * @param id - The id of the page to delete. + * @public + */ + deletePage(id: TLPageId) { + this._deletePage(id) + } + + /** @internal */ + private _deletePage = this.history.createCommand( + 'delete_page', + (id: TLPageId) => { + if (this.isReadOnly) return null + const { pages } = this + if (pages.length === 1) return null + + const deletedPage = this.getPageById(id) + const deletedPageStates = this._pageStates.value.filter((s) => s.pageId === id) + + if (!deletedPage) return null + + if (id === this.currentPageId) { + const index = pages.findIndex((page) => page.id === id) + const next = pages[index - 1] ?? pages[index + 1] + this.setCurrentPageId(next.id) + } + + return { data: { id, deletedPage, deletedPageStates } } + }, + { + do: ({ deletedPage, deletedPageStates }) => { + this.store.remove(deletedPageStates.map((s) => s.id)) // remove the page state + this.store.remove([deletedPage.id]) // remove the page + this.updateCullingBounds() + }, + undo: ({ deletedPage, deletedPageStates }) => { + this.store.put([deletedPage]) + this.store.put(deletedPageStates) + this.updateCullingBounds() + }, + } + ) + + /** + * Update a page state. + * + * @example + * + * ```ts + * app.setInstancePageState({ id: 'page1', editingId: 'shape:123' }) + * app.setInstancePageState({ id: 'page1', editingId: 'shape:123' }, true) + * ``` + * + * @param partial - The partial of the page state object containing the changes. + * @param ephemeral - Whether the command is ephemeral. + * @public + */ + setInstancePageState(partial: Partial, ephemeral = false) { + this._setInstancePageState(partial, ephemeral) + } + + /** @internal */ + private _setInstancePageState = this.history.createCommand( + 'setInstancePageState', + (partial: Partial, ephemeral = false) => { + const prev = this.store.get(partial.id ?? this.pageState.id)! + return { data: { prev, partial }, ephemeral } + }, + { + do: ({ prev, partial }) => { + this.store.update(prev.id, (state) => ({ ...state, ...partial })) + }, + undo: ({ prev }) => { + this.store.update(prev.id, () => prev) + }, + } + ) + + /** + * Set user state. Always ephemeral for now. + * + * @example + * + * ```ts + * app.updateUser({ color: '#923433' }) + * ``` + * + * @param partial - The partial of the user state object containing the changes. + * @public + */ + updateUser(partial: Partial) { + const next = { ...this.user, ...partial } + this.store.put([next]) + } + + /** @internal */ + @computed private get _currentUserPresence() { + return this.store.query.record('user_presence', () => ({ userId: { eq: this.userId } })) + } + + get userPresence() { + return this._currentUserPresence.value + } + + // when a user performs any action in the app, we update their presence record + updateUserPresence = ({ + cursor, + color, + viewportPageBounds, + }: { cursor?: Vec2dModel; color?: string; viewportPageBounds?: Box2dModel } = {}) => { + const presence = this._currentUserPresence.value + if (!presence) { + console.error('No presence found for current user') + return + } + + this.store.put([ + { + ...presence, + cursor: cursor ?? presence.cursor, + color: color ?? presence.color, + viewportPageBounds: viewportPageBounds ?? presence.viewportPageBounds, + lastUsedInstanceId: this.instanceId, + lastActivityTimestamp: Date.now(), + }, + ]) + } + + /** + * Select one or more shapes. + * + * @example + * + * ```ts + * app.setSelectedIds(['id1']) + * app.setSelectedIds(['id1', 'id2']) + * ``` + * + * @param ids - The ids to select. + * @param squashing - Whether the change should create a new history entry or combine with the + * previous (if the previous is the same type). + * @public + */ + setSelectedIds(ids: TLShapeId[], squashing = false) { + this._setSelectedIds(ids, squashing) + return this + } + + /** @internal */ + private _setSelectedIds = this.history.createCommand( + 'setSelectedIds', + (ids: TLShapeId[], squashing = false) => { + const prevSelectedIds = this.pageState.selectedIds + + const prevSet = new Set(this.pageState.selectedIds) + + if (ids.length === prevSet.size && ids.every((id) => prevSet.has(id))) return null + + return { data: { ids, prevSelectedIds }, squashing, preservesRedoStack: true } + }, + { + do: ({ ids }) => { + this.store.update(this.pageState.id, (state) => ({ ...state, selectedIds: ids })) + }, + undo: ({ prevSelectedIds }) => { + this.store.update(this.pageState.id, () => ({ + ...this.pageState, + selectedIds: prevSelectedIds, + })) + }, + squash(prev, next) { + return { ids: next.ids, prevSelectedIds: prev.prevSelectedIds } + }, + } + ) + + /** + * Determine whether or not a shape is selected + * + * @example + * + * ```ts + * app.isSelected('id1') + * ``` + * + * @param id - The id of the shape to check. + * @public + */ + isSelected(id: TLShapeId) { + return this.selectedIdsSet.has(id) + } + + /** + * Determine whether a not a shape is within the current selection. A shape is within the + * selection if it or any of its parents is selected. + * + * @param id - The id of the shape to check. + * @public + */ + isWithinSelection(id: TLShapeId) { + const shape = this.getShapeById(id) + if (!shape) return false + + if (this.isSelected(id)) return true + + return !!this.findAncestor(shape, (parent) => this.isSelected(parent.id)) + } + + /* --------------------- Assets --------------------- */ + + /** @internal */ + @computed private get _assets() { + return this.store.query.records('asset') + } + + /** Get all assets in the app. */ + get assets() { + return this._assets.value + } + + /** + * Create one or more assets. + * + * @example + * + * ```ts + * app.createAssets([...myAssets]) + * ``` + * + * @param assets - The assets to create. + * @public + */ + createAssets(assets: TLAsset[]) { + this._createAssets(assets) + return this + } + + /** @internal */ + private _createAssets = this.history.createCommand( + 'createAssets', + (assets: TLAsset[]) => { + if (this.isReadOnly) return null + if (assets.length <= 0) return null + + return { data: { assets } } + }, + { + do: ({ assets }) => { + this.store.put(assets) + }, + undo: ({ assets }) => { + this.store.remove(assets.map((a) => a.id)) + }, + } + ) + + /** + * Delete one or more assets. + * + * @example + * + * ```ts + * app.deleteAssets(['asset1', 'asset2']) + * ``` + * + * @param ids - The assets to delete. + * @public + */ + deleteAssets(ids: TLAssetId[]) { + this._deleteAssets(ids) + return this + } + + /** @internal */ + private _deleteAssets = this.history.createCommand( + 'deleteAssets', + (ids: TLAssetId[]) => { + if (this.isReadOnly) return + if (ids.length <= 0) return + + const prev = compact(ids.map((id) => this.store.get(id))) + + return { data: { ids, prev } } + }, + { + do: ({ ids }) => { + this.store.remove(ids) + }, + undo: ({ prev }) => { + this.store.put(prev) + }, + } + ) + + /** + * Update one or more assets. + * + * @example + * + * ```ts + * app.updateAssets([{ id: 'asset1', name: 'New name' }]) + * ``` + * + * @param assets - The assets to update. + * @public + */ + updateAssets(assets: TLAssetPartial[]) { + this._updateAssets(assets) + return this + } + + /** @internal */ + private _updateAssets = this.history.createCommand( + 'updateAssets', + (assets: TLAssetPartial[]) => { + if (this.isReadOnly) return + if (assets.length <= 0) return + + const snapshots: Record = {} + + return { data: { snapshots, assets } } + }, + { + do: ({ assets, snapshots }) => { + this.store.put( + assets.map((a) => { + const asset = this.store.get(a.id)! + snapshots[a.id] = asset + + return { + ...asset, + ...a, + } + }) + ) + }, + undo: ({ snapshots }) => { + this.store.put(Object.values(snapshots)) + }, + } + ) + + /** + * Get an asset by its src property. + * + * @example + * + * ```ts + * app.getAssetBySource('https://example.com/image.png') + * ``` + * + * @param src - The source value of the asset. + * @public + */ + getAssetBySrc(src: string) { + return this.assets.find((a) => a.props.src === src) + } + + /** + * Get an asset by its id. + * + * @example + * + * ```ts + * app.getAssetById('asset1') + * ``` + * + * @param id - The id of the asset. + * @public + */ + getAssetById(id: TLAssetId): TLAsset | undefined { + return this.store.get(id) as TLAsset | undefined + } + + /* ------------------- SubCommands ------------------ */ + async getSvg( + ids: TLShapeId[] = (this.selectedIds.length + ? this.selectedIds + : Object.keys(this.shapeIds)) as TLShapeId[], + opts = {} as Partial<{ + scale: number + background: boolean + padding: number + darkMode?: boolean + preserveAspectRatio: React.SVGAttributes['preserveAspectRatio'] + }> + ) { + if (ids.length === 0) return + if (!window.document) throw Error('No document') + + const { + scale = 1, + background = false, + padding = SVG_PADDING, + darkMode = this.userDocumentSettings.isDarkMode, + preserveAspectRatio = false, + } = opts + + const realContainerEl = this.getContainer() + const realContainerStyle = getComputedStyle(realContainerEl) + + // Get the styles from the container. We'll use these to pull out colors etc. + // NOTE: We can force force a light theme here becasue we don't want export + const fakeContainerEl = document.createElement('div') + fakeContainerEl.className = `rs-container rs-theme__${darkMode ? 'dark' : 'light'}` + document.body.appendChild(fakeContainerEl) + + const containerStyle = getComputedStyle(fakeContainerEl) + const fontsUsedInExport = new Map() + + const colors: TLExportColors = { + fill: Object.fromEntries( + STYLES.color.map((color) => [ + color.id, + containerStyle.getPropertyValue(`--palette-${color.id}`), + ]) + ) as Record, + pattern: Object.fromEntries( + STYLES.color.map((color) => [ + color.id, + containerStyle.getPropertyValue(`--palette-${color.id}-pattern`), + ]) + ) as Record, + semi: Object.fromEntries( + STYLES.color.map((color) => [ + color.id, + containerStyle.getPropertyValue(`--palette-${color.id}-semi`), + ]) + ) as Record, + text: containerStyle.getPropertyValue(`--color-text`), + background: containerStyle.getPropertyValue(`--color-background`), + solid: containerStyle.getPropertyValue(`--palette-solid`), + } + + // Remove containerEl from DOM (temp DOM node) + document.body.removeChild(fakeContainerEl) + + // ---Figure out which shapes we need to include + + const shapes = this.getShapesAndDescendantsInOrder(ids) + + // --- Common bounding box of all shapes + + // Get the common bounding box for the selected nodes (with some padding) + const bbox = Box2d.FromPoints( + shapes + .map((shape) => { + const pageMask = this.getPageMaskById(shape.id) + if (pageMask) { + return pageMask + } + const pageTransform = this.getPageTransform(shape)! + const pageOutline = Matrix2d.applyToPoints(pageTransform, this.getOutline(shape)) + return pageOutline + }) + .flat() + ) + + const isSingleFrameShape = ids.length === 1 && shapes[0].type === 'frame' + + if (!isSingleFrameShape) { + // Expand by an extra 32 pixels + bbox.expandBy(padding) + } + + // We want the svg image to be BIGGER THAN USUAL to account for image quality + const w = bbox.width * scale + const h = bbox.height * scale + + // --- Create the SVG + + // Embed our custom fonts + const svg = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg') + + if (preserveAspectRatio) { + svg.setAttribute('preserveAspectRatio', preserveAspectRatio) + } + + svg.setAttribute('direction', 'ltr') + svg.setAttribute('width', w + '') + svg.setAttribute('height', h + '') + svg.setAttribute('viewBox', `${bbox.minX} ${bbox.minY} ${bbox.width} ${bbox.height}`) + svg.setAttribute('stroke-linecap', 'round') + svg.setAttribute('stroke-linejoin', 'round') + // Add current background color, or else background will be transparent + + if (background) { + if (isSingleFrameShape) { + svg.style.setProperty('background', colors.solid) + } else { + svg.style.setProperty('background-color', colors.background) + } + } else { + svg.style.setProperty('background-color', 'transparent') + } + + // Add the defs to the svg + const defs = window.document.createElementNS('http://www.w3.org/2000/svg', 'defs') + + for (const element of Array.from(exportPatternSvgDefs(colors.solid))) { + defs.appendChild(element) + } + + try { + document.body.focus?.() // weird but necessary + } catch (e) { + // not implemented + } + + svg.append(defs) + + // Must happen in order, not using a promise.all, or else the order of the + // elements in the svg will be wrong. + + let shape: TLShape + for (let i = 0, n = shapes.length; i < n; i++) { + shape = shapes[i] + + // Don't render the frame if we're only exporting a single frame + if (isSingleFrameShape && i === 0) continue + + let font: string | undefined + + if ('font' in shape.props) { + if (shape.props.font) { + if (fontsUsedInExport.has(shape.props.font)) { + font = fontsUsedInExport.get(shape.props.font)! + } else { + // For some reason these styles aren't present in the fake element + // so we need to get them from the real element + font = realContainerStyle.getPropertyValue(`--rs-font-${shape.props.font}`) + fontsUsedInExport.set(shape.props.font, font) + } + } + } + + const util = this.getShapeUtil(shape) + + let utilSvgElement = await util.toSvg?.(shape, font, colors) + + if (!utilSvgElement) { + const bounds = this.getPageBounds(shape)! + const elm = window.document.createElementNS('http://www.w3.org/2000/svg', 'rect') + elm.setAttribute('width', bounds.width + '') + elm.setAttribute('height', bounds.height + '') + elm.setAttribute('fill', colors.solid) + elm.setAttribute('stroke', colors.pattern.grey) + elm.setAttribute('stroke-width', '1') + utilSvgElement = elm + } + + // If the node implements toSvg, use that + const shapeSvg = utilSvgElement + + let pageTransform = this.getPageTransform(shape)!.toCssString() + + if ('scale' in shape.props) { + if (shape.props.scale !== 1) { + pageTransform = `${pageTransform} scale(${shape.props.scale}, ${shape.props.scale})` + } + } + + shapeSvg.setAttribute('transform', pageTransform) + if ('opacity' in shape.props) shapeSvg.setAttribute('opacity', shape.props.opacity + '') + + // Create svg mask if shape has a frame as parent + const pageMask = this.getPageMaskById(shape.id) + if (shapeSvg && pageMask) { + // Create a clip path and add it to defs + const clipPathEl = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath') + defs.appendChild(clipPathEl) + const id = nanoid() + clipPathEl.id = id + + // Create a polyline mask that does the clipping + const mask = document.createElementNS('http://www.w3.org/2000/svg', 'path') + mask.setAttribute('d', `M${pageMask.map(({ x, y }) => `${x},${y}`).join('L')}Z`) + clipPathEl.appendChild(mask) + + // Create a group that uses the clip path and wraps the shape + const outerElement = document.createElementNS('http://www.w3.org/2000/svg', 'g') + outerElement.setAttribute('clip-path', `url(#${id})`) + + outerElement.appendChild(shapeSvg) + svg.appendChild(outerElement) + } else { + svg.appendChild(shapeSvg) + } + } + + // Add styles to the defs + let styles = `` + const style = window.document.createElementNS('http://www.w3.org/2000/svg', 'style') + + // Insert fonts into app + const fontInstances: any[] = [] + + if ('fonts' in document) { + document.fonts.forEach((font) => fontInstances.push(font)) + } + + for (const font of fontInstances) { + const fileReader = new FileReader() + + let isUsed = false + + fontsUsedInExport.forEach((fontName) => { + if (fontName.includes(font.family)) { + isUsed = true + } + }) + + if (!isUsed) continue + + const url = (font as any).$$_url + + const fontFaceRule = (font as any).$$_fontface + + if (url) { + const fontFile = await (await fetch(url)).blob() + + const base64Font = await new Promise((resolve, reject) => { + fileReader.onload = () => resolve(fileReader.result as string) + fileReader.onerror = () => reject(fileReader.error) + fileReader.readAsDataURL(fontFile) + }) + + const newFontFaceRule = '\n' + fontFaceRule.replaceAll(url, base64Font) + styles += newFontFaceRule + } + } + + style.textContent = styles + + defs.append(style) + + return svg + } + + /** + * Rename a page. + * + * @example + * + * ```ts + * app.renamePage('page1', 'My Page') + * ``` + * + * @param id - The id of the page to rename. + * @param name - The new name. + * @public + */ + renamePage(id: TLPageId, name: string, squashing = false) { + if (this.isReadOnly) return this + this.updatePage({ id, name }, squashing) + return this + } + + /** + * Move shapes to page. + * + * @example + * + * ```ts + * app.moveShapesToPage(['box1', 'box2'], 'page1') + * ``` + * + * @param ids - The ids of the shapes to move. + * @param pageId - The id of the page where the shapes will be moved. + * @public + */ + moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this { + if (ids.length === 0) return this + if (this.isReadOnly) return this + + const { currentPageId } = this + + if (pageId === currentPageId) return this + if (!this.store.has(pageId)) return this + + // Basically copy the shapes + const content = this.getContent(ids) + + // Just to be sure + if (!content) return this + + // If there is no space on pageId, or if the selected shapes + // would take the new page above the limit, don't move the shapes + if (this.getShapesInPage(pageId).length + content.shapes.length > MAX_SHAPES_PER_PAGE) { + alertMaxShapes(this, pageId) + return this + } + + const fromPageZ = this.camera.z + + this.history.batch(() => { + // Delete the shapes on the current page + this.deleteShapes(ids) + + // Move to the next page + this.setCurrentPageId(pageId) + + // Put the shape content onto the new page; parents and indices will + // be taken care of by the putContent method; make sure to pop any focus + // layers so that the content will be put onto the page. + this.setFocusLayer(null) + this.selectNone() + this.putContent(content, { select: true, preserveIds: true, preservePosition: true }) + + // Force the new page's camera to be at the same zoom level as the + // "from" page's camera, then center the "to" page's camera on the + // pasted shapes + const { + center: { x, y }, + } = this.selectionBounds! + this.setCamera(this.camera.x, this.camera.y, fromPageZ) + this.centerOnPoint(x, y) + }) + + this.emit('moved-to-page', { name: this.currentPage.name, toId: pageId, fromId: currentPageId }) + + return this + } + + lockShapes(_ids: TLShapeId[] = this.pageState.selectedIds): this { + if (this.isReadOnly) return this + // todo + return this + } + + /** + * Reorder shapes. + * + * @param operation - The operation to perform. + * @param ids - The ids to reorder. + * @public + */ + reorderShapes(operation: TLReorderOperation, ids: TLShapeId[]) { + if (this.isReadOnly) return this + if (ids.length === 0) return this + + const parents = this.getParentsMappedToChildren(ids) + + const changes: TLShapePartial[] = [] + + switch (operation) { + case 'toBack': { + parents.forEach((movingSet, parentId) => { + const siblings = compact( + this.getSortedChildIds(parentId).map((id) => this.getShapeById(id)) + ) + + if (movingSet.size === siblings.length) return + + let below: string | undefined + let above: string | undefined + + for (const shape of siblings) { + if (!movingSet.has(shape)) { + above = shape.index + break + } + movingSet.delete(shape) + below = shape.index + } + + if (movingSet.size === 0) return + + const indices = getIndicesBetween(below, above, movingSet.size) + + Array.from(movingSet.values()) + .sort(sortByIndex) + .forEach((node, i) => + changes.push({ id: node.id as any, type: node.type, index: indices[i] }) + ) + }) + + break + } + case 'toFront': { + parents.forEach((movingSet, parentId) => { + const siblings = compact( + this.getSortedChildIds(parentId).map((id) => this.getShapeById(id)) + ) + const len = siblings.length + + if (movingSet.size === len) return + + let below: string | undefined + let above: string | undefined + + for (let i = len - 1; i > -1; i--) { + const shape = siblings[i] + + if (!movingSet.has(shape)) { + below = shape.index + break + } + + movingSet.delete(shape) + above = shape.index + } + + if (movingSet.size === 0) return + + const indices = getIndicesBetween(below, above, movingSet.size) + + Array.from(movingSet.values()) + .sort(sortByIndex) + .forEach((node, i) => + changes.push({ id: node.id as any, type: node.type, index: indices[i] }) + ) + }) + + break + } + case 'forward': { + parents.forEach((movingSet, parentId) => { + const siblings = compact( + this.getSortedChildIds(parentId).map((id) => this.getShapeById(id)) + ) + const len = siblings.length + + if (movingSet.size === len) return + + const movingIndices = new Set(Array.from(movingSet).map((n) => siblings.indexOf(n))) + + let selectIndex = -1 + let isSelecting = false + let below: string | undefined + let above: string | undefined + let count: number + + for (let i = 0; i < len; i++) { + const isMoving = movingIndices.has(i) + + if (!isSelecting && isMoving) { + isSelecting = true + selectIndex = i + above = undefined + } else if (isSelecting && !isMoving) { + isSelecting = false + count = i - selectIndex + below = siblings[i].index + above = siblings[i + 1]?.index + + const indices = getIndicesBetween(below, above, count) + + for (let k = 0; k < count; k++) { + const node = siblings[selectIndex + k] + changes.push({ id: node.id as any, type: node.type, index: indices[k] }) + } + } + } + }) + + break + } + case 'backward': { + parents.forEach((movingSet, parentId) => { + const siblings = compact( + this.getSortedChildIds(parentId).map((id) => this.getShapeById(id)) + ) + const len = siblings.length + + if (movingSet.size === len) return + + const movingIndices = new Set(Array.from(movingSet).map((n) => siblings.indexOf(n))) + + let selectIndex = -1 + let isSelecting = false + let count: number + + for (let i = len - 1; i > -1; i--) { + const isMoving = movingIndices.has(i) + + if (!isSelecting && isMoving) { + isSelecting = true + selectIndex = i + } else if (isSelecting && !isMoving) { + isSelecting = false + count = selectIndex - i + + const indices = getIndicesBetween(siblings[i - 1]?.index, siblings[i].index, count) + + for (let k = 0; k < count; k++) { + const node = siblings[i + k + 1] + changes.push({ id: node.id as any, type: node.type, index: indices[k] }) + } + } + } + }) + + break + } + } + + this.updateShapes(changes) + return this + } + + /** + * Send shapes to the back of the page's object list. + * + * @example + * + * ```ts + * app.sendToBack() + * app.sendToBack(['id1', 'id2']) + * ``` + * + * @param ids - The ids of the shapes to move. Defaults to the ids of the selected shapes. + * @public + */ + sendToBack(ids = this.pageState.selectedIds) { + this.reorderShapes('toBack', ids) + return this + } + + /** + * Send shapes backward in the page's object list. + * + * @example + * + * ```ts + * app.sendBackward() + * app.sendBackward(['id1', 'id2']) + * ``` + * + * @param ids - The ids of the shapes to move. Defaults to the ids of the selected shapes. + * @public + */ + sendBackward(ids = this.pageState.selectedIds) { + this.reorderShapes('backward', ids) + return this + } + + /** + * Bring shapes forward in the page's object list. + * + * @example + * + * ```ts + * app.bringForward() + * app.bringForward(['id1', 'id2']) + * ``` + * + * @param ids - The ids of the shapes to move. Defaults to the ids of the selected shapes. + * @public + */ + bringForward(ids = this.pageState.selectedIds) { + this.reorderShapes('forward', ids) + return this + } + + /** + * Bring shapes to the front of the page's object list. + * + * @example + * + * ```ts + * app.bringToFront() + * app.bringToFront(['id1', 'id2']) + * ``` + * + * @param ids - The ids of the shapes to move. Defaults to the ids of the selected shapes. + * @public + */ + bringToFront(ids = this.pageState.selectedIds) { + this.reorderShapes('toFront', ids) + return this + } + + /** + * Flip shape positions. + * + * @example + * + * ```ts + * app.flipShapes('horizontal') + * app.flipShapes('horizontal', ['box1', 'box2']) + * ``` + * + * @param operation - Whether to flip horizontally or vertically. + * @param ids - The ids of the shapes to flip. Defaults to selected shapes. + * @public + */ + flipShapes(operation: 'horizontal' | 'vertical', ids: TLShapeId[] = this.selectedIds) { + if (this.isReadOnly) return this + + let shapes = compact(ids.map((id) => this.getShapeById(id))) + + if (!shapes.length) return this + + shapes = shapes + .map((shape) => { + if (shape.type === 'group') { + return this.getSortedChildIds(shape.id).map((id) => this.getShapeById(id)) + } + + return shape + }) + .flat() as TLShape[] + + const scaleOriginPage = Box2d.Common(compact(shapes.map((id) => this.getPageBounds(id)))).center + + this.batch(() => { + for (const shape of shapes) { + const util = this.getShapeUtil(shape) + const bounds = util.bounds(shape) + const initialPageTransform = this.getPageTransformById(shape.id) + if (!initialPageTransform) continue + this.resizeShape( + shape.id, + { x: operation === 'horizontal' ? -1 : 1, y: operation === 'vertical' ? -1 : 1 }, + { + initialBounds: bounds, + initialPageTransform, + initialShape: shape, + mode: 'scale_shape', + scaleOrigin: scaleOriginPage, + scaleAxisRotation: 0, + } + ) + } + }) + + return this + } + + /** + * Stack shape. + * + * @example + * + * ```ts + * app.stackShapes('horizontal') + * app.stackShapes('horizontal', ['box1', 'box2']) + * app.stackShapes('horizontal', ['box1', 'box2'], 20) + * ``` + * + * @param operation - Whether to stack horizontally or vertically. + * @param ids - The ids of the shapes to stack. Defaults to selected shapes. + * @param gap - A specific gap to use when stacking. + * @public + */ + stackShapes( + operation: 'horizontal' | 'vertical', + ids: TLShapeId[] = this.pageState.selectedIds, + gap?: number + ) { + if (this.isReadOnly) return this + + const shapes = compact(ids.map((id) => this.getShapeById(id))).filter((shape) => { + if (!shape) return false + + if (TLArrowShapeDef.is(shape)) { + if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') { + return false + } + } + + return true + }) + + const len = shapes.length + + if ((gap === undefined && len < 3) || len < 2) return this + + const pageBounds = Object.fromEntries( + shapes.map((shape) => [shape.id, this.getPageBounds(shape)!]) + ) + + let val: 'x' | 'y' + let min: 'minX' | 'minY' + let max: 'maxX' | 'maxY' + let dim: 'width' | 'height' + + if (operation === 'horizontal') { + val = 'x' + min = 'minX' + max = 'maxX' + dim = 'width' + } else { + val = 'y' + min = 'minY' + max = 'maxY' + dim = 'height' + } + + let shapeGap: number + + if (gap === undefined) { + const gaps: { gap: number; count: number }[] = [] + + shapes.sort((a, b) => pageBounds[a.id][min] - pageBounds[b.id][min]) + + // Collect all of the gaps between shapes. We want to find + // patterns (equal gaps between shapes) and use the most common + // one as the gap for all of the shapes. + for (let i = 0; i < len - 1; i++) { + const shape = shapes[i] + const nextShape = shapes[i + 1] + + const bounds = pageBounds[shape.id] + const nextBounds = pageBounds[nextShape.id] + + const gap = nextBounds[min] - bounds[max] + + const current = gaps.find((g) => g.gap === gap) + + if (current) { + current.count++ + } else { + gaps.push({ gap, count: 1 }) + } + } + + // Which gap is the most common? + let maxCount = 0 + gaps.forEach((g) => { + if (g.count > maxCount) { + maxCount = g.count + shapeGap = g.gap + } + }) + + // If there is no most-common gap, use the average gap. + if (maxCount === 1) { + shapeGap = Math.max(0, gaps.reduce((a, c) => a + c.gap * c.count, 0) / (len - 1)) + } + } else { + // If a gap was provided, then use that instead. + shapeGap = gap + } + + const changes: TLShapePartial[] = [] + + let v = pageBounds[shapes[0].id][max] + + shapes.forEach((shape, i) => { + if (i === 0) return + + const delta = { x: 0, y: 0 } + delta[val] = v + shapeGap - pageBounds[shape.id][val] + + const parent = this.getParentShape(shape) + const localDelta = parent ? Vec2d.Rot(delta, -this.getPageRotation(parent)) : delta + + const translateStartChanges = this.getShapeUtil(shape).onTranslateStart?.(shape) + + changes.push( + translateStartChanges + ? { + ...translateStartChanges, + [val]: shape[val] + localDelta[val], + } + : { + id: shape.id as any, + type: shape.type, + [val]: shape[val] + localDelta[val], + } + ) + + v += pageBounds[shape.id][dim] + shapeGap + }) + + this.updateShapes(changes) + return this + } + + /** + * Pack shapes into a grid centered on their current position. Based on potpack + * (https://github.com/mapbox/potpack) + * + * @param ids - The ids of the shapes to pack. Defaults to selected shapes. + * @param padding - The padding to apply to the packed shapes. + */ + packShapes(ids: TLShapeId[] = this.pageState.selectedIds, padding = 16) { + if (this.isReadOnly) return this + if (ids.length < 2) return this + + const shapes = compact( + ids + .map((id) => this.getShapeById(id)) + .filter((shape) => { + if (!shape) return false + + if (TLArrowShapeDef.is(shape)) { + if (shape.props.start.type === 'binding' || shape.props.end.type === 'binding') { + return false + } + } + + return true + }) + ) + const shapePageBounds: Record = {} + const nextShapePageBounds: Record = {} + + let shape: TLShape, + bounds: Box2d, + area = 0 + + for (let i = 0; i < shapes.length; i++) { + shape = shapes[i] + bounds = this.getPageBounds(shape)! + shapePageBounds[shape.id] = bounds + nextShapePageBounds[shape.id] = bounds.clone() + area += bounds.width * bounds.height + } + + const commonBounds = Box2d.Common(compact(Object.values(shapePageBounds))) + + const maxWidth = commonBounds.width + + // sort the shapes by height, descending + shapes.sort((a, b) => shapePageBounds[b.id].height - shapePageBounds[a.id].height) + + // Start with is (sort of) the square of the area + const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth) + + // first shape fills the width and is infinitely tall + const spaces: Box2d[] = [new Box2d(commonBounds.x, commonBounds.y, startWidth, Infinity)] + + let width = 0 + let height = 0 + let space: Box2d + let last: Box2d + + for (let i = 0; i < shapes.length; i++) { + shape = shapes[i] + bounds = nextShapePageBounds[shape.id] + + // starting at the back (smaller shapes) + for (let i = spaces.length - 1; i >= 0; i--) { + space = spaces[i] + + // find a space that is big enough to contain the shape + if (bounds.width > space.width || bounds.height > space.height) continue + + // add the shape to its top-left corner + bounds.x = space.x + bounds.y = space.y + + height = Math.max(height, bounds.maxY) + width = Math.max(width, bounds.maxX) + + if (bounds.width === space.width && bounds.height === space.height) { + // remove the space on a perfect fit + last = spaces.pop()! + if (i < spaces.length) spaces[i] = last + } else if (bounds.height === space.height) { + // fit the shape into the space (width) + space.x += bounds.width + padding + space.width -= bounds.width + padding + } else if (bounds.width === space.width) { + // fit the shape into the space (height) + space.y += bounds.height + padding + space.height -= bounds.height + padding + } else { + // split the space into two spaces + spaces.push( + new Box2d( + space.x + (bounds.width + padding), + space.y, + space.width - (bounds.width + padding), + bounds.height + ) + ) + space.y += bounds.height + padding + space.height -= bounds.height + padding + } + break + } + } + + const commonAfter = Box2d.Common(Object.values(nextShapePageBounds)) + const centerDelta = Vec2d.Sub(commonBounds.center, commonAfter.center) + + let nextBounds: Box2d + + const changes: TLShapePartial[] = [] + + for (let i = 0; i < shapes.length; i++) { + shape = shapes[i] + bounds = shapePageBounds[shape.id] + nextBounds = nextShapePageBounds[shape.id] + + const delta = this.getDeltaInParentSpace( + shape, + Vec2d.Sub(nextBounds.point, bounds.point).add(centerDelta) + ) + + const change: TLShapePartial = { + id: shape.id, + type: shape.type, + x: shape.x + delta.x, + y: shape.y + delta.y, + } + + const translateStartChange = this.getShapeUtil(shape).onTranslateStart?.({ + ...shape, + ...change, + } as TLShape) + + if (translateStartChange) { + changes.push({ ...change, ...translateStartChange }) + } else { + changes.push(change) + } + } + + if (changes.length) { + this.updateShapes(changes) + } + + return this + } + + /** + * Align shape positions. + * + * @example + * + * ```ts + * app.alignShapes('left') + * app.alignShapes('left', ['box1', 'box2']) + * ``` + * + * @param operation - The align operation to apply. + * @param ids - The ids of the shapes to align. Defaults to selected shapes. + * @public + */ + alignShapes( + operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom', + ids: TLShapeId[] = this.pageState.selectedIds + ) { + if (this.isReadOnly) return this + if (ids.length < 2) return this + + const shapes = compact(ids.map((id) => this.getShapeById(id))) + const shapePageBounds = Object.fromEntries( + shapes.map((shape) => [shape.id, this.getPageBounds(shape)]) + ) + const commonBounds = Box2d.Common(compact(Object.values(shapePageBounds))) + + const changes: TLShapePartial[] = [] + + shapes.forEach((shape) => { + const pageBounds = shapePageBounds[shape.id] + if (!pageBounds) return + + const delta = { x: 0, y: 0 } + + switch (operation) { + case 'top': { + delta.y = commonBounds.minY - pageBounds.minY + break + } + case 'center-vertical': { + delta.y = commonBounds.midY - pageBounds.minY - pageBounds.height / 2 + break + } + case 'bottom': { + delta.y = commonBounds.maxY - pageBounds.minY - pageBounds.height + break + } + case 'left': { + delta.x = commonBounds.minX - pageBounds.minX + break + } + case 'center-horizontal': { + delta.x = commonBounds.midX - pageBounds.minX - pageBounds.width / 2 + break + } + case 'right': { + delta.x = commonBounds.maxX - pageBounds.minX - pageBounds.width + break + } + } + + const parent = this.getParentShape(shape) + const localDelta = parent ? Vec2d.Rot(delta, -this.getPageRotation(parent)) : delta + + const translateChanges = this.getShapeUtil(shape).onTranslateStart?.(shape) + + changes.push( + translateChanges + ? { + ...translateChanges, + x: shape.x + localDelta.x, + y: shape.y + localDelta.y, + } + : { + id: shape.id, + type: shape.type, + x: shape.x + localDelta.x, + y: shape.y + localDelta.y, + } + ) + }) + + this.updateShapes(changes) + return this + } + + /** + * Distribute shape positions. + * + * @example + * + * ```ts + * app.distributeShapes('left') + * app.distributeShapes('left', ['box1', 'box2']) + * ``` + * + * @param operation - Whether to distribute shapes horizontally or vertically. + * @param ids - The ids of the shapes to distribute. Defaults to selected shapes. + * @public + */ + distributeShapes( + operation: 'horizontal' | 'vertical', + ids: TLShapeId[] = this.pageState.selectedIds + ) { + if (this.isReadOnly) return this + if (ids.length < 3) return this + + const len = ids.length + const shapes = compact(ids.map((id) => this.getShapeById(id))) + const pageBounds = Object.fromEntries( + shapes.map((shape) => [shape.id, this.getPageBounds(shape)!]) + ) + + let val: 'x' | 'y' + let min: 'minX' | 'minY' + let max: 'maxX' | 'maxY' + let mid: 'midX' | 'midY' + let dim: 'width' | 'height' + + if (operation === 'horizontal') { + val = 'x' + min = 'minX' + max = 'maxX' + mid = 'midX' + dim = 'width' + } else { + val = 'y' + min = 'minY' + max = 'maxY' + mid = 'midY' + dim = 'height' + } + const changes: TLShapePartial[] = [] + + // Clustered + const first = shapes.sort((a, b) => pageBounds[a.id][min] - pageBounds[b.id][min])[0] + const last = shapes.sort((a, b) => pageBounds[b.id][max] - pageBounds[a.id][max])[0] + + const midFirst = pageBounds[first.id][mid] + const step = (pageBounds[last.id][mid] - midFirst) / (len - 1) + const v = midFirst + step + + shapes + .filter((shape) => shape !== first && shape !== last) + .sort((a, b) => pageBounds[a.id][mid] - pageBounds[b.id][mid]) + .forEach((shape, i) => { + const delta = { x: 0, y: 0 } + delta[val] = v + step * i - pageBounds[shape.id][dim] / 2 - pageBounds[shape.id][val] + + const parent = this.getParentShape(shape) + const localDelta = parent ? Vec2d.Rot(delta, -this.getPageRotation(parent)) : delta + const translateStartChanges = this.getShapeUtil(shape).onTranslateStart?.(shape) + + changes.push( + translateStartChanges + ? { + ...translateStartChanges, + [val]: shape[val] + localDelta[val], + } + : { + id: shape.id, + type: shape.type, + [val]: shape[val] + localDelta[val], + } + ) + }) + + this.updateShapes(changes) + return this + } + + /** @internal */ + private _resizeUnalignedShape( + id: TLShapeId, + scale: VecLike, + options: { + initialBounds: Box2d + scaleOrigin: VecLike + scaleAxisRotation: number + initialShape: TLShape + initialPageTransform: MatLike + } + ) { + const { type } = options.initialShape + // If a shape is not aligned with the scale axis we need to treat it differently to avoid skewing. + // Instead of skewing we normalise the scale aspect ratio (i.e. keep the same scale magnitude in both axes) + // and then after applying the scale to the shape we also rotate it if required and translate it so that it's center + // point ends up in the right place. + + const shapeScale = new Vec2d(scale.x, scale.y) + + // // make sure we are contraining aspect ratio, and using the smallest scale axis to avoid shapes getting bigger + // // than the selection bounding box + if (Math.abs(scale.x) > Math.abs(scale.y)) { + shapeScale.x = Math.sign(scale.x) * Math.abs(scale.y) + } else { + shapeScale.y = Math.sign(scale.y) * Math.abs(scale.x) + } + + // first we can scale the shape about its center point + this.resizeShape(id, shapeScale, { + initialShape: options.initialShape, + initialBounds: options.initialBounds, + }) + + // then if the shape is flipped in one axis only, we need to apply an extra rotation + // to make sure the shape is mirrored correctly + if (Math.sign(scale.x) * Math.sign(scale.y) < 0) { + let { rotation } = Matrix2d.Decompose(options.initialPageTransform) + rotation -= 2 * rotation + this.updateShapes([{ id, type, rotation }], true) + } + + // Next we need to translate the shape so that it's center point ends up in the right place. + // To do that we first need to calculate the center point of the shape in page space before the scale was applied. + const preScaleShapePageCenter = Matrix2d.applyToPoint( + options.initialPageTransform, + options.initialBounds.center + ) + + // And now we scale the center point by the original scale factor + const postScaleShapePageCenter = this._scalePagePoint( + preScaleShapePageCenter, + options.scaleOrigin, + scale, + options.scaleAxisRotation + ) + + // now caculate how far away the shape is from where it needs to be + const currentPageCenter = this.getPageCenterById(id) + const currentPagePoint = this.getPagePointById(id) + if (!currentPageCenter || !currentPagePoint) return this + const pageDelta = Vec2d.Sub(postScaleShapePageCenter, currentPageCenter) + + // and finally figure out what the shape's new position should be + const postScaleShapePagePoint = Vec2d.Add(currentPagePoint, pageDelta) + const { x, y } = this.getPointInParentSpace(id, postScaleShapePagePoint) + + this.updateShapes([{ id, type, x, y }], true) + + return this + } + + /** @internal */ + private _scalePagePoint( + point: VecLike, + scaleOrigin: VecLike, + scale: VecLike, + scaleAxisRotation: number + ) { + const relativePoint = Vec2d.RotWith(point, scaleOrigin, -scaleAxisRotation).sub(scaleOrigin) + + // calculate the new point position relative to the scale origin + const newRelativePagePoint = Vec2d.MulV(relativePoint, scale) + + // and rotate it back to page coords to get the new page point of the resized shape + const destination = Vec2d.Add(newRelativePagePoint, scaleOrigin).rotWith( + scaleOrigin, + scaleAxisRotation + ) + + return destination + } + + resizeShape( + id: TLShapeId, + scale: VecLike, + options?: { + initialBounds?: Box2d + scaleOrigin?: VecLike + scaleAxisRotation?: number + initialShape?: TLShape + initialPageTransform?: MatLike + dragHandle?: TLResizeHandle + mode?: TLResizeMode + } + ) { + if (this.isReadOnly) return this + + if (!Number.isFinite(scale.x)) scale = new Vec2d(1, scale.y) + if (!Number.isFinite(scale.y)) scale = new Vec2d(scale.x, 1) + + const initialShape = options?.initialShape ?? this.getShapeById(id) + if (!initialShape) return this + + const scaleOrigin = options?.scaleOrigin ?? this.getPageBoundsById(id)?.center + if (!scaleOrigin) return this + + const pageRotation = this.getPageRotationById(id) + + if (pageRotation == null) return this + + const scaleAxisRotation = options?.scaleAxisRotation ?? pageRotation + + const pageTransform = options?.initialPageTransform ?? this.getPageTransformById(id) + if (!pageTransform) return this + + const initialBounds = options?.initialBounds ?? this.getBoundsById(id) + + if (!initialBounds) return this + + if (!areAnglesCompatible(pageRotation, scaleAxisRotation)) { + // shape is awkwardly rotated, keep the aspect ratio locked and adopt the scale factor + // from whichever axis is being scaled the least, to avoid the shape getting bigger + // than the bounds of the selection + // const minScale = Math.min(Math.abs(scale.x), Math.abs(scale.y)) + return this._resizeUnalignedShape(id, scale, { + ...options, + initialBounds, + scaleOrigin, + scaleAxisRotation, + initialPageTransform: pageTransform, + initialShape, + }) + } + + const util = this.getShapeUtil(initialShape) + + if (util.isAspectRatioLocked(initialShape)) { + if (Math.abs(scale.x) > Math.abs(scale.y)) { + scale = new Vec2d(scale.x, Math.sign(scale.y) * Math.abs(scale.x)) + } else { + scale = new Vec2d(Math.sign(scale.x) * Math.abs(scale.y), scale.y) + } + } + + if (util.onResize && util.canResize(initialShape)) { + // get the model changes from the shape util + const newPagePoint = this._scalePagePoint( + Matrix2d.applyToPoint(pageTransform, new Vec2d(0, 0)), + scaleOrigin, + scale, + scaleAxisRotation + ) + + const newLocalPoint = this.getPointInParentSpace(initialShape.id, newPagePoint) + + // resize the shape's local bounding box + const myScale = new Vec2d(scale.x, scale.y) + // the shape is algined with the rest of the shpaes in the selection, but may be + // 90deg offset from the main rotation of the selection, in which case + // we need to flip the width and height scale factors + const areWidthAndHeightAlignedWithCorrectAxis = approximately( + (pageRotation - scaleAxisRotation) % Math.PI, + 0 + ) + myScale.x = areWidthAndHeightAlignedWithCorrectAxis ? scale.x : scale.y + myScale.y = areWidthAndHeightAlignedWithCorrectAxis ? scale.y : scale.x + + // adjust initial model for situations where the parent has moved during the resize + // e.g. groups + const initialPagePoint = Matrix2d.applyToPoint(pageTransform, new Vec2d()) + + // need to adjust the shape's x and y points in case the parent has moved since start of resizing + const { x, y } = this.getPointInParentSpace(initialShape.id, initialPagePoint) + + this.updateShapes( + [ + { + id, + type: initialShape.type as any, + x: newLocalPoint.x, + y: newLocalPoint.y, + ...util.onResize( + { ...initialShape, x, y }, + { + newPoint: newLocalPoint, + handle: options?.dragHandle ?? 'bottom_right', + // don't set isSingle to true for children + mode: options?.mode ?? 'scale_shape', + scaleX: myScale.x, + scaleY: myScale.y, + initialBounds, + initialShape, + } + ), + }, + ], + true + ) + } else { + const initialPageCenter = Matrix2d.applyToPoint(pageTransform, initialBounds.center) + // get the model changes from the shape util + const newPageCenter = this._scalePagePoint( + initialPageCenter, + scaleOrigin, + scale, + scaleAxisRotation + ) + + const initialPageCenterInParentSpace = this.getPointInParentSpace( + initialShape.id, + initialPageCenter + ) + const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter) + + const delta = Vec2d.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace) + // apply the changes to the model + this.updateShapes( + [ + { + id, + type: initialShape.type as any, + x: initialShape.x + delta.x, + y: initialShape.y + delta.y, + }, + ], + true + ) + } + + return this + } + + /** + * Stretch shape sizes and positions to fill their common bounding box. + * + * @example + * + * ```ts + * app.stretchShapes('horizontal') + * app.stretchShapes('horizontal', ['box1', 'box2']) + * ``` + * + * @param operation - Whether to stretch shapes horizontally or vertically. + * @param ids - The ids of the shapes to stretch. Defaults to selected shapes. + * @public + */ + stretchShapes( + operation: 'horizontal' | 'vertical', + ids: TLShapeId[] = this.pageState.selectedIds + ) { + if (this.isReadOnly) return this + if (ids.length < 2) return this + + const shapes = compact(ids.map((id) => this.getShapeById(id))) + const shapeBounds = Object.fromEntries(shapes.map((shape) => [shape.id, this.getBounds(shape)])) + const shapePageBounds = Object.fromEntries( + shapes.map((shape) => [shape.id, this.getPageBounds(shape)!]) + ) + const commonBounds = Box2d.Common(compact(Object.values(shapePageBounds))) + + const changes: TLShapePartial[] = [] + + switch (operation) { + case 'vertical': { + this.batch(() => { + for (const shape of shapes) { + const pageRotation = this.getPageRotation(shape) + if (pageRotation % PI2) continue + const bounds = shapeBounds[shape.id] + const pageBounds = shapePageBounds[shape.id] + const localOffset = this.getDeltaInParentSpace( + shape, + new Vec2d(0, commonBounds.minY - pageBounds.minY) + ) + const { x, y } = Vec2d.Add(localOffset, shape) + this.updateShapes([{ id: shape.id, type: shape.type, x, y }], true) + const scale = new Vec2d(1, commonBounds.height / pageBounds.height) + this.resizeShape(shape.id, scale, { + initialBounds: bounds, + scaleOrigin: new Vec2d(pageBounds.center.x, commonBounds.minY), + scaleAxisRotation: 0, + }) + } + }) + break + } + case 'horizontal': { + this.batch(() => { + for (const shape of shapes) { + const bounds = shapeBounds[shape.id] + const pageBounds = shapePageBounds[shape.id] + const pageRotation = this.getPageRotation(shape) + if (pageRotation % PI2) continue + const localOffset = this.getDeltaInParentSpace( + shape, + new Vec2d(commonBounds.minX - pageBounds.minX, 0) + ) + const { x, y } = Vec2d.Add(localOffset, shape) + this.updateShapes([{ id: shape.id, type: shape.type, x, y }], true) + const scale = new Vec2d(commonBounds.width / pageBounds.width, 1) + this.resizeShape(shape.id, scale, { + initialBounds: bounds, + scaleOrigin: new Vec2d(commonBounds.minX, pageBounds.center.y), + scaleAxisRotation: 0, + }) + } + }) + + break + } + } + + this.updateShapes(changes) + return this + } + + /** + * Reparent shapes to a new parent. This operation preserves the shape's current page positions / + * rotations. + * + * @example + * + * ```ts + * app.reparentShapesById(['box1', 'box2'], 'frame1') + * ``` + * + * @param ids - The ids of the shapes to reparent. + * @param parentId - The id of the new parent shape. + * @param insertIndex - The index to insert the children. + * @public + */ + reparentShapesById(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string) { + const changes: TLShapePartial[] = [] + + const parentTransform = TLPage.isId(parentId) + ? Matrix2d.Identity() + : this.getPageTransformById(parentId)! + + const parentPageRotation = parentTransform.decompose().rotation + + let indices: string[] = [] + + const sibs = compact(this.getSortedChildIds(parentId).map((id) => this.getShapeById(id))) + + if (insertIndex) { + const sibWithInsertIndex = sibs.find((s) => s.index === insertIndex) + if (sibWithInsertIndex) { + // If there's a sibling with the same index as the insert index... + const sibAbove = sibs[sibs.indexOf(sibWithInsertIndex) + 1] + if (sibAbove) { + // If the sibling has a sibling above it, insert the shapes + // between the sibling and its sibling above it. + indices = getIndicesBetween(insertIndex, sibAbove.index, ids.length) + } else { + // Or if the sibling is the top sibling, insert the shapes + // above the sibling + indices = getIndicesAbove(insertIndex, ids.length) + } + } else { + // If there's no collision, then we can start at the insert index + const sibAbove = sibs.sort(sortByIndex).find((s) => s.index > insertIndex) + + if (sibAbove) { + // If the siblings include a sibling with a higher index, insert the shapes + // between the insert index and the sibling with the higher index. + indices = getIndicesBetween(insertIndex, sibAbove.index, ids.length) + } else { + // Otherwise, we're at the top of the order, so insert the shapes above + // the insert index. + indices = getIndicesAbove(insertIndex, ids.length) + } + } + } else { + // If insert index is not specified, start the index at the top. + const sib = sibs.length && sibs[sibs.length - 1] + indices = sib ? getIndicesAbove(sib.index, ids.length) : getIndices(ids.length) + } + + let id: TLShapeId + for (let i = 0; i < ids.length; i++) { + id = ids[i] + const shape = this.getShapeById(id) + const pagePoint = this.getPagePointById(id) + + if (!shape || !pagePoint) continue + + const newPoint = Matrix2d.applyToPoint(Matrix2d.Inverse(parentTransform), pagePoint) + const newRotation = this.getPageRotation(shape) - parentPageRotation + + changes.push({ + id: shape.id, + type: shape.type, + parentId: parentId, + x: newPoint.x, + y: newPoint.y, + rotation: newRotation, + index: indices[i], + }) + } + + this.updateShapes(changes) + return this + } + + /** + * Select one or more shapes. + * + * @example + * + * ```ts + * app.select('id1') + * app.select('id1', 'id2') + * ``` + * + * @param ids - The ids to select. + * @public + */ + select(...ids: TLShapeId[]) { + this.setSelectedIds(ids) + return this + } + + /** + * Remove a shpae from the existing set of selected shapes. + * + * @example + * + * ```ts + * app.deselect(shape.id) + * ``` + * + * @public + */ + deselect(...ids: TLShapeId[]) { + const { selectedIds } = this + if (selectedIds.length > 0 && ids.length > 0) { + this.setSelectedIds(selectedIds.filter((id) => !ids.includes(id))) + } + return this + } + + /** + * Select all direct children of the current page. + * + * @example + * + * ```ts + * app.selectAll() + * ``` + * + * @public + */ + selectAll() { + const ids = this.getSortedChildIds(this.currentPageId) + // page might have no shapes + if (ids.length <= 0) return this + this.setSelectedIds(ids) + return this + } + + getShapesAndDescendantsInOrder(ids: TLShapeId[]) { + const idsToInclude: TLShapeId[] = [] + const visitedIds = new Set() + + const idsToCheck = [...ids] + + while (idsToCheck.length > 0) { + const id = idsToCheck.pop() + if (!id) break + if (visitedIds.has(id)) continue + idsToInclude.push(id) + this.getSortedChildIds(id).forEach((id) => { + idsToCheck.push(id) + }) + } + + // Map the ids into nodes AND their descendants + const shapes = idsToInclude.map((s) => this.getShapeById(s)!).filter((s) => s.type !== 'group') + + // Sort by the shape's appearance in the sorted shapes array + const { sortedShapesArray } = this + shapes.sort((a, b) => sortedShapesArray.indexOf(a) - sortedShapesArray.indexOf(b)) + + return shapes + } + + /** + * Clear the selection. + * + * @example + * + * ```ts + * app.selectNone() + * ``` + * + * @public + */ + selectNone() { + if (this.selectedIds.length > 0) { + this.setSelectedIds([]) + } + return this + } + + /** + * Set the current page. + * + * @example + * + * ```ts + * app.setCurrentPageId('page1') + * ``` + * + * @param pageId - The id of the page to set as the current page. + * @param options - Options for setting the current page. + * @public + */ + setCurrentPageId(pageId: TLPageId, { stopFollowing = true }: ViewportOptions = {}) { + this._setCurrentPageId(pageId, { stopFollowing }) + return this + } + + /** @internal */ + private _setCurrentPageId = this.history.createCommand( + 'setCurrentPage', + (pageId: TLPageId, { stopFollowing = true }: ViewportOptions = {}) => { + if (!this.store.has(pageId)) { + console.error("Tried to set the current page id to a page that doesn't exist.") + return + } + + if (stopFollowing && this.instanceState.followingUserId) { + this.stopFollowingUser() + } + + return { + data: { pageId, prev: this.currentPageId }, + squashing: true, + preservesRedoStack: true, + } + }, + { + do: ({ pageId }) => { + if (!this.getPageStateByPageId(pageId)) { + const camera = TLCamera.create({}) + this.store.put([ + camera, + TLInstancePageState.create({ + pageId, + instanceId: this.instanceId, + cameraId: camera.id, + }), + ]) + } + + this.store.put([{ ...this.instanceState, currentPageId: pageId }]) + + this.updateUserPresence({ + viewportPageBounds: this.viewportPageBounds.toJson(), + }) + this.updateCullingBounds() + }, + undo: ({ prev }) => { + this.store.put([{ ...this.instanceState, currentPageId: prev }]) + + this.updateUserPresence({ + viewportPageBounds: this.viewportPageBounds.toJson(), + }) + this.updateCullingBounds() + }, + squash: ({ prev }, { pageId }) => { + return { pageId, prev } + }, + } + ) + + /** Set the current user tab state */ + updateInstanceState( + partial: Partial>, + ephemeral = false, + squashing = false + ) { + this._updateInstanceState(partial, ephemeral, squashing) + return this + } + + /** @internal */ + private _updateInstanceState = this.history.createCommand( + 'updateTabState', + ( + partial: Partial>, + ephemeral = false, + squashing = false + ) => { + const prev = this.instanceState + const next = { ...prev, ...partial } + + return { + data: { prev, next }, + squashing, + ephemeral, + } + }, + { + do: ({ next }) => { + this.store.put([next]) + }, + undo: ({ prev }) => { + this.store.put([prev]) + }, + squash({ prev }, { next }) { + return { prev, next } + }, + } + ) + + @computed get hoveredId() { + return this.pageState.hoveredId + } + + @computed get hoveredShape() { + if (!this.hoveredId) return null + return this.getShapeById(this.hoveredId) ?? null + } + + /** + * Set the current hovered shape. + * + * @example + * + * ```ts + * app.setHoveredId('box1') + * app.setHoveredId() // Clears the hovered shape. + * ``` + * + * @param id - The id of the page to set as the current page + * @public + */ + setHoveredId(id: TLShapeId | null = null) { + if (id === this.pageState.hoveredId) return this + + this.setInstancePageState({ hoveredId: id }, true) + return this + } + + /** + * Set the current erasing shapes. + * + * @example + * + * ```ts + * app.setErasingIds(['box1', 'box2']) + * app.setErasingIds() // Clears the erasing set + * ``` + * + * @param ids - The ids of shapes to set as erasing. + * @public + */ + setErasingIds(ids: TLShapeId[] = []) { + const erasingIds = this.erasingIdsSet + if (ids.length === erasingIds.size && ids.every((id) => erasingIds.has(id))) return this + + this.setInstancePageState({ erasingIds: ids }, true) + return this + } + + /** + * Set the current cursor. + * + * @example + * + * ```ts + * app.setCursor({ type: 'default' }) + * app.setCursor({ type: 'default', rotation: Math.PI / 2, color: 'red' }) + * ``` + * + * @param cursor - A partial of the cursor object. + * @public + */ + setCursor(cursor: Partial) { + const current = this.cursor + const next = { + ...current, + rotation: 0, + ...cursor, + } + + if ( + !( + current.type === next.type && + current.rotation === next.rotation && + current.color === next.color + ) + ) { + this.updateInstanceState({ cursor: next }, true) + } + + return this + } + + /** + * Set the current scribble. + * + * @example + * + * ```ts + * app.setScribble(nextScribble) + * app.setScribble() // clears the scribble + * ``` + * + * @param scribble - The new scribble object. + * @public + */ + setScribble(scribble: TLScribble | null = null) { + this.updateInstanceState({ scribble }, true) + return this + } + + /** + * Set the current brush. + * + * @example + * + * ```ts + * app.setBrush({ x: 0, y: 0, w: 100, h: 100 }) + * app.setBrush() // Clears the brush + * ``` + * + * @param brush - The brush box model to set, or null for no brush model. + * @public + */ + setBrush(brush: Box2dModel | null = null) { + if (!brush && !this.brush) return this + this.updateInstanceState({ brush }, true) + return this + } + + /** + * Set the current zoom brush. + * + * @example + * + * ```ts + * app.setZoomBrush({ x: 0, y: 0, w: 100, h: 100 }) + * app.setZoomBrush() // Clears the zoom + * ``` + * + * @param zoomBrush - The zoom box model to set, or null for no zoom model. + * @public + */ + setZoomBrush(zoomBrush: Box2dModel | null = null) { + if (!zoomBrush && !this.zoomBrush) return this + this.updateInstanceState({ zoomBrush }, true) + return this + } + + /** + * Rotate shapes by a delta in radians. + * + * @example + * + * ```ts + * app.rotateShapesBy(['box1', 'box2'], Math.PI) + * app.rotateShapesBy(['box1', 'box2'], Math.PI / 2) + * ``` + * + * @param ids - The ids of the shapes to move. + * @param delta - The delta in radians to apply to the selection rotation. + */ + rotateShapesBy(ids: TLShapeId[], delta: number): this { + if (ids.length <= 0) return this + + const snapshot = getRotationSnapshot({ app: this }) + applyRotationToSnapshotShapes({ delta, snapshot, app: this, stage: 'one-off' }) + return this + } + + /** + * Move shapes by a delta. + * + * @example + * + * ```ts + * app.nudgeShapes(['box1', 'box2'], { x: 0, y: 1 }) + * app.nudgeShapes(['box1', 'box2'], { x: 0, y: 1 }, true) + * ``` + * + * @param ids - The ids of the shapes to move. + * @param direction - The direction in which to move the shapes. + * @param major - Whether this is a major nudge, e.g. a shift + arrow nudge. + */ + nudgeShapes(ids: TLShapeId[], direction: Vec2dModel, major = false, ephemeral = false) { + if (ids.length <= 0) return this + + const step = this.isGridMode + ? major + ? this.gridSize * GRID_INCREMENT + : this.gridSize + : major + ? MAJOR_NUDGE_FACTOR + : MINOR_NUDGE_FACTOR + + const steppedDelta = Vec2d.Mul(direction, step) + const changes: TLShapePartial[] = [] + + for (const id of ids) { + const shape = this.getShapeById(id) + + if (!shape) { + throw Error(`Could not find a shape with the id ${id}.`) + } + + const localDelta = this.getDeltaInParentSpace(shape, steppedDelta) + const translateStartChanges = this.getShapeUtil(shape).onTranslateStart?.(shape) + + changes.push( + translateStartChanges + ? { + ...translateStartChanges, + x: shape.x + localDelta.x, + y: shape.y + localDelta.y, + } + : { + id, + x: shape.x + localDelta.x, + y: shape.y + localDelta.y, + type: shape.type, + } + ) + } + + this.updateShapes(changes, ephemeral) + + return this + } + + /** + * Duplicate shapes. + * + * @example + * + * ```ts + * app.duplicateShapes() + * app.duplicateShapes(['id1', 'id2']) + * app.duplicateShapes(['id1', 'id2'], { x: 8, y: 8 }) + * ``` + * + * @param ids - The ids of the shapes to duplicate. Defaults to the ids of the selected shapes. + * @param offset - The offset (in pixels) to apply to the duplicated shapes. + * @public + */ + duplicateShapes(ids: TLShapeId[] = this.selectedIds, offset?: VecLike) { + if (ids.length <= 0) return this + + const initialIds = new Set(ids) + const idsToCreate: TLShapeId[] = [] + const idsToCheck = [...ids] + + while (idsToCheck.length > 0) { + const id = idsToCheck.pop() + if (!id) break + idsToCreate.push(id) + this.getSortedChildIds(id).forEach((childId) => idsToCheck.push(childId)) + } + + idsToCreate.reverse() + + const idsMap = new Map(idsToCreate.map((id) => [id, this.createShapeId()])) + + const shapesToCreate = compact( + idsToCreate.map((id) => { + const shape = this.getShapeById(id) + + if (!shape) { + return null + } + + const createId = idsMap.get(id)! + + let ox = 0 + let oy = 0 + + if (offset && initialIds.has(id)) { + const parentTransform = this.getParentTransform(shape) + const vec = new Vec2d(offset.x, offset.y).rot( + -Matrix2d.Decompose(parentTransform).rotation + ) + ox = vec.x + oy = vec.y + } + + const parentId = shape.parentId ?? this.currentPageId + const siblings = this.getSortedChildIds(parentId) + const currentIndex = siblings.indexOf(shape.id) + const siblingAboveId = siblings[currentIndex + 1] + const siblingAbove = siblingAboveId ? this.getShapeById(siblingAboveId) : null + + const index = siblingAbove + ? getIndexBetween(shape.index, siblingAbove.index) + : getIndexAbove(shape.index) + + let newShape: TLShape = deepCopy(shape) + + if (TLArrowShapeDef.is(shape) && TLArrowShapeDef.is(newShape)) { + const info = this.getShapeUtilByDef(TLArrowShapeDef).getArrowInfo(shape) + let newStartShapeId: TLShapeId | undefined = undefined + let newEndShapeId: TLShapeId | undefined = undefined + + if (shape.props.start.type === 'binding') { + newStartShapeId = idsMap.get(shape.props.start.boundShapeId) + + if (!newStartShapeId) { + if (info?.isValid) { + const { x, y } = info.start.point + newShape.props.start = { + type: 'point', + x, + y, + } + } else { + const { start } = getArrowTerminalsInArrowSpace(this, shape) + newShape.props.start = { + type: 'point', + x: start.x, + y: start.y, + } + } + } + } + + if (shape.props.end.type === 'binding') { + newEndShapeId = idsMap.get(shape.props.end.boundShapeId) + if (!newEndShapeId) { + if (info?.isValid) { + const { x, y } = info.end.point + newShape.props.end = { + type: 'point', + x, + y, + } + } else { + const { end } = getArrowTerminalsInArrowSpace(this, shape) + newShape.props.start = { + type: 'point', + x: end.x, + y: end.y, + } + } + } + } + + const infoAfter = getIsArrowStraight(newShape) + ? getStraightArrowInfo(this, newShape) + : getCurvedArrowInfo(this, newShape) + + if (info?.isValid && infoAfter?.isValid && !getIsArrowStraight(shape)) { + const mpA = Vec2d.Med(info.start.handle, info.end.handle) + const distA = Vec2d.Dist(info.middle, mpA) + const distB = Vec2d.Dist(infoAfter.middle, mpA) + if (newShape.props.bend < 0) { + newShape.props.bend += distB - distA + } else { + newShape.props.bend -= distB - distA + } + } + + if (newShape.props.start.type === 'binding' && newStartShapeId) { + newShape.props.start.boundShapeId = newStartShapeId + } + + if (newShape.props.end.type === 'binding' && newEndShapeId) { + newShape.props.end.boundShapeId = newEndShapeId + } + } + + newShape = { ...newShape, id: createId, x: shape.x + ox, y: shape.y + oy, index } + + return newShape + }) + ) + + shapesToCreate.forEach((shape) => { + if (isShapeId(shape.parentId)) { + if (idsMap.has(shape.parentId)) { + shape.parentId = idsMap.get(shape.parentId)! + } + } + }) + + this.history.batch(() => { + const maxShapesReached = shapesToCreate.length + this.shapeIds.size > MAX_SHAPES_PER_PAGE + + if (maxShapesReached) { + alertMaxShapes(this) + } + + const newShapes = maxShapesReached + ? shapesToCreate.slice(0, MAX_SHAPES_PER_PAGE - this.shapeIds.size) + : shapesToCreate + + const ids = newShapes.map((s) => s.id) + + this.createShapes(newShapes) + this.setSelectedIds(ids) + + if (offset !== undefined) { + // If we've offset the duplicated shapes, check to see whether their new bounds is entirely + // contained in the current viewport. If not, then animate the camera to be centered on the + // new shapes. + const { viewportPageBounds, selectedPageBounds } = this + if (selectedPageBounds && !viewportPageBounds.contains(selectedPageBounds)) { + this.centerOnPoint(selectedPageBounds.center.x, selectedPageBounds.center.y, { + duration: ANIMATION_MEDIUM_MS, + }) + } + } + }) + + return this + } + + /** + * Set the current props (generally styles). + * + * @example + * + * ```ts + * app.setProp('color', 'red') + * app.setProp('color', 'red', true) + * ``` + * + * @param key - The key to set. + * @param value - The value to set. + * @param ephemeral - Whether the style is ephemeral. Defaults to false. + * @public + */ + setProp(key: TLShapeProp, value: any, ephemeral = false, squashing = false) { + const children: (TLShape | undefined)[] = [] + // We can have many deep levels of grouped shape + // Making a recursive function to look through all the levels + const getChildProp = (id: TLShape['id']) => { + const childIds = this.getSortedChildIds(id) + for (const childId of childIds) { + const childShape = this.getShapeById(childId) + if (childShape?.type === 'group') { + getChildProp(childShape.id) + } + children.push(childShape) + } + } + + this.history.batch(() => { + this.updateInstanceState( + { + propsForNextShape: setPropsForNextShape(this.instanceState.propsForNextShape, { + [key]: value, + }), + }, + ephemeral, + squashing + ) + + if (this.isIn('select')) { + const { + pageState: { selectedIds }, + } = this + + if (selectedIds.length > 0) { + const shapes = compact( + selectedIds.map((id) => { + const shape = this.getShapeById(id) + if (shape?.type === 'group') { + const childIds = this.getSortedChildIds(shape.id) + for (const childId of childIds) { + const childShape = this.getShapeById(childId) + if (childShape?.type === 'group') { + getChildProp(childShape.id) + } + children.push(childShape) + } + return children + } else { + return shape + } + }) + ) + .flat() + .filter( + (shape) => + shape!.props[key as keyof TLShape['props']] !== undefined && shape?.type !== 'group' + ) as TLShape[] + + this.updateShapes( + shapes.map((shape) => { + const props = { ...shape.props, [key]: value } + if (key === 'color' && 'labelColor' in props) { + props.labelColor = 'black' + } + + return { + id: shape.id, + type: shape.type, + props, + } as TLShape + }), + ephemeral + ) + + if (key !== 'color' && key !== 'opacity') { + const changes: TLShapePartial[] = [] + + for (const shape of shapes) { + const currentShape = this.getShapeById(shape.id) + if (!currentShape) continue + const util = this.getShapeUtil(currentShape) + + const boundsA = util.bounds(shape) + const boundsB = util.bounds(currentShape) + + const change: TLShapePartial = { id: shape.id, type: shape.type } + + let didChange = false + + if (boundsA.width !== boundsB.width) { + didChange = true + + if (TLTextShapeDef.is(shape)) { + switch (shape.props.align) { + case 'middle': { + change.x = currentShape.x + (boundsA.width - boundsB.width) / 2 + break + } + case 'end': { + change.x = currentShape.x + boundsA.width - boundsB.width + break + } + } + } else { + change.x = currentShape.x + (boundsA.width - boundsB.width) / 2 + } + } + + if (boundsA.height !== boundsB.height) { + didChange = true + change.y = currentShape.y + (boundsA.height - boundsB.height) / 2 + } + + if (didChange) { + changes.push(change) + } + } + + if (changes.length) { + this.updateShapes(changes, ephemeral) + } + } + } + } + + this.updateInstanceState( + { + propsForNextShape: setPropsForNextShape(this.instanceState.propsForNextShape, { + [key]: value, + }), + }, + ephemeral, + squashing + ) + }) + + return this + } + + /** @internal */ + private _willSetInitialBounds = true + + /** @internal */ + private _setCamera(x: number, y: number, z = this.camera.z) { + const currentCamera = this.camera + if (currentCamera.x === x && currentCamera.y === y && currentCamera.z === z) return this + this.store.put([{ ...currentCamera, x, y, z }]) + + const { currentScreenPoint } = this.inputs + + this.dispatch({ + type: 'pointer', + target: 'canvas', + name: 'pointer_move', + point: currentScreenPoint, + pointerId: 0, + ctrlKey: this.inputs.ctrlKey, + altKey: this.inputs.altKey, + shiftKey: this.inputs.shiftKey, + button: 0, + isPen: this.isPenMode ?? false, + }) + + this.updateUserPresence({ + viewportPageBounds: this.viewportPageBounds.toJson(), + }) + + this._cameraManager.tick() + + this.emit('change-camera', this.camera) + + return this + } + + /** + * Set the current camera. + * + * @example + * + * ```ts + * app.setCamera(0, 0) + * app.setCamera(0, 0, 1) + * ``` + * + * @param x - The camera's x position. + * @param y - The camera's y position. + * @param z - The camera's z position. Defaults to the current zoom. + * @param options - Options for the camera change. + * @public + */ + setCamera( + x: number, + y: number, + z = this.camera.z, + { stopFollowing = true }: ViewportOptions = {} + ) { + this.stopCameraAnimation() + if (stopFollowing && this.instanceState.followingUserId) { + this.stopFollowingUser() + } + x = Number.isNaN(x) ? 0 : x + y = Number.isNaN(y) ? 0 : y + z = Number.isNaN(z) ? 1 : z + this._setCamera(x, y, z) + return this + } + + /** + * Animate the camera. + * + * @example + * + * ```ts + * app.animateCamera(0, 0) + * app.animateCamera(0, 0, 1) + * app.animateCamera(0, 0, 1, { duration: 1000, easing: (t) => t * t }) + * ``` + * + * @param x - The camera's x position. + * @param y - The camera's y position. + * @param z - The camera's z position. Defaults to the current zoom. + * @param opts - Options for the animation. + * @public + */ + animateCamera( + x: number, + y: number, + z = this.camera.z, + opts: AnimationOptions = DEFAULT_ANIMATION_OPTIONS + ) { + x = Number.isNaN(x) ? 0 : x + y = Number.isNaN(y) ? 0 : y + z = Number.isNaN(z) ? 1 : z + const { width, height } = this.viewportScreenBounds + const w = width / z + const h = height / z + + const targetViewport = new Box2d(-x, -y, w, h) + + return this._animateToViewport(targetViewport, opts) + } + + /** + * Center the camera on a point (in page space). + * + * @example + * + * ```ts + * app.centerOnPoint(100, 100) + * ``` + * + * @param x - The x position of the point. + * @param y - The y position of the point. + * @param opts - The options for an animation. + * @public + */ + centerOnPoint(x: number, y: number, opts?: AnimationOptions): this { + if (!this.canMoveCamera) return this + + const { + viewportPageBounds: { width: pw, height: ph }, + camera, + } = this + + if (opts?.duration) { + this.animateCamera(-(x - pw / 2), -(y - ph / 2), camera.z, opts) + } else { + this.setCamera(-(x - pw / 2), -(y - ph / 2), camera.z) + } + return this + } + + /** + * Zoom the camera to fit the current page's content in the viewport. + * + * @example + * + * ```ts + * app.zoomToFit() + * ``` + * + * @public + */ + zoomToFit(opts?: AnimationOptions): this { + if (!this.canMoveCamera) return this + + const ids = [...this.shapeIds] + if (ids.length <= 0) return this + + const pageBounds = Box2d.Common(compact(ids.map((id) => this.getPageBoundsById(id)))) + this.zoomToBounds( + pageBounds.minX, + pageBounds.minY, + pageBounds.width, + pageBounds.height, + undefined, + opts + ) + return this + } + + /** + * Set the zoom back to 100%. + * + * @example + * + * ```ts + * app.resetZoom() + * ``` + * + * @param opts - The options for an animation. + * @public + */ + resetZoom(point = this.viewportScreenCenter, opts?: AnimationOptions): this { + if (!this.canMoveCamera) return this + + const { x: cx, y: cy, z: cz } = this.camera + const { x, y } = point + if (opts?.duration) { + this.animateCamera(cx + (x / 1 - x) - (x / cz - x), cy + (y / 1 - y) - (y / cz - y), 1, opts) + } else { + this.setCamera(cx + (x / 1 - x) - (x / cz - x), cy + (y / 1 - y) - (y / cz - y), 1) + } + return this + } + + /** + * Zoom the camera in. + * + * @example + * + * ```ts + * app.zoomIn() + * app.zoomIn(app.viewportScreenCenter, { duration: 120 }) + * app.zoomIn(app.inputs.currentScreenPoint, { duration: 120 }) + * ``` + * + * @param opts - The options for an animation. + * @public + */ + zoomIn(point = this.viewportScreenCenter, opts?: AnimationOptions): this { + if (!this.canMoveCamera) return this + + const { x: cx, y: cy, z: cz } = this.camera + + let zoom = MAX_ZOOM + + for (let i = 1; i < ZOOMS.length; i++) { + const z1 = ZOOMS[i - 1] + const z2 = ZOOMS[i] + if (z2 - cz <= (z2 - z1) / 2) continue + zoom = z2 + break + } + + const { x, y } = point + if (opts?.duration) { + this.animateCamera( + cx + (x / zoom - x) - (x / cz - x), + cy + (y / zoom - y) - (y / cz - y), + zoom, + opts + ) + } else { + this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom) + } + return this + } + + /** + * Zoom the camera out. + * + * @example + * + * ```ts + * app.zoomOut() + * app.zoomOut(app.viewportScreenCenter, { duration: 120 }) + * app.zoomOut(app.inputs.currentScreenPoint, { duration: 120 }) + * ``` + * + * @param opts - The options for an animation. + * @public + */ + zoomOut(point = this.viewportScreenCenter, opts?: AnimationOptions): this { + if (!this.canMoveCamera) return this + + const { x: cx, y: cy, z: cz } = this.camera + + let zoom = MIN_ZOOM + + for (let i = ZOOMS.length - 1; i > 0; i--) { + const z1 = ZOOMS[i - 1] + const z2 = ZOOMS[i] + if (z2 - cz >= (z2 - z1) / 2) continue + zoom = z1 + break + } + + const { x, y } = point + + if (opts?.duration) { + this.animateCamera( + cx + (x / zoom - x) - (x / cz - x), + cy + (y / zoom - y) - (y / cz - y), + zoom, + opts + ) + } else { + this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom) + } + return this + } + + /** + * Zoom the camera to fit the current selection in the viewport. + * + * @example + * + * ```ts + * app.zoomToSelection() + * ``` + * + * @param opts - The options for an animation. + * @public + */ + zoomToSelection(opts?: AnimationOptions): this { + if (!this.canMoveCamera) return this + + const ids = this.selectedIds + if (ids.length <= 0) return this + + const selectedBounds = Box2d.Common(compact(ids.map((id) => this.getPageBoundsById(id)))) + + this.zoomToBounds( + selectedBounds.minX, + selectedBounds.minY, + selectedBounds.width, + selectedBounds.height, + Math.max(1, this.camera.z), + opts + ) + return this + } + + /** + * Pan or pan/zoom the selected ids into view. This method tries to not change the zoom if + * possible. + * + * @param ids - The ids of the shapes to pan and zoom into view. + * @param opts - The options for an animation. + * @public + */ + panZoomIntoView(ids: TLShapeId[], opts?: AnimationOptions): this { + if (!this.canMoveCamera) return this + + if (ids.length <= 0) return this + const selectedBounds = Box2d.Common(compact(ids.map((id) => this.getPageBoundsById(id)))) + + const { viewportPageBounds } = this + + if (viewportPageBounds.h < selectedBounds.h || viewportPageBounds.w < selectedBounds.w) { + this.zoomToBounds( + selectedBounds.minX, + selectedBounds.minY, + selectedBounds.width, + selectedBounds.height, + this.camera.z, + opts + ) + + return this + } else { + // TODO: This buffer should calculate the 'active area' of the UI + const bufferOffsets = this._activeAreaManager.offsets.value + + const pageTop = viewportPageBounds.y + bufferOffsets.top + const pageRight = viewportPageBounds.maxY - bufferOffsets.right + const pageBottom = viewportPageBounds.maxY - bufferOffsets.bottom + const pageLeft = viewportPageBounds.x + bufferOffsets.left + + const selectedTop = selectedBounds.y + const selectedRight = selectedBounds.maxX + const selectedBottom = selectedBounds.maxY + const selectedLeft = selectedBounds.x + + let offsetX = 0 + let offsetY = 0 + if (pageBottom < selectedBottom) { + // off bottom + offsetY = pageBottom - selectedBottom + } else if (pageTop > selectedTop) { + // off top + offsetY = pageTop - selectedTop + } else { + // inside y-bounds + } + + if (pageRight < selectedRight) { + // off right + offsetX = pageRight - selectedRight + } else if (pageLeft > selectedLeft) { + // off left + offsetX = pageLeft - selectedLeft + } else { + // inside x-bounds + } + + const { camera } = this + + if (opts?.duration) { + this.animateCamera(camera.x + offsetX, camera.y + offsetY, camera.z, opts) + } else { + this.setCamera(camera.x + offsetX, camera.y + offsetY, camera.z) + } + } + + return this + } + + /** + * Zoom the camera to fit a bounding box (in page space). + * + * @example + * + * ```ts + * app.zoomToBounds(0, 0, 100, 100) + * ``` + * + * @param x - The bounding box's x position. + * @param y - The bounding box's y position. + * @param width - The bounding box's width. + * @param height - The bounding box's height. + * @param targetZoom - The desired zoom level. Defaults to 0.1. + * @public + */ + zoomToBounds( + x: number, + y: number, + width: number, + height: number, + targetZoom?: number, + opts?: AnimationOptions + ): this { + if (!this.canMoveCamera) return this + + const { viewportScreenBounds } = this + + const inset = Math.min(256, viewportScreenBounds.width * 0.28) + + let zoom = clamp( + Math.min( + (viewportScreenBounds.width - inset) / width, + (viewportScreenBounds.height - inset) / height + ), + MIN_ZOOM, + MAX_ZOOM + ) + + if (targetZoom !== undefined) { + zoom = Math.min(targetZoom, zoom) + } + + if (opts?.duration) { + this.animateCamera( + -x + (viewportScreenBounds.width - width * zoom) / 2 / zoom, + -y + (viewportScreenBounds.height - height * zoom) / 2 / zoom, + zoom, + opts + ) + } else { + this.setCamera( + -x + (viewportScreenBounds.width - width * zoom) / 2 / zoom, + -y + (viewportScreenBounds.height - height * zoom) / 2 / zoom, + zoom + ) + } + + return this + } + + /** + * Pan the camera. + * + * @example + * + * ```ts + * app.pan(100, 100) + * app.pan(100, 100, { duration: 1000 }) + * ``` + * + * @param dx - The amount to pan on the x axis. + * @param dy - The amount to pan on the y axis. + * @param opts - The animation options + */ + pan(dx: number, dy: number, opts?: AnimationOptions): this { + if (!this.canMoveCamera) return this + + const { camera } = this + const { x: cx, y: cy, z: cz } = camera + const d = new Vec2d(dx, dy).div(cz) + + if (opts?.duration ?? 0 > 0) { + return this.animateCamera(cx + d.x, cy + d.y, cz, opts) + } else { + this.setCamera(cx + d.x, cy + d.y, cz) + } + + return this + } + + /** + * Stop the current camera animation, if any. + * + * @public + */ + stopCameraAnimation() { + this.emit('stop-camera-animation') + + return this + } + + /** @internal */ + private _viewportAnimation = null as null | { + elapsed: number + duration: number + easing: (t: number) => number + start: Box2d + end: Box2d + } + + /** @internal */ + private _animateViewport(ms: number) { + if (!this._viewportAnimation) return + + const cancel = () => { + this.removeListener('tick', this._animateViewport) + this.removeListener('stop-camera-animation', cancel) + this._viewportAnimation = null + } + + this.once('stop-camera-animation', cancel) + + this._viewportAnimation.elapsed += ms + + const { elapsed, easing, duration, start, end } = this._viewportAnimation + + if (elapsed > duration) { + const z = this.viewportScreenBounds.width / end.width + const x = -end.x + const y = -end.y + + this._setCamera(x, y, z) + cancel() + return + } + + const remaining = duration - elapsed + const t = easing(1 - remaining / duration) + + const left = start.minX + (end.minX - start.minX) * t + const top = start.minY + (end.minY - start.minY) * t + const right = start.maxX + (end.maxX - start.maxX) * t + const bottom = start.maxY + (end.maxY - start.maxY) * t + + const easedViewport = new Box2d(left, top, right - left, bottom - top) + + const z = this.viewportScreenBounds.width / easedViewport.width + const x = -easedViewport.x + const y = -easedViewport.y + + this._setCamera(x, y, z) + } + + /** @internal */ + private _animateToViewport(targetViewportPage: Box2d, opts = {} as AnimationOptions) { + const { duration = 0, easing = EASINGS.easeInOutCubic } = opts + const startViewport = this.viewportPageBounds.clone() + + this.stopCameraAnimation() + if (this.instanceState.followingUserId) { + this.stopFollowingUser() + } + + this._viewportAnimation = { + elapsed: 0, + duration, + easing, + start: startViewport, + end: targetViewportPage, + } + + this.addListener('tick', this._animateViewport) + + return this + } + + slideCamera( + opts = {} as { + speed: number + direction: Vec2d + friction: number + speedThreshold?: number + } + ) { + if (!this.canMoveCamera) return this + + const { speed, direction, friction, speedThreshold = 0.01 } = opts + let currentSpeed = speed + + this.stopCameraAnimation() + + const cancel = () => { + this.removeListener('tick', moveCamera) + this.removeListener('stop-camera-animation', cancel) + } + + this.once('stop-camera-animation', cancel) + + const moveCamera = (elapsed: number) => { + const { x: cx, y: cy, z: cz } = this.camera + const movementVec = direction.clone().mul((currentSpeed * elapsed) / cz) + + // Apply friction + currentSpeed *= 1 - friction + if (currentSpeed < speedThreshold) { + cancel() + } else { + this._setCamera(cx + movementVec.x, cy + movementVec.y, cz) + } + } + + this.addListener('tick', moveCamera) + + return this + } + + /** + * Start viewport-following a user. + * + * @param userId - The id of the user to follow. + * @public + */ + startFollowingUser = (userId: TLUserId) => { + // Currently, we get the leader's viewport page bounds from their user presence. + // This is a placeholder until the ephemeral PR lands. + // After that, we'll be able to get the required data from their instance presence instead. + const leaderPresenceRecord = this.store.query.record('user_presence', () => ({ + userId: { eq: userId }, + })) + + const leaderInstanceRecord = this.store.query.record('instance', () => ({ + userId: { eq: userId }, + })) + + if (!leaderInstanceRecord || !leaderPresenceRecord) { + throw new Error("Couldn't find user to follow") + } + + // If the leader is following us, then we can't follow them + if (leaderInstanceRecord.value?.followingUserId === this.userId) { + return + } + + transact(() => { + this.stopFollowingUser() + + this.updateInstanceState({ + followingUserId: userId, + }) + }) + + const cancel = () => { + this.removeListener('tick', moveTowardsUser) + this.removeListener('stop-following', cancel) + } + + let isCaughtUp = false + + const moveTowardsUser = () => { + // Stop following if we can't find the user + const leaderInstance = leaderInstanceRecord.value + const leaderPresence = leaderPresenceRecord.value + if (!leaderInstance || !leaderPresence) { + this.stopFollowingUser() + return + } + + // Change page if leader is on a different page + const isOnSamePage = leaderInstance.currentPageId === this.currentPageId + const chaseProportion = isOnSamePage ? FOLLOW_CHASE_PROPORTION : 1 + if (!isOnSamePage) { + this.setCurrentPageId(leaderInstance.currentPageId, { stopFollowing: false }) + } + + // Get the bounds of the follower (me) and the leader (them) + const { center, width, height } = this.viewportPageBounds + const { + width: leaderWidth, + height: leaderHeight, + center: leaderCenter, + } = Box2d.From(leaderPresence.viewportPageBounds) + + // At this point, let's check if we're following someone who's following us. + // If so, we can't try to contain their entire viewport + // because that would become a feedback loop where we zoom, they zoom, etc. + const isFollowingFollower = leaderInstance.followingUserId === this.userId + + // Figure out how much to zoom + const desiredWidth = width + (leaderWidth - width) * chaseProportion + const desiredHeight = height + (leaderHeight - height) * chaseProportion + const ratio = !isFollowingFollower + ? Math.min(width / desiredWidth, height / desiredHeight) + : height / desiredHeight + + const targetZoom = clamp(this.camera.z * ratio, MIN_ZOOM, MAX_ZOOM) + const targetWidth = this.viewportScreenBounds.w / targetZoom + const targetHeight = this.viewportScreenBounds.h / targetZoom + + // Figure out where to move the camera + const displacement = leaderCenter.sub(center) + const targetCenter = Vec2d.Add(center, Vec2d.Mul(displacement, chaseProportion)) + + // Now let's assess whether we've caught up to the leader or not + const distance = Vec2d.Sub(targetCenter, center).len() + const zoomChange = Math.abs(targetZoom - this.camera.z) + + // If we're chasing the leader... + // Stop chasing if we're close enough + if (distance < FOLLOW_CHASE_PAN_SNAP && zoomChange < FOLLOW_CHASE_ZOOM_SNAP) { + isCaughtUp = true + return + } + + // If we're already caught up with the leader... + // Only start moving again if we're far enough away + if ( + isCaughtUp && + distance < FOLLOW_CHASE_PAN_UNSNAP && + zoomChange < FOLLOW_CHASE_ZOOM_UNSNAP + ) { + return + } + + // Update the camera! + isCaughtUp = false + this.stopCameraAnimation() + this.setCamera( + -(targetCenter.x - targetWidth / 2), + -(targetCenter.y - targetHeight / 2), + targetZoom, + { stopFollowing: false } + ) + } + + this.once('stop-following', cancel) + this.addListener('tick', moveTowardsUser) + + return this + } + + /** + * Stop viewport-following a user. + * + * @public + */ + stopFollowingUser = () => { + this.updateInstanceState({ + followingUserId: null, + }) + + this.emit('stop-following') + + return this + } + + animateToShape(shapeId: TLShapeId, opts: AnimationOptions = DEFAULT_ANIMATION_OPTIONS): this { + if (!this.canMoveCamera) return this + + const activeArea = getActiveAreaScreenSpace(this) + const viewportAspectRatio = activeArea.width / activeArea.height + + const shapePageBounds = this.getPageBoundsById(shapeId) + + if (!shapePageBounds) return this + + const shapeAspectRatio = shapePageBounds.width / shapePageBounds.height + + const targetViewportPage = shapePageBounds.clone() + + const z = shapePageBounds.width / activeArea.width + targetViewportPage.width += (activeArea.left + activeArea.right) * z + targetViewportPage.height += (activeArea.top + activeArea.bottom) * z + targetViewportPage.x -= activeArea.left * z + targetViewportPage.y -= activeArea.top * z + + if (shapeAspectRatio > viewportAspectRatio) { + targetViewportPage.height = shapePageBounds.width / viewportAspectRatio + targetViewportPage.y -= (targetViewportPage.height - shapePageBounds.height) / 2 + } else { + targetViewportPage.width = shapePageBounds.height * viewportAspectRatio + targetViewportPage.x -= (targetViewportPage.width - shapePageBounds.width) / 2 + } + + return this._animateToViewport(targetViewportPage, opts) + } + + /** + * Blur the app, cancelling any interaction state. + * + * @example + * + * ```ts + * app.blur() + * ``` + * + * @public + */ + blur() { + this.complete() + this.getContainer().blur() + this._isFocused.set(false) + return this + } + + /** + * Focus the app. + * + * @example + * + * ```ts + * app.focus() + * ``` + * + * @public + */ + focus() { + this.getContainer().focus() + this._isFocused.set(true) + return this + } + + /** + * Dispatch a cancel event. + * + * @example + * + * ```ts + * app.cancel() + * ``` + * + * @public + */ + cancel() { + this.dispatch({ type: 'misc', name: 'cancel' }) + return this + } + + /** + * Dispatch an interrupt event. + * + * @example + * + * ```ts + * app.interrupt() + * ``` + * + * @public + */ + interrupt() { + this.dispatch({ type: 'misc', name: 'interrupt' }) + return this + } + + /** + * Dispatch a complete event. + * + * @example + * + * ```ts + * app.complete() + * ``` + * + * @public + */ + complete() { + this.dispatch({ type: 'misc', name: 'complete' }) + return this + } + + /* -------------------- Callbacks ------------------- */ + + /** + * A callback fired when a file is converted to an asset. This callback should return the asset + * partial. + * + * @example + * + * ```ts + * app.onCreateAssetFromFile(myFile) + * ``` + * + * @param file - The file to upload. + * @public + */ + + async onCreateAssetFromFile(file: File): Promise { + return await getMediaAssetFromFile(file) + } + + /** + * A callback fired when a URL is converted to a bookmark. This callback should return the + * metadata for the bookmark. + * + * @example + * + * ```ts + * app.onCreateBookmarkFromUrl(url, id) + * ``` + * + * @param url - The url that was created. + * @public + */ + async onCreateBookmarkFromUrl( + url: string + ): Promise<{ image: string; title: string; description: string }> { + try { + const resp = await fetch(url, { method: 'GET', mode: 'no-cors' }) + const html = await resp.text() + const doc = new DOMParser().parseFromString(html, 'text/html') + + return { + image: doc.head.querySelector('meta[property="og:image"]')?.getAttribute('content') ?? '', + title: doc.head.querySelector('meta[property="og:title"]')?.getAttribute('content') ?? '', + description: + doc.head.querySelector('meta[property="og:description"]')?.getAttribute('content') ?? '', + } + } catch (error) { + console.error(error) + return { image: '', title: '', description: '' } + } + } + + /* ---------------- Text Measurement ---------------- */ + + /** + * A helper for measuring text. + * + * @public + */ + textMeasure: TextManager + + /* --------------------- Groups --------------------- */ + + groupShapes(ids: TLShapeId[] = this.selectedIds, groupId = createShapeId()) { + if (this.isReadOnly) return this + + if (ids.length <= 1) return this + + const shapes = compact(ids.map((id) => this.getShapeById(id))) + const sortedShapeIds = shapes.sort(sortByIndex).map((s) => s.id) + const pageBounds = Box2d.Common(compact(shapes.map((id) => this.getPageBounds(id)))) + + const { x, y } = pageBounds.point + + const parentId = this.findCommonAncestor(shapes) ?? this.currentPageId + + // Only group when the select tool is active + if (this.currentToolId !== 'select') return this + + // If not already in idle, cancel the current interaction (get back to idle) + if (!this.isIn('select.idle')) { + this.cancel() + } + + // Find all the shapes that have the same parentId, and use the highest index. + const shapesWithRootParent = shapes + .filter((shape) => shape.parentId === parentId) + .sort(sortByIndex) + + const highestIndex = shapesWithRootParent[shapesWithRootParent.length - 1]?.index + + this.batch(() => { + this.createShapes([ + { + id: groupId, + type: 'group', + parentId, + index: highestIndex, + x, + y, + props: { + opacity: '1', + }, + }, + ]) + this.reparentShapesById(sortedShapeIds, groupId) + this.select(groupId) + }) + + return this + } + + ungroupShapes(ids: TLShapeId[] = this.selectedIds) { + if (this.isReadOnly) return this + if (ids.length === 0) return this + + // Only ungroup when the select tool is active + if (this.currentToolId !== 'select') return this + + // If not already in idle, cancel the current interaction (get back to idle) + if (!this.isIn('select.idle')) { + this.cancel() + } + + // The ids of the selected shapes after ungrouping; + // these include all of the grouped shapes children, + // plus any shapes that were selected apart from the groups. + const idsToSelect = new Set() + + // Get all groups in the selection + const shapes = compact(ids.map((id) => this.getShapeById(id))) + + const groups: TLGroupShape[] = [] + + shapes.forEach((shape) => { + if (TLGroupShapeDef.is(shape)) { + groups.push(shape) + } else { + idsToSelect.add(shape.id) + } + }) + + if (groups.length === 0) return this + + this.batch(() => { + let group: TLShape + + for (let i = 0, n = groups.length; i < n; i++) { + group = groups[i] as TLGroupShape + const childIds = this.getSortedChildIds(group.id) + + for (let j = 0, n = childIds.length; j < n; j++) { + idsToSelect.add(childIds[j]) + } + + this.reparentShapesById(childIds, group.parentId, group.index) + } + + this.deleteShapes(groups.map((group) => group.id)) + this.select(...idsToSelect) + }) + + return this + } +} + +function alertMaxShapes(app: App, pageId = app.currentPageId) { + const name = app.getPageById(pageId)!.name + app.emit('max-shapes', { name, pageId, count: MAX_SHAPES_PER_PAGE }) +} diff --git a/packages/editor/src/lib/app/derivations/arrowBindingsIndex.test.ts b/packages/editor/src/lib/app/derivations/arrowBindingsIndex.test.ts new file mode 100644 index 000000000..0e8abcf6d --- /dev/null +++ b/packages/editor/src/lib/app/derivations/arrowBindingsIndex.test.ts @@ -0,0 +1,315 @@ +import { createCustomShapeId, TLShapeId } from '@tldraw/tlschema' +import { TestApp } from '../../test/TestApp' + +let app: TestApp + +beforeEach(() => { + app = new TestApp() +}) + +const ids = { + box1: createCustomShapeId('box1'), + box2: createCustomShapeId('box2'), + box3: createCustomShapeId('box3'), + + box4: createCustomShapeId('box4'), + box5: createCustomShapeId('box5'), + box6: createCustomShapeId('box6'), +} + +describe('arrowBindingsIndex', () => { + it('keeps a mapping from bound shapes to the arrows that bind to them', () => { + app.createShapes([ + { + type: 'geo', + id: ids.box1, + x: 0, + y: 0, + props: { + w: 100, + h: 100, + }, + }, + { + type: 'geo', + id: ids.box2, + x: 200, + y: 0, + props: { + w: 100, + h: 100, + }, + }, + ]) + + app.setSelectedTool('arrow') + app.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50) + const arrow = app.onlySelectedShape! + expect(arrow.type).toBe('arrow') + + expect(app.getArrowsBoundTo(ids.box1)).toEqual([{ arrowId: arrow.id, handleId: 'start' }]) + expect(app.getArrowsBoundTo(ids.box2)).toEqual([{ arrowId: arrow.id, handleId: 'end' }]) + }) + + it('works if there are many arrows', () => { + app.createShapes([ + { + type: 'geo', + id: ids.box1, + x: 0, + y: 0, + props: { + w: 100, + h: 100, + }, + }, + { + type: 'geo', + id: ids.box2, + x: 200, + y: 0, + props: { + w: 100, + h: 100, + }, + }, + ]) + + app.setSelectedTool('arrow') + // span both boxes + app.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50) + const arrow1 = app.onlySelectedShape! + expect(arrow1.type).toBe('arrow') + + // start at box 1 and leave + app.setSelectedTool('arrow') + app.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50) + const arrow2 = app.onlySelectedShape! + expect(arrow2.type).toBe('arrow') + + // start outside box 1 and enter + app.setSelectedTool('arrow') + app.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50) + const arrow3 = app.onlySelectedShape! + expect(arrow3.type).toBe('arrow') + + // start at box 2 and leave + app.setSelectedTool('arrow') + app.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50) + const arrow4 = app.onlySelectedShape! + expect(arrow4.type).toBe('arrow') + + // start outside box 2 and enter + app.setSelectedTool('arrow') + app.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50) + const arrow5 = app.onlySelectedShape! + expect(arrow5.type).toBe('arrow') + + expect(app.getArrowsBoundTo(ids.box1)).toEqual([ + { arrowId: arrow1.id, handleId: 'start' }, + { arrowId: arrow2.id, handleId: 'start' }, + { arrowId: arrow3.id, handleId: 'end' }, + ]) + + expect(app.getArrowsBoundTo(ids.box2)).toEqual([ + { arrowId: arrow1.id, handleId: 'end' }, + { arrowId: arrow4.id, handleId: 'start' }, + { arrowId: arrow5.id, handleId: 'end' }, + ]) + }) + + describe('updating shapes', () => { + // ▲ │ │ ▲ + // │ │ │ │ + // b c e d + // ┌───┼─┴─┐ ┌──┴──┼─┐ + // │ │ ▼ │ │ ▼ │ │ + // │ └───┼─────a───┼───► │ │ + // │ 1 │ │ 2 │ + // └───────┘ └───────┘ + let arrowAId: TLShapeId + let arrowBId: TLShapeId + let arrowCId: TLShapeId + let arrowDId: TLShapeId + let arrowEId: TLShapeId + beforeEach(() => { + app.createShapes([ + { + type: 'geo', + id: ids.box1, + x: 0, + y: 0, + props: { + w: 100, + h: 100, + }, + }, + { + type: 'geo', + id: ids.box2, + x: 200, + y: 0, + props: { + w: 100, + h: 100, + }, + }, + ]) + + // span both boxes + app.setSelectedTool('arrow') + app.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50) + arrowAId = app.onlySelectedShape!.id + // start at box 1 and leave + app.setSelectedTool('arrow') + app.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50) + arrowBId = app.onlySelectedShape!.id + // start outside box 1 and enter + app.setSelectedTool('arrow') + app.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50) + arrowCId = app.onlySelectedShape!.id + // start at box 2 and leave + app.setSelectedTool('arrow') + app.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50) + arrowDId = app.onlySelectedShape!.id + // start outside box 2 and enter + app.setSelectedTool('arrow') + app.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50) + arrowEId = app.onlySelectedShape!.id + }) + it('deletes the entry if you delete the bound shapes', () => { + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3) + app.deleteShapes([ids.box2]) + expect(app.getArrowsBoundTo(ids.box2)).toEqual([]) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + }) + it('deletes the entry if you delete an arrow', () => { + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3) + app.deleteShapes([arrowEId]) + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(2) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + + app.deleteShapes([arrowDId]) + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(1) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + + app.deleteShapes([arrowCId]) + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(1) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(2) + + app.deleteShapes([arrowBId]) + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(1) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(1) + + app.deleteShapes([arrowAId]) + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(0) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(0) + }) + + it('deletes the entries in a batch too', () => { + app.deleteShapes([arrowAId, arrowBId, arrowCId, arrowDId, arrowEId]) + + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(0) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(0) + }) + + it('adds new entries after initial creation', () => { + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + + // draw from box 2 to box 1 + app.setSelectedTool('arrow') + app.pointerDown(250, 50).pointerMove(50, 50).pointerUp(50, 50) + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(4) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(4) + + // create a new box + + app.createShapes([ + { + type: 'geo', + id: ids.box3, + x: 400, + y: 0, + props: { + w: 100, + h: 100, + }, + }, + ]) + + // draw from box 2 to box 3 + + app.setSelectedTool('arrow') + app.pointerDown(250, 50).pointerMove(450, 50).pointerUp(450, 50) + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(5) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(4) + expect(app.getArrowsBoundTo(ids.box3)).toHaveLength(1) + }) + + it('works when copy pasting', () => { + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + + app.selectAll() + app.duplicateShapes() + + const [box1Clone, box2Clone] = app.selectedShapes + .filter((s) => s.type === 'geo') + .sort((a, b) => a.x - b.x) + + expect(app.getArrowsBoundTo(box2Clone.id)).toHaveLength(3) + expect(app.getArrowsBoundTo(box1Clone.id)).toHaveLength(3) + }) + + it('allows bound shapes to be moved', () => { + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + + app.nudgeShapes([ids.box2], { x: 0, y: -1 }, true) + + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + }) + + it('allows the arrows bound shape to change', () => { + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(3) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + + // create another box + + app.createShapes([ + { + type: 'geo', + id: ids.box3, + x: 400, + y: 0, + props: { + w: 100, + h: 100, + }, + }, + ]) + + // move arrowA from box2 to box3 + app.updateShapes([ + { + id: arrowAId, + type: 'arrow', + props: { + end: { + type: 'binding', + isExact: false, + boundShapeId: ids.box3, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + }, + }, + ]) + + expect(app.getArrowsBoundTo(ids.box2)).toHaveLength(2) + expect(app.getArrowsBoundTo(ids.box1)).toHaveLength(3) + expect(app.getArrowsBoundTo(ids.box3)).toHaveLength(1) + }) + }) +}) diff --git a/packages/editor/src/lib/app/derivations/arrowBindingsIndex.ts b/packages/editor/src/lib/app/derivations/arrowBindingsIndex.ts new file mode 100644 index 000000000..7a909a968 --- /dev/null +++ b/packages/editor/src/lib/app/derivations/arrowBindingsIndex.ts @@ -0,0 +1,134 @@ +import { TLArrowShape, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema' +import { Computed, RESET_VALUE, computed, isUninitialized } from 'signia' +import { TLArrowShapeDef } from '../shapeutils/TLArrowUtil/TLArrowUtil' + +export type TLArrowBindingsIndex = Record< + TLShapeId, + undefined | { arrowId: TLShapeId; handleId: 'start' | 'end' }[] +> +export const arrowBindingsIndex = (store: TLStore): Computed => { + const shapeHistory = store.query.filterHistory('shape') + const arrowQuery = store.query.records('shape', () => ({ type: { eq: 'arrow' as const } })) + function fromScratch() { + const allArrows = arrowQuery.value as TLArrowShape[] + + const bindings2Arrows: TLArrowBindingsIndex = {} + + for (const arrow of allArrows) { + const { start, end } = arrow.props + if (start.type === 'binding') { + const arrows = bindings2Arrows[start.boundShapeId] + if (arrows) arrows.push({ arrowId: arrow.id, handleId: 'start' }) + else bindings2Arrows[start.boundShapeId] = [{ arrowId: arrow.id, handleId: 'start' }] + } + + if (end.type === 'binding') { + const arrows = bindings2Arrows[end.boundShapeId] + if (arrows) arrows.push({ arrowId: arrow.id, handleId: 'end' }) + else bindings2Arrows[end.boundShapeId] = [{ arrowId: arrow.id, handleId: 'end' }] + } + } + + return bindings2Arrows + } + return computed('arrowBindingsIndex', (_lastValue, lastComputedEpoch) => { + if (isUninitialized(_lastValue)) { + return fromScratch() + } + + const lastValue = _lastValue + + const diff = shapeHistory.getDiffSince(lastComputedEpoch) + + if (diff === RESET_VALUE) { + return fromScratch() + } + + let nextValue: TLArrowBindingsIndex | undefined = undefined + + function ensureNewArray(boundShapeId: TLShapeId) { + // this will never happen + if (!nextValue) { + nextValue = { ...lastValue } + } + if (!nextValue[boundShapeId]) { + nextValue[boundShapeId] = [] + } else if (nextValue[boundShapeId] === lastValue[boundShapeId]) { + nextValue[boundShapeId] = [...nextValue[boundShapeId]!] + } + } + + function removingBinding( + boundShapeId: TLShapeId, + arrowId: TLShapeId, + handleId: 'start' | 'end' + ) { + ensureNewArray(boundShapeId) + nextValue![boundShapeId] = nextValue![boundShapeId]!.filter( + (binding) => binding.arrowId !== arrowId || binding.handleId !== handleId + ) + if (nextValue![boundShapeId]!.length === 0) { + delete nextValue![boundShapeId] + } + } + + function addBinding(boundShapeId: TLShapeId, arrowId: TLShapeId, handleId: 'start' | 'end') { + ensureNewArray(boundShapeId) + nextValue![boundShapeId]!.push({ arrowId, handleId }) + } + + for (const changes of diff) { + for (const newShape of Object.values(changes.added)) { + if (TLArrowShapeDef.is(newShape)) { + const { start, end } = newShape.props + if (start.type === 'binding') { + addBinding(start.boundShapeId, newShape.id, 'start') + } + if (end.type === 'binding') { + addBinding(end.boundShapeId, newShape.id, 'end') + } + } + } + + for (const [prev, next] of Object.values(changes.updated) as [TLShape, TLShape][]) { + if (!TLArrowShapeDef.is(prev) || !TLArrowShapeDef.is(next)) continue + + for (const handle of ['start', 'end'] as const) { + const prevTerminal = prev.props[handle] + const nextTerminal = next.props[handle] + + if (prevTerminal.type === 'binding' && nextTerminal.type === 'point') { + // if the binding was removed + removingBinding(prevTerminal.boundShapeId, prev.id, handle) + } else if (prevTerminal.type === 'point' && nextTerminal.type === 'binding') { + // if the binding was added + addBinding(nextTerminal.boundShapeId, next.id, handle) + } else if ( + prevTerminal.type === 'binding' && + nextTerminal.type === 'binding' && + prevTerminal.boundShapeId !== nextTerminal.boundShapeId + ) { + // if the binding was changed + removingBinding(prevTerminal.boundShapeId, prev.id, handle) + addBinding(nextTerminal.boundShapeId, next.id, handle) + } + } + } + + for (const prev of Object.values(changes.removed)) { + if (TLArrowShapeDef.is(prev)) { + const { start, end } = prev.props + if (start.type === 'binding') { + removingBinding(start.boundShapeId, prev.id, 'start') + } + if (end.type === 'binding') { + removingBinding(end.boundShapeId, prev.id, 'end') + } + } + } + } + + // TODO: add diff entries if we need them + return nextValue ?? lastValue + }) +} diff --git a/packages/editor/src/lib/app/derivations/parentsToChildrenWithIndexes.test.ts b/packages/editor/src/lib/app/derivations/parentsToChildrenWithIndexes.test.ts new file mode 100644 index 000000000..5524dfff9 --- /dev/null +++ b/packages/editor/src/lib/app/derivations/parentsToChildrenWithIndexes.test.ts @@ -0,0 +1,105 @@ +import { createCustomShapeId } from '@tldraw/tlschema' +import { TestApp } from '../../test/TestApp' +import { getIndexAbove, getIndexBetween } from '../../utils/reordering/reordering' + +let app: TestApp + +beforeEach(() => { + app = new TestApp() +}) + +const ids = { + box1: createCustomShapeId('box1'), + box2: createCustomShapeId('box2'), + box3: createCustomShapeId('box3'), + + box4: createCustomShapeId('box4'), + box5: createCustomShapeId('box5'), + box6: createCustomShapeId('box6'), +} + +describe('parentsToChildrenWithIndexes', () => { + it('keeps the children and parents up to date', () => { + app.createShapes([{ type: 'geo', id: ids.box1 }]) + app.createShapes([{ type: 'geo', id: ids.box2 }]) + + expect(app.getSortedChildIds(ids.box1)).toEqual([]) + expect(app.getSortedChildIds(ids.box2)).toEqual([]) + + app.createShapes([{ type: 'geo', id: ids.box3, parentId: ids.box1 }]) + + expect(app.getSortedChildIds(ids.box1)).toEqual([ids.box3]) + expect(app.getSortedChildIds(ids.box2)).toEqual([]) + + app.updateShapes([{ id: ids.box3, type: 'geo', parentId: ids.box2 }]) + + expect(app.getSortedChildIds(ids.box1)).toEqual([]) + expect(app.getSortedChildIds(ids.box2)).toEqual([ids.box3]) + + app.updateShapes([{ id: ids.box1, type: 'geo', parentId: ids.box2 }]) + + expect(app.getSortedChildIds(ids.box2)).toEqual([ids.box3, ids.box1]) + }) + + it('keeps the children of pages too', () => { + app.createShapes([ + { type: 'geo', id: ids.box1 }, + { type: 'geo', id: ids.box2 }, + { type: 'geo', id: ids.box3 }, + ]) + + expect(app.getSortedChildIds(app.currentPageId)).toEqual([ids.box1, ids.box2, ids.box3]) + }) + + it('keeps children sorted', () => { + app.createShapes([ + { type: 'geo', id: ids.box1 }, + { type: 'geo', id: ids.box2 }, + { type: 'geo', id: ids.box3 }, + ]) + + expect(app.getSortedChildIds(app.currentPageId)).toEqual([ids.box1, ids.box2, ids.box3]) + + app.updateShapes([ + { + id: ids.box1, + type: 'geo', + index: getIndexBetween( + app.getShapeById(ids.box2)!.index, + app.getShapeById(ids.box3)!.index + ), + }, + ]) + expect(app.getSortedChildIds(app.currentPageId)).toEqual([ids.box2, ids.box1, ids.box3]) + + app.updateShapes([ + { id: ids.box2, type: 'geo', index: getIndexAbove(app.getShapeById(ids.box3)!.index) }, + ]) + + expect(app.getSortedChildIds(app.currentPageId)).toEqual([ids.box1, ids.box3, ids.box2]) + }) + + it('sorts children of next parent when a shape is reparented', () => { + app.createShapes([ + { type: 'geo', id: ids.box1 }, + { type: 'geo', id: ids.box2, parentId: ids.box1 }, + { type: 'geo', id: ids.box3, parentId: ids.box1 }, + { type: 'geo', id: ids.box4 }, + ]) + + const box2Index = app.getShapeById(ids.box2)!.index + const box3Index = app.getShapeById(ids.box3)!.index + const box4Index = getIndexBetween(box2Index, box3Index) + + app.updateShapes([ + { + id: ids.box4, + type: 'geo', + parentId: ids.box1, + index: box4Index, + }, + ]) + + expect(app.getSortedChildIds(ids.box1)).toEqual([ids.box2, ids.box4, ids.box3]) + }) +}) diff --git a/packages/editor/src/lib/app/derivations/parentsToChildrenWithIndexes.ts b/packages/editor/src/lib/app/derivations/parentsToChildrenWithIndexes.ts new file mode 100644 index 000000000..6db34e5ea --- /dev/null +++ b/packages/editor/src/lib/app/derivations/parentsToChildrenWithIndexes.ts @@ -0,0 +1,114 @@ +import { isShape, TLParentId, TLRecord, TLShapeId, TLStore } from '@tldraw/tlschema' +import { RecordsDiff } from '@tldraw/tlstore' +import { computed, isUninitialized, RESET_VALUE } from 'signia' + +type Parents2Children = Record + +export const parentsToChildrenWithIndexes = (store: TLStore) => { + const shapeIds = store.query.ids<'shape'>('shape') + function fromScratch() { + const result: Parents2Children = {} + + // Populate the result object with an array for each parent. + shapeIds.value.forEach((id) => { + const shape = store.get(id)! + + if (!result[shape.parentId]) { + result[shape.parentId] = [] + } + + result[shape.parentId].push([id, shape.index]) + }) + + // Sort the children by index + Object.values(result).forEach((arr) => arr.sort((a, b) => (a[1] < b[1] ? -1 : 1))) + + return result + } + return computed( + 'parentsToChildrenWithIndexes', + (lastValue, lastComputedEpoch) => { + if (isUninitialized(lastValue)) { + return fromScratch() + } + + const diff = store.history.getDiffSince(lastComputedEpoch) + + if (diff === RESET_VALUE) { + return fromScratch() + } + + if (diff.length === 0) return lastValue + + let newValue: Record | null = null + + const ensureNewArray = (parentId: TLParentId) => { + if (!newValue) { + newValue = { ...lastValue } + } + if (!newValue[parentId]) { + newValue[parentId] = [] + } else if (newValue[parentId] === lastValue[parentId]) { + newValue[parentId] = [...newValue[parentId]!] + } + } + + const toSort = new Set<[id: TLShapeId, index: string][]>() + + let changes: RecordsDiff + + for (let i = 0, n = diff.length; i < n; i++) { + changes = diff[i] + + // Iterate through the added shapes, add them to the new value and mark them for sorting + for (const record of Object.values(changes.added)) { + if (!isShape(record)) continue + ensureNewArray(record.parentId) + newValue![record.parentId].push([record.id, record.index]) + toSort.add(newValue![record.parentId]) + } + + // Iterate through the updated shapes, add them to their parents in the new value and mark them for sorting + for (const [from, to] of Object.values(changes.updated)) { + if (!isShape(to)) continue + if (!isShape(from)) continue + + if (from.parentId !== to.parentId) { + // If the parents have changed, remove the new value from the old parent and add it to the new parent + ensureNewArray(from.parentId) + ensureNewArray(to.parentId) + newValue![from.parentId].splice( + newValue![from.parentId].findIndex((i) => i[0] === to.id), + 1 + ) + newValue![to.parentId].push([to.id, to.index]) + toSort.add(newValue![to.parentId]) + } else if (from.index !== to.index) { + // If the parent is the same but the index has changed (e.g. if they've been reordered), update the parent's array at the new index + ensureNewArray(to.parentId) + const idx = newValue![to.parentId].findIndex((i) => i[0] === to.id) + newValue![to.parentId][idx] = [to.id, to.index] + toSort.add(newValue![to.parentId]) + } + } + + // Iterate through the removed shapes, remove them from their parents in new value + for (const record of Object.values(changes.removed)) { + if (!isShape(record)) continue + ensureNewArray(record.parentId) + newValue![record.parentId].splice( + newValue![record.parentId].findIndex((i) => i[0] === record.id), + 1 + ) + } + } + + // Sort the arrays that have been marked for sorting + for (const arr of toSort) { + arr.sort((a, b) => (a[1] < b[1] ? -1 : 1)) + } + + return newValue ?? lastValue + } + ) +} diff --git a/packages/editor/src/lib/app/derivations/shapeIdsInCurrentPage.test.ts b/packages/editor/src/lib/app/derivations/shapeIdsInCurrentPage.test.ts new file mode 100644 index 000000000..09f34b806 --- /dev/null +++ b/packages/editor/src/lib/app/derivations/shapeIdsInCurrentPage.test.ts @@ -0,0 +1,69 @@ +import { createCustomShapeId, TLPage } from '@tldraw/tlschema' +import { TestApp } from '../../test/TestApp' + +let app: TestApp + +beforeEach(() => { + app = new TestApp() +}) + +const ids = { + box1: createCustomShapeId('box1'), + box2: createCustomShapeId('box2'), + box3: createCustomShapeId('box3'), + + box4: createCustomShapeId('box4'), + box5: createCustomShapeId('box5'), + box6: createCustomShapeId('box6'), +} + +describe('shapeIdsInCurrentPage', () => { + it('keeps the shape ids in the current page', () => { + expect(new Set(app.shapeIds)).toEqual(new Set([])) + app.createShapes([{ type: 'geo', id: ids.box1 }]) + + expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1])) + + app.createShapes([{ type: 'geo', id: ids.box2 }]) + + expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1, ids.box2])) + + app.createShapes([{ type: 'geo', id: ids.box3 }]) + + expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3])) + + app.deleteShapes([ids.box2]) + + expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1, ids.box3])) + + app.deleteShapes([ids.box1]) + + expect(new Set(app.shapeIds)).toEqual(new Set([ids.box3])) + + app.deleteShapes([ids.box3]) + + expect(new Set(app.shapeIds)).toEqual(new Set([])) + }) + + it('changes when the current page changes', () => { + app.createShapes([ + { type: 'geo', id: ids.box1 }, + { type: 'geo', id: ids.box2 }, + { type: 'geo', id: ids.box3 }, + ]) + const id = TLPage.createCustomId('page2') + app.createPage('New Page 2', id) + app.setCurrentPageId(id) + app.createShapes([ + { type: 'geo', id: ids.box4 }, + { type: 'geo', id: ids.box5 }, + { type: 'geo', id: ids.box6 }, + ]) + + expect(new Set(app.shapeIds)).toEqual(new Set([ids.box4, ids.box5, ids.box6])) + + app.setCurrentPageId(app.pages[0].id) + + expect(new Set(app.shapeIds)).toEqual(new Set([ids.box1, ids.box2, ids.box3])) + }) +}) diff --git a/packages/editor/src/lib/app/derivations/shapeIdsInCurrentPage.ts b/packages/editor/src/lib/app/derivations/shapeIdsInCurrentPage.ts new file mode 100644 index 000000000..62a8e870d --- /dev/null +++ b/packages/editor/src/lib/app/derivations/shapeIdsInCurrentPage.ts @@ -0,0 +1,90 @@ +import { isShape, isShapeId, TLPage, TLPageId, TLShape, TLShapeId, TLStore } from '@tldraw/tlschema' +import { IncrementalSetConstructor } from '@tldraw/tlstore' +import { computed, isUninitialized, RESET_VALUE, withDiff } from 'signia' + +/** + * Get whether a shape is in the current page. + * + * @param store - The tldraw store. + * @param pageId - The id of the page to check. + * @param shape - The the shape to check. + */ +const isShapeInPage = (store: TLStore, pageId: TLPageId, shape: TLShape): boolean => { + while (!TLPage.isId(shape.parentId)) { + const parent = store.get(shape.parentId) + if (!parent) return false + shape = parent + } + + return shape.parentId === pageId +} + +/** + * A derivation that returns a list of shape ids in the current page. + * + * @param store - The tldraw store. + * @param getCurrentPageId - A function that returns the current page id. + */ +export const shapeIdsInCurrentPage = (store: TLStore, getCurrentPageId: () => TLPageId) => { + const shapesIndex = store.query.ids('shape') + let lastPageId: null | TLPageId = null + function fromScratch() { + const currentPageId = getCurrentPageId() + lastPageId = currentPageId + return new Set( + [...shapesIndex.value].filter((id) => isShapeInPage(store, currentPageId, store.get(id)!)) + ) + } + return computed>('_shapeIdsInCurrentPage', (prevValue, lastComputedEpoch) => { + if (isUninitialized(prevValue)) { + return fromScratch() + } + + const currentPageId = getCurrentPageId() + + if (currentPageId !== lastPageId) { + return fromScratch() + } + + const diff = store.history.getDiffSince(lastComputedEpoch) + + if (diff === RESET_VALUE) { + return fromScratch() + } + + const builder = new IncrementalSetConstructor( + prevValue + ) as IncrementalSetConstructor + + for (const changes of diff) { + for (const record of Object.values(changes.added)) { + if (isShape(record) && isShapeInPage(store, currentPageId, record)) { + builder.add(record.id) + } + } + + for (const [_from, to] of Object.values(changes.updated)) { + if (isShape(to)) { + if (isShapeInPage(store, currentPageId, to)) { + builder.add(to.id) + } else { + builder.remove(to.id) + } + } + } + + for (const id of Object.keys(changes.removed)) { + if (isShapeId(id)) { + builder.remove(id) + } + } + } + + const result = builder.get() + if (!result) { + return prevValue + } + + return withDiff(result.value, result.diff) + }) +} diff --git a/packages/editor/src/lib/app/managers/ActiveAreaManager.ts b/packages/editor/src/lib/app/managers/ActiveAreaManager.ts new file mode 100644 index 000000000..2ea0668b8 --- /dev/null +++ b/packages/editor/src/lib/app/managers/ActiveAreaManager.ts @@ -0,0 +1,68 @@ +import { atom } from 'signia' +import { App } from '../App' + +type Offsets = { + top: number + left: number + bottom: number + right: number +} +const DEFAULT_OFFSETS = { + top: 10, + left: 10, + bottom: 10, + right: 10, +} + +export function getActiveAreaScreenSpace(app: App) { + const containerEl = app.getContainer() + const el = containerEl.querySelector('*[data-tldraw-area="active-drawing"]') + const out = { + ...DEFAULT_OFFSETS, + width: 0, + height: 0, + } + if (el && containerEl) { + const cBbbox = containerEl.getBoundingClientRect() + const bbox = el.getBoundingClientRect() + out.top = bbox.top + out.left = bbox.left + out.bottom = cBbbox.height - bbox.bottom + out.right = cBbbox.width - bbox.right + } + + out.width = app.viewportScreenBounds.width - out.left - out.right + out.height = app.viewportScreenBounds.height - out.top - out.bottom + return out +} + +export function getActiveAreaPageSpace(app: App) { + const out = getActiveAreaScreenSpace(app) + const z = app.zoomLevel + out.left /= z + out.right /= z + out.top /= z + out.bottom /= z + out.width /= z + out.height /= z + return out +} + +export class ActiveAreaManager { + constructor(public app: App) { + window.addEventListener('resize', this.updateOffsets) + this.app.disposables.add(this.dispose) + } + + offsets = atom('activeAreaOffsets', DEFAULT_OFFSETS) + + updateOffsets = () => { + const offsets = getActiveAreaPageSpace(this.app) + this.offsets.set(offsets) + } + + // Clear the listener + dispose = () => { + window.addEventListener('resize', this.updateOffsets) + } +} diff --git a/packages/editor/src/lib/app/managers/CameraManager.ts b/packages/editor/src/lib/app/managers/CameraManager.ts new file mode 100644 index 000000000..fc8a7728e --- /dev/null +++ b/packages/editor/src/lib/app/managers/CameraManager.ts @@ -0,0 +1,32 @@ +import { atom } from 'signia' +import { App } from '../App' + +const CAMERA_SETTLE_TIMEOUT = 12 + +export class CameraManager { + constructor(public app: App) {} + + state = atom('camera state', 'idle' as 'idle' | 'moving') + + private timeoutRemaining = 0 + + private decay = (elapsed: number) => { + this.timeoutRemaining -= elapsed + if (this.timeoutRemaining <= 0) { + this.state.set('idle') + this.app.off('tick', this.decay) + this.app.updateCullingBounds() + } + } + + tick = () => { + // always reset the timeout + this.timeoutRemaining = CAMERA_SETTLE_TIMEOUT + + // If the state is idle, then start the tick + if (this.state.__unsafe__getWithoutCapture() === 'idle') { + this.state.set('moving') + this.app.on('tick', this.decay) + } + } +} diff --git a/packages/editor/src/lib/app/managers/ClickManager.test.ts b/packages/editor/src/lib/app/managers/ClickManager.test.ts new file mode 100644 index 000000000..996f18a21 --- /dev/null +++ b/packages/editor/src/lib/app/managers/ClickManager.test.ts @@ -0,0 +1,258 @@ +import { TestApp } from '../../test/TestApp' + +let app: TestApp + +beforeEach(() => { + app = new TestApp() + // we want to do this in order to avoid creating text shapes. weird + app.setSelectedTool('eraser') + app._transformPointerDownSpy.mockRestore() + app._transformPointerUpSpy.mockRestore() +}) + +jest.useFakeTimers() + +describe('Handles events', () => { + it('Emits single click events', () => { + const events: any[] = [] + app.addListener('event', (info) => events.push(info)) + + app.pointerDown() + app.pointerUp() + + const eventsBeforeSettle = [{ name: 'pointer_down' }, { name: 'pointer_up' }] + + // allow time for settle + jest.advanceTimersByTime(500) + + // nothing should have settled + expect(events).toMatchObject(eventsBeforeSettle) + + // clear events and click again + // the interaction should have reset + events.length = 0 + app.pointerDown().pointerUp().pointerDown() + expect(events).toMatchObject([ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', type: 'click', phase: 'down' }, + ]) + }) + + it('Emits double click events', () => { + const events: any[] = [] + app.addListener('event', (info) => events.push(info)) + + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + + const eventsBeforeSettle = [ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', type: 'click', phase: 'down' }, + { name: 'pointer_up' }, + // { name: 'pointer_move' }, + { name: 'double_click', type: 'click', phase: 'up' }, + ] + + for (let i = 0; i < eventsBeforeSettle.length; i++) { + expect(events[i]).toMatchObject(eventsBeforeSettle[i]) + } + + // allow double click to settle + jest.advanceTimersByTime(500) + + expect(events).toMatchObject([ + ...eventsBeforeSettle, + { name: 'double_click', type: 'click', phase: 'settle' }, + ]) + + // clear events and click again + // the interaction should have reset + events.length = 0 + app.pointerDown().pointerUp().pointerDown() + expect(events).toMatchObject([ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', type: 'click', phase: 'down' }, + ]) + }) + + it('Emits triple click events', () => { + const events: any[] = [] + app.addListener('event', (info) => events.push(info)) + + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + + const eventsBeforeSettle = [ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', type: 'click', phase: 'down' }, + { name: 'pointer_up' }, + { name: 'double_click', type: 'click', phase: 'up' }, + { name: 'pointer_down' }, + { name: 'triple_click', type: 'click', phase: 'down' }, + { name: 'pointer_up' }, + { name: 'triple_click', type: 'click', phase: 'up' }, + ] + + expect(eventsBeforeSettle).toMatchObject(eventsBeforeSettle) + + // allow double click to settle + jest.advanceTimersByTime(500) + + expect(events).toMatchObject([ + ...eventsBeforeSettle, + { name: 'triple_click', type: 'click', phase: 'settle' }, + ]) + + // clear events and click again + // the interaction should have reset + events.length = 0 + app.pointerDown().pointerUp().pointerDown() + expect(events).toMatchObject([ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', type: 'click', phase: 'down' }, + ]) + }) + + it('Emits quadruple click events', () => { + const events: any[] = [] + app.addListener('event', (info) => events.push(info)) + + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + + const eventsBeforeSettle = [ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', phase: 'down' }, + { name: 'pointer_up' }, + { name: 'double_click', phase: 'up' }, + { name: 'pointer_down' }, + { name: 'triple_click', phase: 'down' }, + { name: 'pointer_up' }, + { name: 'triple_click', phase: 'up' }, + { name: 'pointer_down' }, + { name: 'quadruple_click', phase: 'down' }, + { name: 'pointer_up' }, + { name: 'quadruple_click', phase: 'up' }, + ] + + expect(events).toMatchObject(eventsBeforeSettle) + + // allow double click to settle + jest.advanceTimersByTime(500) + + expect(events).toMatchObject([ + ...eventsBeforeSettle, + { name: 'quadruple_click', type: 'click', phase: 'settle' }, + ]) + + // clear events and click again + // the interaction should have reset + events.length = 0 + app.pointerDown().pointerUp().pointerDown() + expect(events).toMatchObject([ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', type: 'click', phase: 'down' }, + ]) + }) + + it('Emits overflow click events', () => { + const events: any[] = [] + app.addListener('event', (info) => events.push(info)) + + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + app.pointerDown() + app.pointerUp() + + const eventsBeforeSettle = [ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', type: 'click', phase: 'down' }, + { name: 'pointer_up' }, + { name: 'double_click', type: 'click', phase: 'up' }, + { name: 'pointer_down' }, + { name: 'triple_click', type: 'click', phase: 'down' }, + { name: 'pointer_up' }, + { name: 'triple_click', type: 'click', phase: 'up' }, + { name: 'pointer_down' }, + { name: 'quadruple_click', type: 'click', phase: 'down' }, + { name: 'pointer_up' }, + { name: 'quadruple_click', type: 'click', phase: 'up' }, + { name: 'pointer_down' }, + { name: 'pointer_up' }, + ] + + expect(events).toMatchObject(eventsBeforeSettle) + + // allow double click to settle + jest.advanceTimersByTime(500) + + expect(events).toMatchObject(eventsBeforeSettle) + + // clear events and click again + // the interaction should have reset + events.length = 0 + app.pointerDown().pointerUp().pointerDown() + expect(events).toMatchObject([ + { name: 'pointer_down' }, + { name: 'pointer_up' }, + { name: 'pointer_down' }, + { name: 'double_click', type: 'click', phase: 'down' }, + ]) + }) +}) + +it('Cancels when click moves', () => { + let event: any + app.addListener('event', (info) => (event = info)) + app.pointerDown(0, 0) + expect(event.name).toBe('pointer_down') + app.pointerUp(0, 0) + expect(event.name).toBe('pointer_up') + app.pointerDown(0, 20) + expect(event.name).toBe('double_click') + app.pointerUp(0, 20) + expect(event.name).toBe('double_click') + app.pointerDown(0, 45) + expect(event.name).toBe('triple_click') + app.pointerUp(0, 45) + expect(event.name).toBe('triple_click') + // has to be 40 away from previous click location + app.pointerDown(0, 86) + expect(event.name).toBe('pointer_down') + app.pointerUp(0, 86) + expect(event.name).toBe('pointer_up') +}) diff --git a/packages/editor/src/lib/app/managers/ClickManager.ts b/packages/editor/src/lib/app/managers/ClickManager.ts new file mode 100644 index 000000000..775bc56cf --- /dev/null +++ b/packages/editor/src/lib/app/managers/ClickManager.ts @@ -0,0 +1,229 @@ +import { Vec2d } from '@tldraw/primitives' +import { DOUBLE_CLICK_DURATION, DRAG_DISTANCE, MULTI_CLICK_DURATION } from '../../constants' +import { uniqueId } from '../../utils/data' +import type { App } from '../App' +import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types' + +type TLClickState = + | 'idle' + | 'pendingDouble' + | 'pendingTriple' + | 'pendingQuadruple' + | 'pendingOverflow' + | 'overflow' + +const MAX_CLICK_DISTANCE = 40 + +export class ClickManager { + constructor(public app: App) {} + + private _clickId = '' + + private _clickTimeout?: any + + private _clickScreenPoint?: Vec2d + + private _previousScreenPoint?: Vec2d + + private _getClickTimeout = (state: TLClickState, id = uniqueId()) => { + this._clickId = id + clearTimeout(this._clickTimeout) + this._clickTimeout = setTimeout( + () => { + if (this._clickState === state && this._clickId === id) { + switch (this._clickState) { + case 'pendingTriple': { + this.app.dispatch({ + ...this.lastPointerInfo, + type: 'click', + name: 'double_click', + phase: 'settle', + }) + break + } + case 'pendingQuadruple': { + this.app.dispatch({ + ...this.lastPointerInfo, + type: 'click', + name: 'triple_click', + phase: 'settle', + }) + break + } + case 'pendingOverflow': { + this.app.dispatch({ + ...this.lastPointerInfo, + type: 'click', + name: 'quadruple_click', + phase: 'settle', + }) + break + } + default: { + // noop + } + } + + this._clickState = 'idle' + } + }, + state === 'idle' || state === 'pendingDouble' ? DOUBLE_CLICK_DURATION : MULTI_CLICK_DURATION + ) + } + + /** + * The current click state. + * + * @internal + */ + private _clickState?: TLClickState = 'idle' + + /** + * The current click state. + * + * @public + */ + get clickState() { + return this._clickState + } + + lastPointerInfo = {} as TLPointerEventInfo + + /** + * Start the double click timeout. + * + * @param info - The event info. + */ + transformPointerDownEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => { + if (!this._clickState) return info + + this._clickScreenPoint = Vec2d.From(info.point) + + if ( + this._previousScreenPoint && + this._previousScreenPoint.dist(this._clickScreenPoint) > MAX_CLICK_DISTANCE + ) { + this._clickState = 'idle' + } + + this._previousScreenPoint = this._clickScreenPoint + + this.lastPointerInfo = info + + switch (this._clickState) { + case 'idle': { + this._clickState = 'pendingDouble' + this._clickTimeout = this._getClickTimeout(this._clickState) + return info // returns the pointer event + } + case 'pendingDouble': { + this._clickState = 'pendingTriple' + this._clickTimeout = this._getClickTimeout(this._clickState) + return { + ...info, + type: 'click', + name: 'double_click', + phase: 'down', + } + } + case 'pendingTriple': { + this._clickState = 'pendingQuadruple' + this._clickTimeout = this._getClickTimeout(this._clickState) + return { + ...info, + type: 'click', + name: 'triple_click', + phase: 'down', + } + } + case 'pendingQuadruple': { + this._clickState = 'pendingOverflow' + this._clickTimeout = this._getClickTimeout(this._clickState) + return { + ...info, + type: 'click', + name: 'quadruple_click', + phase: 'down', + } + } + case 'pendingOverflow': { + this._clickState = 'overflow' + this._clickTimeout = this._getClickTimeout(this._clickState) + return info + } + default: { + // overflow + this._clickTimeout = this._getClickTimeout(this._clickState) + return info + } + } + } + + /** + * Emit click_up events on pointer up. + * + * @param info - The event info. + */ + transformPointerUpEvent = (info: TLPointerEventInfo): TLPointerEventInfo | TLClickEventInfo => { + if (!this._clickState) return info + + this._clickScreenPoint = Vec2d.From(info.point) + + switch (this._clickState) { + case 'pendingTriple': { + return { + ...this.lastPointerInfo, + type: 'click', + name: 'double_click', + phase: 'up', + } + } + case 'pendingQuadruple': { + return { + ...this.lastPointerInfo, + type: 'click', + name: 'triple_click', + phase: 'up', + } + } + case 'pendingOverflow': { + return { + ...this.lastPointerInfo, + type: 'click', + name: 'quadruple_click', + phase: 'up', + } + } + default: { + // idle, pendingDouble, overflow + return info + } + } + } + + /** + * Cancel the double click timeout. + * + * @internal + */ + cancelDoubleClickTimeout = () => { + this._clickTimeout = clearTimeout(this._clickTimeout) + this._clickState = 'idle' + } + + /** + * Handle a move event, possibly cancelling the click timeout. + * + * @internal + */ + handleMove = () => { + // Cancel a double click event if the user has started dragging. + if ( + this._clickState !== 'idle' && + this._clickScreenPoint && + this._clickScreenPoint.dist(this.app.inputs.currentScreenPoint) > DRAG_DISTANCE + ) { + this.cancelDoubleClickTimeout() + } + } +} diff --git a/packages/editor/src/lib/app/managers/DprManager.ts b/packages/editor/src/lib/app/managers/DprManager.ts new file mode 100644 index 000000000..77e449219 --- /dev/null +++ b/packages/editor/src/lib/app/managers/DprManager.ts @@ -0,0 +1,45 @@ +import { atom } from 'signia' +import { App } from '../App' + +export class DprManager { + private _currentMM: MediaQueryList | undefined + + constructor(public app: App) { + this.rebind() + // Add this class's dispose method (cancel the listener) to the app's disposables + this.app.disposables.add(this.dispose) + } + + // Set a listener to update the dpr when the device pixel ratio changes + rebind() { + this.dispose() + this._currentMM = this.getMedia() + this._currentMM?.addEventListener('change', this.updateDevicePixelRatio) + } + + dpr = atom( + 'devicePixelRatio', + typeof window === 'undefined' ? 1 : window.devicePixelRatio + ) + + // Get the media query list for the device pixel ratio + getMedia() { + // NOTE: This ignore is only for the test environment. + /* @ts-ignore */ + if (window.matchMedia) { + return matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`) + } + } + + // Update the device pixel ratio atom + updateDevicePixelRatio = () => { + this.dpr.set(window.devicePixelRatio) + + this.rebind() + } + + // Clear the listener + dispose = () => { + this._currentMM?.removeEventListener('change', this.updateDevicePixelRatio) + } +} diff --git a/packages/editor/src/lib/app/managers/DragAndDropManager.ts b/packages/editor/src/lib/app/managers/DragAndDropManager.ts new file mode 100644 index 000000000..6fcf85949 --- /dev/null +++ b/packages/editor/src/lib/app/managers/DragAndDropManager.ts @@ -0,0 +1,113 @@ +import { TLShape, TLShapeId } from '@tldraw/tlschema' +import { compact } from '@tldraw/utils' +import type { App } from '../App' + +const LAG_DURATION = 100 + +export class DragAndDropManager { + constructor(public app: App) { + app.disposables.add(this.dispose) + } + + prevDroppingShapeId: TLShapeId | null = null + currDroppingShapeId: TLShapeId | null = null + + droppingNodeTimer: ReturnType | null = null + + updateDroppingNode(movingShapes: TLShape[], cb: () => void) { + if (this.droppingNodeTimer === null) { + const { currentPagePoint } = this.app.inputs + this.currDroppingShapeId = + this.app.getDroppingShape(currentPagePoint, movingShapes)?.id ?? null + this.setDragTimer(movingShapes, LAG_DURATION * 10, cb) + } else if (this.app.inputs.pointerVelocity.len() > 0.5) { + clearInterval(this.droppingNodeTimer) + this.setDragTimer(movingShapes, LAG_DURATION, cb) + } + } + + private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) { + this.droppingNodeTimer = setTimeout(() => { + this.app.batch(() => { + this.handleDrag(movingShapes, cb) + }) + this.droppingNodeTimer = null + }, duration) + } + + private handleDrag(movingShapes: TLShape[], cb?: () => void) { + const { currentPagePoint } = this.app.inputs + + movingShapes = compact(movingShapes.map((shape) => this.app.getShapeById(shape.id))) + + const currDroppingShapeId = + this.app.getDroppingShape(currentPagePoint, movingShapes)?.id ?? null + + if (currDroppingShapeId !== this.currDroppingShapeId) { + this.prevDroppingShapeId = this.currDroppingShapeId + this.currDroppingShapeId = currDroppingShapeId + } + + const { prevDroppingShapeId } = this + + if (currDroppingShapeId === prevDroppingShapeId) { + // we already called onDragShapesOver on this node, no need to do it again + return + } + + const prevDroppingShape = prevDroppingShapeId && this.app.getShapeById(prevDroppingShapeId) + const nextDroppingShape = currDroppingShapeId && this.app.getShapeById(currDroppingShapeId) + + // Even if we don't have a next dropping shape id (i.e. if we're dropping + // onto the page) set the prev to the current, to avoid repeat calls to + // the previous parent's onDragShapesOut + this.prevDroppingShapeId = this.currDroppingShapeId + + if (prevDroppingShape) { + this.app.getShapeUtil(prevDroppingShape).onDragShapesOut?.(prevDroppingShape, movingShapes) + } + + if (nextDroppingShape) { + const res = this.app + .getShapeUtil(nextDroppingShape) + .onDragShapesOver?.(nextDroppingShape, movingShapes) + + if (res && res.shouldHint) { + this.app.setHintingIds([nextDroppingShape.id]) + } + } else { + // If we're dropping onto the page, then clear hinting ids + this.app.setHintingIds([]) + } + + cb?.() + } + + dropShapes(shapes: TLShape[]) { + const { currDroppingShapeId } = this + + this.handleDrag(shapes) + + if (currDroppingShapeId) { + const shape = this.app.getShapeById(currDroppingShapeId) + if (!shape) return + this.app.getShapeUtil(shape).onDropShapesOver?.(shape, shapes) + } + } + + clear() { + this.prevDroppingShapeId = null + this.currDroppingShapeId = null + + if (this.droppingNodeTimer !== null) { + clearInterval(this.droppingNodeTimer) + } + + this.droppingNodeTimer = null + this.app.setHintingIds([]) + } + + dispose = () => { + this.clear() + } +} diff --git a/packages/editor/src/lib/app/managers/HistoryManager.test.ts b/packages/editor/src/lib/app/managers/HistoryManager.test.ts new file mode 100644 index 000000000..d9a89169e --- /dev/null +++ b/packages/editor/src/lib/app/managers/HistoryManager.test.ts @@ -0,0 +1,292 @@ +import { HistoryManager } from './HistoryManager' +import { stack } from './Stack' + +function createCounterHistoryManager() { + const manager = new HistoryManager( + { emit: () => void null }, + () => null, + () => { + return + } + ) + const state = { + count: 0, + name: 'David', + age: 35, + } + const increment = manager.createCommand( + 'increment', + (n = 1, squashing = false) => ({ + data: { n }, + squashing, + }), + { + do: ({ n }) => { + state.count += n + }, + undo: ({ n }) => { + state.count -= n + }, + squash: ({ n: n1 }, { n: n2 }) => ({ n: n1 + n2 }), + } + ) + + const decrement = manager.createCommand( + 'decrement', + (n = 1, squashing = false) => ({ + data: { n }, + squashing, + }), + { + do: ({ n }) => { + state.count -= n + }, + undo: ({ n }) => { + state.count += n + }, + squash: ({ n: n1 }, { n: n2 }) => ({ n: n1 + n2 }), + } + ) + + const setName = manager.createCommand( + 'setName', + (name = 'David') => ({ + data: { name, prev: state.name }, + ephemeral: true, + }), + { + do: ({ name }) => { + state.name = name + }, + undo: ({ prev }) => { + state.name = prev + }, + } + ) + + const setAge = manager.createCommand( + 'setAge', + (age = 35) => ({ + data: { age, prev: state.age }, + preservesRedoStack: true, + }), + { + do: ({ age }) => { + state.age = age + }, + undo: ({ prev }) => { + state.age = prev + }, + } + ) + + const incrementTwice = manager.createCommand('incrementTwice', () => ({ data: {} }), { + do: () => { + increment() + increment() + }, + undo: () => { + decrement() + decrement() + }, + }) + + return { + increment, + incrementTwice, + decrement, + setName, + setAge, + history: manager, + getCount: () => state.count, + getName: () => state.name, + getAge: () => state.age, + } +} + +describe(HistoryManager, () => { + let app = createCounterHistoryManager() + beforeEach(() => { + app = createCounterHistoryManager() + }) + it('creates a serializable undo stack', () => { + expect(app.getCount()).toBe(0) + app.increment() + app.increment() + app.history.mark('stop at 2') + app.increment() + app.increment() + app.decrement() + expect(app.getCount()).toBe(3) + + const undos = [...app.history._undos.value] + const parsedUndos = JSON.parse(JSON.stringify(undos)) + app.history._undos.set(stack(parsedUndos)) + + app.history.undo() + + expect(app.getCount()).toBe(2) + }) + + it('allows undoing and redoing', () => { + expect(app.getCount()).toBe(0) + app.increment() + app.history.mark('stop at 1') + app.increment() + app.history.mark('stop at 2') + app.increment() + app.increment() + app.history.mark('stop at 4') + app.increment() + app.increment() + app.increment() + expect(app.getCount()).toBe(7) + + app.history.undo() + expect(app.getCount()).toBe(4) + app.history.undo() + expect(app.getCount()).toBe(2) + app.history.undo() + expect(app.getCount()).toBe(1) + app.history.undo() + expect(app.getCount()).toBe(0) + app.history.undo() + app.history.undo() + app.history.undo() + expect(app.getCount()).toBe(0) + + app.history.redo() + expect(app.getCount()).toBe(1) + app.history.redo() + expect(app.getCount()).toBe(2) + app.history.redo() + expect(app.getCount()).toBe(4) + app.history.redo() + expect(app.getCount()).toBe(7) + }) + + it('clears the redo stack if you execute commands, but not if you mark stopping points', () => { + expect(app.getCount()).toBe(0) + app.increment() + app.history.mark('stop at 1') + app.increment() + app.history.mark('stop at 2') + app.increment() + app.increment() + app.history.mark('stop at 4') + app.increment() + app.increment() + app.increment() + expect(app.getCount()).toBe(7) + app.history.undo() + app.history.undo() + expect(app.getCount()).toBe(2) + app.history.mark('wayward stopping point') + app.history.redo() + app.history.redo() + expect(app.getCount()).toBe(7) + + app.history.undo() + app.history.undo() + expect(app.getCount()).toBe(2) + app.increment() + expect(app.getCount()).toBe(3) + app.history.redo() + expect(app.getCount()).toBe(3) + app.history.redo() + expect(app.getCount()).toBe(3) + }) + + it('allows squashing of commands', () => { + app.increment() + + app.history.mark('stop at 1') + expect(app.getCount()).toBe(1) + + app.increment(1, true) + app.increment(1, true) + app.increment(1, true) + app.increment(1, true) + + expect(app.getCount()).toBe(5) + + expect(app.history.numUndos).toBe(3) + }) + + it('allows ephemeral commands that do not affect the stack', () => { + app.increment() + app.history.mark('stop at 1') + app.increment() + app.setName('wilbur') + app.increment() + expect(app.getCount()).toBe(3) + expect(app.getName()).toBe('wilbur') + app.history.undo() + expect(app.getCount()).toBe(1) + expect(app.getName()).toBe('wilbur') + }) + + it('allows inconsequential commands that do not clear the redo stack', () => { + app.increment() + app.history.mark('stop at 1') + app.increment() + expect(app.getCount()).toBe(2) + app.history.undo() + expect(app.getCount()).toBe(1) + app.history.mark('stop at age 35') + app.setAge(23) + app.history.mark('stop at age 23') + expect(app.getCount()).toBe(1) + app.history.redo() + expect(app.getCount()).toBe(2) + expect(app.getAge()).toBe(23) + app.history.undo() + expect(app.getCount()).toBe(1) + expect(app.getAge()).toBe(23) + app.history.undo() + expect(app.getCount()).toBe(1) + expect(app.getAge()).toBe(35) + app.history.undo() + expect(app.getCount()).toBe(0) + expect(app.getAge()).toBe(35) + }) + + it('does not allow new history entries to be pushed if a command invokes them while doing or undoing', () => { + app.incrementTwice() + expect(app.history.numUndos).toBe(1) + expect(app.getCount()).toBe(2) + app.history.undo() + expect(app.getCount()).toBe(0) + expect(app.history.numUndos).toBe(0) + }) + + it('does not allow new history entries to be pushed if a command invokes them while bailing', () => { + app.history.mark('0') + app.incrementTwice() + app.history.mark('2') + app.incrementTwice() + app.incrementTwice() + expect(app.history.numUndos).toBe(5) + expect(app.getCount()).toBe(6) + app.history.bail() + expect(app.getCount()).toBe(2) + expect(app.history.numUndos).toBe(2) + app.history.bailToMark('0') + expect(app.history.numUndos).toBe(0) + expect(app.getCount()).toBe(0) + }) + + it('supports bailing to a particular mark', () => { + app.increment() + app.history.mark('1') + app.increment() + app.history.mark('2') + app.increment() + app.history.mark('3') + app.increment() + + expect(app.getCount()).toBe(4) + app.history.bailToMark('2') + expect(app.getCount()).toBe(2) + }) +}) diff --git a/packages/editor/src/lib/app/managers/HistoryManager.ts b/packages/editor/src/lib/app/managers/HistoryManager.ts new file mode 100644 index 000000000..47a683f70 --- /dev/null +++ b/packages/editor/src/lib/app/managers/HistoryManager.ts @@ -0,0 +1,294 @@ +import { devFreeze } from '@tldraw/tlstore' +import { atom, transact } from 'signia' +import { uniqueId } from '../../utils/data' +import { TLCommandHandler, TLHistoryEntry } from '../types/history-types' +import { Stack, stack } from './Stack' + +type CommandFn = (...args: any[]) => + | { + data: Data + squashing?: boolean + ephemeral?: boolean + preservesRedoStack?: boolean + } + | null + | undefined + | void + +type ExtractData = Fn extends CommandFn ? Data : never +type ExtractArgs = Parameters any>> + +export class HistoryManager void }> { + _undos = atom>('HistoryManager.undos', stack()) // Updated by each action that includes and undo + _redos = atom>('HistoryManager.redos', stack()) // Updated when a user undoes + _batchDepth = 0 // A flag for whether the user is in a batch operation + + constructor( + private readonly ctx: CTX, + private readonly onBatchComplete: () => void, + private readonly annotateError: (error: unknown) => void + ) {} + + private _commands: Record> = {} + + get numUndos() { + return this._undos.value.length + } + + get numRedos() { + return this._redos.value.length + } + + createCommand = >( + name: Name, + constructor: Constructor, + handle: TLCommandHandler> + ) => { + if (this._commands[name]) { + throw new Error(`Duplicate command: ${name}`) + } + this._commands[name] = handle + + const exec = (...args: ExtractArgs) => { + if (!this._batchDepth) { + // If we're not batching, run again in a batch + this.batch(() => exec(...args)) + return this.ctx + } + + const result = constructor(...args) + + if (!result) { + return this.ctx + } + + const { data, ephemeral, squashing, preservesRedoStack } = result + + this.ignoringUpdates((undos, redos) => { + handle.do(data) + return { undos, redos } + }) + + if (!ephemeral) { + const prev = this._undos.value.head + if ( + squashing && + prev && + prev.type === 'command' && + prev.name === name && + prev.preservesRedoStack === preservesRedoStack + ) { + // replace the last command with a squashed version + this._undos.update((undos) => + undos.tail.push({ + ...prev, + id: uniqueId(), + data: devFreeze(handle.squash!(prev.data, data)), + }) + ) + } else { + // add to the undo stack + this._undos.update((undos) => + undos.push({ + type: 'command', + name, + data: devFreeze(data), + id: uniqueId(), + preservesRedoStack: preservesRedoStack, + }) + ) + } + + if (!result.preservesRedoStack) { + this._redos.set(stack()) + } + + this.ctx.emit('change-history') + } + + return this.ctx + } + + return exec + } + + batch = (fn: () => void) => { + try { + this._batchDepth++ + if (this._batchDepth === 1) { + transact(() => { + const mostRecentActionId = this._undos.value.head?.id + fn() + if (mostRecentActionId !== this._undos.value.head?.id) { + this.onBatchComplete() + } + }) + } else { + fn() + } + } catch (error) { + this.annotateError(error) + throw error + } finally { + this._batchDepth-- + } + + return this + } + + private ignoringUpdates = ( + fn: ( + undos: Stack, + redos: Stack + ) => { undos: Stack; redos: Stack } + ) => { + let undos = this._undos.value + let redos = this._redos.value + + this._undos.set(stack()) + this._redos.set(stack()) + try { + ;({ undos, redos } = transact(() => fn(undos, redos))) + } finally { + this._undos.set(undos) + this._redos.set(redos) + } + } + + // History + private _undo = ({ + pushToRedoStack, + toMark = undefined, + }: { + pushToRedoStack: boolean + toMark?: string + }) => { + this.ignoringUpdates((undos, redos) => { + if (undos.length === 0) { + this.ctx.emit('change-history') + return { undos, redos } + } + + while (undos.head?.type === 'STOP') { + const mark = undos.head + undos = undos.tail + if (pushToRedoStack) { + redos = redos.push(mark) + } + if (mark.id === toMark) { + this.ctx.emit('change-history') + return { undos, redos } + } + } + + if (undos.length === 0) { + this.ctx.emit('change-history') + return { undos, redos } + } + + while (undos.head) { + const command = undos.head + undos = undos.tail + + if (pushToRedoStack) { + redos = redos.push(command) + } + + if (command.type === 'STOP') { + if (command.onUndo && (!toMark || command.id === toMark)) { + this.ctx.emit('change-history') + return { undos, redos } + } + } else { + const handler = this._commands[command.name] + handler.undo(command.data) + } + } + + this.ctx.emit('change-history') + return { undos, redos } + }) + + return this + } + + undo = () => { + this._undo({ pushToRedoStack: true }) + + return this + } + + redo = () => { + this.ignoringUpdates((undos, redos) => { + if (redos.length === 0) { + this.ctx.emit('change-history') + return { undos, redos } + } + + while (redos.head?.type === 'STOP') { + undos = undos.push(redos.head) + redos = redos.tail + } + + if (redos.length === 0) { + this.ctx.emit('change-history') + return { undos, redos } + } + + while (redos.head) { + const command = redos.head + undos = undos.push(redos.head) + redos = redos.tail + + if (command.type === 'STOP') { + if (command.onRedo) { + break + } + } else { + const handler = this._commands[command.name] + if (handler.redo) { + handler.redo(command.data) + } else { + handler.do(command.data) + } + } + } + + this.ctx.emit('change-history') + return { undos, redos } + }) + + return this + } + + bail = () => { + this._undo({ pushToRedoStack: false }) + + return this + } + + bailToMark = (id: string) => { + this._undo({ pushToRedoStack: false, toMark: id }) + + return this + } + + mark = (id = uniqueId(), onUndo = true, onRedo = true) => { + const mostRecent = this._undos.value.head + // dedupe marks, why not + if (mostRecent && mostRecent.type === 'STOP') { + if (mostRecent.id === id && mostRecent.onUndo === onUndo && mostRecent.onRedo === onRedo) { + return mostRecent.id + } + } + + this._undos.update((undos) => undos.push({ type: 'STOP', id, onUndo, onRedo })) + + return id + } + + clear() { + this._undos.set(stack()) + this._redos.set(stack()) + } +} diff --git a/packages/editor/src/lib/app/managers/ScribbleManager.ts b/packages/editor/src/lib/app/managers/ScribbleManager.ts new file mode 100644 index 000000000..458d134db --- /dev/null +++ b/packages/editor/src/lib/app/managers/ScribbleManager.ts @@ -0,0 +1,149 @@ +import { Vec2d, VecLike } from '@tldraw/primitives' +import { TLScribble, Vec2dModel } from '@tldraw/tlschema' +import { TLTickEvent } from '../types/event-types' + +export class ScribbleManager implements TLScribble { + // Scribble properties + state + points + size + color + opacity + + // Callbacks + private onUpdate: (scribble: TLScribble) => void + private onComplete: () => void + + // Internal state + private prev: VecLike | null = null + private next: VecLike | null = null + + constructor(opts: { + onUpdate: (scribble: TLScribble) => void + onComplete: () => void + size?: TLScribble['size'] + color?: TLScribble['color'] + opacity?: TLScribble['opacity'] + }) { + const { size = 20, color = 'accent', opacity = 0.8, onComplete, onUpdate } = opts + + this.onUpdate = onUpdate + this.onComplete = onComplete + this.size = size + this.color = color + this.opacity = opacity + this.points = [] as Vec2dModel[] + this.state = 'starting' as TLScribble['state'] + + this.prev = null + this.next = null + + this.resume() + } + + resume = () => { + this.state = 'active' + } + + pause = () => { + this.state = 'starting' + } + + /** + * Start stopping the scribble. The scribble won't be removed until its last point is cleared. + * + * @public + */ + stop = () => { + this.state = 'stopping' + } + + /** + * Set the scribble's next point. + * + * @param point - The point to add. + * @public + */ + addPoint = (x: number, y: number) => { + const { prev } = this + const point = { x, y, z: 0.5 } + if (prev && Vec2d.Dist(prev, point) < 1) return + this.next = point + } + + /** + * Get the current TLScribble object from the scribble manager. + * + * @public + */ + getScribble(): TLScribble { + return { + state: this.state, + size: this.size, + color: this.color, + opacity: this.opacity, + points: [...this.points], + } + } + + private updateScribble() { + this.onUpdate(this.getScribble()) + } + + timeoutMs = 0 + + tick: TLTickEvent = (elapsed) => { + this.timeoutMs += elapsed + if (this.timeoutMs >= 16) { + this.timeoutMs = 0 + } + + const { timeoutMs, state, prev, next, points } = this + + switch (state) { + case 'active': { + if (next && next !== prev) { + this.prev = next + points.push(next) + + if (points.length > 8) { + points.shift() + } + + this.updateScribble() + } else { + // While not moving, shrink the scribble from the start + if (timeoutMs === 0 && points.length > 1) { + points.shift() + this.updateScribble() + } + } + break + } + case 'stopping': { + if (timeoutMs === 0) { + // If the scribble is down to one point, we're done! + if (points.length === 1) { + this.state = 'paused' + this.onComplete() + return + } + + // Drop the scribble's size + this.size *= 0.9 + + // Drop the scribble's first point (its tail) + points.shift() + + // otherwise, update the scribble + this.updateScribble() + } + break + } + case 'paused': { + // Nothing to do while paused. + break + } + } + } +} diff --git a/packages/editor/src/lib/app/managers/SnapManager.ts b/packages/editor/src/lib/app/managers/SnapManager.ts new file mode 100644 index 000000000..4b18acd36 --- /dev/null +++ b/packages/editor/src/lib/app/managers/SnapManager.ts @@ -0,0 +1,1460 @@ +import { + Box2d, + flipSelectionHandleX, + flipSelectionHandleY, + isSelectionCorner, + Matrix2d, + rangeIntersection, + rangesOverlap, + SelectionCorner, + SelectionEdge, + Vec2d, + VecLike, +} from '@tldraw/primitives' +import { TLLineShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw/tlschema' +import { compact, dedupe, deepCopy } from '@tldraw/utils' +import { atom, computed, EMPTY_ARRAY } from 'signia' +import { uniqueId } from '../../utils/data' +import { sortByIndex } from '../../utils/reordering/reordering' +import type { App } from '../App' +import { getSplineForLineShape, TLLineShapeDef } from '../shapeutils/TLLineUtil/TLLineUtil' + +export type PointsSnapLine = { + id: string + type: 'points' + points: VecLike[] +} +export type GapsSnapLine = { + id: string + type: 'gaps' + direction: 'horizontal' | 'vertical' + gaps: Array<{ + startEdge: [VecLike, VecLike] + endEdge: [VecLike, VecLike] + }> +} +export type SnapLine = PointsSnapLine | GapsSnapLine + +export type SnapInteractionType = + | { + type: 'translate' + lockedAxis: 'x' | 'y' | null + initialSelectionSnapPoints: Vec2d[] + } + | { + type: 'resize' + } + +export interface SnapPoint { + id: string + x: number + y: number + handle?: SelectionCorner +} + +type SnapPair = { thisPoint: SnapPoint; otherPoint: SnapPoint } + +type NearestPointsSnap = { + // selection snaps to a nearby snap point + type: 'points' + points: SnapPair + nudge: number +} + +type NearestSnap = + | NearestPointsSnap + | { + // selection snaps to the center of a gap + type: 'gap_center' + gap: Gap + nudge: number + } + | { + // selection snaps to create a new gap of equal size to another gap + // on the opposide side of some shape + type: 'gap_duplicate' + gap: Gap + protrusionDirection: 'left' | 'right' | 'top' | 'bottom' + nudge: number + } + +type GapNode = { + id: TLShapeId + pageBounds: Box2d +} +type Gap = { + // e.g. + // start + // edge │ breadth + // │ intersection + // ▼ [40,100] end + // │ │ edge + // ┌───────────┐ │ 100,0 │ │ + // │ │ │ ▼ ▼ + // │ │ │ + // │ start │ │ │ 200,40 │ ┌───────────┐ + // │ node │ │ │ │ │ │ + // │ │ ├────────────┼────────────┤ │ end │ + // │ │ │ │ │ │ node │ + // └───────────┘ │ 100,100 │ │ │ │ + // │ │ │ + // 200,120 │ └───────────┘ + // + // length 100 + // ◄─────────────────────────► + startNode: GapNode + endNode: GapNode + startEdge: [Vec2d, Vec2d] + endEdge: [Vec2d, Vec2d] + length: number + breadthIntersection: [number, number] +} + +interface SnapData { + nudge: Vec2d +} + +const round = (x: number) => { + // round numbers to avoid glitches for floating point rounding errors + const decimalPlacesTolerance = 8 + return Math.round(x * 10 ** decimalPlacesTolerance) / 10 ** decimalPlacesTolerance +} + +function findAdjacentGaps( + gaps: Gap[], + shapeId: TLShapeId, + gapLength: number, + direction: 'forward' | 'backward', + intersection: [number, number] +): Gap[] { + // TODO: take advantage of the fact that gaps is sorted by starting position? + const matches = gaps.filter( + (gap) => + (direction === 'forward' ? gap.startNode.id === shapeId : gap.endNode.id === shapeId) && + round(gap.length) === round(gapLength) && + rangeIntersection( + gap.breadthIntersection[0], + gap.breadthIntersection[1], + intersection[0], + intersection[1] + ) + ) + + if (matches.length === 0) return [] + + const nextNodes = new Set() + + for (const match of matches) { + const node = direction === 'forward' ? match.endNode.id : match.startNode.id + if (!nextNodes.has(node)) { + nextNodes.add(node) + matches.push( + ...findAdjacentGaps( + gaps, + node, + gapLength, + direction, + rangeIntersection( + match.breadthIntersection[0], + match.breadthIntersection[1], + intersection[0], + intersection[1] + )! + ) + ) + } + } + + return matches +} + +function dedupeGapSnaps(snaps: Array>) { + // sort by descending order of number of gaps + snaps.sort((a, b) => b.gaps.length - a.gaps.length) + // pop off any that are included already + for (let i = snaps.length - 1; i > 0; i--) { + const snap = snaps[i] + for (let j = i - 1; j >= 0; j--) { + const otherSnap = snaps[j] + // if every edge in this snap is included in the other snap somewhere, then it's redundant + if ( + otherSnap.direction === snap.direction && + snap.gaps.every( + (gap) => + otherSnap.gaps.some( + (otherGap) => + round(gap.startEdge[0].x) === round(otherGap.startEdge[0].x) && + round(gap.startEdge[0].y) === round(otherGap.startEdge[0].y) && + round(gap.startEdge[1].x) === round(otherGap.startEdge[1].x) && + round(gap.startEdge[1].y) === round(otherGap.startEdge[1].y) + ) && + otherSnap.gaps.some( + (otherGap) => + round(gap.endEdge[0].x) === round(otherGap.endEdge[0].x) && + round(gap.endEdge[0].y) === round(otherGap.endEdge[0].y) && + round(gap.endEdge[1].x) === round(otherGap.endEdge[1].x) && + round(gap.endEdge[1].y) === round(otherGap.endEdge[1].y) + ) + ) + ) { + snaps.splice(i, 1) + break + } + } + } +} + +export class SnapManager { + private _snapLines = atom('snapLines', undefined) + + get lines() { + return this._snapLines.value ?? (EMPTY_ARRAY as SnapLine[]) + } + + clear() { + if (this.lines.length) { + this._snapLines.set(undefined) + } + } + + setLines(lines: SnapLine[]) { + this._snapLines.set(lines) + } + + constructor(public readonly app: App) {} + + @computed get snapPointsCache() { + return this.app.store.createComputedCache('snapPoints', (shape) => { + const pageTransfrorm = this.app.getPageTransformById(shape.id) + if (!pageTransfrorm) return undefined + const util = this.app.getShapeUtil(shape) + const snapPoints = util.snapPoints(shape) + return snapPoints.map((point, i) => { + const { x, y } = Matrix2d.applyToPoint(pageTransfrorm, point) + return { x, y, id: `${shape.id}:${i}` } + }) + }) + } + + get snapThreshold() { + return 8 / this.app.zoomLevel + } + + // TODO: make this an incremental derivation + @computed get visibleShapesNotInSelection() { + const selectedIds = this.app.selectedIds + + const result: Set<{ id: TLShapeId; pageBounds: Box2d }> = new Set() + + const processParent = (parentId: TLParentId) => { + const children = this.app.getSortedChildIds(parentId) + for (const id of children) { + const shape = this.app.getShapeById(id) as TLShape + if (!shape) continue + if (shape.type === 'arrow') continue + if (selectedIds.includes(id)) continue + if (!this.app.isShapeInViewport(shape.id)) continue + + if (shape.type === 'group') { + // snap to children of group but not group itself + processParent(id) + continue + } + + result.add({ id: shape.id, pageBounds: this.app.getPageBoundsById(shape.id)! }) + + // don't snap to children of frame + if (shape.type !== 'frame') { + processParent(id) + } + } + } + + const commonFrameAncestor = this.app.findCommonAncestor( + compact(selectedIds.map((id) => this.app.getShapeById(id))), + (parent) => parent.type === 'frame' + ) + + processParent(commonFrameAncestor ?? this.app.currentPageId) + + return result + } + + @computed get visibleSnapPointsNotInSelection() { + const result: SnapPoint[] = [] + for (const shape of this.visibleShapesNotInSelection) { + const snapPoints = this.snapPointsCache.get(shape.id) + if (snapPoints) { + result.push(...snapPoints) + } + } + return result + } + + @computed get visibleGaps(): { horizontal: Gap[]; vertical: Gap[] } { + const horizontal: Gap[] = [] + const vertical: Gap[] = [] + + const sortedShapesHorizontal = [...this.visibleShapesNotInSelection].sort((a, b) => { + return a.pageBounds.minX - b.pageBounds.minX + }) + + for (let i = 0; i < sortedShapesHorizontal.length; i++) { + const startNode = sortedShapesHorizontal[i] + for (let j = i + 1; j < sortedShapesHorizontal.length; j++) { + const endNode = sortedShapesHorizontal[j] + + if ( + // is there space between the boxes + startNode.pageBounds.maxX < endNode.pageBounds.minX && + // and they overlap in the y axis + rangesOverlap( + startNode.pageBounds.minY, + startNode.pageBounds.maxY, + endNode.pageBounds.minY, + endNode.pageBounds.maxY + ) + ) { + horizontal.push({ + startNode, + endNode, + startEdge: [ + new Vec2d(startNode.pageBounds.maxX, startNode.pageBounds.minY), + new Vec2d(startNode.pageBounds.maxX, startNode.pageBounds.maxY), + ], + endEdge: [ + new Vec2d(endNode.pageBounds.minX, endNode.pageBounds.minY), + new Vec2d(endNode.pageBounds.minX, endNode.pageBounds.maxY), + ], + length: endNode.pageBounds.minX - startNode.pageBounds.maxX, + breadthIntersection: rangeIntersection( + startNode.pageBounds.minY, + startNode.pageBounds.maxY, + endNode.pageBounds.minY, + endNode.pageBounds.maxY + )!, + }) + } + } + } + + const sortedShapesVertical = sortedShapesHorizontal.slice(0).sort((a, b) => { + return a.pageBounds.minY - b.pageBounds.minY + }) + + for (let i = 0; i < sortedShapesVertical.length; i++) { + const startNode = sortedShapesVertical[i] + for (let j = i + 1; j < sortedShapesVertical.length; j++) { + const endNode = sortedShapesVertical[j] + + if ( + // is there space between the boxes + startNode.pageBounds.maxY < endNode.pageBounds.minY && + // do they overlap in the x axis + rangesOverlap( + startNode.pageBounds.minX, + startNode.pageBounds.maxX, + endNode.pageBounds.minX, + endNode.pageBounds.maxX + ) + ) { + vertical.push({ + startNode, + endNode, + startEdge: [ + new Vec2d(startNode.pageBounds.minX, startNode.pageBounds.maxY), + new Vec2d(startNode.pageBounds.maxX, startNode.pageBounds.maxY), + ], + endEdge: [ + new Vec2d(endNode.pageBounds.minX, endNode.pageBounds.minY), + new Vec2d(endNode.pageBounds.maxX, endNode.pageBounds.minY), + ], + length: endNode.pageBounds.minY - startNode.pageBounds.maxY, + breadthIntersection: rangeIntersection( + startNode.pageBounds.minX, + startNode.pageBounds.maxX, + endNode.pageBounds.minX, + endNode.pageBounds.maxX + )!, + }) + } + } + } + + return { horizontal, vertical } + } + + snapTranslate({ + lockedAxis, + initialSelectionPageBounds, + initialSelectionSnapPoints, + dragDelta, + }: { + lockedAxis: 'x' | 'y' | null + initialSelectionSnapPoints: SnapPoint[] + initialSelectionPageBounds: Box2d + dragDelta: Vec2d + }): SnapData { + const isXLocked = lockedAxis === 'x' + const isYLocked = lockedAxis === 'y' + + const selectionPageBounds = initialSelectionPageBounds.clone().translate(dragDelta) + const selectionSnapPoints: SnapPoint[] = initialSelectionSnapPoints.map(({ x, y }, i) => ({ + id: 'selection:' + i, + x: x + dragDelta.x, + y: y + dragDelta.y, + })) + + const otherNodeSnapPoints = this.visibleSnapPointsNotInSelection + + const nearestSnapsX: NearestSnap[] = [] + const nearestSnapsY: NearestSnap[] = [] + const minOffset = new Vec2d(this.snapThreshold, this.snapThreshold) + + this.findPointSnaps({ + minOffset, + nearestSnapsX, + nearestSnapsY, + otherNodeSnapPoints, + selectionSnapPoints, + }) + + this.findGapSnaps({ selectionPageBounds, nearestSnapsX, nearestSnapsY, minOffset }) + + // at the same time, calculate how far we need to nudge the shape to 'snap' to the target point(s) + const nudge = new Vec2d( + isXLocked ? 0 : nearestSnapsX[0]?.nudge ?? 0, + isYLocked ? 0 : nearestSnapsY[0]?.nudge ?? 0 + ) + + // ok we've figured out how much the box should be nudged, now let's find all the snap points + // that are exact after making that translation, so we can render all of them. + // first reset everything and adjust the original shapes to conform to the nudge + minOffset.x = 0 + minOffset.y = 0 + nearestSnapsX.length = 0 + nearestSnapsY.length = 0 + selectionSnapPoints.forEach((s) => { + s.x += nudge.x + s.y += nudge.y + }) + selectionPageBounds.translate(nudge) + + this.findPointSnaps({ + minOffset, + nearestSnapsX, + nearestSnapsY, + otherNodeSnapPoints, + selectionSnapPoints, + }) + + this.findGapSnaps({ + selectionPageBounds, + nearestSnapsX, + nearestSnapsY, + minOffset, + }) + + const pointSnaps = this.getPointSnapLines({ + nearestSnapsX, + nearestSnapsY, + }) + + const gapSnaps = this.getGapSnapLines({ + selectionPageBounds, + nearestSnapsX, + nearestSnapsY, + }) + + this._snapLines.set([...gapSnaps, ...pointSnaps]) + + return { nudge } + } + + // for a handle of a line: + // - find the nearest snap point + // - return the nudge vector to snap to that point + // note: this happens within page space + snapLineHandleTranslate({ + lineId, + handleId, + handlePoint, + }: { + lineId: TLShapeId + handleId: string + handlePoint: Vec2d + }): SnapData { + const line = this.app.getShapeById(lineId) + if (!line) { + return { nudge: new Vec2d(0, 0) } + } + + // We want the line to be able to snap to itself! + // but we don't want it to snap to the current segment we're drawing + // so let's get the splines of all segments except the current one + // and then pass them to the snap function as 'additionalOutlines' + + // First, let's find which handle we're dragging + const util = this.app.getShapeUtilByDef(TLLineShapeDef) + const handles = util.handles(line).sort(sortByIndex) + if (handles.length < 3) return { nudge: new Vec2d(0, 0) } + + const handleNumber = handles.findIndex((h) => h.id === handleId) + const handle = handles[handleNumber] + + // Now, let's figure out which segment this handle is on + // So... there are two types of handles: + // - vertex + // - create + + // And this is how the handles of a line are arranged: + // vertex --- create --- vertex -- create -- vertex + + // And we number them like this: + // v --- c --- v --- c --- v + // 0 --- 1 --- 2 --- 3 --- 4 + + // We want to get the segments made by connecting the vertex handles: + // v --- c --- v --- c --- v + // 0 --- 1 --- 2 --- 3 --- 4 + // |-----------|-----------| + // | segment 0 | segment 1 | + // |-----------|-----------| + + // If we're dragging a vertex handle, we can get its segment number by dividing its handle number by 2 + // If we're dragging a create handle, we can get its segment number by adding 1 to its handle number, then dividing by 2 + const segmentNumber = handle.type === 'vertex' ? handleNumber / 2 : (handleNumber + 1) / 2 + + // Then, get the splines of all segments except the current one + // (and by the way - we want to get the splines in page space, not shape space) + const spline = getSplineForLineShape(line) + const ignoreCount = 1 + const pageTransform = this.app.getPageTransform(line)! + + const pageHeadSegments = spline.segments + .slice(0, Math.max(0, segmentNumber - ignoreCount)) + .map((s) => Matrix2d.applyToPoints(pageTransform, s.lut)) + + const pageTailSegments = spline.segments + .slice(segmentNumber + ignoreCount) + .map((s) => Matrix2d.applyToPoints(pageTransform, s.lut)) + + return this.snapHandleTranslate({ + handlePoint: handlePoint, + additionalOutlines: [...pageHeadSegments, ...pageTailSegments], + }) + } + + // for a handle: + // - find the nearest snap point from all non-selected shapes + // - return the nudge vector to snap to that point + // note: this happens within page space + snapHandleTranslate({ + handlePoint, + additionalOutlines = [], + }: { + handlePoint: Vec2d + additionalOutlines?: Vec2dModel[][] + }): SnapData { + // Get the (page-space) outlines of the shapes that are not in the selection + const visibleShapesNotInSelection = this.visibleShapesNotInSelection + const pageOutlines = [] + for (const visibleShape of visibleShapesNotInSelection) { + const shape = this.app.getShapeById(visibleShape.id)! + + if (shape.type === 'text' || shape.type === 'icon') { + continue + } + + const outline = deepCopy(this.app.getOutlineById(visibleShape.id)) + + const isClosed = this.app.getShapeUtil(shape).isClosed?.(shape) + + if (isClosed) { + outline.push(outline[0]) + } + + pageOutlines.push(Matrix2d.applyToPoints(this.app.getPageTransformById(shape.id)!, outline)) + } + + // Find the nearest point that is within the snap threshold + let minDistance = this.snapThreshold + let nearestPoint: Vec2d | null = null + for (const outline of [...pageOutlines, ...additionalOutlines]) { + for (let i = 0; i < outline.length - 1; i++) { + const C = outline[i] + const D = outline[i + 1] + + const distance = Vec2d.DistanceToLineSegment(C, D, handlePoint) + if (isNaN(distance)) continue + if (distance < minDistance) { + minDistance = distance + nearestPoint = Vec2d.NearestPointOnLineSegment(C, D, handlePoint) + } + } + } + + // If we found a point, display snap lines, and return the nudge + if (nearestPoint) { + const snapLines: SnapLine[] = [] + + snapLines.push({ + id: uniqueId(), + type: 'points', + points: [nearestPoint], + }) + + this._snapLines.set(snapLines) + + return { + nudge: Vec2d.Sub(nearestPoint, handlePoint), + } + } + + return { nudge: new Vec2d(0, 0) } + } + + snapResize({ + initialSelectionPageBounds, + dragDelta, + handle: originalHandle, + isAspectRatioLocked, + isResizingFromCenter, + }: { + // the page bounds when the pointer went down, before any dragging + initialSelectionPageBounds: Box2d + // how far the pointer has been dragged + dragDelta: Vec2d + + handle: SelectionCorner | SelectionEdge + isAspectRatioLocked: boolean + isResizingFromCenter: boolean + }): SnapData { + // first figure out the new bounds of the selection + const { + box: unsnappedResizedPageBounds, + scaleX, + scaleY, + } = Box2d.Resize( + initialSelectionPageBounds, + originalHandle, + isResizingFromCenter ? dragDelta.x * 2 : dragDelta.x, + isResizingFromCenter ? dragDelta.y * 2 : dragDelta.y, + isAspectRatioLocked + ) + + let handle = originalHandle + + if (scaleX < 0) { + handle = flipSelectionHandleX(handle) + } + if (scaleY < 0) { + handle = flipSelectionHandleY(handle) + } + + if (isResizingFromCenter) { + // reposition if resizing from center + unsnappedResizedPageBounds.center = initialSelectionPageBounds.center + } + + const isXLocked = handle === 'top' || handle === 'bottom' + const isYLocked = handle === 'left' || handle === 'right' + + const selectionSnapPoints = getResizeSnapPointsForHandle(handle, unsnappedResizedPageBounds) + + const otherNodeSnapPoints = this.visibleSnapPointsNotInSelection + + const nearestSnapsX: NearestPointsSnap[] = [] + const nearestSnapsY: NearestPointsSnap[] = [] + const minOffset = new Vec2d(this.snapThreshold, this.snapThreshold) + + this.findPointSnaps({ + minOffset, + nearestSnapsX, + nearestSnapsY, + otherNodeSnapPoints, + selectionSnapPoints, + }) + + // at the same time, calculate how far we need to nudge the shape to 'snap' to the target point(s) + const nudge = new Vec2d( + isXLocked ? 0 : nearestSnapsX[0]?.nudge ?? 0, + isYLocked ? 0 : nearestSnapsY[0]?.nudge ?? 0 + ) + + if (isAspectRatioLocked && isSelectionCorner(handle) && nudge.len() !== 0) { + // if the aspect ratio is locked we need to make the nudge diagonal rather than independent in each axis + // so we use the aspect ratio along with one axis value to set the other axis value, but which axis we use + // as a source of truth depends what we have snapped to and how far. + + // if we found a snap in both axes, pick the closest one and discard the other + const primaryNudgeAxis: 'x' | 'y' = + nearestSnapsX.length && nearestSnapsY.length + ? Math.abs(nudge.x) < Math.abs(nudge.y) + ? 'x' + : 'y' + : nearestSnapsX.length + ? 'x' + : 'y' + + const ratio = initialSelectionPageBounds.aspectRatio + + if (primaryNudgeAxis === 'x') { + nearestSnapsY.length = 0 + nudge.y = nudge.x / ratio + if (handle === 'bottom_left' || handle === 'top_right') { + nudge.y = -nudge.y + } + } else { + nearestSnapsX.length = 0 + nudge.x = nudge.y * ratio + if (handle === 'bottom_left' || handle === 'top_right') { + nudge.x = -nudge.x + } + } + } + + // now resize the box after nudging, calculate the snaps again, and return the snap lines to match + // the fully resized box + const snappedDelta = Vec2d.Add(dragDelta, nudge) + + // first figure out the new bounds of the selection + const { box: snappedResizedPageBounds } = Box2d.Resize( + initialSelectionPageBounds, + originalHandle, + isResizingFromCenter ? snappedDelta.x * 2 : snappedDelta.x, + isResizingFromCenter ? snappedDelta.y * 2 : snappedDelta.y, + isAspectRatioLocked + ) + + if (isResizingFromCenter) { + // reposition if resizing from center + snappedResizedPageBounds.center = initialSelectionPageBounds.center + } + + const snappedSelectionPoints = getResizeSnapPointsForHandle('any', snappedResizedPageBounds) + // calculate snaps again using all points + nearestSnapsX.length = 0 + nearestSnapsY.length = 0 + minOffset.x = 0 + minOffset.y = 0 + + this.findPointSnaps({ + minOffset, + nearestSnapsX, + nearestSnapsY, + otherNodeSnapPoints, + selectionSnapPoints: snappedSelectionPoints, + }) + const pointSnaps = this.getPointSnapLines({ + nearestSnapsX, + nearestSnapsY, + }) + + this._snapLines.set([...pointSnaps]) + + return { nudge } + } + + private findPointSnaps({ + selectionSnapPoints, + otherNodeSnapPoints, + minOffset, + nearestSnapsX, + nearestSnapsY, + }: { + selectionSnapPoints: SnapPoint[] + otherNodeSnapPoints: SnapPoint[] + minOffset: Vec2d + nearestSnapsX: NearestSnap[] + nearestSnapsY: NearestSnap[] + }) { + // for each snap point on the bounding box of the selection, find the set of points + // which are closest to it in each axis + for (const thisSnapPoint of selectionSnapPoints) { + for (const otherSnapPoint of otherNodeSnapPoints) { + const offset = Vec2d.Sub(thisSnapPoint, otherSnapPoint) + const offsetX = Math.abs(offset.x) + const offsetY = Math.abs(offset.y) + + if (round(offsetX) <= round(minOffset.x)) { + if (round(offsetX) < round(minOffset.x)) { + // we found a point that is significantly closer than all previous points + // so wipe the slate clean and start over + nearestSnapsX.length = 0 + } + + nearestSnapsX.push({ + type: 'points', + points: { thisPoint: thisSnapPoint, otherPoint: otherSnapPoint }, + nudge: otherSnapPoint.x - thisSnapPoint.x, + }) + minOffset.x = offsetX + } + + if (round(offsetY) <= round(minOffset.y)) { + if (round(offsetY) < round(minOffset.y)) { + // we found a point that is significantly closer than all previous points + // so wipe the slate clean and start over + nearestSnapsY.length = 0 + } + nearestSnapsY.push({ + type: 'points', + points: { thisPoint: thisSnapPoint, otherPoint: otherSnapPoint }, + nudge: otherSnapPoint.y - thisSnapPoint.y, + }) + minOffset.y = offsetY + } + } + } + } + + private findGapSnaps({ + selectionPageBounds, + minOffset, + nearestSnapsX, + nearestSnapsY, + }: { + selectionPageBounds: Box2d + minOffset: Vec2d + nearestSnapsX: NearestSnap[] + nearestSnapsY: NearestSnap[] + }) { + for (const gap of this.visibleGaps.horizontal) { + // ignore this gap if the selection doesn't overlap with it in the y axis + if ( + !rangesOverlap( + gap.breadthIntersection[0], + gap.breadthIntersection[1], + selectionPageBounds.minY, + selectionPageBounds.maxY + ) + ) { + continue + } + + // check for center match + const gapMidX = gap.startEdge[0].x + gap.length / 2 + const centerNudge = gapMidX - selectionPageBounds.center.x + const gapIsLargerThanSelection = gap.length > selectionPageBounds.width + + if (gapIsLargerThanSelection && round(Math.abs(centerNudge)) <= round(minOffset.x)) { + if (round(Math.abs(centerNudge)) < round(minOffset.x)) { + // reset if we found a closer snap + nearestSnapsX.length = 0 + } + minOffset.x = Math.abs(centerNudge) + + const snap: NearestSnap = { + type: 'gap_center', + gap, + nudge: centerNudge, + } + + // we need to avoid creating visual noise with too many center snaps in situations + // where there are lots of adjacent items with even spacing + // so let's only show other center snaps where the gap's breadth does not overlap with this one + // i.e. + // ┌───────────────┐ + // │ │ + // └──────┬────┬───┘ + // ┼ │ + // ┌─────┴┐ │ + // │ │ ┼ + // └─────┬┘ │ + // ┼ │ + // ┌───┴────┴───────┐ + // │ │ ◄──── i'm dragging this one + // └───┬────┬───────┘ + // ─────► ┼ │ + // ┌─────┴┐ │ don't show these + // show these │ │ ┼ larger gaps since + // smaller └─────┬┘ │ ◄───────────── the smaller ones + // gaps ┼ │ cover the same + // ─────► ┌┴────┴─────┐ information + // │ │ + // └───────────┘ + // + // but we want to show all of these ones since the gap breadths don't overlap + // ┌─────────────┐ + // │ │ + // ┌────┐ └───┬─────────┘ + // │ │ │ + // └──┬─┘ ┼ + // ┼ │ + // ┌──┴───────────┴─┐ + // │ │ ◄───── i'm dragging this one + // └──┬───────────┬─┘ + // ┼ │ + // ┌──┴────┐ ┼ + // │ │ │ + // └───────┘ ┌─┴───────┐ + // │ │ + // └─────────┘ + + const otherCenterSnap = nearestSnapsX.find(({ type }) => type === 'gap_center') as + | Extract + | undefined + + const gapBreadthsOverlap = + otherCenterSnap && + rangeIntersection( + gap.breadthIntersection[0], + gap.breadthIntersection[1], + otherCenterSnap.gap.breadthIntersection[0], + otherCenterSnap.gap.breadthIntersection[1] + ) + + // if there is another center snap and it's bigger than this one, and it overlaps with this one, replace it + if (otherCenterSnap && otherCenterSnap.gap.length > gap.length && gapBreadthsOverlap) { + nearestSnapsX[nearestSnapsX.indexOf(otherCenterSnap)] = snap + } else if (!otherCenterSnap || !gapBreadthsOverlap) { + nearestSnapsX.push(snap) + } + } + + // check for duplication left match + const duplicationLeftX = gap.startNode.pageBounds.minX - gap.length + const selectionRightX = selectionPageBounds.maxX + + const duplicationLeftNudge = duplicationLeftX - selectionRightX + if (round(Math.abs(duplicationLeftNudge)) <= round(minOffset.x)) { + if (round(Math.abs(duplicationLeftNudge)) < round(minOffset.x)) { + // reset if we found a closer snap + nearestSnapsX.length = 0 + } + minOffset.x = Math.abs(duplicationLeftNudge) + + nearestSnapsX.push({ + type: 'gap_duplicate', + gap, + protrusionDirection: 'left', + nudge: duplicationLeftNudge, + }) + } + + // check for duplication right match + const duplicationRightX = gap.endNode.pageBounds.maxX + gap.length + const selectionLeftX = selectionPageBounds.minX + + const duplicationRightNudge = duplicationRightX - selectionLeftX + if (round(Math.abs(duplicationRightNudge)) <= round(minOffset.x)) { + if (round(Math.abs(duplicationRightNudge)) < round(minOffset.x)) { + // reset if we found a closer snap + nearestSnapsX.length = 0 + } + minOffset.x = Math.abs(duplicationRightNudge) + + nearestSnapsX.push({ + type: 'gap_duplicate', + gap, + protrusionDirection: 'right', + nudge: duplicationRightNudge, + }) + } + } + + for (const gap of this.visibleGaps.vertical) { + // ignore this gap if the selection doesn't overlap with it in the y axis + if ( + !rangesOverlap( + gap.breadthIntersection[0], + gap.breadthIntersection[1], + selectionPageBounds.minX, + selectionPageBounds.maxX + ) + ) { + continue + } + + // check for center match + const gapMidY = gap.startEdge[0].y + gap.length / 2 + const centerNudge = gapMidY - selectionPageBounds.center.y + + const gapIsLargerThanSelection = gap.length > selectionPageBounds.height + + if (gapIsLargerThanSelection && round(Math.abs(centerNudge)) <= round(minOffset.y)) { + if (round(Math.abs(centerNudge)) < round(minOffset.y)) { + // reset if we found a closer snap + nearestSnapsY.length = 0 + } + minOffset.y = Math.abs(centerNudge) + + const snap: NearestSnap = { + type: 'gap_center', + gap, + nudge: centerNudge, + } + + // we need to avoid creating visual noise with too many center snaps in situations + // where there are lots of adjacent items with even spacing + // so let's only show other center snaps where the gap's breadth does not overlap with this one + // i.e. + // ┌───────────────┐ + // │ │ + // └──────┬────┬───┘ + // ┼ │ + // ┌─────┴┐ │ + // │ │ ┼ + // └─────┬┘ │ + // ┼ │ + // ┌───┴────┴───────┐ + // │ │ ◄──── i'm dragging this one + // └───┬────┬───────┘ + // ─────► ┼ │ + // ┌─────┴┐ │ don't show these + // show these │ │ ┼ larger gaps since + // smaller └─────┬┘ │ ◄───────────── the smaller ones + // gaps ┼ │ cover the same + // ─────► ┌┴────┴─────┐ information + // │ │ + // └───────────┘ + // + // but we want to show all of these ones since the gap breadths don't overlap + // ┌─────────────┐ + // │ │ + // ┌────┐ └───┬─────────┘ + // │ │ │ + // └──┬─┘ ┼ + // ┼ │ + // ┌──┴───────────┴─┐ + // │ │ ◄───── i'm dragging this one + // └──┬───────────┬─┘ + // ┼ │ + // ┌──┴────┐ ┼ + // │ │ │ + // └───────┘ ┌─┴───────┐ + // │ │ + // └─────────┘ + + const otherCenterSnap = nearestSnapsY.find(({ type }) => type === 'gap_center') as + | Extract + | undefined + + const gapBreadthsOverlap = + otherCenterSnap && + rangesOverlap( + otherCenterSnap.gap.breadthIntersection[0], + otherCenterSnap.gap.breadthIntersection[1], + gap.breadthIntersection[0], + gap.breadthIntersection[1] + ) + + // if there is another center snap and it's bigger than this one, and it overlaps with this one, replace it + if (otherCenterSnap && otherCenterSnap.gap.length > gap.length && gapBreadthsOverlap) { + nearestSnapsY[nearestSnapsY.indexOf(otherCenterSnap)] = snap + } else if (!otherCenterSnap || !gapBreadthsOverlap) { + nearestSnapsY.push(snap) + } + continue + } + + // check for duplication top match + const duplicationTopY = gap.startNode.pageBounds.minY - gap.length + const selectionBottomY = selectionPageBounds.maxY + + const duplicationTopNudge = duplicationTopY - selectionBottomY + if (round(Math.abs(duplicationTopNudge)) <= round(minOffset.y)) { + if (round(Math.abs(duplicationTopNudge)) < round(minOffset.y)) { + // reset if we found a closer snap + nearestSnapsY.length = 0 + } + minOffset.y = Math.abs(duplicationTopNudge) + + nearestSnapsY.push({ + type: 'gap_duplicate', + gap, + protrusionDirection: 'top', + nudge: duplicationTopNudge, + }) + } + + // check for duplication bottom match + const duplicationBottomY = gap.endNode.pageBounds.maxY + gap.length + const selectionTopY = selectionPageBounds.minY + + const duplicationBottomNudge = duplicationBottomY - selectionTopY + if (round(Math.abs(duplicationBottomNudge)) <= round(minOffset.y)) { + if (round(Math.abs(duplicationBottomNudge)) < round(minOffset.y)) { + // reset if we found a closer snap + nearestSnapsY.length = 0 + } + minOffset.y = Math.abs(duplicationBottomNudge) + + nearestSnapsY.push({ + type: 'gap_duplicate', + gap, + protrusionDirection: 'bottom', + nudge: duplicationBottomNudge, + }) + } + } + } + + getPointSnapLines({ + nearestSnapsX, + nearestSnapsY, + }: { + nearestSnapsX: NearestSnap[] + nearestSnapsY: NearestSnap[] + }) { + // point snaps may align on multiple parallel lines so we need to split the pairs + // into groups based on where they are in their their snap axes + const snapGroupsX = {} as { [key: string]: SnapPair[] } + const snapGroupsY = {} as { [key: string]: SnapPair[] } + const result: PointsSnapLine[] = [] + + if (nearestSnapsX.length > 0) { + for (const snap of nearestSnapsX) { + if (snap.type === 'points') { + const key = round(snap.points.otherPoint.x) + if (!snapGroupsX[key]) { + snapGroupsX[key] = [] + } + snapGroupsX[key].push(snap.points) + } + } + } + + if (nearestSnapsY.length > 0) { + for (const snap of nearestSnapsY) { + if (snap.type === 'points') { + const key = round(snap.points.otherPoint.y) + if (!snapGroupsY[key]) { + snapGroupsY[key] = [] + } + snapGroupsY[key].push(snap.points) + } + } + } + + // and finally create all the snap lines for the UI to render + for (const [_, snapGroup] of Object.entries(snapGroupsX).concat(Object.entries(snapGroupsY))) { + result.push({ + id: uniqueId(), + type: 'points', + points: dedupe( + snapGroup + .map((snap) => Vec2d.From(snap.otherPoint)) + // be sure to nudge over the selection snap points + .concat(snapGroup.map((snap) => Vec2d.From(snap.thisPoint))), + (a: Vec2d, b: Vec2d) => a.equals(b) + ), + }) + } + + return result + } + + getGapSnapLines({ + selectionPageBounds, + nearestSnapsX, + nearestSnapsY, + }: { + selectionPageBounds: Box2d + nearestSnapsX: NearestSnap[] + nearestSnapsY: NearestSnap[] + }): GapsSnapLine[] { + const selectionSides: Record = { + top: selectionPageBounds.sides[0], + right: selectionPageBounds.sides[1], + // need bottom and left to be sorted asc, which .sides is not. + bottom: [selectionPageBounds.corners[3], selectionPageBounds.corners[2]], + left: [selectionPageBounds.corners[0], selectionPageBounds.corners[3]], + } + + const result: GapsSnapLine[] = [] + + if (nearestSnapsX.length > 0) { + for (const snap of nearestSnapsX) { + if (snap.type === 'gap_center') { + // create + const newGapsLength = (snap.gap.length - selectionPageBounds.width) / 2 + const gapBreadthIntersection = rangeIntersection( + snap.gap.breadthIntersection[0], + snap.gap.breadthIntersection[1], + selectionPageBounds.minY, + selectionPageBounds.maxY + )! + result.push({ + type: 'gaps', + direction: 'horizontal', + id: uniqueId(), + gaps: [ + ...findAdjacentGaps( + this.visibleGaps.horizontal, + snap.gap.startNode.id, + newGapsLength, + 'backward', + gapBreadthIntersection + ), + { + startEdge: snap.gap.startEdge, + endEdge: selectionSides.left, + }, + { + startEdge: selectionSides.right, + endEdge: snap.gap.endEdge, + }, + ...findAdjacentGaps( + this.visibleGaps.horizontal, + snap.gap.endNode.id, + newGapsLength, + 'forward', + gapBreadthIntersection + ), + ], + }) + } + if (snap.type === 'gap_duplicate') { + // create + const gapBreadthIntersection = rangeIntersection( + snap.gap.breadthIntersection[0], + snap.gap.breadthIntersection[1], + selectionPageBounds.minY, + selectionPageBounds.maxY + )! + result.push({ + type: 'gaps', + direction: 'horizontal', + id: uniqueId(), + gaps: + snap.protrusionDirection === 'left' + ? [ + { + startEdge: selectionSides.right, + endEdge: [ + Vec2d.Add(snap.gap.startEdge[0], { + x: -snap.gap.startNode.pageBounds.width, + y: 0, + }), + Vec2d.Add(snap.gap.startEdge[1], { + x: -snap.gap.startNode.pageBounds.width, + y: 0, + }), + ], + }, + { + startEdge: snap.gap.startEdge, + endEdge: snap.gap.endEdge, + }, + ...findAdjacentGaps( + this.visibleGaps.horizontal, + snap.gap.endNode.id, + snap.gap.length, + 'forward', + gapBreadthIntersection + ), + ] + : [ + ...findAdjacentGaps( + this.visibleGaps.horizontal, + snap.gap.startNode.id, + snap.gap.length, + 'backward', + gapBreadthIntersection + ), + { + startEdge: snap.gap.startEdge, + endEdge: snap.gap.endEdge, + }, + { + startEdge: [ + Vec2d.Add(snap.gap.endEdge[0], { + x: snap.gap.endNode.pageBounds.width, + y: 0, + }), + Vec2d.Add(snap.gap.endEdge[1], { + x: snap.gap.endNode.pageBounds.width, + y: 0, + }), + ], + endEdge: selectionSides.left, + }, + ], + }) + } + } + } + + if (nearestSnapsY.length > 0) { + for (const snap of nearestSnapsY) { + if (snap.type === 'gap_center') { + const newGapsLength = (snap.gap.length - selectionPageBounds.height) / 2 + const gapBreadthIntersection = rangeIntersection( + snap.gap.breadthIntersection[0], + snap.gap.breadthIntersection[1], + selectionPageBounds.minX, + selectionPageBounds.maxX + )! + result.push({ + type: 'gaps', + direction: 'vertical', + id: uniqueId(), + gaps: [ + ...findAdjacentGaps( + this.visibleGaps.vertical, + snap.gap.startNode.id, + newGapsLength, + 'backward', + gapBreadthIntersection + ), + { + startEdge: snap.gap.startEdge, + endEdge: selectionSides.top, + }, + { + startEdge: selectionSides.bottom, + endEdge: snap.gap.endEdge, + }, + ...findAdjacentGaps( + this.visibleGaps.vertical, + snap.gap.endNode.id, + newGapsLength, + 'forward', + gapBreadthIntersection + ), + ], + }) + } + + if (snap.type === 'gap_duplicate') { + const gapBreadthIntersection = rangeIntersection( + snap.gap.breadthIntersection[0], + snap.gap.breadthIntersection[1], + selectionPageBounds.minX, + selectionPageBounds.maxX + )! + result.push({ + type: 'gaps', + direction: 'vertical', + id: uniqueId(), + gaps: + snap.protrusionDirection === 'top' + ? [ + { + startEdge: selectionSides.bottom, + endEdge: [ + Vec2d.Add(snap.gap.startEdge[0], { + x: 0, + y: -snap.gap.startNode.pageBounds.height, + }), + Vec2d.Add(snap.gap.startEdge[1], { + x: 0, + y: -snap.gap.startNode.pageBounds.height, + }), + ], + }, + { + startEdge: snap.gap.startEdge, + endEdge: snap.gap.endEdge, + }, + ...findAdjacentGaps( + this.visibleGaps.vertical, + snap.gap.endNode.id, + snap.gap.length, + 'forward', + gapBreadthIntersection + ), + ] + : [ + ...findAdjacentGaps( + this.visibleGaps.vertical, + snap.gap.startNode.id, + snap.gap.length, + 'backward', + gapBreadthIntersection + ), + { + startEdge: snap.gap.startEdge, + endEdge: snap.gap.endEdge, + }, + { + startEdge: [ + Vec2d.Add(snap.gap.endEdge[0], { + x: 0, + y: snap.gap.endNode.pageBounds.height, + }), + Vec2d.Add(snap.gap.endEdge[1], { + x: 0, + y: snap.gap.endNode.pageBounds.height, + }), + ], + endEdge: selectionSides.top, + }, + ], + }) + } + } + } + + dedupeGapSnaps(result) + return result + } +} + +function getResizeSnapPointsForHandle( + handle: SelectionCorner | SelectionEdge | 'any', + selectionPageBounds: Box2d +): SnapPoint[] { + const { minX, maxX, minY, maxY } = selectionPageBounds + const result: SnapPoint[] = [] + + // top left corner + switch (handle) { + case 'top': + case 'left': + case 'top_left': + case 'any': + result.push({ + id: 'top_left', + handle: 'top_left', + x: minX, + y: minY, + }) + } + + // top right corner + switch (handle) { + case 'top': + case 'right': + case 'top_right': + case 'any': + result.push({ + id: 'top_right', + handle: 'top_right', + x: maxX, + y: minY, + }) + } + + // bottom right corner + switch (handle) { + case 'bottom': + case 'right': + case 'bottom_right': + case 'any': + result.push({ + id: 'bottom_right', + handle: 'bottom_right', + x: maxX, + y: maxY, + }) + } + + // bottom left corner + switch (handle) { + case 'bottom': + case 'left': + case 'bottom_left': + case 'any': + result.push({ + id: 'bottom_left', + handle: 'bottom_left', + x: minX, + y: maxY, + }) + } + + return result +} diff --git a/packages/editor/src/lib/app/managers/Stack.ts b/packages/editor/src/lib/app/managers/Stack.ts new file mode 100644 index 000000000..1b2e75146 --- /dev/null +++ b/packages/editor/src/lib/app/managers/Stack.ts @@ -0,0 +1,68 @@ +import { EMPTY_ARRAY } from 'signia' + +export type Stack = StackItem | EmptyStackItem + +export function stack(items?: Array): Stack { + if (items) { + let result = EMPTY_STACK_ITEM as Stack + while (items.length) { + result = result.push(items.pop()!) + } + return result + } + return EMPTY_STACK_ITEM as any +} + +class EmptyStackItem implements Iterable { + readonly length = 0 + readonly head = null + readonly tail: Stack = this + + push(head: T): Stack { + return new StackItem(head, this) + } + + toArray() { + return EMPTY_ARRAY + } + + [Symbol.iterator]() { + return { + next() { + return { value: undefined, done: true as const } + }, + } + } +} + +const EMPTY_STACK_ITEM = new EmptyStackItem() + +class StackItem implements Iterable { + length: number + constructor(public readonly head: T, public readonly tail: Stack) { + this.length = tail.length + 1 + } + + push(head: T): Stack { + return new StackItem(head, this) + } + + toArray() { + return Array.from(this) + } + + [Symbol.iterator]() { + let stack = this as Stack + return { + next() { + if (stack.length) { + const value = stack.head! + stack = stack.tail + return { value, done: false as const } + } else { + return { value: undefined, done: true as const } + } + }, + } + } +} diff --git a/packages/editor/src/lib/app/managers/TextManager.ts b/packages/editor/src/lib/app/managers/TextManager.ts new file mode 100644 index 000000000..e6777f93d --- /dev/null +++ b/packages/editor/src/lib/app/managers/TextManager.ts @@ -0,0 +1,256 @@ +import { Box2dModel, TLAlignType } from '@tldraw/tlschema' +import { uniqueId } from '../../utils/data' +import { App } from '../App' +import { TextHelpers } from '../shapeutils/TLTextUtil/TextHelpers' + +const wordSeparator = new RegExp( + `${[0x0020, 0x00a0, 0x1361, 0x10100, 0x10101, 0x1039, 0x1091] + .map((c) => String.fromCodePoint(c)) + .join('|')}` +) + +const textAlignmentsForLtr: Record = { + start: 'left', + middle: 'center', + end: 'right', +} + +export class TextManager { + constructor(public app: App) {} + + getTextElement() { + const elm = document.createElement('div') + this.app.getContainer().appendChild(elm) + + elm.id = `__textMeasure_${uniqueId()}` + elm.classList.add('rs-text') + elm.classList.add('rs-text-measure') + elm.tabIndex = -1 + + return elm + } + + measureText = (opts: { + text: string + fontStyle: string + fontWeight: string + fontFamily: string + fontSize: number + lineHeight: number + width: string + minWidth?: string + maxWidth: string + padding: string + }): Box2dModel => { + const elm = this.getTextElement() + + elm.setAttribute('dir', 'ltr') + elm.style.setProperty('font-family', opts.fontFamily) + elm.style.setProperty('font-style', opts.fontStyle) + elm.style.setProperty('font-weight', opts.fontWeight) + elm.style.setProperty('font-size', opts.fontSize + 'px') + elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px') + elm.style.setProperty('width', opts.width) + elm.style.setProperty('min-width', opts.minWidth ?? null) + elm.style.setProperty('max-width', opts.maxWidth) + elm.style.setProperty('padding', opts.padding) + + elm.textContent = TextHelpers.normalizeTextForDom(opts.text) + + const rect = elm.getBoundingClientRect() + + elm.remove() + + return { + x: 0, + y: 0, + w: rect.width, + h: rect.height, + } + } + + getTextLines(opts: { + text: string + wrap: boolean + width: number + height: number + padding: number + fontSize: number + fontWeight: string + fontFamily: string + fontStyle: string + lineHeight: number + textAlign: TLAlignType + }): string[] { + const elm = this.getTextElement() + + elm.style.setProperty('width', opts.width - opts.padding * 2 + 'px') + elm.style.setProperty('height', 'min-content') + elm.style.setProperty('dir', 'ltr') + elm.style.setProperty('font-size', opts.fontSize + 'px') + elm.style.setProperty('font-family', opts.fontFamily) + elm.style.setProperty('font-weight', opts.fontWeight) + elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px') + elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign]) + + // Split the text into words + const words: string[] = [] + + opts.text.split(wordSeparator).flatMap((word) => + word + .split('\n') + .map((w) => w.trim()) + .join('\n') + ) + + for (const str of opts.text.split(wordSeparator)) { + if (str.includes('\n')) { + const splits = str.split('\n') + for (let i = 0; i < splits.length; i++) { + splits[i] = splits[i].trim() + if (splits[i]) words.push(splits[i]) + if (i < splits.length - 1) words.push('\n') + } + } else { + words.push(str) + } + } + + const finalWords: string[] = [] + + for (let i = 0; i < words.length; i++) { + const word = words[i] + finalWords.push(word) + if (word === '\n') { + if (words[i + 1] === '') { + i++ + } + } + } + + let i = finalWords.length - 1 + + while (finalWords[i] === '') { + finalWords.pop() + if (i === 0) break + i-- + } + + let currentLine: string[] = [] + const lines: string[][] = [] + + elm.textContent = '' + let prevHeight = elm.offsetHeight + + let prevTextContent = elm.textContent + + for (let i = 0; i < finalWords.length; i++) { + const word = finalWords[i] + + // add the word to the text element + const wordWithSpace = word === '\n' ? word : `${word} ` + elm.textContent += wordWithSpace + + // measure its height + const newHeight = elm.offsetHeight + + if (newHeight <= prevHeight) { + // If the height has not increased, then add the word to the current line + currentLine.push(currentLine.length ? ' ' + word.trim() : word.trim()) + } else { + // Hey, we've just caused a line break! + if (!opts.wrap || word === '\n') { + // If we're not wrapping, or if the word is a newline, then start a new line. + currentLine = [] + lines.push(currentLine) + } else { + // If we're wrapping, then buckle the fuck up, because we need to + // see whether we can fit the word on a single line or else break it + // into multiple lines in order to replicate break-word. + + // We need to make sure that the first word on this line isn't so + // long that it ALSO causes a line break. If it does, then we'll + // need to manually create the effect of CSS's 'break-word', splitting + // the word into multiple lines. + + // Save the state of the text content that caused the break to occur. + // We'll put this back again at the end of the loop, so that we can + // continue from this point. + const afterTextContent: string = elm.textContent + + // Set the text content to the previous text content, before adding + // the word, so that we can begin to find line breaks. + elm.textContent = prevTextContent + + // Force a new line, since we know that the text will break the line + // and we want to start measuring from the start of the line. + elm.textContent += '\n' + + // Split the word into individual characters. + const chars = [...word] + + // Add the first character to the measurement element's text content. + elm.textContent += chars[0] + + // Set the "previous height" to the text element's scroll height. + prevHeight = elm.offsetHeight + + // Similar to how we're breaking with words, we're not going to loop + // through each character looking for new lines within the word (sublines). + // We'll start with a collection of one subline that contains the first + // character in the word. + let currentSubLine: string[] = [chars[0]] + const subLines: string[][] = [currentSubLine] + + // For each remaining character in the word... + for (let i = 1; i < chars.length; i++) { + const char = chars[i] + + // ...add the character to the text element + elm.textContent += char + + // ...and measure the height + const newHeight = elm.offsetHeight + + if (newHeight > prevHeight) { + // If the height has increased, then we've triggered a "break-word". + // Create a new current subline containing the character, and add + // it to the sublines array. + currentSubLine = [char] + subLines.push(currentSubLine) + + // Also update the prev height for next time + prevHeight = newHeight + } else { + // If the height hasn't increased, then we're still on the same + // subline and can just push the char in. + currentSubLine.push(char) + } + } + + // Finally, turn each subline of characters into a string and push + // each line into the lines array. + const joinedSubLines = subLines.map((b) => [b.join('')]) + lines.push(...joinedSubLines) + + // Set the current line to the last subline + currentLine = lines[lines.length - 1] + + // Restore the text content that caused the line break to occur + elm.textContent = afterTextContent + + // And set prevHeight to the new height + prevHeight = elm.offsetHeight + } + } + + prevTextContent = elm.textContent + } + + const result = lines.map((line) => line.join('').trim()) + + elm.remove() + + return result + } +} diff --git a/packages/editor/src/lib/app/managers/TickManager.ts b/packages/editor/src/lib/app/managers/TickManager.ts new file mode 100644 index 000000000..6f98d3ce9 --- /dev/null +++ b/packages/editor/src/lib/app/managers/TickManager.ts @@ -0,0 +1,78 @@ +import { Vec2d } from '@tldraw/primitives' +import { App } from '../App' + +export class TickManager { + constructor(public app: App) { + this.app.disposables.add(this.dispose) + this.start() + } + + raf: any + isPaused = true + last = 0 + t = 0 + + start = () => { + this.isPaused = false + cancelAnimationFrame(this.raf) + this.raf = requestAnimationFrame(this.tick) + this.last = Date.now() + } + + tick = () => { + if (this.isPaused) { + return + } + + const now = Date.now() + const elapsed = now - this.last + this.last = now + this.t += elapsed + + if (this.t < 16) { + this.raf = requestAnimationFrame(this.tick) + return + } + + this.t -= 16 + this.updatePointerVelocity(elapsed) + this.app.emit('tick', elapsed) + this.raf = requestAnimationFrame(this.tick) + } + + // Clear the listener + dispose = () => { + this.isPaused = true + cancelAnimationFrame(this.raf) + } + + private prevPoint = new Vec2d() + + private updatePointerVelocity = (elapsed: number) => { + const { + prevPoint, + app: { + inputs: { currentScreenPoint, pointerVelocity }, + }, + } = this + + if (elapsed === 0) return + + const delta = Vec2d.Sub(currentScreenPoint, prevPoint) + this.prevPoint = currentScreenPoint.clone() + + const length = delta.len() + const direction = length ? delta.div(length) : new Vec2d(0, 0) + + // consider adjusting this with an easing rather than a linear interpolation + const next = pointerVelocity.clone().lrp(direction.mul(length / elapsed), 0.5) + + // if the velocity is very small, just set it to 0 + if (Math.abs(next.x) < 0.01) next.x = 0 + if (Math.abs(next.y) < 0.01) next.y = 0 + + if (!pointerVelocity.equals(next)) { + this.app.inputs.pointerVelocity = next + } + } +} diff --git a/packages/editor/src/lib/app/shapeutils/TLArrowUtil/TLArrowUtil.test.ts b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/TLArrowUtil.test.ts new file mode 100644 index 000000000..89046cc71 --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/TLArrowUtil.test.ts @@ -0,0 +1,581 @@ +import { TAU } from '@tldraw/primitives' +import { createCustomShapeId, TLArrowShape, TLArrowTerminal, TLShapeId } from '@tldraw/tlschema' +import { assert } from '@tldraw/utils' +import { TestApp } from '../../../test/TestApp' +import { TLArrowShapeDef } from './TLArrowUtil' + +let app: TestApp + +const ids = { + box1: createCustomShapeId('box1'), + box2: createCustomShapeId('box2'), + box3: createCustomShapeId('box3'), + box4: createCustomShapeId('box4'), + arrow1: createCustomShapeId('arrow1'), +} + +jest.useFakeTimers() + +window.requestAnimationFrame = function requestAnimationFrame(cb) { + return setTimeout(cb, 1000 / 60) +} + +window.cancelAnimationFrame = function cancelAnimationFrame(id) { + clearTimeout(id) +} + +beforeEach(() => { + app = new TestApp() + app + .selectAll() + .deleteShapes() + .createShapes([ + { id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }, + { id: ids.box2, type: 'geo', x: 300, y: 300, props: { w: 100, h: 100 } }, + { + id: ids.arrow1, + type: 'arrow', + x: 150, + y: 150, + props: { + start: { + type: 'binding', + isExact: false, + boundShapeId: ids.box1, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + end: { + type: 'binding', + isExact: false, + boundShapeId: ids.box2, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + }, + }, + ]) +}) + +describe('When translating a bound shape', () => { + it('updates the arrow when straight', () => { + app.select(ids.box2) + app.pointerDown(250, 250, { target: 'shape', shape: app.getShapeById(ids.box2) }) + app.pointerMove(300, 300) // move box 2 by 50, 50 + app.expectShapeToMatch({ + id: ids.box2, + x: 350, + y: 350, + }) + app.expectShapeToMatch({ + id: ids.arrow1, + type: 'arrow', + x: 150, + y: 150, + props: { + start: { + type: 'binding', + isExact: false, + boundShapeId: ids.box1, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + end: { + type: 'binding', + isExact: false, + boundShapeId: ids.box2, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + }, + }) + }) + + it('updates the arrow when curved', () => { + app.updateShapes([{ id: ids.arrow1, type: 'arrow', props: { bend: 20 } }]) + app.select(ids.box2) + app.pointerDown(250, 250, { target: 'shape', shape: app.getShapeById(ids.box2) }) + app.pointerMove(300, 300) // move box 2 by 50, 50 + app.expectShapeToMatch({ + id: ids.box2, + x: 350, + y: 350, + }) + app.expectShapeToMatch({ + id: ids.arrow1, + type: 'arrow', + x: 150, + y: 150, + props: { + start: { + type: 'binding', + isExact: false, + boundShapeId: ids.box1, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + end: { + type: 'binding', + isExact: false, + boundShapeId: ids.box2, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + }, + }) + }) +}) + +describe('When translating the arrow', () => { + it('unbinds all handles if neither bound shape is not also translating', () => { + app.select(ids.arrow1) + app.pointerDown(200, 200, { target: 'shape', shape: app.getShapeById(ids.arrow1)! }) + app.pointerMove(200, 190) + app.expectShapeToMatch({ + id: ids.arrow1, + type: 'arrow', + x: 150, + y: 140, + props: { + start: { type: 'point', x: 0, y: 0 }, + end: { type: 'point', x: 200, y: 200 }, + }, + }) + }) + + it('retains all handles if either bound shape is also translating', () => { + app.select(ids.arrow1, ids.box2) + expect(app.selectedPageBounds).toMatchObject({ + x: 200, + y: 200, + w: 200, + h: 200, + }) + app.pointerDown(300, 300, { target: 'selection' }) + app.pointerMove(300, 250) + app.expectShapeToMatch({ + id: ids.arrow1, + type: 'arrow', + x: 150, + y: 100, + props: { + start: { + type: 'binding', + isExact: false, + boundShapeId: ids.box1, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + end: { + type: 'binding', + isExact: false, + boundShapeId: ids.box2, + normalizedAnchor: { x: 0.5, y: 0.5 }, + }, + }, + }) + }) +}) + +describe('Other cases when arrow are moved', () => { + it('nudge', () => { + app.select(ids.arrow1, ids.box2) + + // When box one is not selected, unbinds box1 and keeps binding to box2 + app.nudgeShapes(app.selectedIds, { x: 0, y: -1 }) + + expect(app.getShapeById(ids.arrow1)).toMatchObject({ + props: { + start: { type: 'binding', boundShapeId: ids.box1 }, + end: { type: 'binding', boundShapeId: ids.box2 }, + }, + }) + + // unbinds when only the arrow is selected (not its bound shapes) + app.select(ids.arrow1) + app.nudgeShapes(app.selectedIds, { x: 0, y: -1 }) + + expect(app.getShapeById(ids.arrow1)).toMatchObject({ + props: { start: { type: 'point' }, end: { type: 'point' } }, + }) + }) + + it('align', () => { + app.createShapes([{ id: ids.box3, type: 'geo', x: 500, y: 300, props: { w: 100, h: 100 } }]) + + // When box one is not selected, unbinds box1 and keeps binding to box2 + app.select(ids.arrow1, ids.box2, ids.box3) + app.alignShapes('right') + jest.advanceTimersByTime(1000) + + expect(app.getShapeById(ids.arrow1)).toMatchObject({ + props: { + start: { type: 'binding', boundShapeId: ids.box1 }, + end: { type: 'binding', boundShapeId: ids.box2 }, + }, + }) + + // unbinds when only the arrow is selected (not its bound shapes) + app.select(ids.arrow1, ids.box3) + app.alignShapes('top') + jest.advanceTimersByTime(1000) + + expect(app.getShapeById(ids.arrow1)).toMatchObject({ + props: { + start: { + type: 'point', + }, + end: { + type: 'point', + }, + }, + }) + }) + + it('distribute', () => { + app.createShapes([ + { id: ids.box3, type: 'geo', x: 0, y: 300, props: { w: 100, h: 100 } }, + { id: ids.box4, type: 'geo', x: 0, y: 600, props: { w: 100, h: 100 } }, + ]) + + // When box one is not selected, unbinds box1 and keeps binding to box2 + app.select(ids.arrow1, ids.box2, ids.box3) + app.distributeShapes('horizontal') + jest.advanceTimersByTime(1000) + + expect(app.getShapeById(ids.arrow1)).toMatchObject({ + props: { + start: { + type: 'binding', + boundShapeId: ids.box1, + }, + end: { + type: 'binding', + boundShapeId: ids.box2, + }, + }, + }) + + // unbinds when only the arrow is selected (not its bound shapes) if the arrow itself has moved + app.select(ids.arrow1, ids.box3, ids.box4) + app.distributeShapes('vertical') + jest.advanceTimersByTime(1000) + + // The arrow didn't actually move + expect(app.getShapeById(ids.arrow1)).toMatchObject({ + props: { + start: { + type: 'binding', + boundShapeId: ids.box1, + }, + end: { + type: 'binding', + boundShapeId: ids.box2, + }, + }, + }) + + // The arrow will move this time, so it should unbind + app.updateShapes([{ id: ids.box4, type: 'geo', y: -600 }]) + app.distributeShapes('vertical') + jest.advanceTimersByTime(1000) + + expect(app.getShapeById(ids.arrow1)).toMatchObject({ + props: { + start: { + type: 'point', + }, + end: { + type: 'point', + }, + }, + }) + }) + + it('when translating with a group that the arrow is bound into', () => { + // create shapes in a group: + app + .selectAll() + .deleteShapes() + .createShapes([ + { id: ids.box3, type: 'geo', x: 0, y: 300, props: { w: 100, h: 100 } }, + { id: ids.box4, type: 'geo', x: 0, y: 600, props: { w: 100, h: 100 } }, + ]) + .selectAll() + .groupShapes() + + app.setSelectedTool('arrow').pointerDown(1000, 1000).pointerMove(50, 350).pointerUp(50, 350) + let arrow = app.shapesArray[app.shapesArray.length - 1] + assert(TLArrowShapeDef.is(arrow)) + assert(arrow.props.end.type === 'binding') + expect(arrow.props.end.boundShapeId).toBe(ids.box3) + + // translate: + app.selectAll().nudgeShapes(app.selectedIds, { x: 0, y: 1 }) + + // arrow should still be bound to box3 + arrow = app.getShapeById(arrow.id)! + assert(TLArrowShapeDef.is(arrow)) + assert(arrow.props.end.type === 'binding') + expect(arrow.props.end.boundShapeId).toBe(ids.box3) + }) +}) + +describe('When a shape it rotated', () => { + it('binds correctly', () => { + app.setSelectedTool('arrow').pointerDown(0, 0).pointerMove(375, 375) + + const arrow = app.shapesArray[app.shapesArray.length - 1] + + expect(app.getShapeById(arrow.id)).toMatchObject({ + props: { + start: { type: 'point' }, + end: { + type: 'binding', + boundShapeId: ids.box2, + normalizedAnchor: { x: 0.75, y: 0.75 }, // moving slowly + }, + }, + }) + + app.updateShapes([{ id: ids.box2, type: 'geo', rotation: TAU }]) + + app.pointerMove(225, 350) + + expect(app.getShapeById(arrow.id)).toMatchObject({ + props: { + start: { type: 'point' }, + end: { type: 'binding', boundShapeId: ids.box2 }, + }, + }) + + const anchor = ( + app.getShapeById(arrow.id)!.props.end as TLArrowTerminal & { type: 'binding' } + ).normalizedAnchor + expect(anchor.x).toBeCloseTo(0.5) + expect(anchor.y).toBeCloseTo(0.75) + }) +}) + +describe('resizing', () => { + it('resizes', () => { + app + .selectAll() + .deleteShapes() + .setSelectedTool('arrow') + .pointerDown(0, 0) + .pointerMove(200, 200) + .pointerUp() + .setSelectedTool('arrow') + .pointerDown(100, 100) + .pointerMove(300, 300) + .pointerUp() + .setSelectedTool('select') + + const arrow1 = app.shapesArray.at(-2)! + const arrow2 = app.shapesArray.at(-1)! + + app + .select(arrow1.id, arrow2.id) + .pointerDown(150, 300, { target: 'selection', handle: 'bottom' }) + .pointerMove(150, 600) + + .expectPathToBe('root.select.resizing') + + expect(app.getShapeById(arrow1.id)).toMatchObject({ + x: 0, + y: 0, + props: { + start: { + x: 0, + y: 0, + }, + end: { + x: 200, + y: 400, + }, + }, + }) + + expect(app.getShapeById(arrow2.id)).toMatchObject({ + x: 100, + y: 200, + props: { + start: { + x: 0, + y: 0, + }, + end: { + x: 200, + y: 400, + }, + }, + }) + }) + + it('flips bend when flipping x or y', () => { + app + .selectAll() + .deleteShapes() + .setSelectedTool('arrow') + .pointerDown(0, 0) + .pointerMove(200, 200) + .pointerUp() + .setSelectedTool('arrow') + .pointerDown(100, 100) + .pointerMove(300, 300) + .pointerUp() + .setSelectedTool('select') + + const arrow1 = app.shapesArray.at(-2)! + const arrow2 = app.shapesArray.at(-1)! + + app.updateShapes([{ id: arrow1.id, type: 'arrow', props: { bend: 50 } }]) + + app + .select(arrow1.id, arrow2.id) + .pointerDown(150, 300, { target: 'selection', handle: 'bottom' }) + .pointerMove(150, -300) + + .expectPathToBe('root.select.resizing') + + expect(app.getShapeById(arrow1.id)).toCloselyMatchObject({ + props: { + bend: -50, + }, + }) + + expect(app.getShapeById(arrow2.id)).toCloselyMatchObject({ + props: { + bend: 0, + }, + }) + + app.pointerMove(150, 300) + + expect(app.getShapeById(arrow1.id)).toCloselyMatchObject({ + props: { + bend: 50, + }, + }) + + expect(app.getShapeById(arrow2.id)).toCloselyMatchObject({ + props: { + bend: 0, + }, + }) + }) +}) + +describe("an arrow's parents", () => { + // Frame + // ┌───────────────────┐ + // │ ┌────┐ │ ┌────┐ + // │ │ A │ │ │ C │ + // │ └────┘ │ └────┘ + // │ │ + // │ │ + // │ ┌────┐ │ + // │ │ B │ │ + // │ └────┘ │ + // └───────────────────┘ + let frameId: TLShapeId + let boxAid: TLShapeId + let boxBid: TLShapeId + let boxCid: TLShapeId + + beforeEach(() => { + app.selectAll().deleteShapes() + + app.setSelectedTool('frame') + app.pointerDown(0, 0).pointerMove(100, 100).pointerUp() + frameId = app.onlySelectedShape!.id + + app.setSelectedTool('geo') + app.pointerDown(10, 10).pointerMove(20, 20).pointerUp() + boxAid = app.onlySelectedShape!.id + app.setSelectedTool('geo') + app.pointerDown(10, 80).pointerMove(20, 90).pointerUp() + boxBid = app.onlySelectedShape!.id + app.setSelectedTool('geo') + app.pointerDown(110, 10).pointerMove(120, 20).pointerUp() + boxCid = app.onlySelectedShape!.id + }) + + it("are updated when the arrow's bound shapes change", () => { + // draw arrow from a to empty space within frame, but don't pointer up yet + app.setSelectedTool('arrow') + app.pointerDown(15, 15).pointerMove(50, 50) + const arrowId = app.onlySelectedShape!.id + + expect(app.getShapeById(arrowId)).toMatchObject({ + props: { + start: { type: 'binding', boundShapeId: boxAid }, + end: { type: 'binding', boundShapeId: frameId }, + }, + }) + expect(app.getShapeById(arrowId)?.parentId).toBe(app.currentPageId) + + // move arrow to b + app.pointerMove(15, 85) + expect(app.getShapeById(arrowId)?.parentId).toBe(frameId) + expect(app.getShapeById(arrowId)).toMatchObject({ + props: { + start: { type: 'binding', boundShapeId: boxAid }, + end: { type: 'binding', boundShapeId: boxBid }, + }, + }) + + // move back to empty space + app.pointerMove(50, 50) + expect(app.getShapeById(arrowId)?.parentId).toBe(app.currentPageId) + expect(app.getShapeById(arrowId)).toMatchObject({ + props: { + start: { type: 'binding', boundShapeId: boxAid }, + end: { type: 'binding', boundShapeId: frameId }, + }, + }) + }) + + it('reparents when one of the shapes is moved outside of the frame', () => { + // draw arrow from a to b + app.setSelectedTool('arrow') + app.pointerDown(15, 15).pointerMove(15, 85).pointerUp() + const arrowId = app.onlySelectedShape!.id + + expect(app.getShapeById(arrowId)).toMatchObject({ + parentId: frameId, + props: { + start: { type: 'binding', boundShapeId: boxAid }, + end: { type: 'binding', boundShapeId: boxBid }, + }, + }) + // move b outside of frame + app.select(boxBid).translateSelection(200, 0) + expect(app.getShapeById(arrowId)).toMatchObject({ + parentId: app.currentPageId, + props: { + start: { type: 'binding', boundShapeId: boxAid }, + end: { type: 'binding', boundShapeId: boxBid }, + }, + }) + }) + + it('reparents to the frame when an arrow created outside has both its parents moved inside', () => { + // draw arrow from a to c + app.setSelectedTool('arrow') + app.pointerDown(15, 15).pointerMove(115, 15).pointerUp() + const arrowId = app.onlySelectedShape!.id + expect(app.getShapeById(arrowId)).toMatchObject({ + parentId: app.currentPageId, + props: { + start: { type: 'binding', boundShapeId: boxAid }, + end: { type: 'binding', boundShapeId: boxCid }, + }, + }) + + // move c inside of frame + app.select(boxCid).translateSelection(-40, 0) + + expect(app.getShapeById(arrowId)).toMatchObject({ + parentId: frameId, + props: { + start: { type: 'binding', boundShapeId: boxAid }, + end: { type: 'binding', boundShapeId: boxCid }, + }, + }) + }) +}) diff --git a/packages/editor/src/lib/app/shapeutils/TLArrowUtil/TLArrowUtil.tsx b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/TLArrowUtil.tsx new file mode 100644 index 000000000..23aa59524 --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/TLArrowUtil.tsx @@ -0,0 +1,1127 @@ +import { + Box2d, + getPointOnCircle, + linesIntersect, + longAngleDist, + Matrix2d, + shortAngleDist, + toDomPrecision, + Vec2d, + VecLike, +} from '@tldraw/primitives' +import { + arrowShapeMigrations, + arrowShapeTypeValidator, + TLArrowheadType, + TLArrowShape, + TLColorType, + TLFillType, + TLHandle, + TLShapeId, + TLShapePartial, + Vec2dModel, +} from '@tldraw/tlschema' +import { ComputedCache } from '@tldraw/tlstore' +import { deepCopy, last, minBy } from '@tldraw/utils' +import * as React from 'react' +import { computed, EMPTY_ARRAY } from 'signia' +import { SVGContainer } from '../../../components/SVGContainer' +import { defineShape } from '../../../config/TLShapeDefinition' +import { ARROW_LABEL_FONT_SIZES, FONT_FAMILIES, TEXT_PROPS } from '../../../constants' +import { getPerfectDashProps } from '../shared/getPerfectDashProps' +import { getTextSvgElement } from '../shared/getTextSvgElement' +import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill' +import { TLExportColors } from '../shared/TLExportColors' +import { + OnEditEndHandler, + OnHandleChangeHandler, + OnResizeHandler, + OnTranslateStartHandler, + TLShapeUtil, + TLShapeUtilFlag, +} from '../TLShapeUtil' +import { ArrowInfo } from './arrow/arrow-types' +import { getArrowheadPathForType } from './arrow/arrowheads' +import { + getCurvedArrowHandlePath, + getCurvedArrowInfo, + getSolidCurvedArrowPath, +} from './arrow/curved-arrow' +import { getArrowTerminalsInArrowSpace, getIsArrowStraight } from './arrow/shared' +import { + getSolidStraightArrowPath, + getStraightArrowHandlePath, + getStraightArrowInfo, +} from './arrow/straight-arrow' +import { ArrowTextLabel } from './components/ArrowTextLabel' + +let globalRenderIndex = 0 + +/** @public */ +export class TLArrowUtil extends TLShapeUtil { + static type = 'arrow' + + override canEdit = () => true + override canBind = () => false + override isClosed = () => false + override hideResizeHandles: TLShapeUtilFlag = () => true + override hideRotateHandle: TLShapeUtilFlag = () => true + override hideSelectionBoundsFg: TLShapeUtilFlag = () => true + override hideSelectionBoundsBg: TLShapeUtilFlag = () => true + + override defaultProps(): TLArrowShape['props'] { + return { + opacity: '1', + dash: 'draw', + size: 'm', + fill: 'none', + color: 'black', + labelColor: 'black', + bend: 0, + start: { type: 'point', x: 0, y: 0 }, + end: { type: 'point', x: 0, y: 0 }, + arrowheadStart: 'none', + arrowheadEnd: 'arrow', + text: '', + font: 'draw', + } + } + + getCenter(shape: TLArrowShape): Vec2d { + return this.bounds(shape).center + } + + getBounds(shape: TLArrowShape) { + return Box2d.FromPoints(this.getOutlineWithoutLabel(shape)) + } + + getOutlineWithoutLabel(shape: TLArrowShape) { + const info = this.getArrowInfo(shape) + + if (!info) { + return [] + } + + if (info.isStraight) { + if (info.isValid) { + return [info.start.point, info.end.point] + } else { + return [new Vec2d(0, 0), new Vec2d(1, 1)] + } + } + + if (!info.isValid) { + return [new Vec2d(0, 0), new Vec2d(1, 1)] + } + + const pointsToPush = Math.max(5, Math.ceil(Math.abs(info.bodyArc.length) / 16)) + + if (pointsToPush <= 0 && !isFinite(pointsToPush)) { + return [new Vec2d(0, 0), new Vec2d(1, 1)] + } + + const results: Vec2d[] = Array(pointsToPush) + + const startAngle = Vec2d.Angle(info.bodyArc.center, info.start.point) + const endAngle = Vec2d.Angle(info.bodyArc.center, info.end.point) + + const a = info.bodyArc.sweepFlag ? endAngle : startAngle + const b = info.bodyArc.sweepFlag ? startAngle : endAngle + const l = info.bodyArc.largeArcFlag ? -longAngleDist(a, b) : shortAngleDist(a, b) + + const r = Math.max(1, info.bodyArc.radius) + + for (let i = 0; i < pointsToPush; i++) { + const t = i / (pointsToPush - 1) + const angle = a + l * t + const point = getPointOnCircle(info.bodyArc.center.x, info.bodyArc.center.y, r, angle) + results[i] = point + } + + return results + } + + getOutline(shape: TLArrowShape): Vec2dModel[] { + const outlineWithoutLabel = this.getOutlineWithoutLabel(shape) + + const labelBounds = this.getLabelBounds(shape) + if (!labelBounds) { + return outlineWithoutLabel + } + + const sides = labelBounds.sides + const sideIndexes = [0, 1, 2, 3] + + // start with the first point... + let prevPoint = outlineWithoutLabel[0] + let didAddLabel = false + const result = [prevPoint] + for (let i = 1; i < outlineWithoutLabel.length; i++) { + // ...and use the next point to form a line segment for the outline. + const nextPoint = outlineWithoutLabel[i] + + if (!didAddLabel) { + // find the index of the side of the label bounds that intersects the line segment + const nearestIntersectingSideIndex = minBy( + sideIndexes.filter((sideIndex) => + linesIntersect(sides[sideIndex][0], sides[sideIndex][1], prevPoint, nextPoint) + ), + (sideIndex) => + Vec2d.DistanceToLineSegment(sides[sideIndex][0], sides[sideIndex][1], prevPoint) + ) + + // if we've found one, start at that index and trace around all four corners of the label bounds + if (nearestIntersectingSideIndex !== undefined) { + const intersectingPoint = Vec2d.NearestPointOnLineSegment( + sides[nearestIntersectingSideIndex][0], + sides[nearestIntersectingSideIndex][1], + prevPoint + ) + + result.push(intersectingPoint) + for (let j = 0; j < 4; j++) { + const sideIndex = (nearestIntersectingSideIndex + j) % 4 + result.push(sides[sideIndex][1]) + } + result.push(intersectingPoint) + + // we've added the label, so we can just continue with the rest of the outline as normal + didAddLabel = true + } + } + + result.push(nextPoint) + prevPoint = nextPoint + } + + return result + } + + snapPoints(_shape: TLArrowShape): Vec2d[] { + return EMPTY_ARRAY + } + + @computed + private get infoCache() { + return this.app.store.createComputedCache( + 'arrow infoCache', + (shape) => { + return getIsArrowStraight(shape) + ? getStraightArrowInfo(this.app, shape) + : getCurvedArrowInfo(this.app, shape) + } + ) + } + + getArrowInfo(shape: TLArrowShape) { + return this.infoCache.get(shape.id) + } + + getHandles(shape: TLArrowShape): TLHandle[] { + const info = this.infoCache.get(shape.id)! + return [ + { + id: 'start', + type: 'vertex', + index: 'a0', + x: info.start.handle.x, + y: info.start.handle.y, + canBind: true, + }, + { + id: 'middle', + type: 'vertex', + index: 'a2', + x: info.middle.x, + y: info.middle.y, + canBind: false, + }, + { + id: 'end', + type: 'vertex', + index: 'a3', + x: info.end.handle.x, + y: info.end.handle.y, + canBind: true, + }, + ] + } + + onHandleChange: OnHandleChangeHandler = (shape, { handle, isPrecise }) => { + const next = deepCopy(shape) + + switch (handle.id) { + case 'start': + case 'end': { + const pageTransform = this.app.getPageTransformById(next.id)! + const pointInPageSpace = Matrix2d.applyToPoint(pageTransform, handle) + + const target = this.app.inputs.ctrlKey + ? undefined + : last( + this.app.getShapesAtPoint(pointInPageSpace).filter((hitShape) => { + if (hitShape.id === shape.id) return + const util = this.app.getShapeUtil(hitShape) + + return ( + util.canBind(next) && + util.hitTestPoint( + hitShape, + this.app.getPointInShapeSpace(hitShape, pointInPageSpace) + ) + ) + }) + ) + + if (target) { + const targetBounds = this.app.getBounds(target) + const pointInTargetSpace = this.app.getPointInShapeSpace(target, pointInPageSpace) + + const prevHandle = next.props[handle.id] + + const startBindingId = + shape.props.start.type === 'binding' && shape.props.start.boundShapeId + const endBindingId = shape.props.end.type === 'binding' && shape.props.end.boundShapeId + + let precise = + // If externally precise, then always precise + isPrecise || + // If the other handle is bound to the same shape, then precise + ((startBindingId || endBindingId) && startBindingId === endBindingId) || + // If the other shape is not closed, then precise + !this.app.getShapeUtil(target).isClosed(next) + + if ( + // If we're switching to a new bound shape, then precise only if moving slowly + prevHandle.type === 'point' || + (prevHandle.type === 'binding' && target.id !== prevHandle.boundShapeId) + ) { + precise = this.app.inputs.pointerVelocity.len() < 0.5 + } + + if (precise) { + // Funky math but we want the snap distance to be 4 at the minimum and either + // 16 or 15% of the smaller dimension of the target shape, whichever is smaller + precise = + Vec2d.Dist(pointInTargetSpace, targetBounds.center) > + Math.max(4, Math.min(Math.min(targetBounds.width, targetBounds.height) * 0.15, 16)) / + this.app.zoomLevel + } + + next.props[handle.id] = { + type: 'binding', + boundShapeId: target.id, + normalizedAnchor: precise + ? { + x: (pointInTargetSpace.x - targetBounds.minX) / targetBounds.width, + y: (pointInTargetSpace.y - targetBounds.minY) / targetBounds.height, + } + : { x: 0.5, y: 0.5 }, + isExact: this.app.inputs.altKey, + } + } else { + next.props[handle.id] = { + type: 'point', + x: handle.x, + y: handle.y, + } + } + + break + } + + case 'middle': { + const { start, end } = getArrowTerminalsInArrowSpace(this.app, next) + + const delta = Vec2d.Sub(end, start) + const v = Vec2d.Per(delta) + + const med = Vec2d.Med(end, start) + const A = Vec2d.Sub(med, v) + const B = Vec2d.Add(med, v) + + const point = Vec2d.NearestPointOnLineSegment(A, B, handle, false) + let bend = Vec2d.Dist(point, med) + if (Vec2d.Clockwise(point, end, med)) bend *= -1 + next.props.bend = bend + break + } + } + + return next + } + + onTranslateStart: OnTranslateStartHandler = (shape) => { + let startBinding: TLShapeId | null = + shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : null + let endBinding: TLShapeId | null = + shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : null + + // If at least one bound shape is in the selection, do nothing; + // If no bound shapes are in the selection, unbind any bound shapes + + if ( + (startBinding && this.app.isWithinSelection(startBinding)) || + (endBinding && this.app.isWithinSelection(endBinding)) + ) { + return + } + + startBinding = null + endBinding = null + + const { start, end } = getArrowTerminalsInArrowSpace(this.app, shape) + + return { + id: shape.id, + type: shape.type, + props: { + ...shape.props, + start: { + type: 'point', + x: start.x, + y: start.y, + }, + end: { + type: 'point', + x: end.x, + y: end.y, + }, + }, + } + } + + onResize: OnResizeHandler = (shape, info) => { + const { scaleX, scaleY } = info + + const terminals = getArrowTerminalsInArrowSpace(this.app, shape) + + const { start, end } = deepCopy(shape.props) + let { bend } = shape.props + + // Rescale start handle if it's not bound to a shape + if (start.type === 'point') { + start.x = terminals.start.x * scaleX + start.y = terminals.start.y * scaleY + } + + // Rescale end handle if it's not bound to a shape + if (end.type === 'point') { + end.x = terminals.end.x * scaleX + end.y = terminals.end.y * scaleY + } + + // todo: we should only change the normalized anchor positions + // of the shape's handles if the bound shape is also being resized + + const mx = Math.abs(scaleX) + const my = Math.abs(scaleY) + + if (scaleX < 0 && scaleY >= 0) { + if (bend !== 0) { + bend *= -1 + bend *= Math.max(mx, my) + } + + if (start.type === 'binding') { + start.normalizedAnchor.x = 1 - start.normalizedAnchor.x + } + + if (end.type === 'binding') { + end.normalizedAnchor.x = 1 - end.normalizedAnchor.x + } + } else if (scaleX >= 0 && scaleY < 0) { + if (bend !== 0) { + bend *= -1 + bend *= Math.max(mx, my) + } + + if (start.type === 'binding') { + start.normalizedAnchor.y = 1 - start.normalizedAnchor.y + } + + if (end.type === 'binding') { + end.normalizedAnchor.y = 1 - end.normalizedAnchor.y + } + } else if (scaleX >= 0 && scaleY >= 0) { + if (bend !== 0) { + bend *= Math.max(mx, my) + } + } else if (scaleX < 0 && scaleY < 0) { + if (bend !== 0) { + bend *= Math.max(mx, my) + } + + if (start.type === 'binding') { + start.normalizedAnchor.x = 1 - start.normalizedAnchor.x + start.normalizedAnchor.y = 1 - start.normalizedAnchor.y + } + + if (end.type === 'binding') { + end.normalizedAnchor.x = 1 - end.normalizedAnchor.x + end.normalizedAnchor.y = 1 - end.normalizedAnchor.y + } + } + + const next = { + props: { + start, + end, + bend, + }, + } + + return next + } + + onDoubleClickHandle = ( + shape: TLArrowShape, + handle: TLHandle + ): TLShapePartial | void => { + switch (handle.id) { + case 'start': { + return { + id: shape.id, + type: shape.type, + props: { + ...shape.props, + arrowheadStart: shape.props.arrowheadStart === 'none' ? 'arrow' : 'none', + }, + } + } + case 'end': { + return { + id: shape.id, + type: shape.type, + props: { + ...shape.props, + arrowheadEnd: shape.props.arrowheadEnd === 'none' ? 'arrow' : 'none', + }, + } + } + } + } + + hitTestPoint(shape: TLArrowShape, point: VecLike): boolean { + const outline = this.outline(shape) + + for (let i = 0; i < outline.length - 1; i++) { + const C = outline[i] + const D = outline[i + 1] + + if (Vec2d.DistanceToLineSegment(C, D, point) < 4) return true + } + + return false + } + + hitTestLineSegment(shape: TLArrowShape, A: VecLike, B: VecLike): boolean { + const outline = this.outline(shape) + + for (let i = 0; i < outline.length - 1; i++) { + const C = outline[i] + const D = outline[i + 1] + if (linesIntersect(A, B, C, D)) return true + } + + return false + } + + render(shape: TLArrowShape) { + // Not a class component, but eslint can't tell that :( + const onlySelectedShape = this.app.onlySelectedShape + const shouldDisplayHandles = + this.app.isInAny( + 'select.idle', + 'select.pointing_handle', + 'select.dragging_handle', + 'arrow.dragging' + ) && !this.app.isReadOnly + + const info = this.getArrowInfo(shape) + const bounds = this.bounds(shape) + const labelSize = this.getLabelBounds(shape) + + // eslint-disable-next-line react-hooks/rules-of-hooks + const changeIndex = React.useMemo(() => { + return this.app.isSafari ? (globalRenderIndex += 1) : 0 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shape]) + + if (!info?.isValid) return null + + const strokeWidth = this.app.getStrokeWidth(shape.props.size) + + const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth) + const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth) + + const path = info.isStraight ? getSolidStraightArrowPath(info) : getSolidCurvedArrowPath(info) + + let handlePath: null | JSX.Element = null + + if (onlySelectedShape === shape && shouldDisplayHandles) { + const sw = 2 + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + info.isStraight + ? Vec2d.Dist(info.start.handle, info.end.handle) + : Math.abs(info.handleArc.length), + sw, + { + end: 'skip', + start: 'skip', + lengthRatio: 2.5, + } + ) + + handlePath = + shape.props.start.type === 'binding' || shape.props.end.type === 'binding' ? ( + + ) : null + } + + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + info.isStraight ? info.length : Math.abs(info.bodyArc.length), + strokeWidth, + { + style: shape.props.dash, + } + ) + + const maskStartArrowhead = !( + info.start.arrowhead === 'none' || info.start.arrowhead === 'arrow' + ) + const maskEndArrowhead = !(info.end.arrowhead === 'none' || info.end.arrowhead === 'arrow') + const includeMask = maskStartArrowhead || maskEndArrowhead || labelSize + + // NOTE: I know right setting `changeIndex` hacky-as right! But we need this because otherwise safari loses + // the mask, see + const maskId = (shape.id + '_clip_' + changeIndex).replace(':', '_') + + return ( + <> + + {includeMask && ( + + + + {labelSize && ( + + )} + {as && maskStartArrowhead && ( + + )} + {ae && maskEndArrowhead && ( + + )} + + + )} + + {handlePath} + {/* firefox will clip if you provide a maskURL even if there is no mask matching that URL in the DOM */} + + {/* This rect needs to be here if we're creating a mask due to an svg quirk on Chrome */} + {includeMask && ( + + )} + + + {as && maskStartArrowhead && shape.props.fill !== 'none' && ( + + )} + {ae && maskEndArrowhead && shape.props.fill !== 'none' && ( + + )} + {as && } + {ae && } + + + + + + ) + } + + indicator(shape: TLArrowShape) { + const { start, end } = getArrowTerminalsInArrowSpace(this.app, shape) + + const info = this.getArrowInfo(shape) + const bounds = this.bounds(shape) + const labelSize = this.getLabelBounds(shape) + + if (!info) return null + if (Vec2d.Equals(start, end)) return null + + const strokeWidth = this.app.getStrokeWidth(shape.props.size) + + const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth) + const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth) + + const path = info.isStraight ? getSolidStraightArrowPath(info) : getSolidCurvedArrowPath(info) + + const includeMask = + (as && info.start.arrowhead !== 'arrow') || + (ae && info.end.arrowhead !== 'arrow') || + labelSize !== null + + const maskId = (shape.id + '_clip').replace(':', '_') + + return ( + + {includeMask && ( + + + + {labelSize && ( + + )} + {as && ( + + )} + {ae && ( + + )} + + + )} + {/* firefox will clip if you provide a maskURL even if there is no mask matching that URL in the DOM */} + + {/* This rect needs to be here if we're creating a mask due to an svg quirk on Chrome */} + {includeMask && ( + + )} + + + + {as && } + {ae && } + {labelSize && ( + + )} + + ) + } + + @computed get labelBoundsCache(): ComputedCache { + return this.app.store.createComputedCache('labelBoundsCache', (shape) => { + const info = this.getArrowInfo(shape) + const bounds = this.bounds(shape) + const { text, font, size } = shape.props + + if (!info) return null + if (!text.trim()) return null + + const { w, h } = this.app.textMeasure.measureText({ + ...TEXT_PROPS, + text, + fontFamily: FONT_FAMILIES[font], + fontSize: ARROW_LABEL_FONT_SIZES[size], + width: 'fit-content', + }) + + let width = w + let height = h + + if (bounds.width > bounds.height) { + width = Math.max(Math.min(w, 64), Math.min(bounds.width - 64, w)) + + const { w: squishedWidth, h: squishedHeight } = this.app.textMeasure.measureText({ + ...TEXT_PROPS, + text, + fontFamily: FONT_FAMILIES[font], + fontSize: ARROW_LABEL_FONT_SIZES[size], + width: width + 'px', + }) + + width = squishedWidth + height = squishedHeight + } + + if (width > 16 * ARROW_LABEL_FONT_SIZES[size]) { + width = 16 * ARROW_LABEL_FONT_SIZES[size] + + const { w: squishedWidth, h: squishedHeight } = this.app.textMeasure.measureText({ + ...TEXT_PROPS, + text, + fontFamily: FONT_FAMILIES[font], + fontSize: ARROW_LABEL_FONT_SIZES[size], + width: width + 'px', + }) + + width = squishedWidth + height = squishedHeight + } + + return new Box2d( + info.middle.x - (width + 8) / 2, + info.middle.y - (height + 8) / 2, + width + 8, + height + 8 + ) + }) + } + + getLabelBounds(shape: TLArrowShape): Box2d | null { + return this.labelBoundsCache.get(shape.id) || null + } + + getEditingBounds = (shape: TLArrowShape): Box2d => { + return this.getLabelBounds(shape) ?? new Box2d() + } + + onEditEnd: OnEditEndHandler = (shape) => { + const { + id, + type, + props: { text }, + } = shape + + if (text.trim() !== shape.props.text) { + this.app.updateShapes([ + { + id, + type, + props: { + text: text.trim(), + }, + }, + ]) + } + } + + toSvg(shape: TLArrowShape, font: string, colors: TLExportColors) { + const color = colors.fill[shape.props.color] + + const info = this.getArrowInfo(shape) + + const strokeWidth = this.app.getStrokeWidth(shape.props.size) + + // Group for arrow + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g') + if (!info) return g + + // Arrowhead start path + const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth) + // Arrowhead end path + const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth) + + const bounds = this.bounds(shape) + const labelSize = this.getLabelBounds(shape) + + const maskId = (shape.id + '_clip').replace(':', '_') + + // If we have any arrowheads, then mask the arrowheads + if (as || ae) { + // Create mask for arrowheads + + // Create defs + const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs') + + // Create mask + const mask = document.createElementNS('http://www.w3.org/2000/svg', 'mask') + mask.id = maskId + + // Create large white shape for mask + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') + rect.setAttribute('x', bounds.minX - 100 + '') + rect.setAttribute('y', bounds.minY - 100 + '') + rect.setAttribute('width', bounds.width + 200 + '') + rect.setAttribute('height', bounds.height + 200 + '') + rect.setAttribute('fill', 'white') + mask.appendChild(rect) + + // add arrowhead start mask + if (as) mask.appendChild(getArrowheadSvgMask(as, info.start.arrowhead)) + + // add arrowhead end mask + if (ae) mask.appendChild(getArrowheadSvgMask(ae, info.end.arrowhead)) + + // Mask out text label if text is present + if (labelSize) { + const labelMask = document.createElementNS('http://www.w3.org/2000/svg', 'rect') + labelMask.setAttribute('x', labelSize.x + '') + labelMask.setAttribute('y', labelSize.y + '') + labelMask.setAttribute('width', labelSize.w + '') + labelMask.setAttribute('height', labelSize.h + '') + labelMask.setAttribute('fill', 'black') + + mask.appendChild(labelMask) + } + + defs.appendChild(mask) + g.appendChild(defs) + } + + const g2 = document.createElementNS('http://www.w3.org/2000/svg', 'g') + g2.setAttribute('mask', `url(#${maskId})`) + g.appendChild(g2) + + // Dumb mask fix thing + const rect2 = document.createElementNS('http://www.w3.org/2000/svg', 'rect') + rect2.setAttribute('x', '-100') + rect2.setAttribute('y', '-100') + rect2.setAttribute('width', bounds.width + 200 + '') + rect2.setAttribute('height', bounds.height + 200 + '') + rect2.setAttribute('fill', 'transparent') + rect2.setAttribute('stroke', 'none') + g2.appendChild(rect2) + + // Arrowhead body path + const path = getArrowSvgPath( + info.isStraight ? getSolidStraightArrowPath(info) : getSolidCurvedArrowPath(info), + color, + strokeWidth + ) + + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + info.isStraight ? info.length : Math.abs(info.bodyArc.length), + strokeWidth, + { + style: shape.props.dash, + } + ) + + path.setAttribute('stroke-dasharray', strokeDasharray) + path.setAttribute('stroke-dashoffset', strokeDashoffset) + + g2.appendChild(path) + + // Arrowhead start path + if (as) { + g.appendChild( + getArrowheadSvgPath( + as, + shape.props.color, + strokeWidth, + shape.props.arrowheadStart === 'arrow' ? 'none' : shape.props.fill, + colors + ) + ) + } + // Arrowhead end path + if (ae) { + g.appendChild( + getArrowheadSvgPath( + ae, + shape.props.color, + strokeWidth, + shape.props.arrowheadEnd === 'arrow' ? 'none' : shape.props.fill, + colors + ) + ) + } + + // Text Label + if (labelSize) { + const opts = { + fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size], + lineHeight: TEXT_PROPS.lineHeight, + fontFamily: font, + padding: 0, + textAlign: 'middle' as const, + width: labelSize.w, + height: labelSize.h, + fontStyle: 'normal', + fontWeight: 'normal', + } + + const lines = this.app.textMeasure.getTextLines({ + text: shape.props.text, + wrap: true, + ...opts, + width: labelSize.w - 8, + }) + + const textElm = getTextSvgElement(this.app, { + lines, + ...opts, + width: labelSize.w - 8, + }) + + textElm.setAttribute('fill', colors.fill[shape.props.labelColor]) + + const children = Array.from(textElm.children) as unknown as SVGTSpanElement[] + + children.forEach((child) => { + const x = parseFloat(child.getAttribute('x') || '0') + const y = parseFloat(child.getAttribute('y') || '0') + + child.setAttribute('x', x + 4 + labelSize!.x + 'px') + child.setAttribute('y', y + labelSize!.y + 'px') + }) + + g.appendChild(textElm) + } + + return g + } +} + +function getArrowheadSvgMask(d: string, arrowhead: TLArrowheadType) { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') + path.setAttribute('d', d) + path.setAttribute('fill', arrowhead === 'arrow' ? 'none' : 'black') + path.setAttribute('stroke', 'none') + return path +} + +function getArrowSvgPath(d: string, color: string, strokeWidth: number) { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') + path.setAttribute('d', d) + path.setAttribute('fill', 'none') + path.setAttribute('stroke', color) + path.setAttribute('stroke-width', strokeWidth + '') + return path +} + +function getArrowheadSvgPath( + d: string, + color: TLColorType, + strokeWidth: number, + fill: TLFillType, + colors: TLExportColors +) { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') + path.setAttribute('d', d) + path.setAttribute('fill', 'none') + path.setAttribute('stroke', colors.fill[color]) + path.setAttribute('stroke-width', strokeWidth + '') + + // Get the fill element, if any + const shapeFill = getShapeFillSvg({ + d, + fill, + color, + colors, + }) + + if (shapeFill) { + // If there is a fill element, return a group containing the fill and the path + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g') + g.appendChild(shapeFill) + g.appendChild(path) + return g + } else { + // Otherwise, just return the path + return path + } +} + +function isPrecise(normalizedAnchor: Vec2dModel) { + return normalizedAnchor.x !== 0.5 || normalizedAnchor.y !== 0.5 +} + +/** @public */ +export const TLArrowShapeDef = defineShape({ + type: 'arrow', + getShapeUtil: () => TLArrowUtil, + validator: arrowShapeTypeValidator, + migrations: arrowShapeMigrations, +}) diff --git a/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/arrow-types.ts b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/arrow-types.ts new file mode 100644 index 000000000..4db802739 --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/arrow-types.ts @@ -0,0 +1,36 @@ +import { VecLike } from '@tldraw/primitives' +import { TLArrowheadType } from '@tldraw/tlschema' + +export type ArrowPoint = { + handle: VecLike + point: VecLike + arrowhead: TLArrowheadType +} + +export interface ArcInfo { + center: VecLike + radius: number + size: number + length: number + largeArcFlag: number + sweepFlag: number +} + +export type ArrowInfo = + | { + isStraight: false + start: ArrowPoint + end: ArrowPoint + middle: VecLike + handleArc: ArcInfo + bodyArc: ArcInfo + isValid: boolean + } + | { + isStraight: true + start: ArrowPoint + end: ArrowPoint + middle: VecLike + isValid: boolean + length: number + } diff --git a/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/arrowheads.ts b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/arrowheads.ts new file mode 100644 index 000000000..93977f482 --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/arrowheads.ts @@ -0,0 +1,138 @@ +import { intersectCircleCircle, PI, TAU, Vec2d, VecLike } from '@tldraw/primitives' +import { ArrowInfo } from './arrow-types' + +type TLArrowPointsInfo = { + point: VecLike + int: VecLike +} + +function getArrowPoints( + info: ArrowInfo, + side: 'start' | 'end', + strokeWidth: number +): TLArrowPointsInfo { + const PT = side === 'end' ? info.end.point : info.start.point + const PB = side === 'end' ? info.start.point : info.end.point + + const compareLength = info.isStraight ? Vec2d.Dist(PB, PT) : Math.abs(info.bodyArc.length) // todo: arc length for curved arrows + + const length = Math.max(Math.min(compareLength / 5, strokeWidth * 3), strokeWidth) + + let P0: VecLike + + if (info.isStraight) { + P0 = Vec2d.Nudge(PT, PB, length) + } else { + const ints = intersectCircleCircle(PT, length, info.handleArc.center, info.handleArc.radius) + P0 = + side === 'end' + ? info.handleArc.sweepFlag + ? ints[0] + : ints[1] + : info.handleArc.sweepFlag + ? ints[1] + : ints[0] + } + + return { + point: PT, + int: P0, + } +} + +export function getArrowhead({ point, int }: TLArrowPointsInfo) { + const PL = Vec2d.RotWith(int, point, PI / 6) + const PR = Vec2d.RotWith(int, point, -PI / 6) + + return `M ${PL.x} ${PL.y} L ${point.x} ${point.y} L ${PR.x} ${PR.y}` +} + +export function getTriangleHead({ point, int }: TLArrowPointsInfo) { + const PL = Vec2d.RotWith(int, point, PI / 6) + const PR = Vec2d.RotWith(int, point, -PI / 6) + + return `M ${PL.x} ${PL.y} L ${point.x} ${point.y} L ${PR.x} ${PR.y} Z` +} + +export function getInvertedTriangleHead({ point, int }: TLArrowPointsInfo) { + const d = Vec2d.Sub(int, point).div(2) + const PL = Vec2d.Add(point, Vec2d.Rot(d, TAU)) + const PR = Vec2d.Sub(point, Vec2d.Rot(d, TAU)) + + return `M ${PL.x} ${PL.y} L ${int.x} ${int.y} L ${PR.x} ${PR.y} Z` +} + +export function getDotHead({ point, int }: TLArrowPointsInfo) { + const A = Vec2d.Lrp(point, int, 0.45) + const r = Vec2d.Dist(A, point) + + return `M ${A.x - r},${A.y} + a ${r},${r} 0 1,0 ${r * 2},0 + a ${r},${r} 0 1,0 -${r * 2},0 ` +} + +export function getDiamondHead({ point, int }: TLArrowPointsInfo) { + const PB = Vec2d.Lrp(point, int, 0.75) + const PL = Vec2d.RotWith(PB, point, PI / 4) + const PR = Vec2d.RotWith(PB, point, -PI / 4) + + const PQ = Vec2d.Lrp(PL, PR, 0.5) + PQ.add(Vec2d.Sub(PQ, point)) + + return `M ${PQ.x} ${PQ.y} L ${PL.x} ${PL.y} ${point.x} ${point.y} L ${PR.x} ${PR.y} Z` +} + +export function getSquareHead({ int, point }: TLArrowPointsInfo) { + const PB = Vec2d.Lrp(point, int, 0.85) + const d = Vec2d.Sub(PB, point).div(2) + const PL1 = Vec2d.Add(point, Vec2d.Rot(d, TAU)) + const PR1 = Vec2d.Sub(point, Vec2d.Rot(d, TAU)) + const PL2 = Vec2d.Add(PB, Vec2d.Rot(d, TAU)) + const PR2 = Vec2d.Sub(PB, Vec2d.Rot(d, TAU)) + + return `M ${PL1.x} ${PL1.y} L ${PL2.x} ${PL2.y} L ${PR2.x} ${PR2.y} L ${PR1.x} ${PR1.y} Z` +} + +export function getBarHead({ int, point }: TLArrowPointsInfo) { + const d = Vec2d.Sub(int, point).div(2) + + const PL = Vec2d.Add(point, Vec2d.Rot(d, TAU)) + const PR = Vec2d.Sub(point, Vec2d.Rot(d, TAU)) + + return `M ${PL.x} ${PL.y} L ${PR.x} ${PR.y}` +} + +export function getPipeHead() { + return '' +} + +export function getArrowheadPathForType( + info: ArrowInfo, + side: 'start' | 'end', + strokeWidth: number +): string | undefined { + const type = side === 'end' ? info.end.arrowhead : info.start.arrowhead + if (type === 'none') return + + const points = getArrowPoints(info, side, strokeWidth) + if (!points) return + + switch (type) { + case 'bar': + return getBarHead(points) + case 'square': + return getSquareHead(points) + case 'diamond': + return getDiamondHead(points) + case 'dot': + return getDotHead(points) + case 'inverted': + return getInvertedTriangleHead(points) + case 'arrow': + return getArrowhead(points) + case 'triangle': + return getTriangleHead(points) + } + + return '' +} diff --git a/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/curved-arrow.ts b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/curved-arrow.ts new file mode 100644 index 000000000..7a1430ad6 --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/curved-arrow.ts @@ -0,0 +1,410 @@ +import { + Box2d, + getArcLength, + getPointOnCircle, + intersectCirclePolygon, + intersectCirclePolyline, + lerpAngles, + Matrix2d, + PI, + PI2, + shortAngleDist, + Vec2d, + VecLike, +} from '@tldraw/primitives' +import { TLArrowShape } from '@tldraw/tlschema' +import { + BOUND_ARROW_OFFSET, + MIN_ARROW_LENGTH, + WAY_TOO_BIG_ARROW_BEND_FACTOR, +} from '../../../../constants' +import type { App } from '../../../App' +import { ArcInfo, ArrowInfo } from './arrow-types' +import { getArrowTerminalsInArrowSpace, getBoundShapeInfoForTerminal } from './shared' +import { getStraightArrowInfo } from './straight-arrow' + +export function getCurvedArrowInfo(app: App, shape: TLArrowShape, extraBend = 0): ArrowInfo { + const { arrowheadEnd, arrowheadStart } = shape.props + const bend = shape.props.bend + extraBend + + if (Math.abs(bend) > Math.abs(shape.props.bend * WAY_TOO_BIG_ARROW_BEND_FACTOR)) { + return getStraightArrowInfo(app, shape) + } + + const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(app, shape) + + const med = Vec2d.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end) // point between start and end + const u = Vec2d.Sub(terminalsInArrowSpace.end, terminalsInArrowSpace.start).uni() // unit vector between start and end + const middle = Vec2d.Add(med, u.per().mul(-bend)) // middle handle + + const startShapeInfo = getBoundShapeInfoForTerminal(app, shape.props.start) + const endShapeInfo = getBoundShapeInfoForTerminal(app, shape.props.end) + + // The positions of the body of the arrow, which may be different + // than the arrow's start / end points if the arrow is bound to shapes + const a = terminalsInArrowSpace.start.clone() + const b = terminalsInArrowSpace.end.clone() + const c = middle.clone() + + const handleArc = getArcInfo(a, b, c) + + const arrowPageTransform = app.getPageTransform(shape)! + + if (startShapeInfo && !startShapeInfo.isExact) { + // Points in page space + const startInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, a) + const endInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, b) + const centerInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, handleArc.center) + + // Points in local space of the start shape + const inverseTransform = Matrix2d.Inverse(startShapeInfo.transform) + const startInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, startInPageSpace) + const endInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace) + const centerInStartShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace) + + const isClosed = startShapeInfo.util.isClosed(startShapeInfo.shape) + const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline + + let point: VecLike | undefined + + let intersections = fn( + centerInStartShapeLocalSpace, + handleArc.radius, + startShapeInfo.util.outline(startShapeInfo.shape) + ) + + if (intersections) { + intersections = intersections.filter( + (pt) => + +Vec2d.Clockwise(startInStartShapeLocalSpace, pt, endInStartShapeLocalSpace) === + handleArc.sweepFlag + ) + + const angleToMiddle = Vec2d.Angle(handleArc.center, middle) + const angleToStart = Vec2d.Angle(handleArc.center, terminalsInArrowSpace.start) + const comparisonAngle = lerpAngles(angleToMiddle, angleToStart, 0.5) + + intersections.sort( + (p0, p1) => + Math.abs(shortAngleDist(comparisonAngle, centerInStartShapeLocalSpace.angle(p0))) - + Math.abs(shortAngleDist(comparisonAngle, centerInStartShapeLocalSpace.angle(p1))) + ) + + point = intersections[0] ?? (isClosed ? undefined : startInStartShapeLocalSpace) + } else { + point = isClosed ? undefined : startInStartShapeLocalSpace + } + + if (point) { + a.setTo( + app.getPointInShapeSpace(shape, Matrix2d.applyToPoint(startShapeInfo.transform, point)) + ) + + startShapeInfo.didIntersect = true + + if (arrowheadStart !== 'none') { + const offset = + BOUND_ARROW_OFFSET + + app.getStrokeWidth(shape.props.size) / 2 + + ('size' in startShapeInfo.shape.props + ? app.getStrokeWidth(startShapeInfo.shape.props.size) / 2 + : 0) + + a.setTo( + getPointOnCircle( + handleArc.center.x, + handleArc.center.y, + handleArc.radius, + lerpAngles( + Vec2d.Angle(handleArc.center, a), + Vec2d.Angle(handleArc.center, middle), + offset / Math.abs(getArcLength(handleArc.center, handleArc.radius, a, middle)) + ) + ) + ) + } + } + } + + if (endShapeInfo && !endShapeInfo.isExact) { + // get points in shape's coordinates? + const startInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, a) + const endInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, b) + const centerInPageSpace = Matrix2d.applyToPoint(arrowPageTransform, handleArc.center) + + const inverseTransform = Matrix2d.Inverse(endShapeInfo.transform) + + const startInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, startInPageSpace) + const endInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, endInPageSpace) + const centerInEndShapeLocalSpace = Matrix2d.applyToPoint(inverseTransform, centerInPageSpace) + + const isClosed = endShapeInfo.util.isClosed(endShapeInfo.shape) + const fn = isClosed ? intersectCirclePolygon : intersectCirclePolyline + + const angleToMiddle = Vec2d.Angle(handleArc.center, middle) + const angleToEnd = Vec2d.Angle(handleArc.center, terminalsInArrowSpace.end) + const comparisonAngle = lerpAngles(angleToMiddle, angleToEnd, 0.5) + + let point: VecLike | undefined + + let intersections = fn( + centerInEndShapeLocalSpace, + handleArc.radius, + endShapeInfo.util.outline(endShapeInfo.shape) + ) + + if (intersections) { + intersections = intersections.filter( + (pt) => + +Vec2d.Clockwise(startInEndShapeLocalSpace, pt, endInEndShapeLocalSpace) === + handleArc.sweepFlag + ) + + intersections.sort( + (p0, p1) => + Math.abs(shortAngleDist(comparisonAngle, centerInEndShapeLocalSpace.angle(p0))) - + Math.abs(shortAngleDist(comparisonAngle, centerInEndShapeLocalSpace.angle(p1))) + ) + + point = intersections[0] ?? (isClosed ? undefined : endInEndShapeLocalSpace) + } else { + point = isClosed ? undefined : endInEndShapeLocalSpace + } + + if (point) { + // Set b to target local point -> page point -> shape local point + b.setTo(app.getPointInShapeSpace(shape, Matrix2d.applyToPoint(endShapeInfo.transform, point))) + + endShapeInfo.didIntersect = true + + if (arrowheadEnd !== 'none') { + let offset = + BOUND_ARROW_OFFSET + + app.getStrokeWidth(shape.props.size) / 2 + + ('size' in endShapeInfo.shape.props + ? app.getStrokeWidth(endShapeInfo.shape.props.size) / 2 + : 0) + + if (Vec2d.Dist(a, b) < MIN_ARROW_LENGTH) { + offset *= -2 + } + + b.setTo( + getPointOnCircle( + handleArc.center.x, + handleArc.center.y, + handleArc.radius, + lerpAngles( + Vec2d.Angle(handleArc.center, b), + Vec2d.Angle(handleArc.center, middle), + offset / Math.abs(getArcLength(handleArc.center, handleArc.radius, b, middle)) + ) + ) + ) + } + } + } + + const length = Math.abs(getArcLength(handleArc.center, handleArc.radius, a, b)) + + if (length < MIN_ARROW_LENGTH / 2) { + a.setTo(terminalsInArrowSpace.start) + b.setTo(terminalsInArrowSpace.end) + } + + if ( + startShapeInfo && + endShapeInfo && + startShapeInfo.shape !== endShapeInfo.shape && + !startShapeInfo.isExact && + !endShapeInfo.isExact + ) { + // If we missed an intersection, then try + const startAngle = Vec2d.Angle(handleArc.center, a) + const endAngle = Vec2d.Angle(handleArc.center, b) + + const offset = handleArc.sweepFlag ? MIN_ARROW_LENGTH : -MIN_ARROW_LENGTH + const arcLength = getArcLength(handleArc.center, handleArc.radius, b, a) + const { + center: { x, y }, + radius, + } = handleArc + + if (startShapeInfo && !startShapeInfo.didIntersect) { + a.setTo(getPointOnCircle(x, y, radius, lerpAngles(startAngle, endAngle, offset / arcLength))) + } + + if (endShapeInfo && !endShapeInfo.didIntersect) { + b.setTo(getPointOnCircle(x, y, radius, lerpAngles(startAngle, endAngle, -offset / arcLength))) + } + } + + let midAngle = lerpAngles(Vec2d.Angle(handleArc.center, a), Vec2d.Angle(handleArc.center, b), 0.5) + let midPoint = getPointOnCircle( + handleArc.center.x, + handleArc.center.y, + handleArc.radius, + midAngle + ) + + if (+Vec2d.Clockwise(a, midPoint, b) !== handleArc.sweepFlag) { + midAngle += PI + midPoint = getPointOnCircle(handleArc.center.x, handleArc.center.y, handleArc.radius, midAngle) + } + + c.setTo(midPoint) + + const bodyArc = getArcInfo(a, b, c) + + return { + isStraight: false, + start: { + point: a, + handle: terminalsInArrowSpace.start, + arrowhead: shape.props.arrowheadStart, + }, + end: { + point: b, + handle: terminalsInArrowSpace.end, + arrowhead: shape.props.arrowheadEnd, + }, + middle: c, + handleArc, + bodyArc, + isValid: bodyArc.length !== 0 && isFinite(bodyArc.center.x) && isFinite(bodyArc.center.y), + } +} + +/** + * Get a solid path for a curved arrow's handles. + * + * @param info - The arrow info. + */ +export function getCurvedArrowHandlePath(info: ArrowInfo & { isStraight: false }) { + const { + start, + end, + handleArc: { radius, largeArcFlag, sweepFlag }, + } = info + return `M${start.handle.x},${start.handle.y} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.handle.x},${end.handle.y}` +} + +/** + * Get a solid path for a curved arrow's body. + * + * @param info - The arrow info. + */ +export function getSolidCurvedArrowPath(info: ArrowInfo & { isStraight: false }) { + const { + start, + end, + bodyArc: { radius, largeArcFlag, sweepFlag }, + } = info + return `M${start.point.x},${start.point.y} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end.point.x},${end.point.y}` +} + +/** + * Get a point along an arc. + * + * @param center - The arc's center. + * @param radius - The arc's radius. + * @param startAngle - The start point of the arc. + * @param size - The size of the arc. + * @param t - The point along the arc to get. + */ +export function getPointOnArc( + center: VecLike, + radius: number, + startAngle: number, + size: number, + t: number +) { + const angle = startAngle + size * t + return new Vec2d(center.x + radius * Math.cos(angle), center.y + radius * Math.sin(angle)) +} + +/** + * Get a bounding box for an arc. + * + * @param center - The arc's center. + * @param radius - The arc's radius. + * @param start - The start point of the arc. + * @param size - The size of the arc. + */ +export function getArcBoundingBox(center: VecLike, radius: number, start: VecLike, size: number) { + let minX = Infinity + let minY = Infinity + let maxX = -Infinity + let maxY = -Infinity + + const startAngle = Vec2d.Angle(center, start) + + // Test 20 points along the arc + for (let i = 0; i < 20; i++) { + const angle = startAngle + size * (i / 19) + const x = center.x + radius * Math.cos(angle) + const y = center.y + radius * Math.sin(angle) + + minX = Math.min(x, minX) + minY = Math.min(y, minY) + maxX = Math.max(x, maxX) + maxY = Math.max(y, maxY) + } + + return new Box2d(minX, minY, maxX - minX, maxY - minY) +} + +/** + * Get info about an arc formed by three points. + * + * @param a - The start of the arc + * @param b - The end of the arc + * @param c - A point on the arc + */ +export function getArcInfo(a: VecLike, b: VecLike, c: VecLike): ArcInfo { + // find a circle from the three points + const u = -2 * (a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y) + + const center = { + x: + ((a.x * a.x + a.y * a.y) * (c.y - b.y) + + (b.x * b.x + b.y * b.y) * (a.y - c.y) + + (c.x * c.x + c.y * c.y) * (b.y - a.y)) / + u, + y: + ((a.x * a.x + a.y * a.y) * (b.x - c.x) + + (b.x * b.x + b.y * b.y) * (c.x - a.x) + + (c.x * c.x + c.y * c.y) * (a.x - b.x)) / + u, + } + + const radius = Vec2d.Dist(center, a) + + // Whether to draw the arc clockwise or counter-clockwise (are the points clockwise?) + const sweepFlag = +Vec2d.Clockwise(a, c, b) + + // The base angle of the arc in radians + const ab = Math.hypot(a.y - b.y, a.x - b.x) + const bc = Math.hypot(b.y - c.y, b.x - c.x) + const ca = Math.hypot(c.y - a.y, c.x - a.x) + + const theta = Math.acos((bc * bc + ca * ca - ab * ab) / (2 * bc * ca)) * 2 + + // Whether to draw the long arc or short arc + const largeArcFlag = +(PI > theta) + + // The size of the arc to draw in radians + const size = (PI2 - theta) * (sweepFlag ? 1 : -1) + + // The length of the arc to draw in distance units + const length = size * radius + + return { + center, + radius, + size, + length, + largeArcFlag, + sweepFlag, + } +} diff --git a/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/shared.ts b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/shared.ts new file mode 100644 index 000000000..de063d121 --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/shared.ts @@ -0,0 +1,74 @@ +import { Matrix2d, Vec2d } from '@tldraw/primitives' +import { TLArrowShape, TLArrowTerminal, TLShape } from '@tldraw/tlschema' +import { App } from '../../../App' +import { TLShapeUtil } from '../../TLShapeUtil' + +export function getIsArrowStraight(shape: TLArrowShape) { + return Math.abs(shape.props.bend) < 8 // snap to +-8px +} + +export type BoundShapeInfo = { + shape: T + util: TLShapeUtil + didIntersect: boolean + isExact: boolean + transform: Matrix2d + // toLocalPoint: (v: VecLike) => Vec2d + // toPagePoint: (v: VecLike) => Vec2d +} + +export function getBoundShapeInfoForTerminal( + app: App, + terminal: TLArrowTerminal +): BoundShapeInfo | undefined { + if (terminal.type === 'point') { + return + } + + const shape = app.getShapeById(terminal.boundShapeId)! + const util = app.getShapeUtil(shape) + const transform = app.getPageTransform(shape)! + + return { + shape, + util, + transform, + isExact: terminal.isExact, + didIntersect: false, + } +} + +export function getArrowTerminalInArrowSpace( + app: App, + arrowPageTransform: Matrix2d, + terminal: TLArrowTerminal +) { + if (terminal.type === 'point') { + return Vec2d.From(terminal) + } + + const boundShape = app.getShapeById(terminal.boundShapeId) + + if (!boundShape) { + console.error('Expected a bound shape!') + return new Vec2d(0, 0) + } else { + // Find the actual local point of the normalized terminal on + // the bound shape and transform it to page space, then transform + // it to arrow space + const { point, size } = app.getBounds(boundShape) + const shapePoint = Vec2d.Add(point, Vec2d.MulV(terminal.normalizedAnchor, size)) + const pagePoint = Matrix2d.applyToPoint(app.getPageTransform(boundShape)!, shapePoint) + const arrowPoint = Matrix2d.applyToPoint(Matrix2d.Inverse(arrowPageTransform), pagePoint) + return arrowPoint + } +} + +export function getArrowTerminalsInArrowSpace(app: App, shape: TLArrowShape) { + const arrowPageTransform = app.getPageTransform(shape)! + + const start = getArrowTerminalInArrowSpace(app, arrowPageTransform, shape.props.start) + const end = getArrowTerminalInArrowSpace(app, arrowPageTransform, shape.props.end) + + return { start, end } +} diff --git a/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/straight-arrow.ts b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/straight-arrow.ts new file mode 100644 index 000000000..6ebaaff9b --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/arrow/straight-arrow.ts @@ -0,0 +1,227 @@ +import { + Box2d, + intersectLineSegmentPolygon, + intersectLineSegmentPolyline, + Matrix2d, + Matrix2dModel, + Vec2d, + VecLike, +} from '@tldraw/primitives' +import { TLArrowShape } from '@tldraw/tlschema' +import { BOUND_ARROW_OFFSET, MIN_ARROW_LENGTH } from '../../../../constants' +import { App } from '../../../App' +import { ArrowInfo } from './arrow-types' +import { + BoundShapeInfo, + getArrowTerminalsInArrowSpace, + getBoundShapeInfoForTerminal, +} from './shared' + +export function getStraightArrowInfo(app: App, shape: TLArrowShape): ArrowInfo { + const { start, end, arrowheadStart, arrowheadEnd } = shape.props + + const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(app, shape) + + const a = terminalsInArrowSpace.start.clone() + const b = terminalsInArrowSpace.end.clone() + const c = Vec2d.Med(a, b) + const uAB = Vec2d.Sub(b, a).uni() + + // Update the arrowhead points using intersections with the bound shapes, if any. + + const startShapeInfo = getBoundShapeInfoForTerminal(app, start) + const endShapeInfo = getBoundShapeInfoForTerminal(app, end) + + const arrowPageTransform = app.getPageTransform(shape)! + + // Update the position of the arrowhead's end point + updateArrowheadPointWithBoundShape( + b, // <-- will be mutated + terminalsInArrowSpace.start, + arrowPageTransform, + endShapeInfo + ) + + // Then update the position of the arrowhead's end point + updateArrowheadPointWithBoundShape( + a, // <-- will be mutated + terminalsInArrowSpace.end, + arrowPageTransform, + startShapeInfo + ) + + let minDist = MIN_ARROW_LENGTH + + const isSelfIntersection = + startShapeInfo && endShapeInfo && startShapeInfo.shape === endShapeInfo.shape + + if ( + startShapeInfo && + endShapeInfo && + !isSelfIntersection && + !startShapeInfo.isExact && + !endShapeInfo.isExact + ) { + if (endShapeInfo.didIntersect && !startShapeInfo.didIntersect) { + // ...and if only the end shape intersected, then make it + // a short arrow ending at the end shape intersection. + if (startShapeInfo.util.isClosed(startShapeInfo.shape)) { + a.setTo(Vec2d.Nudge(b, a, minDist)) + } + } else if (!endShapeInfo.didIntersect) { + // ...and if only the end shape intersected, or if neither + // shape intersected, then make it a short arrow starting + // at the start shape intersection. + if (endShapeInfo.util.isClosed(endShapeInfo.shape)) { + b.setTo(Vec2d.Nudge(a, b, minDist)) + } + } + } + + const u = Vec2d.Sub(b, a).uni() + const didFlip = !Vec2d.Equals(u, uAB) + + // If the arrow is bound non-exact to a start shape and the + // start point has an arrowhead offset the start point + if (!isSelfIntersection) { + if (startShapeInfo && arrowheadStart !== 'none' && !startShapeInfo.isExact) { + const offset = + BOUND_ARROW_OFFSET + + app.getStrokeWidth(shape.props.size) / 2 + + ('size' in startShapeInfo.shape.props + ? app.getStrokeWidth(startShapeInfo.shape.props.size) / 2 + : 0) + + minDist -= offset + a.nudge(b, offset * (didFlip ? -1 : 1)) + } + + // If the arrow is bound non-exact to an end shape and the + // end point has an arrowhead offset the end point + if (endShapeInfo && arrowheadEnd !== 'none' && !endShapeInfo.isExact) { + const offset = + BOUND_ARROW_OFFSET + + app.getStrokeWidth(shape.props.size) / 2 + + ('size' in endShapeInfo.shape.props + ? app.getStrokeWidth(endShapeInfo.shape.props.size) / 2 + : 0) + + minDist -= offset + b.nudge(a, offset * (didFlip ? -1 : 1)) + } + } + + if (startShapeInfo && endShapeInfo) { + // If we have two bound shapes... + if (didFlip) { + // If we flipped, then make the arrow a short arrow from + // the start point towards where the end point should be. + b.setTo(Vec2d.Add(a, u.mul(-minDist))) + } else if (Vec2d.Dist(a, b) < MIN_ARROW_LENGTH / 2) { + // Otherwise, if the arrow is too short, make it a short + // arrow from the start point towards where the end point + // should be. + b.setTo(Vec2d.Add(a, u.mul(MIN_ARROW_LENGTH / 2))) + } + } + + // If the handles flipped their order, then set the center handle + // to the midpoint of the terminals (rather than the midpoint of the + // arrow body); otherwise, it may not be "between" the other terminals. + if (didFlip) { + c.setTo(Vec2d.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end)) + } else { + c.setTo(Vec2d.Med(a, b)) + } + + const length = Vec2d.Dist(a, b) + + return { + isStraight: true, + start: { + handle: terminalsInArrowSpace.start, + point: a, + arrowhead: shape.props.arrowheadStart, + }, + end: { + handle: terminalsInArrowSpace.end, + point: b, + arrowhead: shape.props.arrowheadEnd, + }, + middle: c, + isValid: length > 0, + length, + } +} + +/** Get an intersection point from A -> B with bound shape (target) from shape (arrow). */ +function updateArrowheadPointWithBoundShape( + point: Vec2d, + opposite: Vec2d, + arrowPageTransform: Matrix2dModel, + targetShapeInfo?: BoundShapeInfo +) { + if (targetShapeInfo === undefined) { + // No bound shape? The arrowhead point will be at the arrow terminal. + return + } + + if (targetShapeInfo.isExact) { + // Exact type binding? The arrowhead point will be at the arrow terminal. + return + } + + // From and To in page space + const pageFrom = Matrix2d.applyToPoint(arrowPageTransform, opposite) + const pageTo = Matrix2d.applyToPoint(arrowPageTransform, point) + + // From and To in local space of the target shape + const targetFrom = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageFrom) + const targetTo = Matrix2d.applyToPoint(Matrix2d.Inverse(targetShapeInfo.transform), pageTo) + + const isClosed = targetShapeInfo.util.isClosed(targetShapeInfo.shape) + const fn = isClosed ? intersectLineSegmentPolygon : intersectLineSegmentPolyline + + const intersection = fn(targetFrom, targetTo, targetShapeInfo.util.outline(targetShapeInfo.shape)) + + let targetInt: VecLike | undefined + + if (intersection !== null) { + targetInt = + intersection.sort((p1, p2) => Vec2d.Dist(p1, targetFrom) - Vec2d.Dist(p2, targetFrom))[0] ?? + (isClosed ? undefined : targetTo) + } + + if (targetInt === undefined) { + // No intersection? The arrowhead point will be at the arrow terminal. + return + } + + const pageInt = Matrix2d.applyToPoint(targetShapeInfo.transform, targetInt) + const arrowInt = Matrix2d.applyToPoint(Matrix2d.Inverse(arrowPageTransform), pageInt) + + point.setTo(arrowInt) + + targetShapeInfo.didIntersect = true +} + +export function getStraightArrowHandlePath(info: ArrowInfo & { isStraight: true }) { + return getArrowPath(info.start.handle, info.end.handle) +} + +export function getSolidStraightArrowPath(info: ArrowInfo & { isStraight: true }) { + return getArrowPath(info.start.point, info.end.point) +} + +function getArrowPath(start: VecLike, end: VecLike) { + return `M${start.x},${start.y}L${end.x},${end.y}` +} + +export function getStraightArrowBoundingBox(start: VecLike, end: VecLike) { + return new Box2d( + Math.min(start.x, end.x), + Math.min(start.y, end.y), + Math.abs(start.x - end.x), + Math.abs(start.y - end.y) + ) +} diff --git a/packages/editor/src/lib/app/shapeutils/TLArrowUtil/components/ArrowTextLabel.tsx b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/components/ArrowTextLabel.tsx new file mode 100644 index 000000000..fa5a583c7 --- /dev/null +++ b/packages/editor/src/lib/app/shapeutils/TLArrowUtil/components/ArrowTextLabel.tsx @@ -0,0 +1,75 @@ +import { VecLike } from '@tldraw/primitives' +import { TLArrowShape, TLShapeId } from '@tldraw/tlschema' +import * as React from 'react' +import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../../../constants' +import { stopEventPropagation } from '../../../../utils/dom' +import { TextHelpers } from '../../TLTextUtil/TextHelpers' +import { useEditableText } from '../../shared/useEditableText' + +export const ArrowTextLabel = React.memo(function ArrowTextLabel({ + id, + text, + size, + font, + position, + width, + labelColor, +}: { id: TLShapeId; position: VecLike; width?: number; labelColor: string } & Pick< + TLArrowShape['props'], + 'text' | 'size' | 'font' +>) { + const { rInput, isEditing, handleFocus, handleBlur, handleKeyDown, handleChange, isEmpty } = + useEditableText(id, 'arrow', text) + + if (!isEditing && isEmpty) { + return null + } + + return ( +
+
+

+ {text ? TextHelpers.normalizeTextForDom(text) : ' '} +

+ {isEditing && ( + // Consider replacing with content-editable +