1010 lines
32 KiB
JavaScript
1010 lines
32 KiB
JavaScript
import * as THREE from "./lib/three.module.js";
|
|
import { OrbitControls } from "./lib/jsm/controls/OrbitControls.js";
|
|
|
|
import { EffectComposer } from './lib/jsm/postprocessing/EffectComposer.js';
|
|
import { RenderPass } from './lib/jsm/postprocessing/RenderPass.js';
|
|
import { ShaderPass } from './lib/jsm/postprocessing/ShaderPass.js';
|
|
import { SobelOperatorShader } from './lib/jsm/shaders/SobelOperatorShader.js';
|
|
import { LuminosityShader } from './lib/jsm/shaders/LuminosityShader.js';
|
|
import { ExposureShader } from './lib/jsm/shaders/ExposureShader.js';
|
|
import { BrightnessContrastShader } from './lib/jsm/shaders/BrightnessContrastShader.js';
|
|
|
|
import { Depth, Outline, Inline, BlackAll, sobelShader, cannyEdgeShader, thresholdShader } from "./lib/Toon.js";
|
|
import { Hub } from "./lib/Hub.js";
|
|
import { Files } from "./lib/Files.js";
|
|
import { Tools } from "./lib/Tools.js";
|
|
import { Pool } from "./lib/Pool.js";
|
|
|
|
import { $el } from "./lib/utils.js";
|
|
|
|
// Class ThreeCanvas
|
|
export class ThreeCanvas {
|
|
|
|
constructor(node, widget, params ={} ) {
|
|
const {
|
|
w = 512,
|
|
h = 512,
|
|
r = 1,
|
|
offset = 10,
|
|
savedData = {},
|
|
views3 = false,
|
|
fixCamers = true
|
|
} = params
|
|
|
|
|
|
this.widgeImageThree = widget;
|
|
this.node = node;
|
|
this.size = { w, h, r, offset };
|
|
|
|
this.savedData = savedData;
|
|
|
|
this.objects = [];
|
|
this.needResize = false;
|
|
this.pixelRatio = 1;
|
|
|
|
this.enableDrag = false;
|
|
this.withHelper = true;
|
|
this.fov = 55;
|
|
|
|
// lock scale if false
|
|
this.autoScale = !true;
|
|
this.autoAutoAnim = false;
|
|
this.useLinesComposer = true;
|
|
|
|
// Views3
|
|
this.VIEWS3 = views3
|
|
|
|
// Fix camers
|
|
this.fixCamers = fixCamers;
|
|
|
|
// List models
|
|
this.listModels = [];
|
|
this.api = null;
|
|
|
|
}
|
|
|
|
// import api directlly break index.html preview !!
|
|
setApi( api ){ this.api = api; }
|
|
|
|
getDom(idx=0) {
|
|
return this.tools[idx].renderer.domElement;
|
|
}
|
|
|
|
// all loader is on Pool.js
|
|
getLoader( type = 'glb' ){
|
|
return Pool.getLoader( type );
|
|
}
|
|
|
|
getWrapper(idx=0){
|
|
return this.wrappers[idx];
|
|
}
|
|
|
|
createWrappers(canvasNames){
|
|
// Operators "Sobel", "Scharr", "Prewitt"
|
|
const linesOperators = ["Sobel", "Prewitt", "Scharr", "Canny"];
|
|
const listLines = $el("select.linesMethod_box",{
|
|
style: {
|
|
fontSize: "0.5rem",
|
|
color: "var(--p-gray-300)",
|
|
position: "absolute",
|
|
opacity: 0.7,
|
|
gap: "3px",
|
|
top: "-15px",
|
|
outline: 0
|
|
},
|
|
onchange: (e)=> {
|
|
this.sobelPassSelect = e.target.value
|
|
this.composer = null
|
|
this.render()
|
|
},
|
|
title: "Selectd operator for lines"
|
|
}, linesOperators.map((val, idx)=> $el("option", {
|
|
style: { fontSize: "1rem" },
|
|
value: idx,
|
|
textContent: val,
|
|
selected: val === "Sobel",
|
|
|
|
}))
|
|
)
|
|
|
|
// Wrapper
|
|
const wrapperCanvas = canvasNames.map((data, idx)=> {
|
|
|
|
const wrapper = $el(`div.wrapperThreeViewCanvas.threeview_wrapper_${data.name}`, {
|
|
style: { borderColor: data.color, },
|
|
});
|
|
|
|
if(idx === 1) wrapper.append(listLines)
|
|
wrapper.append(this.getDom(idx))
|
|
|
|
return wrapper
|
|
})
|
|
|
|
return wrapperCanvas
|
|
}
|
|
|
|
// 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(
|
|
"div.threeCanvasPanel",
|
|
[
|
|
$el("button.threeCanvasButton.threeCanvasAdd", {
|
|
textContent: "Load",
|
|
onclick: (e) => {
|
|
this.load()
|
|
},
|
|
}),
|
|
$el("button.threeCanvasButton.threeCanvasDel", {
|
|
textContent: "X",
|
|
onclick: (e) => this.clear(true),
|
|
}),
|
|
$el("select.threeCanvasListModels", {
|
|
onchange: (e) => {
|
|
const target = e.target
|
|
this.getLoader('glb').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) => {
|
|
try {
|
|
let w = +prompt("Widht:", this.size.w);
|
|
let h = +prompt("Height:", this.size.h);
|
|
|
|
// Simple check...
|
|
if (!w || w <= 0) w = this.size.w;
|
|
if (!h || h <= 0) h = this.size.h;
|
|
|
|
if (w) this.setCanvasSize(w, h);
|
|
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
|
|
// add validate check size
|
|
},
|
|
}),
|
|
$el("div.threeCanvasViews3Box", {
|
|
},[
|
|
$el("button.threeCanvasButton.threeCanvasViews3", {
|
|
textContent: "All Views",
|
|
onclick: (e) => {
|
|
this.VIEWS3 = !this.VIEWS3;
|
|
|
|
this.panelWrapper.querySelector(".threeCanvasViews3_camerafix").style.display = this.VIEWS3 ? "block": "none";
|
|
// threeCanvas.fixCamers = threeCanvas.VIEWS3
|
|
this.setViews3(this.wrappers);
|
|
},
|
|
}),
|
|
$el("input.threeCanvasViews3_camerafix", {
|
|
type: "checkbox",
|
|
title: "Fix all camers",
|
|
checked: this.fixCamers,
|
|
style: {
|
|
display: "none",
|
|
},
|
|
onchange: (e)=>{
|
|
this.fixCamers = !!e.target.checked
|
|
this.composer = null
|
|
},
|
|
})]),
|
|
]
|
|
),
|
|
]);
|
|
|
|
this.getModelRequest(panelWrapper)
|
|
|
|
return panelWrapper
|
|
}
|
|
|
|
init( canvasNames ) {
|
|
// Calculate aspect ratio
|
|
this.aspectRatio = this.size.w / this.size.h;
|
|
|
|
// Set three.js scene
|
|
const scene = new THREE.Scene();
|
|
|
|
// Renderer setup 1 COLOR
|
|
const renderer1 = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
|
renderer1.setSize( this.size.w, this.size.h, false );
|
|
renderer1.setPixelRatio( this.pixelRatio );
|
|
renderer1.setClearColor( 0x000000, 1 );
|
|
renderer1.shadowMap.enabled = true;
|
|
renderer1.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer1.toneMappingExposure = 0.8;
|
|
//renderer1.outputColorSpace = THREE.SRGBColorSpace;
|
|
// renderer1.domElement.style.cssText = "position:absolute; margin:0; padding:0; border:1px solid black;";
|
|
renderer1.domElement.setAttribute("view", "LEFT")
|
|
renderer1.domElement.classList.add("threeview_renderer")
|
|
|
|
Pool.renderer = renderer1;
|
|
|
|
// Renderer setup 2 LINES
|
|
const renderer2 = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
|
renderer2.setSize( this.size.w, this.size.h, false );
|
|
renderer2.setPixelRatio( this.pixelRatio );
|
|
renderer2.setClearColor( 0x000000, 1 );
|
|
renderer1.shadowMap.enabled = false;
|
|
renderer2.autoClear = false;
|
|
// renderer2.domElement.style.cssText = "position:absolute; margin:0; padding:0; border:1px solid red;";
|
|
renderer2.domElement.setAttribute("view", "TOP")
|
|
renderer2.domElement.classList.add("threeview_renderer")
|
|
|
|
// Renderer setup 3 DEPTH
|
|
const renderer3 = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
|
renderer3.setSize( this.size.w, this.size.h, false );
|
|
renderer3.setPixelRatio( this.pixelRatio );
|
|
renderer3.setClearColor( 0x000000, 1 );
|
|
// renderer3.domElement.style.cssText = "position:absolute; margin:0; padding:0; border:1px solid yellow;";
|
|
renderer3.domElement.setAttribute("view", "FRONT")
|
|
renderer3.domElement.classList.add("threeview_renderer")
|
|
|
|
// Renderer setup 4 NORMAL
|
|
const renderer4 = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
|
renderer4.setSize( this.size.w, this.size.h, false );
|
|
renderer4.setPixelRatio( this.pixelRatio );
|
|
renderer4.setClearColor( 0x8080ff, 1 );
|
|
// renderer4.domElement.style.cssText = "position:absolute; margin:0; padding:0; border:1px solid #8080ff;";
|
|
renderer4.domElement.setAttribute("view", "FRONT")
|
|
renderer4.domElement.classList.add("threeview_renderer")
|
|
|
|
// Camera setup 1
|
|
const camera1 = new THREE.PerspectiveCamera(this.fov, this.size.r, 0.1, 10);
|
|
camera1.position.set(0, 0, 5); // front
|
|
camera1.lookAt(0, 0, 0);
|
|
|
|
// Camera setup 2
|
|
const camera2 = new THREE.PerspectiveCamera(this.fov, this.size.r, 0.1, 10);
|
|
camera2.position.set(0, 0, 5); // top
|
|
camera2.lookAt(0, 0, 0);
|
|
|
|
// Camera setup 3
|
|
const camera3 = new THREE.PerspectiveCamera(this.fov, this.size.r, 0.1, 10);
|
|
camera3.position.set(0, 0, 5); // front
|
|
camera3.lookAt(0, 0, 0);
|
|
|
|
// Camera setup 4
|
|
const camera4 = new THREE.PerspectiveCamera(this.fov, this.size.r, 0.1, 10);
|
|
camera3.position.set(0, 0, 5); // front
|
|
camera3.lookAt(0, 0, 0);
|
|
|
|
|
|
|
|
|
|
const depthMaterial = new THREE.MeshDepthMaterial()
|
|
|
|
|
|
//depthMaterial.displacementScale = 10
|
|
const normalMaterial = new THREE.MeshNormalMaterial()
|
|
|
|
|
|
// Renderers
|
|
this.tools = [
|
|
{ type:'color', renderer: renderer1, camera: camera1, material:null },
|
|
{ type:'lines', renderer: renderer2, camera: camera2, material: null }, // delete normal map incorrect show lines
|
|
{ type:'depth', renderer: renderer3, camera: camera3, material:depthMaterial },
|
|
{ type:'normal', renderer: renderer4, camera: camera4, material:normalMaterial }
|
|
];
|
|
|
|
// Create wrappers for canvases
|
|
this.wrappers = this.createWrappers(canvasNames)
|
|
this.threeWrapper = $el("div.threeWrapper", {}, this.wrappers)
|
|
|
|
// Create panel
|
|
this.panelWrapper = this.createPanel()
|
|
|
|
// Views3
|
|
this.setViews3()
|
|
|
|
/*let m0 = new THREE.Mesh(new THREE.BoxGeometry(5,5,5))
|
|
let b0 = new THREE.BoxHelper(m0)
|
|
let m1 = new THREE.Mesh(new THREE.BoxGeometry(3,3,3))
|
|
let b1 = new THREE.BoxHelper(m1, 0xFF0000 )
|
|
scene.add(b0, b1)*/
|
|
|
|
|
|
const controls = new OrbitControls( camera1, renderer1.domElement );
|
|
controls.enableDamping = false;
|
|
controls.maxDistance = 8;
|
|
controls.minDistance = 0.01;
|
|
controls.target.set(0, 0, 0);
|
|
controls.update();
|
|
controls.addEventListener( 'change', this.render.bind(this));
|
|
//controls.addEventListener( 'end', this.sendFileToServer.bind(this, this.widgeImageThree.value));
|
|
|
|
// drop model direcly on view
|
|
|
|
if( this.enableDrag ){
|
|
document.body.addEventListener( 'dragover', function(e){ e.preventDefault() }, false );
|
|
document.body.addEventListener( 'dragend', function(e){ e.preventDefault() }, false );
|
|
document.body.addEventListener( 'dragleave', function(e){ e.preventDefault()}, false );
|
|
document.body.addEventListener( 'drop', this.drop.bind(this), false );
|
|
}
|
|
|
|
|
|
|
|
this.camera = camera1;
|
|
this.controls = controls;
|
|
this.scene = scene;
|
|
|
|
// hub for all setting
|
|
this.hub = new Hub( this );
|
|
|
|
this.helper = new THREE.Group()
|
|
this.scene.add(this.helper);
|
|
|
|
this.light = new THREE.Group()
|
|
this.scene.add(this.light);
|
|
|
|
this.initLight();
|
|
this.initEnvmap();
|
|
//this.initLoader();
|
|
|
|
if(!this.savedData?.currentModel) this.addHeadTest();
|
|
|
|
this.render();
|
|
|
|
if(this.autoAutoAnim) this.animate();
|
|
}
|
|
|
|
|
|
|
|
initEnvmap( url = `./assets/clear.hdr` ){
|
|
|
|
const envUrl = new URL(url, import.meta.url);
|
|
const self = this;
|
|
|
|
this.getLoader('hdr').load( envUrl, function ( texture ) {
|
|
|
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
|
|
//scene.background = texture;
|
|
self.scene.environment = texture;
|
|
//self.scene.environmentIntensity = 0.8
|
|
});
|
|
|
|
|
|
}
|
|
|
|
initLight(){
|
|
|
|
this.sun = new THREE.DirectionalLight( 0xFFFFFF, 0 );
|
|
this.light.add( this.sun, this.sun.target );
|
|
|
|
// debug sun position
|
|
//this.sunHelper = new THREE.CameraHelper( this.sun.shadow.camera )
|
|
//this.light.add( this.sunHelper );
|
|
|
|
//this.light.add( new THREE.AmbientLight( 0xffffFF, 0.2) );
|
|
|
|
}
|
|
|
|
setLight( option = {} ){
|
|
|
|
const o = {
|
|
position:[0,1,0],
|
|
target:[0,0,0],
|
|
intensity:3,
|
|
quality:1024,
|
|
bias:-0.0005,
|
|
radius:4,
|
|
range:0.2,
|
|
near:0.1,
|
|
far:4,
|
|
...option
|
|
}
|
|
|
|
const sun = this.sun;
|
|
sun.intensity = o.intensity;
|
|
sun.position.fromArray(o.position);
|
|
sun.target.position.fromArray(o.target);
|
|
//sun.updateWorldMatrix( true, true );
|
|
//sun.target.updateWorldMatrix( true, true );
|
|
const s = sun.shadow;
|
|
const c = s.camera;
|
|
|
|
c.top = c.right = o.range;
|
|
c.bottom = c.left = -o.range;
|
|
c.near = o.near;
|
|
c.far = o.far;
|
|
c.updateProjectionMatrix();
|
|
s.mapSize.width = s.mapSize.height = o.quality;
|
|
s.radius = o.radius;
|
|
s.bias = o.bias;
|
|
s.needsUpdate = true;
|
|
sun.castShadow = true;
|
|
|
|
if(this.sunHelper) this.sunHelper.update();
|
|
|
|
}
|
|
|
|
setViews3(){
|
|
if( this.VIEWS3 ){
|
|
this.threeWrapper.classList.add("threeWrapper3Views");
|
|
} else {
|
|
this.threeWrapper.classList.remove("threeWrapper3Views");
|
|
}
|
|
|
|
this.wrappers.slice(1).map((v)=> Object.assign(v.style, {
|
|
display: this.VIEWS3 ? "block" : "none"
|
|
}))
|
|
}
|
|
|
|
clear(b){
|
|
|
|
Tools.reset();
|
|
if(this.hub) this.hub.reset();
|
|
|
|
if( this.model ) this.scene.remove(this.model);
|
|
this.helper.children = [];
|
|
if(b) this.render();
|
|
|
|
}
|
|
|
|
async load(params = {}){
|
|
|
|
const loadData = await new Promise((res)=>{
|
|
Files.load({ type:'glb', callback: (content, fname, ftype) => res({content, fname, ftype}) })
|
|
})
|
|
|
|
if( !loadData ) {
|
|
console.log("Error load model!");
|
|
return;
|
|
}
|
|
|
|
const {content, fname, ftype} = loadData;
|
|
|
|
|
|
this.directParser( content, fname, ftype, params );
|
|
|
|
if(this.listModels.includes(fname)){
|
|
console.log(`Model name ${fname} already exist in the list loaded models!`)
|
|
return;
|
|
}
|
|
|
|
if(!this.api) return;
|
|
|
|
// Load file to server
|
|
const body = new FormData();
|
|
body.append("image", new Blob([content]), fname);
|
|
body.append("subfolder", "ThreeViewModels");
|
|
|
|
const resp = await this.api.fetchApi("/upload/image", {
|
|
method: "POST",
|
|
body,
|
|
});
|
|
|
|
if (resp.status === 200) {
|
|
const data = await resp.json();
|
|
let path = data.name;
|
|
if (data.subfolder) path = data.subfolder + "/" + path;
|
|
this.savedData.currentModel = path
|
|
}
|
|
|
|
}
|
|
|
|
drop( e ){
|
|
|
|
e.preventDefault();
|
|
const file = e.dataTransfer.files[0];
|
|
const reader = new FileReader();
|
|
const name = file.name;
|
|
const type = name.substring(name.lastIndexOf('.')+1, name.length );
|
|
const finalName = name.substring( name.lastIndexOf('/')+1, name.lastIndexOf('.') );
|
|
reader.readAsArrayBuffer( file );
|
|
|
|
if( type==='glb' ) e.stopPropagation()
|
|
|
|
reader.onload = function ( e ) {
|
|
if( type==='glb' ) this.directParser( e.target.result, finalName )
|
|
}.bind(this);
|
|
|
|
}
|
|
|
|
directParser( data, name, type = 'glb' ){
|
|
|
|
console.log('>>', data, name, type)
|
|
|
|
if(type === 'stl'){
|
|
const geometry = this.getLoader(type).parse( data );
|
|
geometry.rotateX(-Math.PI*0.5);
|
|
//geometry.computeVertexNormals()
|
|
const material = new THREE.MeshStandardMaterial({color:0xEEEEEE, roughness:0.02, metalness:0.1, vertexColors:false, flatShading:true });
|
|
let object = new THREE.Mesh( geometry, material );
|
|
this.addModel( object );
|
|
return
|
|
}
|
|
|
|
|
|
|
|
if(type === 'glb' ||type === 'gltf') this.getLoader(type).parse( data, name, ( object ) => { this.addModel( object ); })
|
|
else {
|
|
const object = this.getLoader(type).parse(data);
|
|
this.addModel( object );
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
addHeadTest( url = `./assets/head.glb` ){
|
|
|
|
const self = this;
|
|
const scene = this.scene;
|
|
const headModel = new URL(url, import.meta.url);
|
|
|
|
let u = headModel.href;
|
|
this.defPath = u.substring( 0, u.lastIndexOf('/')+1 );
|
|
|
|
//this.loaderGltf.
|
|
this.getLoader('glb').load( headModel.href, ( glb ) => {
|
|
this.addModel( glb );
|
|
// Load camera data
|
|
if( this.savedData?.camera ){
|
|
this.loadCameraState( this.savedData.camera )
|
|
}
|
|
})
|
|
|
|
|
|
}
|
|
|
|
addModel( object ){
|
|
|
|
this.clear();
|
|
|
|
console.log( ">>>", object )
|
|
|
|
let model;
|
|
if(object.scene) model = object.scene || glb.scenes[0];
|
|
if(!model) model = object;
|
|
|
|
const clips = object.animations || [];
|
|
|
|
// Apply automorph to model //!
|
|
// Convention model name is ModelNale__M__TypeOfMorph
|
|
const haveMorph = Tools.autoMorph( model );
|
|
|
|
// get skinning
|
|
const haveSkin = Tools.autoSkinning( model );
|
|
|
|
|
|
// active shadow
|
|
Tools.autoShadow( model );
|
|
|
|
// [BUG] Set position, when delete model and add again, position unknown and no visible model
|
|
model.position.set(0,0,0)
|
|
|
|
model.updateMatrixWorld();
|
|
|
|
// test full size
|
|
let b0 = new THREE.BoxHelper( model, 0x201924 );
|
|
//this.helper.add(b0)
|
|
b0.geometry.computeBoundingSphere()
|
|
b0.geometry.computeBoundingBox()
|
|
const box = b0.geometry.boundingBox;
|
|
const sizer = box.getSize(new THREE.Vector3()).length()
|
|
const radius = b0.geometry.boundingSphere.radius;
|
|
const center = box.getCenter(new THREE.Vector3());
|
|
|
|
if(haveMorph) this.hub.addMorph( Tools.MorphModel );
|
|
if(haveSkin) this.hub.addBones( Tools.SkinModel );
|
|
//Tools.setMorph(Tools.MorphModel.face,'Smile_Open', 1)
|
|
|
|
let lightpos = center.clone().add( new THREE.Vector3(-radius*0.3,radius*2, radius) )
|
|
|
|
let pos = center.clone().add( new THREE.Vector3(0,0,radius*2) )
|
|
//let near = radius/4;
|
|
//let far = radius*4;
|
|
|
|
let near = sizer*0.1;
|
|
let far = sizer*2;
|
|
|
|
this.scene.add(model);
|
|
this.model = model;
|
|
|
|
this.setLight({
|
|
position:lightpos.toArray(),
|
|
target:center.toArray(),
|
|
range:radius*2,
|
|
near:near,
|
|
far:far,
|
|
})
|
|
|
|
// update camera and render
|
|
this.setCamera( pos, center, near, far );
|
|
|
|
}
|
|
|
|
/*addObjectToScene(type, parameters = {}) {
|
|
|
|
const objectNew = new ThreeObject(type, parameters);
|
|
|
|
if (parameters.update && parameters.update instanceof Function)
|
|
objectNew.updateObject = parameters.update.bind(objectNew);
|
|
|
|
|
|
this.objects.push(objectNew);
|
|
this.scene.add(objectNew.object);
|
|
this.render();
|
|
|
|
}*/
|
|
|
|
setCamera( position, center, near, far ) {
|
|
|
|
const self = this;
|
|
|
|
//this.controls.reset()
|
|
|
|
this.tools.map((data)=>{
|
|
|
|
const camera = data.camera
|
|
if( camera ){
|
|
camera.aspect = self.size.r;
|
|
camera.fov = self.fov;
|
|
camera.position.copy( position );
|
|
camera.lookAt( center );
|
|
camera.near = near;
|
|
camera.far = far;
|
|
camera.updateProjectionMatrix();
|
|
}
|
|
|
|
})
|
|
|
|
this.controls.minDistance = near;
|
|
this.controls.maxDistance = far;
|
|
this.controls.target.copy(center);
|
|
this.controls.update();
|
|
//this.controls.saveState();
|
|
|
|
}
|
|
|
|
resize() {
|
|
|
|
if (!this.needResize) return;
|
|
|
|
this.tools.map((data)=>{
|
|
const {renderer, camera} = data
|
|
renderer.setSize(this.size.w, this.size.h, false)
|
|
|
|
if(camera){
|
|
camera.aspect = this.size.r;
|
|
camera.updateProjectionMatrix();
|
|
}
|
|
|
|
})
|
|
|
|
if(this.composer) this.composer.setSize(this.size.w, this.size.h);
|
|
|
|
this.needResize = false;
|
|
|
|
|
|
}
|
|
|
|
animate() {
|
|
requestAnimationFrame(this.animate.bind(this));
|
|
this.objects.forEach((o) => o.updateObject());
|
|
this.render();
|
|
}
|
|
|
|
initComposer(renderer, camera){
|
|
|
|
const operatorLines = this?.sobelPassSelect ?? 0
|
|
|
|
// init only one composer
|
|
if(this.composer){
|
|
return;
|
|
}
|
|
|
|
const composer = new EffectComposer(renderer);
|
|
|
|
const renderPass = new RenderPass(this.scene, camera);
|
|
composer.addPass(renderPass);
|
|
|
|
// color to grayscale conversion
|
|
|
|
const effectGrayScale = new ShaderPass( LuminosityShader );
|
|
composer.addPass( effectGrayScale );
|
|
|
|
const effectExposure = new ShaderPass( ExposureShader )
|
|
effectExposure.uniforms[ 'exposure' ].value = 0.75;
|
|
composer.addPass( effectExposure )
|
|
|
|
if(operatorLines <= 2){
|
|
const effectSobel = new ShaderPass( SobelOperatorShader );
|
|
effectSobel.uniforms[ 'resolution' ].value.x = this.size.w;
|
|
effectSobel.uniforms[ 'resolution' ].value.y = this.size.h;
|
|
|
|
// Set changed line type
|
|
effectSobel.uniforms.lineType.value = operatorLines
|
|
|
|
composer.addPass( effectSobel )
|
|
|
|
} else if(operatorLines == 3){
|
|
// Canny edge
|
|
const effectCannyEdge = new ShaderPass( cannyEdgeShader );
|
|
effectCannyEdge.uniforms.resolution.value.x = this.size.w;
|
|
effectCannyEdge.uniforms.resolution.value.y = this.size.h;
|
|
composer.addPass( effectCannyEdge )
|
|
}
|
|
|
|
// this.effectSobel = new ShaderPass(sobelShader);
|
|
// composer.addPass(this.effectSobel);
|
|
|
|
const thresholdPass = new ShaderPass(thresholdShader);
|
|
thresholdPass.uniforms[ 'threshold' ].value = 0.2;
|
|
composer.addPass(thresholdPass);
|
|
|
|
this.composer = composer;
|
|
|
|
}
|
|
|
|
async render() {
|
|
|
|
if (this.autoScale) this.resize();
|
|
|
|
// TODO: find better way to add hub on top
|
|
if(!this.hub.ready) this.hub.add(this.getDom(0))
|
|
|
|
this.tools.forEach((data)=>{
|
|
|
|
let camera = this.VIEWS3 && !this.fixCamers ? data.camera : this.camera
|
|
|
|
this.scene.overrideMaterial = !data.material ? null: data.material;
|
|
|
|
if( data.type !== 'color' ) this.helper.visible = false
|
|
else if( this.withHelper ) this.helper.visible = true
|
|
|
|
if( data.type === 'lines' ){
|
|
|
|
if(this.useLinesComposer){
|
|
|
|
this.initComposer(data.renderer, camera)
|
|
return this.composer.render(this.scene)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return data.renderer.render(this.scene, camera)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
setCanvasSize(w, h) {
|
|
|
|
if (this.size.w === w) return;
|
|
this.size.w = w;
|
|
this.size.h = h;
|
|
this.size.r = this.size.w / this.size.h;
|
|
|
|
this.tools.map((data)=>{
|
|
const {renderer, camera} = data
|
|
renderer.setSize(this.size.w, this.size.h, false)
|
|
if(camera){
|
|
camera.aspect = this.size.r;
|
|
camera.updateProjectionMatrix();
|
|
}
|
|
})
|
|
|
|
if(this?.node) this.node.title = `${this.node.type} [${this.size.w}x${this.size.h}]`;
|
|
this.render();
|
|
this.node?.onResize();
|
|
|
|
}
|
|
|
|
/*async update(widgetWidth, posY) {
|
|
|
|
let w = widgetWidth - this.size.offset;
|
|
if (this.size.w === w) return;
|
|
this.size.w = w;
|
|
this.size.h = w * this.size.r;
|
|
this.needResize = true;
|
|
|
|
}*/
|
|
|
|
// Get object saves property
|
|
getSavedOptions(){
|
|
return {
|
|
size: this.size,
|
|
currentModel: this.savedData.currentModel,
|
|
camera: this.saveCameraState()
|
|
}
|
|
}
|
|
|
|
saveCameraState(){
|
|
return {
|
|
position: this.camera.position.clone(),
|
|
quaternion: this.camera.quaternion.clone(),
|
|
target: this.controls.target.clone()
|
|
}
|
|
}
|
|
|
|
loadCameraState(savesData = null){
|
|
if(!savesData && Object.keys(savesData).length === 0) return;
|
|
|
|
|
|
this.camera.position.copy(savesData.position);
|
|
this.camera.quaternion.copy(savesData.quaternion);
|
|
this.controls.target.copy(savesData.target);
|
|
|
|
this.camera.updateMatrixWorld();
|
|
this.camera.updateProjectionMatrix();
|
|
this.controls.update();
|
|
|
|
this.render()
|
|
}
|
|
|
|
// Function send image to server
|
|
async sendFileToServer(fileName, idx) {
|
|
this.render();
|
|
|
|
|
|
return new Promise((res) => {
|
|
const uploadFile = async (blobFile) => {
|
|
try {
|
|
const resp = await fetch("/upload/image", {
|
|
method: "POST",
|
|
body: blobFile,
|
|
});
|
|
|
|
if (resp.status === 200) {
|
|
console.log(`Image saved successfully: ${fileName}`);
|
|
res(true);
|
|
} else {
|
|
console.error(`Error saving image: ${resp.status} - ${resp.statusText}`);
|
|
res(false);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error during file upload: ${error}`);
|
|
res(false);
|
|
}
|
|
};
|
|
|
|
this.getDom(idx).toBlob(function (blob) {
|
|
if (!blob) {
|
|
console.error("Blob creation failed for", fileName);
|
|
res(false);
|
|
return;
|
|
}
|
|
let formData = new FormData();
|
|
formData.append("image", blob, fileName);
|
|
formData.append("overwrite", "true");
|
|
uploadFile(formData);
|
|
}, "image/png");
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
class ThreeObject {
|
|
constructor(type, parameters = {}) {
|
|
this.type = type;
|
|
this.addObject(parameters);
|
|
}
|
|
|
|
static constructorCall(constr, args){
|
|
return new constr(...args)
|
|
}
|
|
|
|
addObject(parameters = {}) {
|
|
const {
|
|
update = null,
|
|
geo = [],
|
|
mat = {},
|
|
pos = {},
|
|
scale = {}
|
|
} = parameters;
|
|
|
|
|
|
const _material = {
|
|
color: `rgb(${Math.floor(Math.random() * 255)},${Math.floor(
|
|
Math.random() * 255
|
|
)},${Math.floor(Math.random() * 255)})`,
|
|
wireframe: true,
|
|
...mat,
|
|
};
|
|
const _position = { x: 0, y: 0, z: 0, ...pos };
|
|
const _scale = { x: 1, y: 1, z: 1, ...scale };
|
|
let _geometry;
|
|
|
|
switch (this.type) {
|
|
case "model":
|
|
this.object = geo
|
|
geo.children[0].material = new THREE.MeshStandardMaterial({color:0xffffff})
|
|
break;
|
|
case "sphere":
|
|
_geometry = ThreeObject.constructorCall(THREE.SphereGeometry, geo)
|
|
break;
|
|
default:
|
|
_geometry = ThreeObject.constructorCall(THREE.BoxGeometry, geo);
|
|
}
|
|
|
|
if(["cube", "sphere"].includes(this.type)) {
|
|
const material = new THREE.MeshBasicMaterial(_material);
|
|
this.object = new THREE.Mesh(_geometry, material);
|
|
}
|
|
|
|
this.object.position.set(_position.x, _position.y, _position.z);
|
|
this.object.scale.set(_scale.x, _scale.y, _scale.z);
|
|
}
|
|
|
|
updateObject() {
|
|
this.object.rotation.y += 0.001;
|
|
const time = performance.now() / 1000;
|
|
const scale = Math.abs(Math.sin(time)) + 10
|
|
this.object.scale.set(scale,scale,scale)
|
|
|
|
}
|
|
}
|
|
*/ |