propertyTabComponent.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import * as React from "react";
  2. import { GlobalState } from '../../globalState';
  3. import { Nullable } from 'babylonjs/types';
  4. import { ButtonLineComponent } from '../../sharedComponents/buttonLineComponent';
  5. import { LineContainerComponent } from '../../sharedComponents/lineContainerComponent';
  6. import { StringTools } from '../../stringTools';
  7. import { FileButtonLineComponent } from '../../sharedComponents/fileButtonLineComponent';
  8. import { Tools } from 'babylonjs/Misc/tools';
  9. import { SerializationTools } from '../../serializationTools';
  10. import { CheckBoxLineComponent } from '../../sharedComponents/checkBoxLineComponent';
  11. import { DataStorage } from 'babylonjs/Misc/dataStorage';
  12. import { GraphNode } from '../../diagram/graphNode';
  13. import { SliderLineComponent } from '../../sharedComponents/sliderLineComponent';
  14. import { GraphFrame } from '../../diagram/graphFrame';
  15. import { TextLineComponent } from '../../sharedComponents/textLineComponent';
  16. import { Engine } from 'babylonjs/Engines/engine';
  17. import { FramePropertyTabComponent } from '../../diagram/properties/framePropertyComponent';
  18. import { FrameNodePortPropertyTabComponent } from '../../diagram/properties/frameNodePortPropertyComponent';
  19. import { InputBlock } from 'babylonjs/Materials/Node/Blocks/Input/inputBlock';
  20. import { NodeMaterialBlockConnectionPointTypes } from 'babylonjs/Materials/Node/Enums/nodeMaterialBlockConnectionPointTypes';
  21. import { Color3LineComponent } from '../../sharedComponents/color3LineComponent';
  22. import { FloatLineComponent } from '../../sharedComponents/floatLineComponent';
  23. import { Color4LineComponent } from '../../sharedComponents/color4LineComponent';
  24. import { Vector2LineComponent } from '../../sharedComponents/vector2LineComponent';
  25. import { Vector3LineComponent } from '../../sharedComponents/vector3LineComponent';
  26. import { Vector4LineComponent } from '../../sharedComponents/vector4LineComponent';
  27. import { Observer } from 'babylonjs/Misc/observable';
  28. import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
  29. import { FrameNodePort } from '../../diagram/frameNodePort';
  30. import { isFramePortData } from '../../diagram/graphCanvas';
  31. require("./propertyTab.scss");
  32. interface IPropertyTabComponentProps {
  33. globalState: GlobalState;
  34. }
  35. export class PropertyTabComponent extends React.Component<IPropertyTabComponentProps, { currentNode: Nullable<GraphNode>, currentFrame: Nullable<GraphFrame>, currentFrameNodePort: Nullable<FrameNodePort> }> {
  36. private _onBuiltObserver: Nullable<Observer<void>>;
  37. constructor(props: IPropertyTabComponentProps) {
  38. super(props)
  39. this.state = { currentNode: null, currentFrame: null, currentFrameNodePort: null };
  40. }
  41. componentDidMount() {
  42. this.props.globalState.onSelectionChangedObservable.add(selection => {
  43. if (selection instanceof GraphNode) {
  44. this.setState({ currentNode: selection, currentFrame: null, currentFrameNodePort: null });
  45. } else if (selection instanceof GraphFrame) {
  46. this.setState({ currentNode: null, currentFrame: selection, currentFrameNodePort: null });
  47. } else if(isFramePortData(selection)) {
  48. this.setState({ currentNode: null, currentFrame: selection.frame, currentFrameNodePort: selection.port });
  49. } else {
  50. this.setState({ currentNode: null, currentFrame: null, currentFrameNodePort: null });
  51. }
  52. });
  53. this._onBuiltObserver = this.props.globalState.onBuiltObservable.add(() => {
  54. this.forceUpdate();
  55. });
  56. }
  57. componentWillUnmount() {
  58. this.props.globalState.onBuiltObservable.remove(this._onBuiltObserver);
  59. }
  60. processInputBlockUpdate(ib: InputBlock) {
  61. this.props.globalState.onUpdateRequiredObservable.notifyObservers();
  62. if (ib.isConstant) {
  63. this.props.globalState.onRebuildRequiredObservable.notifyObservers();
  64. }
  65. }
  66. renderInputBlock(block: InputBlock) {
  67. switch (block.type) {
  68. case NodeMaterialBlockConnectionPointTypes.Float:
  69. let cantDisplaySlider = (isNaN(block.min) || isNaN(block.max) || block.min === block.max);
  70. return (
  71. <div key={block.uniqueId} >
  72. {
  73. block.isBoolean &&
  74. <CheckBoxLineComponent key={block.uniqueId} label={block.name} target={block} propertyName="value"
  75. onValueChanged={() => {
  76. this.processInputBlockUpdate(block);
  77. }}/>
  78. }
  79. {
  80. !block.isBoolean && cantDisplaySlider &&
  81. <FloatLineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block} propertyName="value"
  82. onChange={() => this.processInputBlockUpdate(block)}/>
  83. }
  84. {
  85. !block.isBoolean && !cantDisplaySlider &&
  86. <SliderLineComponent key={block.uniqueId} label={block.name} target={block} propertyName="value"
  87. step={(block.max - block.min) / 100.0} minimum={block.min} maximum={block.max}
  88. onChange={() => this.processInputBlockUpdate(block)}/>
  89. }
  90. </div>
  91. );
  92. case NodeMaterialBlockConnectionPointTypes.Color3:
  93. return (
  94. <Color3LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block}
  95. propertyName="value"
  96. onChange={() => this.processInputBlockUpdate(block)}
  97. />
  98. )
  99. case NodeMaterialBlockConnectionPointTypes.Color4:
  100. return (
  101. <Color4LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block} propertyName="value"
  102. onChange={() => this.processInputBlockUpdate(block)}/>
  103. )
  104. case NodeMaterialBlockConnectionPointTypes.Vector2:
  105. return (
  106. <Vector2LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block}
  107. propertyName="value"
  108. onChange={() => this.processInputBlockUpdate(block)}/>
  109. )
  110. case NodeMaterialBlockConnectionPointTypes.Vector3:
  111. return (
  112. <Vector3LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block}
  113. propertyName="value"
  114. onChange={() => this.processInputBlockUpdate(block)}/>
  115. )
  116. case NodeMaterialBlockConnectionPointTypes.Vector4:
  117. return (
  118. <Vector4LineComponent globalState={this.props.globalState} key={block.uniqueId} label={block.name} target={block}
  119. propertyName="value"
  120. onChange={() => this.processInputBlockUpdate(block)}/>
  121. )
  122. }
  123. return null;
  124. }
  125. load(file: File) {
  126. Tools.ReadFile(file, (data) => {
  127. let decoder = new TextDecoder("utf-8");
  128. SerializationTools.Deserialize(JSON.parse(decoder.decode(data)), this.props.globalState);
  129. }, undefined, true);
  130. }
  131. save() {
  132. let json = SerializationTools.Serialize(this.props.globalState.nodeMaterial, this.props.globalState);
  133. StringTools.DownloadAsFile(this.props.globalState.hostDocument, json, "nodeMaterial.json");
  134. }
  135. customSave() {
  136. this.props.globalState.onLogRequiredObservable.notifyObservers({message: "Saving your material to Babylon.js snippet server...", isError: false});
  137. this.props.globalState.customSave!.action(SerializationTools.Serialize(this.props.globalState.nodeMaterial, this.props.globalState)).then(() => {
  138. this.props.globalState.onLogRequiredObservable.notifyObservers({message: "Material saved successfully", isError: false});
  139. }).catch(err => {
  140. this.props.globalState.onLogRequiredObservable.notifyObservers({message: err, isError: true});
  141. });
  142. }
  143. saveToSnippetServer() {
  144. const material = this.props.globalState.nodeMaterial;
  145. const xmlHttp = new XMLHttpRequest();
  146. let json = SerializationTools.Serialize(material, this.props.globalState);
  147. xmlHttp.onreadystatechange = () => {
  148. if (xmlHttp.readyState == 4) {
  149. if (xmlHttp.status == 200) {
  150. var snippet = JSON.parse(xmlHttp.responseText);
  151. const oldId = material.snippetId;
  152. material.snippetId = snippet.id;
  153. if (snippet.version && snippet.version != "0") {
  154. material.snippetId += "#" + snippet.version;
  155. }
  156. this.forceUpdate();
  157. if (navigator.clipboard) {
  158. navigator.clipboard.writeText(material.snippetId);
  159. }
  160. let windowAsAny = window as any;
  161. if (windowAsAny.Playground && oldId) {
  162. windowAsAny.Playground.onRequestCodeChangeObservable.notifyObservers({
  163. regex: new RegExp(oldId, "g"),
  164. replace: material.snippetId
  165. });
  166. }
  167. alert("NodeMaterial saved with ID: " + material.snippetId + " (please note that the id was also saved to your clipboard)");
  168. }
  169. else {
  170. alert(`Unable to save your node material. It may be too large (${(dataToSend.payload.length / 1024).toFixed(2)} KB) because of embedded textures. Please reduce texture sizes or point to a specific url instead of embedding them and try again.`);
  171. }
  172. }
  173. }
  174. xmlHttp.open("POST", NodeMaterial.SnippetUrl + (material.snippetId ? "/" + material.snippetId : ""), true);
  175. xmlHttp.setRequestHeader("Content-Type", "application/json");
  176. var dataToSend = {
  177. payload : JSON.stringify({
  178. nodeMaterial: json
  179. }),
  180. name: "",
  181. description: "",
  182. tags: ""
  183. };
  184. xmlHttp.send(JSON.stringify(dataToSend));
  185. }
  186. loadFromSnippet() {
  187. const material = this.props.globalState.nodeMaterial;
  188. const scene = material.getScene();
  189. let snippedID = window.prompt("Please enter the snippet ID to use");
  190. if (!snippedID) {
  191. return;
  192. }
  193. this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
  194. NodeMaterial.ParseFromSnippetAsync(snippedID, scene, "", material).then(() => {
  195. material.build();
  196. this.props.globalState.onResetRequiredObservable.notifyObservers();
  197. }).catch(err => {
  198. alert("Unable to load your node material: " + err);
  199. });
  200. }
  201. render() {
  202. if (this.state.currentNode) {
  203. return (
  204. <div id="propertyTab">
  205. <div id="header">
  206. <img id="logo" src="https://www.babylonjs.com/Assets/logo-babylonjs-social-twitter.png" />
  207. <div id="title">
  208. NODE MATERIAL EDITOR
  209. </div>
  210. </div>
  211. {this.state.currentNode.renderProperties()}
  212. </div>
  213. );
  214. }
  215. if (this.state.currentFrameNodePort && this.state.currentFrame) {
  216. return (
  217. <FrameNodePortPropertyTabComponent globalState={this.props.globalState} frame={this.state.currentFrame} frameNodePort={this.state.currentFrameNodePort}/>
  218. );
  219. }
  220. if (this.state.currentFrame) {
  221. return (
  222. <FramePropertyTabComponent globalState={this.props.globalState} frame={this.state.currentFrame}/>
  223. );
  224. }
  225. let gridSize = DataStorage.ReadNumber("GridSize", 20);
  226. return (
  227. <div id="propertyTab">
  228. <div id="header">
  229. <img id="logo" src="https://www.babylonjs.com/Assets/logo-babylonjs-social-twitter.png" />
  230. <div id="title">
  231. NODE MATERIAL EDITOR
  232. </div>
  233. </div>
  234. <div>
  235. <LineContainerComponent title="GENERAL">
  236. <TextLineComponent label="Version" value={Engine.Version}/>
  237. <TextLineComponent label="Help" value="doc.babylonjs.com" underline={true} onLink={() => window.open('https://doc.babylonjs.com/how_to/node_material', '_blank')}/>
  238. <ButtonLineComponent label="Reset to default" onClick={() => {
  239. this.props.globalState.nodeMaterial!.setToDefault();
  240. this.props.globalState.onResetRequiredObservable.notifyObservers();
  241. }} />
  242. </LineContainerComponent>
  243. <LineContainerComponent title="UI">
  244. <ButtonLineComponent label="Zoom to fit" onClick={() => {
  245. this.props.globalState.onZoomToFitRequiredObservable.notifyObservers();
  246. }} />
  247. <ButtonLineComponent label="Reorganize" onClick={() => {
  248. this.props.globalState.onReOrganizedRequiredObservable.notifyObservers();
  249. }} />
  250. </LineContainerComponent>
  251. <LineContainerComponent title="OPTIONS">
  252. <CheckBoxLineComponent label="Embed textures when saving"
  253. isSelected={() => DataStorage.ReadBoolean("EmbedTextures", true)}
  254. onSelect={(value: boolean) => {
  255. DataStorage.WriteBoolean("EmbedTextures", value);
  256. }}
  257. />
  258. <SliderLineComponent label="Grid size" minimum={0} maximum={100} step={5}
  259. decimalCount={0}
  260. directValue={gridSize}
  261. onChange={value => {
  262. DataStorage.WriteNumber("GridSize", value);
  263. this.props.globalState.onGridSizeChanged.notifyObservers();
  264. this.forceUpdate();
  265. }}
  266. />
  267. <CheckBoxLineComponent label="Show grid"
  268. isSelected={() => DataStorage.ReadBoolean("ShowGrid", true)}
  269. onSelect={(value: boolean) => {
  270. DataStorage.WriteBoolean("ShowGrid", value);
  271. this.props.globalState.onGridSizeChanged.notifyObservers();
  272. }}
  273. />
  274. </LineContainerComponent>
  275. <LineContainerComponent title="FILE">
  276. <FileButtonLineComponent label="Load" onClick={(file) => this.load(file)} accept=".json" />
  277. <ButtonLineComponent label="Save" onClick={() => {
  278. this.save();
  279. }} />
  280. <ButtonLineComponent label="Generate code" onClick={() => {
  281. StringTools.DownloadAsFile(this.props.globalState.hostDocument, this.props.globalState.nodeMaterial!.generateCode(), "code.txt");
  282. }} />
  283. <ButtonLineComponent label="Export shaders" onClick={() => {
  284. StringTools.DownloadAsFile(this.props.globalState.hostDocument, this.props.globalState.nodeMaterial!.compiledShaders, "shaders.txt");
  285. }} />
  286. {
  287. this.props.globalState.customSave &&
  288. <ButtonLineComponent label={this.props.globalState.customSave!.label} onClick={() => {
  289. this.customSave();
  290. }} />
  291. }
  292. </LineContainerComponent>
  293. {
  294. !this.props.globalState.customSave &&
  295. <LineContainerComponent title="SNIPPET">
  296. {
  297. this.props.globalState.nodeMaterial!.snippetId &&
  298. <TextLineComponent label="Snippet ID" value={this.props.globalState.nodeMaterial!.snippetId} />
  299. }
  300. <ButtonLineComponent label="Load from snippet server" onClick={() => this.loadFromSnippet()} />
  301. <ButtonLineComponent label="Save to snippet server" onClick={() => {
  302. this.saveToSnippetServer();
  303. }} />
  304. </LineContainerComponent>
  305. }
  306. <LineContainerComponent title="INPUTS">
  307. {
  308. this.props.globalState.nodeMaterial.getInputBlocks().map(ib => {
  309. if (!ib.isUniform || ib.isSystemValue || !ib.name) {
  310. return null;
  311. }
  312. return this.renderInputBlock(ib);
  313. })
  314. }
  315. </LineContainerComponent>
  316. </div>
  317. </div>
  318. );
  319. }
  320. }