diff --git a/js/ThreeCanvas.js b/js/ThreeCanvas.js index 0c7c962..0882473 100644 --- a/js/ThreeCanvas.js +++ b/js/ThreeCanvas.js @@ -56,7 +56,10 @@ export class ThreeCanvas { this.VIEWS3 = views3 // Fix camers - this.fixCamers = fixCamers; + this.fixCamers = fixCamers; + + // List models + this.listModels = [] } getDom(idx=0) { @@ -111,7 +114,54 @@ export class ThreeCanvas { return wrapperCanvas } - createPanel(){ + // get models function + async getModelRequest (pWrapper) { + // Create options elements + const addOptionsEl = (result) => { + const threeCanvasList = pWrapper.querySelector(".threeCanvasListModels") + const models_options = [] + + models_options.push($el("option", {textContent: "Select model", disabled: true, selected: true})) + threeCanvasList.innerHTML = "" + + for(let type in result.models){ + models_options.push($el("option", {textContent: `[${type[0].toUpperCase()+type.slice(1)}]`, disabled: true})) + + + models_options.push(...result.models[type].map((m)=> { + let path = m.path + const [subfolder, name] = path.split("/") + + if(path.indexOf("ThreeViewModels/") === -1){ + path = new URL(path, import.meta.url).href + } else { + path = `/view?filename=${name}&type=input&subfolder=${subfolder}` + } + + this.listModels.push(name) + return $el("option", {value: path, textContent: m.name, dataset: {path: m.path}}) + })) + } + threeCanvasList.append(...models_options) + } + + // Get models + return fetch("/lth/models", { cache: "no-store" }).then((resp)=> resp?.json()).then((result)=>{ + if(result?.models){ + addOptionsEl(result) + } + }).catch((err)=>{ + console.log("Error loading list, loading default list!") + fetch("./models_default.json", { cache: "no-store" }).then((resp)=>resp.json()).then((result)=>{ + if(result){ + addOptionsEl(result) + } + }) + }); + } + + + createPanel(){ // Add panel widget const panelWrapper = $el("div.threeCanvasPanelWrapper", {}, [ $el( @@ -127,6 +177,32 @@ export class ThreeCanvas { textContent: "X", onclick: (e) => this.clear(true), }), + $el("select.threeCanvasListModels", { + onchange: (e) => { + const target = e.target + this.loaderGltf.load(target.value, ( glb ) => { + this.addModel( glb ); + + // Save currentModel path + this.savedData.currentModel = target.options[target.selectedIndex].dataset?.path + panelWrapper.parentElement.dispatchEvent( new CustomEvent("threeview_model_added", { bubbles: true, detail: {currentModel: this.savedData.currentModel}}) ) + + }, + ( data ) => {/* console.log( `Loaded data: ${data.loaded}/${data.total}` */}, + ( err ) => { + console.log( err ); + this.addHeadTest(); + }) + }, + }), + $el("button.threeCanvasListModelsRefresh", { + textContent: "↺", + onclick: (e)=>{ + e.target.style.disabled = true + this.getModelRequest(panelWrapper).then(()=> {e.target.style.disabled = false}) + + } + }), $el("button.threeCanvasButton.threeCanvasSize", { textContent: "Canvas size", onclick: (e) => { @@ -173,7 +249,9 @@ export class ThreeCanvas { })]), ] ), - ]); + ]); + + this.getModelRequest(panelWrapper) return panelWrapper } @@ -445,6 +523,11 @@ export class ThreeCanvas { this.directGlb( content, fname, params ); + if(this.listModels.includes(fname)){ + console.log(`Model name ${fname} already exist in the list loaded models!`) + return; + } + // Load file to server const body = new FormData(); body.append("image", new Blob([content]), fname); @@ -580,7 +663,6 @@ export class ThreeCanvas { // update camera and render this.setCamera( pos, center, near, far ); - } /*addObjectToScene(type, parameters = {}) { diff --git a/js/css/styles.css b/js/css/styles.css index 91b736f..3fb4a75 100644 --- a/js/css/styles.css +++ b/js/css/styles.css @@ -1,11 +1,15 @@ .threeWrapper { - display: grid; - grid-template-columns: 1fr; - gap: 5px; + display: grid; + grid-template-columns: 1fr; + gap: 5px; } -.threeWrapper3Views{ - grid-template-columns: 1fr 1fr; +.threeCanvasListModels { + max-width: 100px; +} + +.threeWrapper3Views { + grid-template-columns: 1fr 1fr; } .threeCanvasPanelWrapper { @@ -41,7 +45,7 @@ } .wrapperThreeViewCanvas { - position: relative; + position: relative; margin: 0px; padding: 0px; display: block; @@ -49,7 +53,7 @@ border-width: 1px; } -.lil-gui { +.lil-gui { --padding: 2px; --spacing: 2px; --widget-height: 14px; @@ -58,14 +62,14 @@ --widget-border-radius: 0px; --slider-input-min-width: 30px; --color-input-min-width: 30px; - --width:150px; - --name-width:30%; - --background-color:none; - --title-background-color:none; + --width: 150px; + --name-width: 30%; + --background-color: none; + --title-background-color: none; --text-color: #ebebeb; --title-text-color: #ebebeb; - --widget-color: rgba(20,20,30,0.6); - --hover-color: rgba(30,30,50,0.8); + --widget-color: rgba(20, 20, 30, 0.6); + --hover-color: rgba(30, 30, 50, 0.8); --focus-color: #595959; --number-color: #2cc9ff; --string-color: #a2db3c; diff --git a/js/models_default.json b/js/models_default.json new file mode 100644 index 0000000..38a6e63 --- /dev/null +++ b/js/models_default.json @@ -0,0 +1,56 @@ +{ + "models": { + "default": [ + { + "name": "column.glb", + "size": 35128, + "path": "./assets/column.glb" + }, + { + "name": "dog.glb", + "size": 37312, + "path": "./assets/dog.glb" + }, + { + "name": "eva00.glb", + "size": 450112, + "path": "./assets/eva00.glb" + }, + { + "name": "eva01.glb", + "size": 446184, + "path": "./assets/eva01.glb" + }, + { + "name": "eva02.glb", + "size": 450816, + "path": "./assets/eva02.glb" + }, + { + "name": "head.glb", + "size": 2346688, + "path": "./assets/head.glb" + }, + { + "name": "head2.glb", + "size": 125672, + "path": "./assets/head2.glb" + }, + { + "name": "lee.glb", + "size": 107424, + "path": "./assets/lee.glb" + }, + { + "name": "man.glb", + "size": 462112, + "path": "./assets/man.glb" + }, + { + "name": "woman.glb", + "size": 465272, + "path": "./assets/woman.glb" + } + ] + } +} \ No newline at end of file diff --git a/js/three_view.js b/js/three_view.js index 6d21788..e391c00 100644 --- a/js/three_view.js +++ b/js/three_view.js @@ -61,7 +61,7 @@ async function widgetThreeJS(node, nodeData, inputData, app, params = {}) { widget = node.addDOMWidget("threeCanvas", "custom_widget", threeCanvas.threeWrapper, { getValue() { return threeCanvas.getSavedOptions(); - }, + }, }); const origDraw = widget.draw; @@ -85,8 +85,13 @@ async function widgetThreeJS(node, nodeData, inputData, app, params = {}) { widget.threeCanvas = threeCanvas; widget.threeWrapper = threeCanvas.threeWrapper; + document.body.addEventListener("threeview_model_added", ({detail})=>{ + // console.log("Detail:", detail?.currentModel) + }) + widget.callback = () => {}; + // Note: If the node is collapsed, the draw method does not work and the canvas will not update. // const animator = () => { // threeCanvas.update(node.size[0]); @@ -240,12 +245,18 @@ app.registerExtension({ // Loading last model added if(currentModel){ - const [subfolder, name] = currentModel.split("/") - const urlData = await api.apiURL( - `/view?filename=${encodeURIComponent( - name - )}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` - ); + + let urlData = "" + if(currentModel.indexOf("assets/") === -1){ + const [subfolder, name] = currentModel.split("/") + urlData = await api.apiURL( + `/view?filename=${encodeURIComponent( + name + )}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}` + ); + } else { + urlData = new URL(currentModel, import.meta.url).href + } await threeCnavasWidget.loaderGltf.load(urlData, ( glb ) => { threeCnavasWidget.addModel( glb ); diff --git a/three_view.py b/three_view.py index 7bbdd9e..a32c27e 100644 --- a/three_view.py +++ b/three_view.py @@ -12,6 +12,7 @@ from server import PromptServer import time from aiohttp import web import folder_paths +import json # Message Handling @@ -42,14 +43,96 @@ import folder_paths DEBUG = True THREEVIEW_DICT = {} # ThreeView dict instances +# Directory threejs +extension_threejs = os.path.dirname(os.path.realpath(__file__)) + +# Support extension models +support_ext = ("glb",) +def getModelsInDir(folder, path): + list_models = [] + for model in os.scandir(folder): + if not model.is_file(): + continue + + model_file_size = model.stat().st_size + if model_file_size == 0: + continue + + model_file = model.name.split(".") + ext = model_file[-1] + + if ext not in support_ext: + continue + + pathFile = os.path.join(path, model.name) + + # model_file_name = ".".join(model_file[:-1]) + list_models.append({"name": model.name, "size": model_file_size, "path": pathFile}) + + return list_models + + +def createJsonFileStartup(): + models_js_dir = os.path.join(extension_threejs, "js") + models_assets_dir = os.path.join(models_js_dir, "assets") + + # Assets directory + if not os.path.exists(models_assets_dir): + return + + models = { + "models": { + "default": getModelsInDir(models_assets_dir, "./assets/"), + } + } + + saveFilePath = os.path.join(models_js_dir, "models_default.json") + with open(saveFilePath, "w+", encoding="utf-8") as file: + file.write(json.dumps(models, indent=4)) + + +# Create on startup default models json file in assets directory +createJsonFileStartup() + + +@PromptServer.instance.routes.get("/lth/models") +async def getModels(request): + try: + models_assets_dir = os.path.join(extension_threejs, "js", "assets") + models_input_dir = os.path.join(folder_paths.get_input_directory(), "ThreeViewModels") + + models = { + "default": [], + "loaded": [] + } + + # Assets directory + if os.path.exists(models_assets_dir): + models["default"] = getModelsInDir(models_assets_dir, "assets/") + else: + print(f"ThreeView: Path {models_assets_dir} not exists!") + + # Input > ThreeViewModels directory + if os.path.exists(models_input_dir): + models["loaded"] = getModelsInDir(models_input_dir, "ThreeViewModels/") + else: + print(f"ThreeView: Path {models_input_dir} not exists!") + + return web.json_response({"status":"ok", "models": models}, status=200) + + except Exception as err: + print("ThreeView: Get models error", err) + return web.json_response({"status":"error"}, status=500) + @PromptServer.instance.routes.get("/api/lth/") async def indexPage(request): - pathIndex = os.path.join(os.path.dirname(os.path.realpath(__file__)), "js", "index.html") + pathIndex = os.path.join(extension_threejs, "js", "index.html") response = web.FileResponse(pathIndex) response.headers['Cache-Control'] = 'no-cache' response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" - return response + return response + @PromptServer.instance.routes.post("/lth/save_complete") async def checkSaveImage(request): @@ -61,7 +144,7 @@ async def checkSaveImage(request): return web.json_response({"status": "Complete"}, status=200) return web.json_response({"status": "Error invalid unique_id"}, status=400) - + def waitSaveImage(unique_id, time_out=100): for _ in range(time_out):