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
This commit is contained in:
parent
68b54ad6b7
commit
c123dad50c
20
.oxfmtrc.json
Normal file
20
.oxfmtrc.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
101
bun.lock
101
bun.lock
|
|
@ -15,7 +15,10 @@
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@typescript/native-preview": "^7.0.0-dev.20260227.1",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"oxfmt": "^0.35.0",
|
||||||
|
"oxlint": "^1.50.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^7.0.4",
|
"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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
|
|
|
||||||
43
oxlint.json
Normal file
43
oxlint.json
Normal file
|
|
@ -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.*"]
|
||||||
|
}
|
||||||
13
package.json
13
package.json
|
|
@ -7,7 +7,11 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview",
|
"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": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
|
@ -17,11 +21,14 @@
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@typescript/native-preview": "^7.0.0-dev.20260227.1",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"oxfmt": "^0.35.0",
|
||||||
|
"oxlint": "^1.50.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^7.0.4",
|
"vite": "^7.0.4"
|
||||||
"@tauri-apps/cli": "^2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
305
src/App.tsx
305
src/App.tsx
|
|
@ -1,8 +1,10 @@
|
||||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||||
|
|
||||||
import { getCanvasKit } from './engine/canvaskit'
|
import { getCanvasKit } from './engine/canvaskit'
|
||||||
import { SceneGraph } from './engine/scene-graph'
|
|
||||||
import { SkiaRenderer } from './engine/renderer'
|
import { SkiaRenderer } from './engine/renderer'
|
||||||
|
import { SceneGraph } from './engine/scene-graph'
|
||||||
import { UndoManager } from './engine/undo'
|
import { UndoManager } from './engine/undo'
|
||||||
|
|
||||||
import type { NodeType, Fill } from './engine/scene-graph'
|
import type { NodeType, Fill } from './engine/scene-graph'
|
||||||
|
|
||||||
type Tool = 'SELECT' | 'FRAME' | 'RECTANGLE' | 'ELLIPSE' | 'LINE'
|
type Tool = 'SELECT' | 'FRAME' | 'RECTANGLE' | 'ELLIPSE' | 'LINE'
|
||||||
|
|
@ -12,14 +14,14 @@ const TOOL_SHORTCUTS: Record<string, Tool> = {
|
||||||
f: 'FRAME',
|
f: 'FRAME',
|
||||||
r: 'RECTANGLE',
|
r: 'RECTANGLE',
|
||||||
o: 'ELLIPSE',
|
o: 'ELLIPSE',
|
||||||
l: 'LINE',
|
l: 'LINE'
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOL_COLORS: Record<string, { r: number; g: number; b: number; a: number }> = {
|
const TOOL_COLORS: Record<string, { r: number; g: number; b: number; a: number }> = {
|
||||||
FRAME: { r: 1, g: 1, b: 1, a: 1 },
|
FRAME: { r: 1, g: 1, b: 1, a: 1 },
|
||||||
RECTANGLE: { r: 0.83, g: 0.83, b: 0.83, 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 },
|
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() {
|
function App() {
|
||||||
|
|
@ -35,8 +37,14 @@ function App() {
|
||||||
startY: number
|
startY: number
|
||||||
nodeId: string
|
nodeId: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const panningRef = useRef<{ startX: number; startY: number; panX: number; panY: number } | null>(null)
|
const panningRef = useRef<{ startX: number; startY: number; panX: number; panY: number } | null>(
|
||||||
const movingRef = useRef<{ startX: number; startY: number; originals: Map<string, { x: number; y: number }> } | null>(null)
|
null
|
||||||
|
)
|
||||||
|
const movingRef = useRef<{
|
||||||
|
startX: number
|
||||||
|
startY: number
|
||||||
|
originals: Map<string, { x: number; y: number }>
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
const requestRender = useCallback(() => {
|
const requestRender = useCallback(() => {
|
||||||
const renderer = rendererRef.current
|
const renderer = rendererRef.current
|
||||||
|
|
@ -86,7 +94,15 @@ function App() {
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 500,
|
height: 500,
|
||||||
fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }],
|
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, {
|
graph.createNode('RECTANGLE', graph.rootId, {
|
||||||
|
|
@ -96,8 +112,19 @@ function App() {
|
||||||
width: 240,
|
width: 240,
|
||||||
height: 160,
|
height: 160,
|
||||||
cornerRadius: 12,
|
cornerRadius: 12,
|
||||||
fills: [{ type: 'SOLID', color: { r: 0.23, g: 0.51, b: 0.96, a: 1 }, opacity: 1, visible: true }],
|
fills: [
|
||||||
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 }],
|
{ 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, {
|
graph.createNode('ELLIPSE', graph.rootId, {
|
||||||
|
|
@ -106,7 +133,9 @@ function App() {
|
||||||
y: 160,
|
y: 160,
|
||||||
width: 120,
|
width: 120,
|
||||||
height: 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, {
|
graph.createNode('RECTANGLE', graph.rootId, {
|
||||||
|
|
@ -116,7 +145,9 @@ function App() {
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 100,
|
height: 100,
|
||||||
cornerRadius: 8,
|
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, {
|
graph.createNode('RECTANGLE', graph.rootId, {
|
||||||
|
|
@ -126,11 +157,13 @@ function App() {
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 56,
|
height: 56,
|
||||||
cornerRadius: 28,
|
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)
|
setNodeCount(graph.nodes.size - 1)
|
||||||
renderer.render(graph, selectedIds)
|
renderer.render(graph, new Set())
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
@ -139,6 +172,7 @@ function App() {
|
||||||
destroyed = true
|
destroyed = true
|
||||||
rendererRef.current?.destroy()
|
rendererRef.current?.destroy()
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Re-render when selection changes
|
// Re-render when selection changes
|
||||||
|
|
@ -193,7 +227,7 @@ function App() {
|
||||||
const node = graph.getNode(id)
|
const node = graph.getNode(id)
|
||||||
if (!node) continue
|
if (!node) continue
|
||||||
const snapshot = { ...node }
|
const snapshot = { ...node }
|
||||||
const parentId = node.parentId!
|
const parentId = node.parentId ?? graphRef.current.rootId
|
||||||
undo.apply({
|
undo.apply({
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
forward: () => graph.deleteNode(id),
|
forward: () => graph.deleteNode(id),
|
||||||
|
|
@ -201,7 +235,7 @@ function App() {
|
||||||
const restored = graph.createNode(snapshot.type, parentId, snapshot)
|
const restored = graph.createNode(snapshot.type, parentId, snapshot)
|
||||||
// ID won't match, but good enough for PoC
|
// ID won't match, but good enough for PoC
|
||||||
void restored
|
void restored
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
undo.commitBatch()
|
undo.commitBatch()
|
||||||
|
|
@ -215,16 +249,22 @@ function App() {
|
||||||
|
|
||||||
function handleMouseDown(e: React.MouseEvent<HTMLCanvasElement>) {
|
function handleMouseDown(e: React.MouseEvent<HTMLCanvasElement>) {
|
||||||
const renderer = rendererRef.current
|
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 sx = e.clientX - rect.left
|
||||||
const sy = e.clientY - rect.top
|
const sy = e.clientY - rect.top
|
||||||
const { x, y } = renderer.screenToCanvas(sx, sy)
|
const { x, y } = renderer.screenToCanvas(sx, sy)
|
||||||
|
|
||||||
// Space + click = pan
|
// Space + click = pan
|
||||||
if (e.button === 1 || activeTool === 'SELECT' && e.altKey) {
|
if (e.button === 1 || (activeTool === 'SELECT' && e.altKey)) {
|
||||||
panningRef.current = { startX: e.clientX, startY: e.clientY, panX: renderer.panX, panY: renderer.panY }
|
panningRef.current = {
|
||||||
|
startX: e.clientX,
|
||||||
|
startY: e.clientY,
|
||||||
|
panX: renderer.panX,
|
||||||
|
panY: renderer.panY
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,7 +274,8 @@ function App() {
|
||||||
const newSelected = e.shiftKey
|
const newSelected = e.shiftKey
|
||||||
? (() => {
|
? (() => {
|
||||||
const s = new Set(selectedIds)
|
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
|
return s
|
||||||
})()
|
})()
|
||||||
: new Set([hit.id])
|
: new Set([hit.id])
|
||||||
|
|
@ -258,7 +299,7 @@ function App() {
|
||||||
FRAME: 'FRAME',
|
FRAME: 'FRAME',
|
||||||
RECTANGLE: 'RECTANGLE',
|
RECTANGLE: 'RECTANGLE',
|
||||||
ELLIPSE: 'ELLIPSE',
|
ELLIPSE: 'ELLIPSE',
|
||||||
LINE: 'LINE',
|
LINE: 'LINE'
|
||||||
}
|
}
|
||||||
const nodeType = typeMap[activeTool]
|
const nodeType = typeMap[activeTool]
|
||||||
if (!nodeType) return
|
if (!nodeType) return
|
||||||
|
|
@ -267,7 +308,7 @@ function App() {
|
||||||
type: 'SOLID',
|
type: 'SOLID',
|
||||||
color: TOOL_COLORS[activeTool] ?? { r: 0.83, g: 0.83, b: 0.83, a: 1 },
|
color: TOOL_COLORS[activeTool] ?? { r: 0.83, g: 0.83, b: 0.83, a: 1 },
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
visible: true,
|
visible: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = graphRef.current.createNode(nodeType, graphRef.current.rootId, {
|
const node = graphRef.current.createNode(nodeType, graphRef.current.rootId, {
|
||||||
|
|
@ -275,7 +316,7 @@ function App() {
|
||||||
y,
|
y,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
fills: [fill],
|
fills: [fill]
|
||||||
})
|
})
|
||||||
|
|
||||||
drawingRef.current = { startX: x, startY: y, nodeId: node.id }
|
drawingRef.current = { startX: x, startY: y, nodeId: node.id }
|
||||||
|
|
@ -285,7 +326,8 @@ function App() {
|
||||||
|
|
||||||
function handleMouseMove(e: React.MouseEvent<HTMLCanvasElement>) {
|
function handleMouseMove(e: React.MouseEvent<HTMLCanvasElement>) {
|
||||||
const renderer = rendererRef.current
|
const renderer = rendererRef.current
|
||||||
if (!renderer) return
|
const canvas = canvasRef.current
|
||||||
|
if (!renderer || !canvas) return
|
||||||
|
|
||||||
// Pan
|
// Pan
|
||||||
if (panningRef.current) {
|
if (panningRef.current) {
|
||||||
|
|
@ -297,7 +339,7 @@ function App() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = canvasRef.current!.getBoundingClientRect()
|
const rect = canvas.getBoundingClientRect()
|
||||||
const sx = e.clientX - rect.left
|
const sx = e.clientX - rect.left
|
||||||
const sy = e.clientY - rect.top
|
const sy = e.clientY - rect.top
|
||||||
const { x, y } = renderer.screenToCanvas(sx, sy)
|
const { x, y } = renderer.screenToCanvas(sx, sy)
|
||||||
|
|
@ -322,7 +364,7 @@ function App() {
|
||||||
x: w < 0 ? x : startX,
|
x: w < 0 ? x : startX,
|
||||||
y: h < 0 ? y : startY,
|
y: h < 0 ? y : startY,
|
||||||
width: Math.abs(w),
|
width: Math.abs(w),
|
||||||
height: Math.abs(h),
|
height: Math.abs(h)
|
||||||
})
|
})
|
||||||
requestRender()
|
requestRender()
|
||||||
}
|
}
|
||||||
|
|
@ -346,7 +388,7 @@ function App() {
|
||||||
},
|
},
|
||||||
inverse: () => {
|
inverse: () => {
|
||||||
for (const [id, pos] of originals) graph.updateNode(id, pos)
|
for (const [id, pos] of originals) graph.updateNode(id, pos)
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
movingRef.current = null
|
movingRef.current = null
|
||||||
}
|
}
|
||||||
|
|
@ -369,12 +411,13 @@ function App() {
|
||||||
|
|
||||||
function handleWheel(e: React.WheelEvent<HTMLCanvasElement>) {
|
function handleWheel(e: React.WheelEvent<HTMLCanvasElement>) {
|
||||||
const renderer = rendererRef.current
|
const renderer = rendererRef.current
|
||||||
if (!renderer) return
|
const canvas = canvasRef.current
|
||||||
|
if (!renderer || !canvas) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
// Zoom
|
// Zoom
|
||||||
const rect = canvasRef.current!.getBoundingClientRect()
|
const rect = canvas.getBoundingClientRect()
|
||||||
const sx = e.clientX - rect.left
|
const sx = e.clientX - rect.left
|
||||||
const sy = e.clientY - rect.top
|
const sy = e.clientY - rect.top
|
||||||
|
|
||||||
|
|
@ -398,20 +441,50 @@ function App() {
|
||||||
{ key: 'FRAME', label: '# Frame', shortcut: 'F' },
|
{ key: 'FRAME', label: '# Frame', shortcut: 'F' },
|
||||||
{ key: 'RECTANGLE', label: '□ Rect', shortcut: 'R' },
|
{ key: 'RECTANGLE', label: '□ Rect', shortcut: 'R' },
|
||||||
{ key: 'ELLIPSE', label: '○ Ellipse', shortcut: 'O' },
|
{ key: 'ELLIPSE', label: '○ Ellipse', shortcut: 'O' },
|
||||||
{ key: 'LINE', label: '/ Line', shortcut: 'L' },
|
{ key: 'LINE', label: '/ Line', shortcut: 'L' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const selectedNode = selectedIds.size === 1
|
const selectedNode =
|
||||||
? graphRef.current.getNode([...selectedIds][0])
|
selectedIds.size === 1 ? graphRef.current.getNode([...selectedIds][0]) : undefined
|
||||||
: undefined
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column', background: '#1e1e1e', color: '#e0e0e0', fontFamily: 'Inter, system-ui, sans-serif', fontSize: 13, overflow: 'hidden', userSelect: 'none' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
background: '#1e1e1e',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
fontSize: 13,
|
||||||
|
overflow: 'hidden',
|
||||||
|
userSelect: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Canvas */}
|
{/* Canvas */}
|
||||||
<div style={{ flex: 1, position: 'relative', display: 'flex' }}>
|
<div style={{ flex: 1, position: 'relative', display: 'flex' }}>
|
||||||
{/* Left panel — Layers */}
|
{/* Left panel — Layers */}
|
||||||
<div style={{ width: 240, background: '#2a2a2a', borderRight: '1px solid #3a3a3a', overflowY: 'auto', padding: '8px 0' }}>
|
<div
|
||||||
<div style={{ padding: '4px 12px', fontSize: 11, color: '#888', textTransform: 'uppercase', letterSpacing: 1 }}>Layers</div>
|
style={{
|
||||||
|
width: 240,
|
||||||
|
background: '#2a2a2a',
|
||||||
|
borderRight: '1px solid #3a3a3a',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '8px 0'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '4px 12px',
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#888',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Layers
|
||||||
|
</div>
|
||||||
{graphRef.current.getChildren(graphRef.current.rootId).map((node) => (
|
{graphRef.current.getChildren(graphRef.current.rootId).map((node) => (
|
||||||
<div
|
<div
|
||||||
key={node.id}
|
key={node.id}
|
||||||
|
|
@ -424,7 +497,7 @@ function App() {
|
||||||
margin: '1px 4px',
|
margin: '1px 4px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 12 }}>
|
<span style={{ fontSize: 12 }}>
|
||||||
|
|
@ -438,7 +511,13 @@ function App() {
|
||||||
{/* Canvas */}
|
{/* Canvas */}
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
style={{ flex: 1, display: 'block', cursor: activeTool === 'SELECT' ? 'default' : 'crosshair', minWidth: 0, minHeight: 0 }}
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'block',
|
||||||
|
cursor: activeTool === 'SELECT' ? 'default' : 'crosshair',
|
||||||
|
minWidth: 0,
|
||||||
|
minHeight: 0
|
||||||
|
}}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
|
|
@ -447,39 +526,129 @@ function App() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Right panel — Properties */}
|
{/* Right panel — Properties */}
|
||||||
<div style={{ width: 260, background: '#2a2a2a', borderLeft: '1px solid #3a3a3a', overflowY: 'auto', padding: '8px 0' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
width: 260,
|
||||||
|
background: '#2a2a2a',
|
||||||
|
borderLeft: '1px solid #3a3a3a',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '8px 0'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{selectedNode ? (
|
{selectedNode ? (
|
||||||
<>
|
<>
|
||||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid #3a3a3a' }}>
|
<div style={{ padding: '8px 12px', borderBottom: '1px solid #3a3a3a' }}>
|
||||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>{selectedNode.name}</div>
|
<div style={{ fontWeight: 600, marginBottom: 4 }}>{selectedNode.name}</div>
|
||||||
<div style={{ fontSize: 11, color: '#888' }}>{selectedNode.type} · {selectedNode.id}</div>
|
<div style={{ fontSize: 11, color: '#888' }}>
|
||||||
|
{selectedNode.type} · {selectedNode.id}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '8px 12px' }}>
|
<div style={{ padding: '8px 12px' }}>
|
||||||
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>Appearance</div>
|
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>Appearance</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4, fontSize: 12 }}>
|
<div
|
||||||
<label>W: <input type="number" value={Math.round(selectedNode.width)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { width: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4, fontSize: 12 }}
|
||||||
<label>H: <input type="number" value={Math.round(selectedNode.height)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { height: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
>
|
||||||
<label>X: <input type="number" value={Math.round(selectedNode.x)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { x: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
<label>
|
||||||
<label>Y: <input type="number" value={Math.round(selectedNode.y)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { y: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
W:{' '}
|
||||||
<label>R: <input type="number" value={Math.round(selectedNode.rotation)} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { rotation: +e.target.value }); requestRender() }} style={inputStyle} />°</label>
|
<input
|
||||||
<label>↻: <input type="number" value={selectedNode.cornerRadius} onChange={(e) => { graphRef.current.updateNode(selectedNode.id, { cornerRadius: +e.target.value }); requestRender() }} style={inputStyle} /></label>
|
type="number"
|
||||||
|
value={Math.round(selectedNode.width)}
|
||||||
|
onChange={(e) => {
|
||||||
|
graphRef.current.updateNode(selectedNode.id, { width: +e.target.value })
|
||||||
|
requestRender()
|
||||||
|
}}
|
||||||
|
style={inputStyle}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
H:{' '}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={Math.round(selectedNode.height)}
|
||||||
|
onChange={(e) => {
|
||||||
|
graphRef.current.updateNode(selectedNode.id, { height: +e.target.value })
|
||||||
|
requestRender()
|
||||||
|
}}
|
||||||
|
style={inputStyle}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
X:{' '}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={Math.round(selectedNode.x)}
|
||||||
|
onChange={(e) => {
|
||||||
|
graphRef.current.updateNode(selectedNode.id, { x: +e.target.value })
|
||||||
|
requestRender()
|
||||||
|
}}
|
||||||
|
style={inputStyle}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Y:{' '}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={Math.round(selectedNode.y)}
|
||||||
|
onChange={(e) => {
|
||||||
|
graphRef.current.updateNode(selectedNode.id, { y: +e.target.value })
|
||||||
|
requestRender()
|
||||||
|
}}
|
||||||
|
style={inputStyle}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
R:{' '}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={Math.round(selectedNode.rotation)}
|
||||||
|
onChange={(e) => {
|
||||||
|
graphRef.current.updateNode(selectedNode.id, { rotation: +e.target.value })
|
||||||
|
requestRender()
|
||||||
|
}}
|
||||||
|
style={inputStyle}
|
||||||
|
/>
|
||||||
|
°
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
↻:{' '}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={selectedNode.cornerRadius}
|
||||||
|
onChange={(e) => {
|
||||||
|
graphRef.current.updateNode(selectedNode.id, {
|
||||||
|
cornerRadius: +e.target.value
|
||||||
|
})
|
||||||
|
requestRender()
|
||||||
|
}}
|
||||||
|
style={inputStyle}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: '8px 12px', borderTop: '1px solid #3a3a3a' }}>
|
<div style={{ padding: '8px 12px', borderTop: '1px solid #3a3a3a' }}>
|
||||||
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>Fill</div>
|
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>Fill</div>
|
||||||
{selectedNode.fills.map((fill, i) => (
|
{selectedNode.fills.map((fill, i) => (
|
||||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<div style={{
|
<div
|
||||||
width: 24,
|
style={{
|
||||||
height: 24,
|
width: 24,
|
||||||
borderRadius: 4,
|
height: 24,
|
||||||
background: `rgba(${fill.color.r * 255}, ${fill.color.g * 255}, ${fill.color.b * 255}, ${fill.color.a})`,
|
borderRadius: 4,
|
||||||
border: '1px solid #555',
|
background: `rgba(${fill.color.r * 255}, ${fill.color.g * 255}, ${fill.color.b * 255}, ${fill.color.a})`,
|
||||||
}} />
|
border: '1px solid #555'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span style={{ fontSize: 12 }}>
|
<span style={{ fontSize: 12 }}>
|
||||||
#{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.r * 255)
|
||||||
{Math.round(fill.color.b * 255).toString(16).padStart(2, '0')}
|
.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')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -500,15 +669,24 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: '12px', color: '#666' }}>
|
<div style={{ padding: '12px', color: '#666' }}>No selection</div>
|
||||||
No selection
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom toolbar */}
|
{/* Bottom toolbar */}
|
||||||
<div style={{ height: 48, background: '#2a2a2a', borderTop: '1px solid #3a3a3a', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2, padding: '0 16px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
height: 48,
|
||||||
|
background: '#2a2a2a',
|
||||||
|
borderTop: '1px solid #3a3a3a',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 2,
|
||||||
|
padding: '0 16px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{tools.map((t) => (
|
{tools.map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t.key}
|
key={t.key}
|
||||||
|
|
@ -523,7 +701,7 @@ function App() {
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
transition: 'background 0.1s',
|
transition: 'background 0.1s'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
|
|
@ -531,7 +709,8 @@ function App() {
|
||||||
))}
|
))}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<span style={{ fontSize: 11, color: '#666' }}>
|
<span style={{ fontSize: 11, color: '#666' }}>
|
||||||
{nodeCount} nodes · Zoom: {rendererRef.current ? `${Math.round(rendererRef.current.zoom * 100)}%` : '100%'}
|
{nodeCount} nodes · Zoom:{' '}
|
||||||
|
{rendererRef.current ? `${Math.round(rendererRef.current.zoom * 100)}%` : '100%'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -546,7 +725,7 @@ const inputStyle: React.CSSProperties = {
|
||||||
color: '#e0e0e0',
|
color: '#e0e0e0',
|
||||||
padding: '2px 4px',
|
padding: '2px 4px',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginLeft: 4,
|
marginLeft: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ let instance: CanvasKit | null = null
|
||||||
export async function getCanvasKit(): Promise<CanvasKit> {
|
export async function getCanvasKit(): Promise<CanvasKit> {
|
||||||
if (instance) return instance
|
if (instance) return instance
|
||||||
instance = await CanvasKitInit({
|
instance = await CanvasKitInit({
|
||||||
locateFile: () => '/canvaskit.wasm',
|
locateFile: () => '/canvaskit.wasm'
|
||||||
})
|
})
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { CanvasKit, Surface, Canvas, Paint } from 'canvaskit-wasm'
|
|
||||||
import type { SceneNode, SceneGraph, Fill } from './scene-graph'
|
import type { SceneNode, SceneGraph, Fill } from './scene-graph'
|
||||||
|
import type { CanvasKit, Surface, Canvas, Paint } from 'canvaskit-wasm'
|
||||||
|
|
||||||
export class SkiaRenderer {
|
export class SkiaRenderer {
|
||||||
private ck: CanvasKit
|
private ck: CanvasKit
|
||||||
|
|
@ -106,11 +106,7 @@ export class SkiaRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.rotation !== 0) {
|
if (node.rotation !== 0) {
|
||||||
canvas.rotate(
|
canvas.rotate(node.rotation, node.x + node.width / 2, node.y + node.height / 2)
|
||||||
node.rotation,
|
|
||||||
node.x + node.width / 2,
|
|
||||||
node.y + node.height / 2
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderShape(canvas, node)
|
this.renderShape(canvas, node)
|
||||||
|
|
@ -127,12 +123,7 @@ export class SkiaRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderShape(canvas: Canvas, node: SceneNode): void {
|
private renderShape(canvas: Canvas, node: SceneNode): void {
|
||||||
const rect = this.ck.LTRBRect(
|
const rect = this.ck.LTRBRect(node.x, node.y, node.x + node.width, node.y + node.height)
|
||||||
node.x,
|
|
||||||
node.y,
|
|
||||||
node.x + node.width,
|
|
||||||
node.y + node.height
|
|
||||||
)
|
|
||||||
|
|
||||||
const hasRadius =
|
const hasRadius =
|
||||||
node.cornerRadius > 0 ||
|
node.cornerRadius > 0 ||
|
||||||
|
|
@ -161,15 +152,21 @@ export class SkiaRenderer {
|
||||||
const rrect = this.ck.RRectXY(rect, node.cornerRadius, node.cornerRadius)
|
const rrect = this.ck.RRectXY(rect, node.cornerRadius, node.cornerRadius)
|
||||||
// For independent corners, build a proper RRect
|
// For independent corners, build a proper RRect
|
||||||
const radii = [
|
const radii = [
|
||||||
node.topLeftRadius, node.topLeftRadius,
|
node.topLeftRadius,
|
||||||
node.topRightRadius, node.topRightRadius,
|
node.topLeftRadius,
|
||||||
node.bottomRightRadius, node.bottomRightRadius,
|
node.topRightRadius,
|
||||||
node.bottomLeftRadius, node.bottomLeftRadius,
|
node.topRightRadius,
|
||||||
|
node.bottomRightRadius,
|
||||||
|
node.bottomRightRadius,
|
||||||
|
node.bottomLeftRadius,
|
||||||
|
node.bottomLeftRadius
|
||||||
]
|
]
|
||||||
const rrectIndep = new Float32Array([
|
const rrectIndep = new Float32Array([
|
||||||
node.x, node.y,
|
node.x,
|
||||||
node.x + node.width, node.y + node.height,
|
node.y,
|
||||||
...radii,
|
node.x + node.width,
|
||||||
|
node.y + node.height,
|
||||||
|
...radii
|
||||||
])
|
])
|
||||||
canvas.drawRRect(rrectIndep, this.fillPaint)
|
canvas.drawRRect(rrectIndep, this.fillPaint)
|
||||||
void rrect
|
void rrect
|
||||||
|
|
@ -182,13 +179,7 @@ export class SkiaRenderer {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'LINE':
|
case 'LINE':
|
||||||
canvas.drawLine(
|
canvas.drawLine(node.x, node.y, node.x + node.width, node.y + node.height, this.fillPaint)
|
||||||
node.x,
|
|
||||||
node.y,
|
|
||||||
node.x + node.width,
|
|
||||||
node.y + node.height,
|
|
||||||
this.fillPaint
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
canvas.drawRect(rect, this.fillPaint)
|
canvas.drawRect(rect, this.fillPaint)
|
||||||
|
|
@ -233,7 +224,7 @@ export class SkiaRenderer {
|
||||||
screenToCanvas(sx: number, sy: number): { x: number; y: number } {
|
screenToCanvas(sx: number, sy: number): { x: number; y: number } {
|
||||||
return {
|
return {
|
||||||
x: (sx - this.panX) / this.zoom,
|
x: (sx - this.panX) / this.zoom,
|
||||||
y: (sy - this.panY) / this.zoom,
|
y: (sy - this.panY) / this.zoom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,7 @@ function generateId(): string {
|
||||||
return `0:${nextLocalID++}`
|
return `0:${nextLocalID++}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultNode(
|
function createDefaultNode(type: NodeType, overrides: Partial<SceneNode> = {}): SceneNode {
|
||||||
type: NodeType,
|
|
||||||
overrides: Partial<SceneNode> = {}
|
|
||||||
): SceneNode {
|
|
||||||
return {
|
return {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
type,
|
type,
|
||||||
|
|
@ -110,7 +107,7 @@ function createDefaultNode(
|
||||||
cornerSmoothing: 0,
|
cornerSmoothing: 0,
|
||||||
visible: true,
|
visible: true,
|
||||||
locked: false,
|
locked: false,
|
||||||
...overrides,
|
...overrides
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,7 +119,7 @@ export class SceneGraph {
|
||||||
const root = createDefaultNode('FRAME', {
|
const root = createDefaultNode('FRAME', {
|
||||||
name: 'Document',
|
name: 'Document',
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0
|
||||||
})
|
})
|
||||||
this.rootId = root.id
|
this.rootId = root.id
|
||||||
this.nodes.set(root.id, root)
|
this.nodes.set(root.id, root)
|
||||||
|
|
@ -140,11 +137,7 @@ export class SceneGraph {
|
||||||
.filter((n): n is SceneNode => n !== undefined)
|
.filter((n): n is SceneNode => n !== undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
createNode(
|
createNode(type: NodeType, parentId: string, overrides: Partial<SceneNode> = {}): SceneNode {
|
||||||
type: NodeType,
|
|
||||||
parentId: string,
|
|
||||||
overrides: Partial<SceneNode> = {}
|
|
||||||
): SceneNode {
|
|
||||||
const node = createDefaultNode(type, overrides)
|
const node = createDefaultNode(type, overrides)
|
||||||
node.parentId = parentId
|
node.parentId = parentId
|
||||||
this.nodes.set(node.id, node)
|
this.nodes.set(node.id, node)
|
||||||
|
|
@ -176,7 +169,7 @@ export class SceneGraph {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete children recursively
|
// Delete children recursively
|
||||||
for (const childId of [...node.childIds]) {
|
for (const childId of Array.from(node.childIds)) {
|
||||||
this.deleteNode(childId)
|
this.deleteNode(childId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,9 +178,7 @@ export class SceneGraph {
|
||||||
|
|
||||||
hitTest(px: number, py: number): SceneNode | null {
|
hitTest(px: number, py: number): SceneNode | null {
|
||||||
// Reverse order = topmost first
|
// Reverse order = topmost first
|
||||||
const allNodes = [...this.nodes.values()].filter(
|
const allNodes = [...this.nodes.values()].filter((n) => n.id !== this.rootId && n.visible)
|
||||||
(n) => n.id !== this.rootId && n.visible
|
|
||||||
)
|
|
||||||
|
|
||||||
for (let i = allNodes.length - 1; i >= 0; i--) {
|
for (let i = allNodes.length - 1; i >= 0; i--) {
|
||||||
const n = allNodes[i]
|
const n = allNodes[i]
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export class UndoManager {
|
||||||
this.undoStack.push({
|
this.undoStack.push({
|
||||||
label,
|
label,
|
||||||
forward: () => entries.forEach((e) => e.forward()),
|
forward: () => entries.forEach((e) => e.forward()),
|
||||||
inverse: () => [...entries].reverse().forEach((e) => e.inverse()),
|
inverse: () => [...entries].reverse().forEach((e) => e.inverse())
|
||||||
})
|
})
|
||||||
this.redoStack = []
|
this.redoStack = []
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +90,6 @@ export function createPropertyChange(
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
forward: () => Object.assign(node, changes),
|
forward: () => Object.assign(node, changes),
|
||||||
inverse: () => Object.assign(node, previous),
|
inverse: () => Object.assign(node, previous)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue