From c123dad50cd38101bcfabc16c0bf75d7b611f951 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Fri, 27 Feb 2026 15:52:04 +0300 Subject: [PATCH] Add oxlint, oxfmt, typescript-go - oxlint with react, react-hooks, typescript, import, unicorn plugins - oxfmt (no trailing commas, single quotes, sorted imports) - tsgo (typescript-go native preview) for fast typechecking - Fix all lint warnings: non-null assertions, unused expressions, stale closures - Scripts: lint, format, typecheck, check --- .oxfmtrc.json | 20 +++ bun.lock | 101 +++++++++++++ oxlint.json | 43 ++++++ package.json | 13 +- src/App.tsx | 305 ++++++++++++++++++++++++++++++-------- src/engine/canvaskit.ts | 2 +- src/engine/renderer.ts | 45 +++--- src/engine/scene-graph.ts | 21 +-- src/engine/undo.ts | 4 +- src/main.tsx | 3 +- 10 files changed, 445 insertions(+), 112 deletions(-) create mode 100644 .oxfmtrc.json create mode 100644 oxlint.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..9cd4513fe --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,20 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "trailingComma": "none", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "printWidth": 100, + "arrowParens": "always", + "experimentalSortImports": { + "order": "asc", + "ignoreCase": true, + "newlinesBetween": true, + "groups": [ + ["builtin", "external"], + "internal", + ["parent", "sibling", "index"], + "type" + ] + } +} diff --git a/bun.lock b/bun.lock index 33e4dec1f..4f69ddc96 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,10 @@ "@tauri-apps/cli": "^2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@typescript/native-preview": "^7.0.0-dev.20260227.1", "@vitejs/plugin-react": "^4.6.0", + "oxfmt": "^0.35.0", + "oxlint": "^1.50.0", "typescript": "~5.8.3", "vite": "^7.0.4", }, @@ -122,6 +125,82 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.35.0", "", { "os": "android", "cpu": "arm" }, "sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.35.0", "", { "os": "android", "cpu": "arm64" }, "sha512-/O+EbuAJYs6nde/anv+aID6uHsGQApyE9JtYBo/79KyU8e6RBN3DMbT0ix97y1SOnCglurmL2iZ+hlohjP2PnQ=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.35.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pGqRtqlNdn9d4VrmGUWVyQjkw79ryhI6je9y2jfqNUIZCfqceob+R97YYAoG7C5TFyt8ILdLVoN+L2vw/hSFyA=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.35.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8GmsDcSozTPjrCJeGpp+sCmS9+9V5yRrdEZ1p/sTWxPG5nYeAfSLuS0nuEYjXSO+CtdSbStIW6dxa+4NM58yRw=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.35.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QyfKfTe0ytHpFKHAcHCGQEzN45QSqq1AHJOYYxQMgLM3KY4xu8OsXHpCnINjDsV4XGnQzczJDU9e04Zmd8XqIQ=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.35.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u+kv3JD6P3J38oOyUaiCqgY5TNESzBRZJ5lyZQ6c2czUW2v5SIN9E/KWWa9vxoc+P8AFXQFUVrdzGy1tK+nbPQ=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.35.0", "", { "os": "linux", "cpu": "arm" }, "sha512-1NiZroCiV57I7Pf8kOH4XGR366kW5zir3VfSMBU2D0V14GpYjiYmPYFAoJboZvp8ACnZKUReWyMkNKSa5ad58A=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7Q0Xeg7ZnW2nxnZ4R7aF6DEbCFls4skgHZg+I63XitpNvJCbVIU8MFOTZlvZGRsY9+rPgWPQGeUpLHlyx0wvMA=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Okqi+uhYFxwKz8hcnUftNNwdm8BCkf6GSCbcz9xJxYMm87k1E4p7PEmAAbhLTk7cjSdDre6TDL0pDzNX+Y22Q=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.35.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9k66pbZQXM/lBJWys3Xbc5yhl4JexyfqkEf/tvtq8976VIJnLAAL3M127xHA3ifYSqxdVHfVGTg84eiBHCGcNw=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.35.0", "", { "os": "linux", "cpu": "none" }, "sha512-aUcY9ofKPtjO52idT6t0SAQvEF6ctjzUQa1lLp7GDsRpSBvuTrBQGeq0rYKz3gN8dMIQ7mtMdGD9tT4LhR8jAQ=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.35.0", "", { "os": "linux", "cpu": "none" }, "sha512-C6yhY5Hvc2sGM+mCPek9ZLe5xRUOC/BvhAt2qIWFAeXMn4il04EYIjl3DsWiJr0xDMTJhvMOmD55xTRPlNp39w=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.35.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-RG2hlvOMK4OMZpO3mt8MpxLQ0AAezlFqhn5mI/g5YrVbPFyoCv9a34AAvbSJS501ocOxlFIRcKEuw5hFvddf9g=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wzmh90Pwvqj9xOKHJjkQYBpydRkaXG77ZvDz+iFDRRQpnqIEqGm5gmim2s6vnZIkDGsvKCuTdtxm0GFmBjM1+w=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+HCqYCJPCUy5I+b2cf+gUVaApfgtoQT3HdnSg/l7NIcLHOhKstlYaGyrFZLmUpQt4WkFbpGKZZayG6zjRU0KFA=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.35.0", "", { "os": "none", "cpu": "arm64" }, "sha512-kFYmWfR9YL78XyO5ws+1dsxNvZoD973qfVMNFOS4e9bcHXGF7DvGC2tY5UDFwyMCeB33t3sDIuGONKggnVNSJA=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.35.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-uD/NGdM65eKNCDGyTGdO8e9n3IHX+wwuorBvEYrPJXhDXL9qz6gzddmXH8EN04ejUXUujlq4FsoSeCfbg0Y+Jg=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.35.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-oSRD2k8J2uxYDEKR2nAE/YTY9PobOEnhZgCmspHu0+yBQ665yH8lFErQVSTE7fcGJmJp/cC6322/gc8VFuQf7g=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.35.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WCDJjlS95NboR0ugI2BEwzt1tYvRDorDRM9Lvctls1SLyKYuNRCyrPwp1urUPFBnwgBNn9p2/gnmo7gFMySRoQ=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-G7MRGk/6NCe+L8ntonRdZP7IkBfEpiZ/he3buLK6JkLgMHgJShXZ+BeOwADmspXez7U7F7L1Anf4xLSkLHiGTg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GeSuMoJWCVpovJi/e3xDSNgjeR8WEZ6MCXL6EtPiCIM2NTzv7LbflARINTXTJy2oFBYyvdf/l2PwHzYo6EdXvg=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w3SY5YtxGnxCHPJ8Twl3KmS9oja1gERYk3AMoZ7Hv8P43ZtB6HVfs02TxvarxfL214Tm3uzvc2vn+DhtUNeKnw=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hNfogDqy7tvmllXKBSlHo6k5x7dhTUVOHbMSE15CCAcXzmqf5883aPvBYPOq9AE7DpDUQUZ1kVE22YbiGW+tuw=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ykZevOWEyu0nsxolA911ucxpEv0ahw8jfEeGWOwwb/VPoE4xoexuTOAiPNlWZNJqANlJl7yp8OyzCtXTUAxotw=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hif3iDk7vo5GGJ4OLCCZAf2vjnU9FztGw4L0MbQL0M2iY9LKFtDMMiQAHmkF0PQGQMVbTYtPdXCLKVgdkiqWXQ=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-dVp9iSssiGAnTNey2Ruf6xUaQhdnvcFOJyRWd/mu5o2jVbFK15E5fbWGeFRfmuobu5QXuROtFga44+7DOS3PLg=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1cT7yz2HA910CKA9NkH1ZJo50vTtmND2fkoW1oyiSb0j6WvNtJ0Wx2zoySfXWc/c+7HFoqRK5AbEoL41LOn9oA=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-++B3k/HEPFVlj89cOz8kWfQccMZB/aWL9AhsW7jPIkG++63Mpwb2cE9XOEsd0PATbIan78k2Gky+09uWM1d/gQ=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z9b/KpFMkx66w3gVBqjIC1AJBTZAGoI9+U+K5L4QM0CB/G0JSNC1es9b3Y0Vcrlvtdn8A+IQTkYjd/Q0uCSaZw=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-jvmuIw8wRSohsQlFNIST5uUwkEtEJmOQYr33bf/K2FrFPXHhM4KqGekI3ShYJemFS/gARVacQFgBzzJKCAyJjg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-x+UrN47oYNh90nmAAyql8eQaaRpHbDPu5guasDg10+OpszUQ3/1+1J6zFMmV4xfIEgTcUXG/oI5fxJhF4eWCNA=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-i/JLi2ljLUIVfekMj4ISmdt+Hn11wzYUdRRrkVUYsCWw7zAy5xV7X9iA+KMyM156LTFympa7s3oKBjuCLoTAUQ=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/C7brhn6c6UUPccgSPCcpLQXcp+xKIW/3sji/5VZ8/OItL3tQ2U7KalHz887UxxSQeEOmd1kY6lrpuwFnmNqOA=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-oDR1f+bGOYU8LfgtEW8XtotWGB63ghtcxk5Jm6IDTCk++rTA/IRMsjOid2iMd+1bW+nP9Mdsmcdc7VbPD3+iyQ=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-4CmRGPp5UpvXyu4jjP9Tey/SrXDQLRvZXm4pb4vdZBxAzbFZkCyh0KyRy4txld/kZKTJlW4TO8N1JKrNEk+mWw=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Fq0M6vsGcFsSfeuWAACDhd5KJrO85ckbEfe1EGuBj+KPyJz7KeWte2fSFrFGmNKNXyhEMyx4tbgxiWRujBM2KQ=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-qTdWR9KwY/vxJGhHVIZG2eBOhidOQvOwzDxnX+jhW/zIVacal1nAhR8GLkiywW8BIFDkQKXo/zOfT+/DY+ns/w=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-682t7npLC4G2Ca+iNlI9fhAKTcFPYYXJjwoa88H4q+u5HHHlsnL/gHULapX3iqp+A8FIJbgdylL5KMYo2LaluQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], @@ -216,6 +295,22 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260227.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260227.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260227.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260227.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260227.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260227.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260227.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260227.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-MCQZuIj7mk152lHB0kcDpHZrPjfoEijPhhigE62cHtrxukheNy+beYt5rW44wxbiapsfJWsFbRPt+cB90M/A/A=="], + + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260227.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IXHoygCPooHUX/n5LyAI98Ig8vTtgxUcKaR2odN9FLWhHSv3X6adsl7glmpdD5EqVHRUva9GqFjsO0qr7hJQag=="], + + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260227.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pH7vB05JUO+638PCApWNcHEn/ZCB4ABy5HmqAfdjntYJ18K+/HL4P7cjxQfwA/t6LcuLd9miY9DoVh5c8yElVw=="], + + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260227.1", "", { "os": "linux", "cpu": "arm" }, "sha512-vTs7ICDzfK4v4R1Kh2zg9J0WH088jvkC2lTUS9Co8nFe2DaJ2I0QRTCg7m25GF8V+GskwpLH/k2/rGB9IA1QRA=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260227.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-BMgz0ZrSCjYpA8mAW5mMeepwfUFKnq/KKgBoyFWb/AcjiOwOhUJjC/222pSaBs5tkwgwajgIyDXaUDpiDW+5/w=="], + + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260227.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XrwuolZ0X9WoMiz3dGDXVH0bH+jHERdV6h1z2N0nAYod8Q5/jmpLLATvbq5A0fa0NQbTpZRzr7uXOVtGrcorkQ=="], + + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260227.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-XuTvdN8u1Sl+HG7WlwlQlM54gSrPlp3PicT8Ih/AnjiduQmNp0g1Cjt0kBrNMHUbXWmoJJfpCaweAY+PCT/glA=="], + + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260227.1", "", { "os": "win32", "cpu": "x64" }, "sha512-T26qE0bTzw+rS17OVE+vnHdBBMeOo5veWg04rYRRtiTAXvGZyscq8/C7v1GfnVotpxWi6ckjJ112wn0gJspjnQ=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@webgpu/types": ["@webgpu/types@0.1.21", "", {}, "sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow=="], @@ -260,6 +355,10 @@ "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "oxfmt": ["oxfmt@0.35.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.35.0", "@oxfmt/binding-android-arm64": "0.35.0", "@oxfmt/binding-darwin-arm64": "0.35.0", "@oxfmt/binding-darwin-x64": "0.35.0", "@oxfmt/binding-freebsd-x64": "0.35.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.35.0", "@oxfmt/binding-linux-arm-musleabihf": "0.35.0", "@oxfmt/binding-linux-arm64-gnu": "0.35.0", "@oxfmt/binding-linux-arm64-musl": "0.35.0", "@oxfmt/binding-linux-ppc64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-musl": "0.35.0", "@oxfmt/binding-linux-s390x-gnu": "0.35.0", "@oxfmt/binding-linux-x64-gnu": "0.35.0", "@oxfmt/binding-linux-x64-musl": "0.35.0", "@oxfmt/binding-openharmony-arm64": "0.35.0", "@oxfmt/binding-win32-arm64-msvc": "0.35.0", "@oxfmt/binding-win32-ia32-msvc": "0.35.0", "@oxfmt/binding-win32-x64-msvc": "0.35.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q=="], + + "oxlint": ["oxlint@1.50.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.50.0", "@oxlint/binding-android-arm64": "1.50.0", "@oxlint/binding-darwin-arm64": "1.50.0", "@oxlint/binding-darwin-x64": "1.50.0", "@oxlint/binding-freebsd-x64": "1.50.0", "@oxlint/binding-linux-arm-gnueabihf": "1.50.0", "@oxlint/binding-linux-arm-musleabihf": "1.50.0", "@oxlint/binding-linux-arm64-gnu": "1.50.0", "@oxlint/binding-linux-arm64-musl": "1.50.0", "@oxlint/binding-linux-ppc64-gnu": "1.50.0", "@oxlint/binding-linux-riscv64-gnu": "1.50.0", "@oxlint/binding-linux-riscv64-musl": "1.50.0", "@oxlint/binding-linux-s390x-gnu": "1.50.0", "@oxlint/binding-linux-x64-gnu": "1.50.0", "@oxlint/binding-linux-x64-musl": "1.50.0", "@oxlint/binding-openharmony-arm64": "1.50.0", "@oxlint/binding-win32-arm64-msvc": "1.50.0", "@oxlint/binding-win32-ia32-msvc": "1.50.0", "@oxlint/binding-win32-x64-msvc": "1.50.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.14.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -282,6 +381,8 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], diff --git a/oxlint.json b/oxlint.json new file mode 100644 index 000000000..6443c3dd1 --- /dev/null +++ b/oxlint.json @@ -0,0 +1,43 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "react-hooks", "typescript", "import", "unicorn"], + "env": { + "browser": true, + "es2024": true + }, + "rules": { + "no-unused-vars": "warn", + "no-console": "off", + + "react/jsx-key": "error", + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-undef": "error", + "react/no-children-prop": "error", + "react/no-danger-with-children": "error", + "react/no-direct-mutation-state": "error", + "react/no-string-refs": "error", + "react/require-render-return": "error", + + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + + "typescript/no-explicit-any": "warn", + "typescript/no-non-null-assertion": "warn", + + "import/no-cycle": "error", + "import/no-self-import": "error", + + "unicorn/no-instanceof-array": "error", + "unicorn/no-typeof-undefined": "error" + }, + "overrides": [ + { + "files": ["**/*.test.ts", "**/*.test.tsx"], + "rules": { + "typescript/no-explicit-any": "off", + "typescript/no-non-null-assertion": "off" + } + } + ], + "ignorePatterns": ["node_modules", "dist", "src-tauri", "*.config.*"] +} diff --git a/package.json b/package.json index 68b5070aa..e069aaccc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,11 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "tauri": "tauri" + "tauri": "tauri", + "lint": "oxlint -c oxlint.json src/", + "format": "oxfmt --write src/", + "typecheck": "tsgo --noEmit", + "check": "bun run lint && bun run typecheck" }, "dependencies": { "@tauri-apps/api": "^2", @@ -17,11 +21,14 @@ "react-dom": "^19.1.0" }, "devDependencies": { + "@tauri-apps/cli": "^2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@typescript/native-preview": "^7.0.0-dev.20260227.1", "@vitejs/plugin-react": "^4.6.0", + "oxfmt": "^0.35.0", + "oxlint": "^1.50.0", "typescript": "~5.8.3", - "vite": "^7.0.4", - "@tauri-apps/cli": "^2" + "vite": "^7.0.4" } } diff --git a/src/App.tsx b/src/App.tsx index e2b1d9b0a..3dffa13d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ import { useEffect, useRef, useState, useCallback } from 'react' + import { getCanvasKit } from './engine/canvaskit' -import { SceneGraph } from './engine/scene-graph' import { SkiaRenderer } from './engine/renderer' +import { SceneGraph } from './engine/scene-graph' import { UndoManager } from './engine/undo' + import type { NodeType, Fill } from './engine/scene-graph' type Tool = 'SELECT' | 'FRAME' | 'RECTANGLE' | 'ELLIPSE' | 'LINE' @@ -12,14 +14,14 @@ const TOOL_SHORTCUTS: Record = { f: 'FRAME', r: 'RECTANGLE', o: 'ELLIPSE', - l: 'LINE', + l: 'LINE' } const TOOL_COLORS: Record = { FRAME: { r: 1, g: 1, b: 1, a: 1 }, RECTANGLE: { r: 0.83, g: 0.83, b: 0.83, a: 1 }, ELLIPSE: { r: 0.83, g: 0.83, b: 0.83, a: 1 }, - LINE: { r: 0, g: 0, b: 0, a: 1 }, + LINE: { r: 0, g: 0, b: 0, a: 1 } } function App() { @@ -35,8 +37,14 @@ function App() { startY: number nodeId: string } | null>(null) - const panningRef = useRef<{ startX: number; startY: number; panX: number; panY: number } | null>(null) - const movingRef = useRef<{ startX: number; startY: number; originals: Map } | null>(null) + const panningRef = useRef<{ startX: number; startY: number; panX: number; panY: number } | null>( + null + ) + const movingRef = useRef<{ + startX: number + startY: number + originals: Map + } | null>(null) const requestRender = useCallback(() => { const renderer = rendererRef.current @@ -86,7 +94,15 @@ function App() { width: 800, height: 500, fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }], - strokes: [{ color: { r: 0.87, g: 0.87, b: 0.87, a: 1 }, weight: 1, opacity: 1, visible: true, align: 'INSIDE' }], + strokes: [ + { + color: { r: 0.87, g: 0.87, b: 0.87, a: 1 }, + weight: 1, + opacity: 1, + visible: true, + align: 'INSIDE' + } + ] }) graph.createNode('RECTANGLE', graph.rootId, { @@ -96,8 +112,19 @@ function App() { width: 240, height: 160, cornerRadius: 12, - fills: [{ type: 'SOLID', color: { r: 0.23, g: 0.51, b: 0.96, a: 1 }, opacity: 1, visible: true }], - effects: [{ type: 'DROP_SHADOW', color: { r: 0, g: 0, b: 0, a: 0.15 }, offset: { x: 0, y: 4 }, radius: 12, spread: 0, visible: true }], + fills: [ + { type: 'SOLID', color: { r: 0.23, g: 0.51, b: 0.96, a: 1 }, opacity: 1, visible: true } + ], + effects: [ + { + type: 'DROP_SHADOW', + color: { r: 0, g: 0, b: 0, a: 0.15 }, + offset: { x: 0, y: 4 }, + radius: 12, + spread: 0, + visible: true + } + ] }) graph.createNode('ELLIPSE', graph.rootId, { @@ -106,7 +133,9 @@ function App() { y: 160, width: 120, height: 120, - fills: [{ type: 'SOLID', color: { r: 0.13, g: 0.77, b: 0.42, a: 1 }, opacity: 1, visible: true }], + fills: [ + { type: 'SOLID', color: { r: 0.13, g: 0.77, b: 0.42, a: 1 }, opacity: 1, visible: true } + ] }) graph.createNode('RECTANGLE', graph.rootId, { @@ -116,7 +145,9 @@ function App() { width: 200, height: 100, cornerRadius: 8, - fills: [{ type: 'SOLID', color: { r: 0.96, g: 0.52, b: 0.13, a: 1 }, opacity: 1, visible: true }], + fills: [ + { type: 'SOLID', color: { r: 0.96, g: 0.52, b: 0.13, a: 1 }, opacity: 1, visible: true } + ] }) graph.createNode('RECTANGLE', graph.rootId, { @@ -126,11 +157,13 @@ function App() { width: 300, height: 56, cornerRadius: 28, - fills: [{ type: 'SOLID', color: { r: 0.55, g: 0.36, b: 0.96, a: 1 }, opacity: 1, visible: true }], + fills: [ + { type: 'SOLID', color: { r: 0.55, g: 0.36, b: 0.96, a: 1 }, opacity: 1, visible: true } + ] }) setNodeCount(graph.nodes.size - 1) - renderer.render(graph, selectedIds) + renderer.render(graph, new Set()) } init() @@ -139,6 +172,7 @@ function App() { destroyed = true rendererRef.current?.destroy() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Re-render when selection changes @@ -193,7 +227,7 @@ function App() { const node = graph.getNode(id) if (!node) continue const snapshot = { ...node } - const parentId = node.parentId! + const parentId = node.parentId ?? graphRef.current.rootId undo.apply({ label: 'Delete', forward: () => graph.deleteNode(id), @@ -201,7 +235,7 @@ function App() { const restored = graph.createNode(snapshot.type, parentId, snapshot) // ID won't match, but good enough for PoC void restored - }, + } }) } undo.commitBatch() @@ -215,16 +249,22 @@ function App() { function handleMouseDown(e: React.MouseEvent) { const renderer = rendererRef.current - if (!renderer) return + const canvas = canvasRef.current + if (!renderer || !canvas) return - const rect = canvasRef.current!.getBoundingClientRect() + const rect = canvas.getBoundingClientRect() const sx = e.clientX - rect.left const sy = e.clientY - rect.top const { x, y } = renderer.screenToCanvas(sx, sy) // Space + click = pan - if (e.button === 1 || activeTool === 'SELECT' && e.altKey) { - panningRef.current = { startX: e.clientX, startY: e.clientY, panX: renderer.panX, panY: renderer.panY } + if (e.button === 1 || (activeTool === 'SELECT' && e.altKey)) { + panningRef.current = { + startX: e.clientX, + startY: e.clientY, + panX: renderer.panX, + panY: renderer.panY + } return } @@ -234,7 +274,8 @@ function App() { const newSelected = e.shiftKey ? (() => { const s = new Set(selectedIds) - s.has(hit.id) ? s.delete(hit.id) : s.add(hit.id) + if (s.has(hit.id)) s.delete(hit.id) + else s.add(hit.id) return s })() : new Set([hit.id]) @@ -258,7 +299,7 @@ function App() { FRAME: 'FRAME', RECTANGLE: 'RECTANGLE', ELLIPSE: 'ELLIPSE', - LINE: 'LINE', + LINE: 'LINE' } const nodeType = typeMap[activeTool] if (!nodeType) return @@ -267,7 +308,7 @@ function App() { type: 'SOLID', color: TOOL_COLORS[activeTool] ?? { r: 0.83, g: 0.83, b: 0.83, a: 1 }, opacity: 1, - visible: true, + visible: true } const node = graphRef.current.createNode(nodeType, graphRef.current.rootId, { @@ -275,7 +316,7 @@ function App() { y, width: 0, height: 0, - fills: [fill], + fills: [fill] }) drawingRef.current = { startX: x, startY: y, nodeId: node.id } @@ -285,7 +326,8 @@ function App() { function handleMouseMove(e: React.MouseEvent) { const renderer = rendererRef.current - if (!renderer) return + const canvas = canvasRef.current + if (!renderer || !canvas) return // Pan if (panningRef.current) { @@ -297,7 +339,7 @@ function App() { return } - const rect = canvasRef.current!.getBoundingClientRect() + const rect = canvas.getBoundingClientRect() const sx = e.clientX - rect.left const sy = e.clientY - rect.top const { x, y } = renderer.screenToCanvas(sx, sy) @@ -322,7 +364,7 @@ function App() { x: w < 0 ? x : startX, y: h < 0 ? y : startY, width: Math.abs(w), - height: Math.abs(h), + height: Math.abs(h) }) requestRender() } @@ -346,7 +388,7 @@ function App() { }, inverse: () => { for (const [id, pos] of originals) graph.updateNode(id, pos) - }, + } }) movingRef.current = null } @@ -369,12 +411,13 @@ function App() { function handleWheel(e: React.WheelEvent) { const renderer = rendererRef.current - if (!renderer) return + const canvas = canvasRef.current + if (!renderer || !canvas) return e.preventDefault() if (e.ctrlKey || e.metaKey) { // Zoom - const rect = canvasRef.current!.getBoundingClientRect() + const rect = canvas.getBoundingClientRect() const sx = e.clientX - rect.left const sy = e.clientY - rect.top @@ -398,20 +441,50 @@ function App() { { key: 'FRAME', label: '# Frame', shortcut: 'F' }, { key: 'RECTANGLE', label: '□ Rect', shortcut: 'R' }, { key: 'ELLIPSE', label: '○ Ellipse', shortcut: 'O' }, - { key: 'LINE', label: '/ Line', shortcut: 'L' }, + { key: 'LINE', label: '/ Line', shortcut: 'L' } ] - const selectedNode = selectedIds.size === 1 - ? graphRef.current.getNode([...selectedIds][0]) - : undefined + const selectedNode = + selectedIds.size === 1 ? graphRef.current.getNode([...selectedIds][0]) : undefined return ( -
+
{/* Canvas */}
{/* Left panel — Layers */} -
-
Layers
+
+
+ Layers +
{graphRef.current.getChildren(graphRef.current.rootId).map((node) => (
@@ -438,7 +511,13 @@ function App() { {/* Canvas */} {/* Right panel — Properties */} -
+
{selectedNode ? ( <>
{selectedNode.name}
-
{selectedNode.type} · {selectedNode.id}
+
+ {selectedNode.type} · {selectedNode.id} +
Appearance
-
- - - - - - +
+ + + + + +
Fill
{selectedNode.fills.map((fill, i) => (
-
+
- #{Math.round(fill.color.r * 255).toString(16).padStart(2, '0')} - {Math.round(fill.color.g * 255).toString(16).padStart(2, '0')} - {Math.round(fill.color.b * 255).toString(16).padStart(2, '0')} + # + {Math.round(fill.color.r * 255) + .toString(16) + .padStart(2, '0')} + {Math.round(fill.color.g * 255) + .toString(16) + .padStart(2, '0')} + {Math.round(fill.color.b * 255) + .toString(16) + .padStart(2, '0')}
))} @@ -500,15 +669,24 @@ function App() {
) : ( -
- No selection -
+
No selection
)}
{/* Bottom toolbar */} -
+
{tools.map((t) => (
@@ -546,7 +725,7 @@ const inputStyle: React.CSSProperties = { color: '#e0e0e0', padding: '2px 4px', fontSize: 12, - marginLeft: 4, + marginLeft: 4 } export default App diff --git a/src/engine/canvaskit.ts b/src/engine/canvaskit.ts index 31ffb8d31..f8d58dd6d 100644 --- a/src/engine/canvaskit.ts +++ b/src/engine/canvaskit.ts @@ -5,7 +5,7 @@ let instance: CanvasKit | null = null export async function getCanvasKit(): Promise { if (instance) return instance instance = await CanvasKitInit({ - locateFile: () => '/canvaskit.wasm', + locateFile: () => '/canvaskit.wasm' }) return instance } diff --git a/src/engine/renderer.ts b/src/engine/renderer.ts index 743484852..85ccc57d0 100644 --- a/src/engine/renderer.ts +++ b/src/engine/renderer.ts @@ -1,5 +1,5 @@ -import type { CanvasKit, Surface, Canvas, Paint } from 'canvaskit-wasm' import type { SceneNode, SceneGraph, Fill } from './scene-graph' +import type { CanvasKit, Surface, Canvas, Paint } from 'canvaskit-wasm' export class SkiaRenderer { private ck: CanvasKit @@ -106,11 +106,7 @@ export class SkiaRenderer { } if (node.rotation !== 0) { - canvas.rotate( - node.rotation, - node.x + node.width / 2, - node.y + node.height / 2 - ) + canvas.rotate(node.rotation, node.x + node.width / 2, node.y + node.height / 2) } this.renderShape(canvas, node) @@ -127,12 +123,7 @@ export class SkiaRenderer { } private renderShape(canvas: Canvas, node: SceneNode): void { - const rect = this.ck.LTRBRect( - node.x, - node.y, - node.x + node.width, - node.y + node.height - ) + const rect = this.ck.LTRBRect(node.x, node.y, node.x + node.width, node.y + node.height) const hasRadius = node.cornerRadius > 0 || @@ -161,15 +152,21 @@ export class SkiaRenderer { const rrect = this.ck.RRectXY(rect, node.cornerRadius, node.cornerRadius) // For independent corners, build a proper RRect const radii = [ - node.topLeftRadius, node.topLeftRadius, - node.topRightRadius, node.topRightRadius, - node.bottomRightRadius, node.bottomRightRadius, - node.bottomLeftRadius, node.bottomLeftRadius, + node.topLeftRadius, + node.topLeftRadius, + node.topRightRadius, + node.topRightRadius, + node.bottomRightRadius, + node.bottomRightRadius, + node.bottomLeftRadius, + node.bottomLeftRadius ] const rrectIndep = new Float32Array([ - node.x, node.y, - node.x + node.width, node.y + node.height, - ...radii, + node.x, + node.y, + node.x + node.width, + node.y + node.height, + ...radii ]) canvas.drawRRect(rrectIndep, this.fillPaint) void rrect @@ -182,13 +179,7 @@ export class SkiaRenderer { } break case 'LINE': - canvas.drawLine( - node.x, - node.y, - node.x + node.width, - node.y + node.height, - this.fillPaint - ) + canvas.drawLine(node.x, node.y, node.x + node.width, node.y + node.height, this.fillPaint) break default: canvas.drawRect(rect, this.fillPaint) @@ -233,7 +224,7 @@ export class SkiaRenderer { screenToCanvas(sx: number, sy: number): { x: number; y: number } { return { x: (sx - this.panX) / this.zoom, - y: (sy - this.panY) / this.zoom, + y: (sy - this.panY) / this.zoom } } diff --git a/src/engine/scene-graph.ts b/src/engine/scene-graph.ts index cbf88d1e5..dc67b0b1d 100644 --- a/src/engine/scene-graph.ts +++ b/src/engine/scene-graph.ts @@ -82,10 +82,7 @@ function generateId(): string { return `0:${nextLocalID++}` } -function createDefaultNode( - type: NodeType, - overrides: Partial = {} -): SceneNode { +function createDefaultNode(type: NodeType, overrides: Partial = {}): SceneNode { return { id: generateId(), type, @@ -110,7 +107,7 @@ function createDefaultNode( cornerSmoothing: 0, visible: true, locked: false, - ...overrides, + ...overrides } } @@ -122,7 +119,7 @@ export class SceneGraph { const root = createDefaultNode('FRAME', { name: 'Document', width: 0, - height: 0, + height: 0 }) this.rootId = root.id this.nodes.set(root.id, root) @@ -140,11 +137,7 @@ export class SceneGraph { .filter((n): n is SceneNode => n !== undefined) } - createNode( - type: NodeType, - parentId: string, - overrides: Partial = {} - ): SceneNode { + createNode(type: NodeType, parentId: string, overrides: Partial = {}): SceneNode { const node = createDefaultNode(type, overrides) node.parentId = parentId this.nodes.set(node.id, node) @@ -176,7 +169,7 @@ export class SceneGraph { } // Delete children recursively - for (const childId of [...node.childIds]) { + for (const childId of Array.from(node.childIds)) { this.deleteNode(childId) } @@ -185,9 +178,7 @@ export class SceneGraph { hitTest(px: number, py: number): SceneNode | null { // Reverse order = topmost first - const allNodes = [...this.nodes.values()].filter( - (n) => n.id !== this.rootId && n.visible - ) + const allNodes = [...this.nodes.values()].filter((n) => n.id !== this.rootId && n.visible) for (let i = allNodes.length - 1; i >= 0; i--) { const n = allNodes[i] diff --git a/src/engine/undo.ts b/src/engine/undo.ts index f4a4d1518..298c887cb 100644 --- a/src/engine/undo.ts +++ b/src/engine/undo.ts @@ -55,7 +55,7 @@ export class UndoManager { this.undoStack.push({ label, forward: () => entries.forEach((e) => e.forward()), - inverse: () => [...entries].reverse().forEach((e) => e.inverse()), + inverse: () => [...entries].reverse().forEach((e) => e.inverse()) }) this.redoStack = [] } @@ -90,6 +90,6 @@ export function createPropertyChange( return { label, forward: () => Object.assign(node, changes), - inverse: () => Object.assign(node, previous), + inverse: () => Object.assign(node, previous) } } diff --git a/src/main.tsx b/src/main.tsx index eaad24487..e9a8bc0cb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,9 @@ import React from 'react' import ReactDOM from 'react-dom/client' + import App from './App' -ReactDOM.createRoot(document.getElementById('root')!).render( +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(