diff --git a/package-lock.json b/package-lock.json index be1cf8857ec..57bb38a5b21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11956,18 +11956,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", - "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", + "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/type-utils": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "graphemer": "^1.4.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/type-utils": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" @@ -11980,7 +11979,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.1", + "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -11995,16 +11994,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "engines": { @@ -12020,9 +12019,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -12034,14 +12033,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/tsconfig-utils": "^8.50.0", + "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "engines": { @@ -12056,9 +12055,9 @@ } }, "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -12070,14 +12069,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1" + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12088,9 +12087,9 @@ } }, "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -12102,9 +12101,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", "dev": true, "license": "MIT", "engines": { @@ -12119,15 +12118,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", + "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -12144,9 +12143,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -12172,16 +12171,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/project-service": "8.50.0", + "@typescript-eslint/tsconfig-utils": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -12200,9 +12199,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -12214,16 +12213,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1" + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12238,9 +12237,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -12252,13 +12251,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -12270,9 +12269,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -18760,12 +18759,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/graphlib": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", @@ -30497,16 +30490,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", - "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", + "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.1", - "@typescript-eslint/parser": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1" + "@typescript-eslint/eslint-plugin": "8.50.0", + "@typescript-eslint/parser": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/packages/dev/inspector-v2/src/components/properties/materials/pbrMaterialProperties.tsx b/packages/dev/inspector-v2/src/components/properties/materials/pbrMaterialProperties.tsx index 50a8d2680f4..37e3141510d 100644 --- a/packages/dev/inspector-v2/src/components/properties/materials/pbrMaterialProperties.tsx +++ b/packages/dev/inspector-v2/src/components/properties/materials/pbrMaterialProperties.tsx @@ -5,6 +5,7 @@ import { BoundProperty } from "../boundProperty"; import { Color3PropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/colorPropertyLine"; import { SyncedSliderPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/syncedSliderPropertyLine"; import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; +import { BoundTextureProperty } from "../textures/boundTextureProperty"; /** * Displays the lighting and color properties of a PBR material. @@ -26,3 +27,31 @@ export const PBRMaterialLightingAndColorProperties: FunctionComponent<{ material ); }; + +/** + * Displays the texture channel properties of a PBR material. + * @param props - The required properties + * @returns A JSX element representing the texture channels. + */ +export const PBRMaterialTextureProperties: FunctionComponent<{ material: PBRMaterial }> = (props) => { + const { material } = props; + const scene = material.getScene(); + + return ( + <> + + + + + + + + + + + + + + + ); +}; diff --git a/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx b/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx index cd61d20fa68..fd62257fccf 100644 --- a/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx +++ b/packages/dev/inspector-v2/src/components/properties/textures/baseTextureProperties.tsx @@ -3,14 +3,10 @@ import type { FunctionComponent } from "react"; import type { BaseTexture } from "core/index"; import type { DropdownOption } from "shared-ui-components/fluent/primitives/dropdown"; -import { useCallback } from "react"; - import { Constants } from "core/Engines/constants"; -import { CubeTexture } from "core/Materials/Textures/cubeTexture"; import { Texture } from "core/Materials/Textures/texture"; -import { ReadFile } from "core/Misc/fileTools"; import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine"; -import { FileUploadLine } from "shared-ui-components/fluent/hoc/fileUploadLine"; +import { TextureUpload } from "shared-ui-components/fluent/hoc/textureUpload"; import { BooleanBadgePropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/booleanBadgePropertyLine"; import { NumberDropdownPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/dropdownPropertyLine"; import { TextInputPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/inputPropertyLine"; @@ -26,56 +22,10 @@ import { TexturePreview } from "./texturePreview"; export const BaseTexturePreviewProperties: FunctionComponent<{ texture: BaseTexture }> = (props) => { const { texture } = props; - const isUpdatable = texture instanceof Texture || texture instanceof CubeTexture; - - const updateTexture = useCallback( - (file: File) => { - ReadFile( - file, - (data) => { - const blob = new Blob([data], { type: "octet/stream" }); - - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => { - const base64data = reader.result as string; - - if (texture instanceof CubeTexture) { - let extension: string | undefined = undefined; - if (file.name.toLowerCase().indexOf(".dds") > 0) { - extension = ".dds"; - } else if (file.name.toLowerCase().indexOf(".env") > 0) { - extension = ".env"; - } - - texture.updateURL(base64data, extension); - } else if (texture instanceof Texture) { - texture.updateURL(base64data); - } - }; - }, - undefined, - true - ); - }, - [texture] - ); - return ( <> - {/* TODO: This should probably be dynamically fetching a list of supported texture extensions. */} - {isUpdatable && ( - { - if (files.length > 0) { - updateTexture(files[0]); - } - }} - /> - )} + {}} /> ); diff --git a/packages/dev/inspector-v2/src/components/properties/textures/boundTextureProperty.tsx b/packages/dev/inspector-v2/src/components/properties/textures/boundTextureProperty.tsx new file mode 100644 index 00000000000..f1f9fd55282 --- /dev/null +++ b/packages/dev/inspector-v2/src/components/properties/textures/boundTextureProperty.tsx @@ -0,0 +1,47 @@ +import { ChooseTexturePropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/chooseTexturePropertyLine"; +import { useProperty } from "../../../hooks/compoundPropertyHooks"; +import { usePropertyChangedNotifier } from "../../../contexts/propertyContext"; +import type { Scene } from "core/scene"; +import type { BaseTexture } from "core/Materials/Textures/baseTexture"; +import type { Nullable } from "core/types"; + +/** + * Type alias for objects with texture properties + */ +type TextureHolder = Record>; + +/** + * Props for BoundTextureProperty + */ +type BoundTexturePropertyProps = { + label: string; + target: TextureHolder; + propertyKey: K; + scene: Scene; + cubeOnly?: boolean; +}; + +/** + * Helper to bind texture properties without needing defaultValue + * @param props - The required properties + * @returns ChooseTexturePropertyLine component + */ +export function BoundTextureProperty(props: BoundTexturePropertyProps) { + const { label, target, propertyKey, scene, cubeOnly } = props; + const value = useProperty(target, propertyKey); + const notifyPropertyChanged = usePropertyChangedNotifier(); + + return ( + { + const oldValue = target[propertyKey]; + target[propertyKey] = texture; + notifyPropertyChanged(target, propertyKey, oldValue, texture); + }} + scene={scene} + cubeOnly={cubeOnly} + /> + ); +} diff --git a/packages/dev/inspector-v2/src/services/panes/properties/materialPropertiesService.tsx b/packages/dev/inspector-v2/src/services/panes/properties/materialPropertiesService.tsx index 96f9ec30faf..58c2afc687e 100644 --- a/packages/dev/inspector-v2/src/services/panes/properties/materialPropertiesService.tsx +++ b/packages/dev/inspector-v2/src/services/panes/properties/materialPropertiesService.tsx @@ -21,7 +21,7 @@ import { PBRBaseMaterialIridescenceProperties, PBRBaseMaterialSheenProperties, } from "../../../components/properties/materials/pbrBaseMaterialProperties"; -import { PBRMaterialLightingAndColorProperties } from "../../../components/properties/materials/pbrMaterialProperties"; +import { PBRMaterialLightingAndColorProperties, PBRMaterialTextureProperties } from "../../../components/properties/materials/pbrMaterialProperties"; import { OpenPBRMaterialBaseProperties, OpenPBRMaterialCoatProperties, @@ -114,6 +114,10 @@ export const MaterialPropertiesServiceDefinition: ServiceDefinition<[], [IProper key: "PBR Material Properties", predicate: (entity: unknown) => entity instanceof PBRMaterial, content: [ + { + section: "Textures", + component: ({ context }) => , + }, { section: "Lighting & Colors", component: ({ context }) => , diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/fileUploadLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/fileUploadLine.tsx index 4e4aeccd428..4eaef6403a9 100644 --- a/packages/dev/sharedUiComponents/src/fluent/hoc/fileUploadLine.tsx +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/fileUploadLine.tsx @@ -1,35 +1,25 @@ -import { useRef } from "react"; import type { FunctionComponent } from "react"; -import { ButtonLine } from "./buttonLine"; +import { LineContainer } from "./propertyLines/propertyLine"; +import { UploadButton } from "../primitives/uploadButton"; import type { ButtonProps } from "../primitives/button"; -import { ArrowUploadRegular } from "@fluentui/react-icons"; type FileUploadLineProps = Omit & { onClick: (files: FileList) => void; - label: string; // Require a label when button is the entire line (by default, label is optional on a button) + label: string; // Require a label when button is the entire line (by default, label is optional on an UploadButton accept: string; }; -export const FileUploadLine: FunctionComponent = (props) => { +/** + * A full-width line with an upload button. + * For just the button without the line wrapper, use UploadButton directly. + * @returns An UploadButton wrapped in a LineContainer + */ +export const FileUploadLine: FunctionComponent = ({ onClick, label, accept, ...buttonProps }) => { FileUploadLine.displayName = "FileUploadLine"; - const inputRef = useRef(null); - - const handleButtonClick = () => { - inputRef.current?.click(); - }; - - const handleChange = (evt: React.ChangeEvent) => { - const files = evt.target.files; - if (files && files.length) { - props.onClick(files); - } - evt.target.value = ""; - }; return ( - <> - - - + + + ); }; diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/chooseTexturePropertyLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/chooseTexturePropertyLine.tsx new file mode 100644 index 00000000000..c2411de1c1f --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/chooseTexturePropertyLine.tsx @@ -0,0 +1,26 @@ +import type { FunctionComponent } from "react"; +import type { BaseTexture } from "core/Materials/Textures/baseTexture"; +import type { Nullable } from "core/types"; +import type { PropertyLineProps } from "./propertyLine"; +import type { ChooseTextureProps } from "../../primitives/chooseTexture"; + +import { PropertyLine } from "./propertyLine"; +import { ChooseTexture } from "../../primitives/chooseTexture"; + +type ChooseTexturePropertyLineProps = PropertyLineProps> & ChooseTextureProps; + +/** + * A property line with a ComboBox for selecting from existing scene textures + * and a button for uploading new texture files. + * @param props - ChooseTextureProps & PropertyLineProps + * @returns property-line wrapped ChooseTexture component + */ +export const ChooseTexturePropertyLine: FunctionComponent = (props) => { + ChooseTexturePropertyLine.displayName = "ChooseTexturePropertyLine"; + + return ( + + + + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/comboBoxPropertyLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/comboBoxPropertyLine.tsx new file mode 100644 index 00000000000..609bdc18a76 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/propertyLines/comboBoxPropertyLine.tsx @@ -0,0 +1,23 @@ +import type { FunctionComponent } from "react"; +import type { PropertyLineProps } from "./propertyLine"; +import type { ComboBoxProps } from "../../primitives/comboBox"; + +import { PropertyLine } from "./propertyLine"; +import { ComboBox } from "../../primitives/comboBox"; + +type ComboBoxPropertyLineProps = ComboBoxProps & PropertyLineProps; + +/** + * A property line with a filterable ComboBox + * @param props - ComboBoxProps & PropertyLineProps + * @returns property-line wrapped ComboBox component + */ +export const ComboBoxPropertyLine: FunctionComponent = (props) => { + ComboBoxPropertyLine.displayName = "ComboBoxPropertyLine"; + + return ( + + + + ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/textureUpload.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/textureUpload.tsx new file mode 100644 index 00000000000..883ef51eb1c --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/hoc/textureUpload.tsx @@ -0,0 +1,109 @@ +import type { FunctionComponent } from "react"; +import { useCallback } from "react"; +import type { BaseTexture } from "core/Materials/Textures/baseTexture"; +import type { Scene } from "core/scene"; +import { Texture } from "core/Materials/Textures/texture"; +import { CubeTexture } from "core/Materials/Textures/cubeTexture"; +import { ReadFile } from "core/Misc/fileTools"; +import { UploadButton } from "../primitives/uploadButton"; + +type TextureUploadUpdateProps = { + /** + * Existing texture to update via updateURL + */ + texture: BaseTexture; + /** + * Callback after texture is updated + */ + onChange?: (texture: BaseTexture) => void; + scene?: never; + cubeOnly?: never; +}; + +type TextureUploadCreateProps = { + /** + * The scene to create the texture in + */ + scene: Scene; + /** + * Callback when a new texture is created + */ + onChange: (texture: BaseTexture) => void; + /** + * Whether to create cube textures + */ + cubeOnly?: boolean; + texture?: never; +}; + +type TextureUploadProps = TextureUploadUpdateProps | TextureUploadCreateProps; + +/** + * A button that uploads a file and either: + * - Updates an existing Texture or CubeTexture via updateURL (if texture prop is provided) + * - Creates a new Texture or CubeTexture (if scene/onChange props are provided) + * @param props TextureUploadProps + * @returns UploadButton component that handles texture upload + */ +export const TextureUpload: FunctionComponent = (props) => { + TextureUpload.displayName = "TextureUpload"; + const label = props.texture ? "Upload Texture" : undefined; + // TODO: This should probably be dynamically fetching a list of supported texture extensions + const accept = ".jpg, .png, .tga, .dds, .env, .exr"; + const handleUpload = useCallback( + (files: FileList) => { + const file = files[0]; + if (!file) { + return; + } + + ReadFile( + file, + (data) => { + const blob = new Blob([data], { type: "octet/stream" }); + + // Update existing texture + if (props.texture) { + const { texture, onChange } = props; + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => { + const base64data = reader.result as string; + + if (texture instanceof CubeTexture) { + let extension: string | undefined = undefined; + if (file.name.toLowerCase().indexOf(".dds") > 0) { + extension = ".dds"; + } else if (file.name.toLowerCase().indexOf(".env") > 0) { + extension = ".env"; + } + texture.updateURL(base64data, extension, () => onChange?.(texture)); + } else if (texture instanceof Texture) { + texture.updateURL(base64data, null, () => onChange?.(texture)); + } + }; + } else { + // Create new texture + const { scene, cubeOnly, onChange } = props; + const url = URL.createObjectURL(blob); + const extension = file.name.split(".").pop()?.toLowerCase(); + + // Revoke the object URL after texture loads to prevent memory leak + const revokeUrl = () => URL.revokeObjectURL(url); + + const newTexture = cubeOnly + ? new CubeTexture(url, scene, [], false, undefined, revokeUrl, undefined, undefined, false, extension ? "." + extension : undefined) + : new Texture(url, scene, false, false, undefined, revokeUrl); + + onChange(newTexture); + } + }, + undefined, + true + ); + }, + [props] + ); + + return ; +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/chooseTexture.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/chooseTexture.tsx new file mode 100644 index 00000000000..7bba2497a55 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/chooseTexture.tsx @@ -0,0 +1,69 @@ +import type { BaseTexture } from "core/Materials/Textures/baseTexture"; +import type { Scene } from "core/scene"; +import type { Nullable } from "core/types"; +import type { FunctionComponent } from "react"; +import type { PrimitiveProps } from "./primitive"; + +import { makeStyles, tokens } from "@fluentui/react-components"; +import { useMemo } from "react"; +import { TextureUpload } from "../hoc/textureUpload"; +import { ComboBox } from "./comboBox"; + +const useStyles = makeStyles({ + container: { + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: tokens.spacingHorizontalS, + }, +}); + +export type ChooseTextureProps = PrimitiveProps> & { + /** + * The scene to get textures from + */ + scene: Scene; + /** + * File types to accept for upload + */ + accept?: string; + /** + * Whether to only allow cube textures + */ + cubeOnly?: boolean; +}; + +/** + * A primitive component with a ComboBox for selecting from existing scene textures + * and a button for uploading new texture files. + * @param props ChooseTextureProps + * @returns ChooseTexture component + */ +export const ChooseTexture: FunctionComponent = (props) => { + ChooseTexture.displayName = "ChooseTexture"; + const { scene, cubeOnly, value, onChange } = props; + const classes = useStyles(); + + // Get sorted texture names from scene + const textureOptions = useMemo(() => { + return scene.textures + .filter((t) => t.name && (!cubeOnly || t.isCube)) + .map((t) => t.displayName || t.name) + .sort((a, b) => a.localeCompare(b)); + }, [scene.textures, cubeOnly]); + + const handleTextureSelect = (textureName: string) => { + const texture = scene.textures.find((t) => (t.displayName || t.name) === textureName); + onChange(texture ?? null); + }; + + // Get current texture name for initial display + const currentTextureName = value ? value.displayName || value.name : ""; + + return ( +
+ + +
+ ); +}; diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/comboBox.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/comboBox.tsx index ae6e4eb43ba..249a1b9c39c 100644 --- a/packages/dev/sharedUiComponents/src/fluent/primitives/comboBox.tsx +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/comboBox.tsx @@ -1,7 +1,11 @@ import type { FunctionComponent } from "react"; -import { useState } from "react"; +import { useState, useContext, useEffect } from "react"; import { Combobox as FluentComboBox, makeStyles, useComboboxFilter, useId } from "@fluentui/react-components"; import type { OptionOnSelectData, SelectionEvents } from "@fluentui/react-components"; +import { ToolContext } from "../hoc/fluentToolWrapper"; +import { CustomTokens } from "./utils"; +import type { PrimitiveProps } from "./primitive"; + const useStyles = makeStyles({ root: { // Stack the label above the field with a gap @@ -11,12 +15,22 @@ const useStyles = makeStyles({ gap: "2px", maxWidth: "400px", }, + comboBox: { + width: CustomTokens.inputWidth, + minWidth: CustomTokens.inputWidth, + boxSizing: "border-box", + }, + input: { + minWidth: 0, // Override Fluent's default minWidth on the input + }, }); -export type ComboBoxProps = { +export type ComboBoxProps = PrimitiveProps & { label: string; - value: string[]; - onChange: (value: string) => void; + /** + * The list of options to display + */ + options: string[]; }; /** * Wrapper around a Fluent ComboBox that allows for filtering options @@ -27,9 +41,16 @@ export const ComboBox: FunctionComponent = (props) => { ComboBox.displayName = "ComboBox"; const comboId = useId(); const styles = useStyles(); + const { size } = useContext(ToolContext); + + const [query, setQuery] = useState(props.value ?? ""); + + // Sync query with props.value when it changes externally + useEffect(() => { + setQuery(props.value ?? ""); + }, [props.value]); - const [query, setQuery] = useState(""); - const children = useComboboxFilter(query, props.value, { + const children = useComboboxFilter(query, props.options, { noOptionsMessage: "No items match your search.", }); const onOptionSelect = (_e: SelectionEvents, data: OptionOnSelectData) => { @@ -40,7 +61,17 @@ export const ComboBox: FunctionComponent = (props) => { return (
- setQuery(ev.target.value)} value={query}> + props.onChange(query)} + aria-labelledby={comboId} + placeholder="Search.." + onChange={(ev) => setQuery(ev.target.value)} + value={query} + > {children}
diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/uploadButton.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/uploadButton.tsx new file mode 100644 index 00000000000..ad819e90899 --- /dev/null +++ b/packages/dev/sharedUiComponents/src/fluent/primitives/uploadButton.tsx @@ -0,0 +1,51 @@ +import type { FunctionComponent } from "react"; +import { useRef } from "react"; +import { ArrowUploadRegular } from "@fluentui/react-icons"; +import { Button } from "./button"; +import type { ButtonProps } from "./button"; + +type UploadButtonProps = Omit & { + /** + * Callback when files are selected + */ + onUpload: (files: FileList) => void; + /** + * File types to accept (e.g., ".jpg, .png, .dds") + */ + accept?: string; + /** + * Text label to display on the button (optional) + */ + label?: string; +}; + +/** + * A button that triggers a file upload dialog. + * Combines a Button with a hidden file input. + * @param props UploadButtonProps + * @returns UploadButton component + */ +export const UploadButton: FunctionComponent = (props) => { + const { onUpload, accept, label, ...buttonProps } = props; + UploadButton.displayName = "UploadButton"; + const inputRef = useRef(null); + + const handleClick = () => { + inputRef.current?.click(); + }; + + const handleChange = (evt: React.ChangeEvent) => { + const files = evt.target.files; + if (files && files.length) { + onUpload(files); + } + evt.target.value = ""; + }; + + return ( + <> +