From 6a289d5b30c75e34076d35dbad8f905f021f2a71 Mon Sep 17 00:00:00 2001 From: AlekPet Date: Fri, 6 Dec 2024 00:14:27 +0300 Subject: [PATCH 1/2] update code Add support insert model on scene used select list, storage models --- js/ThreeCanvas.js | 40 ++++++++++++++++++- js/models_default.json | 54 ++++++++++++++++++++++++++ three_view.py | 87 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 js/models_default.json diff --git a/js/ThreeCanvas.js b/js/ThreeCanvas.js index 0c7c962..1cdfb78 100644 --- a/js/ThreeCanvas.js +++ b/js/ThreeCanvas.js @@ -127,6 +127,19 @@ export class ThreeCanvas { textContent: "X", onclick: (e) => this.clear(true), }), + $el("select.threeCanvasListModels", { + onchange: (e) => { + // console.log(e.target.value) + this.loaderGltf.load(e.target.value, ( glb ) => { + this.addModel( glb ); + }, + ( data ) => {/* console.log( `Loaded data: ${data.loaded}/${data.total}` */}, + ( err ) => { + console.log( err ); + this.addHeadTest(); + }) + }, + }), $el("button.threeCanvasButton.threeCanvasSize", { textContent: "Canvas size", onclick: (e) => { @@ -173,7 +186,32 @@ export class ThreeCanvas { })]), ] ), - ]); + ]); + + const addOptionsEl = (result) => { + const models_options = [] + models_options.push($el("option", {textContent: "Select model", disabled: true, selected: true})) + 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)=> $el("option", {value: m.path.indexOf("/view") === -1 ? new URL(m.path, import.meta.url).href: m.path, textContent: m.name}))) + } + panelWrapper.querySelector(".threeCanvasListModels").append(...models_options) + } + + // Get models + fetch("/lth/models").then((resp)=> resp?.json()).then((result)=>{ + if(result?.models){ + addOptionsEl(result) + } + }).catch((err)=>{ + console.log(err) + fetch("./models_default.json").then((resp)=>resp.json()).then((result)=>{ + console.log("!!!",result ) + if(result){ + addOptionsEl({models: result}) + } + }) + }); return panelWrapper } diff --git a/js/models_default.json b/js/models_default.json new file mode 100644 index 0000000..0e40bc5 --- /dev/null +++ b/js/models_default.json @@ -0,0 +1,54 @@ +{ + "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/three_view.py b/three_view.py index 7bbdd9e..f1b7c6d 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,94 @@ 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 = { + "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"] = list(map(lambda x: {**x, "path": f"/view?filename={x['name']}&type=input&subfolder=ThreeViewModels"}, getModelsInDir(models_input_dir, ""))) + 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 +142,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): From ee351ea6ba371120f61fdfc2f19e28eac329182b Mon Sep 17 00:00:00 2001 From: AlekPet Date: Fri, 6 Dec 2024 18:55:48 +0300 Subject: [PATCH 2/2] Inplemented select model form list Support saves selected model in list when reloaded and loaded page browser. Write backend code get models in json format, and create file json default models for local server. Add buttons refresh model list --- js/ThreeCanvas.js | 104 ++++++++++++++++++++++++++++------------ js/css/styles.css | 30 +++++++----- js/models_default.json | 106 +++++++++++++++++++++-------------------- js/three_view.js | 25 +++++++--- three_view.py | 10 ++-- 5 files changed, 169 insertions(+), 106 deletions(-) diff --git a/js/ThreeCanvas.js b/js/ThreeCanvas.js index 1cdfb78..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( @@ -129,9 +179,14 @@ export class ThreeCanvas { }), $el("select.threeCanvasListModels", { onchange: (e) => { - // console.log(e.target.value) - this.loaderGltf.load(e.target.value, ( glb ) => { - this.addModel( glb ); + 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 ) => { @@ -140,6 +195,14 @@ export class ThreeCanvas { }) }, }), + $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) => { @@ -188,30 +251,7 @@ export class ThreeCanvas { ), ]); - const addOptionsEl = (result) => { - const models_options = [] - models_options.push($el("option", {textContent: "Select model", disabled: true, selected: true})) - 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)=> $el("option", {value: m.path.indexOf("/view") === -1 ? new URL(m.path, import.meta.url).href: m.path, textContent: m.name}))) - } - panelWrapper.querySelector(".threeCanvasListModels").append(...models_options) - } - - // Get models - fetch("/lth/models").then((resp)=> resp?.json()).then((result)=>{ - if(result?.models){ - addOptionsEl(result) - } - }).catch((err)=>{ - console.log(err) - fetch("./models_default.json").then((resp)=>resp.json()).then((result)=>{ - console.log("!!!",result ) - if(result){ - addOptionsEl({models: result}) - } - }) - }); + this.getModelRequest(panelWrapper) return panelWrapper } @@ -483,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); @@ -618,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 index 0e40bc5..38a6e63 100644 --- a/js/models_default.json +++ b/js/models_default.json @@ -1,54 +1,56 @@ { - "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" - } - ] + "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 f1b7c6d..a32c27e 100644 --- a/three_view.py +++ b/three_view.py @@ -81,7 +81,9 @@ def createJsonFileStartup(): return models = { - "default": getModelsInDir(models_assets_dir, "./assets/"), + "models": { + "default": getModelsInDir(models_assets_dir, "./assets/"), + } } saveFilePath = os.path.join(models_js_dir, "models_default.json") @@ -101,18 +103,18 @@ async def getModels(request): models = { "default": [], - "loaded": [] + "loaded": [] } # Assets directory if os.path.exists(models_assets_dir): - models["default"] = getModelsInDir(models_assets_dir, "./assets/") + 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"] = list(map(lambda x: {**x, "path": f"/view?filename={x['name']}&type=input&subfolder=ThreeViewModels"}, getModelsInDir(models_input_dir, ""))) + models["loaded"] = getModelsInDir(models_input_dir, "ThreeViewModels/") else: print(f"ThreeView: Path {models_input_dir} not exists!")