mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
Enhance displacement calculation and UV mapping; add gizmo visualization in viewer
This commit is contained in:
+31
-7
@@ -28,6 +28,13 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
|
|
||||||
const tmpPos = new THREE.Vector3();
|
const tmpPos = new THREE.Vector3();
|
||||||
const tmpNrm = new THREE.Vector3();
|
const tmpNrm = new THREE.Vector3();
|
||||||
|
// Reusable vectors for per-face normal computation
|
||||||
|
const vA = new THREE.Vector3();
|
||||||
|
const vB = new THREE.Vector3();
|
||||||
|
const vC = new THREE.Vector3();
|
||||||
|
const edge1 = new THREE.Vector3();
|
||||||
|
const edge2 = new THREE.Vector3();
|
||||||
|
const faceNrm = new THREE.Vector3();
|
||||||
|
|
||||||
const REPORT_EVERY = 5000;
|
const REPORT_EVERY = 5000;
|
||||||
|
|
||||||
@@ -35,7 +42,24 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
tmpPos.fromBufferAttribute(posAttr, i);
|
tmpPos.fromBufferAttribute(posAttr, i);
|
||||||
tmpNrm.fromBufferAttribute(nrmAttr, i);
|
tmpNrm.fromBufferAttribute(nrmAttr, i);
|
||||||
|
|
||||||
const uvResult = computeUV(tmpPos, tmpNrm, settings.mappingMode, settings, bounds);
|
// Compute a stable face normal from the triangle's own vertex positions.
|
||||||
|
// The subdivider deduplicates vertices by position only, so shared corner
|
||||||
|
// vertices pick up whichever face's normal happened to be stored first.
|
||||||
|
// For hard-edged meshes (e.g. a cube) this corrupts the stored normals at
|
||||||
|
// edges/corners. Recomputing from the triangle geometry is always correct
|
||||||
|
// for the flat-shaded STL source data and gives the right normal for both
|
||||||
|
// displacement direction and UV projection.
|
||||||
|
const base = Math.floor(i / 3) * 3;
|
||||||
|
vA.fromBufferAttribute(posAttr, base);
|
||||||
|
vB.fromBufferAttribute(posAttr, base + 1);
|
||||||
|
vC.fromBufferAttribute(posAttr, base + 2);
|
||||||
|
edge1.subVectors(vB, vA);
|
||||||
|
edge2.subVectors(vC, vA);
|
||||||
|
faceNrm.crossVectors(edge1, edge2);
|
||||||
|
// Fall back to the stored vertex normal for degenerate triangles
|
||||||
|
const useNrm = faceNrm.lengthSq() > 1e-10 ? faceNrm.normalize() : tmpNrm;
|
||||||
|
|
||||||
|
const uvResult = computeUV(tmpPos, useNrm, settings.mappingMode, settings, bounds);
|
||||||
|
|
||||||
let grey;
|
let grey;
|
||||||
if (uvResult.triplanar) {
|
if (uvResult.triplanar) {
|
||||||
@@ -50,13 +74,13 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
|
|
||||||
const disp = grey * settings.amplitude;
|
const disp = grey * settings.amplitude;
|
||||||
|
|
||||||
newPos[i*3] = tmpPos.x + tmpNrm.x * disp;
|
newPos[i*3] = tmpPos.x + useNrm.x * disp;
|
||||||
newPos[i*3+1] = tmpPos.y + tmpNrm.y * disp;
|
newPos[i*3+1] = tmpPos.y + useNrm.y * disp;
|
||||||
newPos[i*3+2] = tmpPos.z + tmpNrm.z * disp;
|
newPos[i*3+2] = tmpPos.z + useNrm.z * disp;
|
||||||
|
|
||||||
newNrm[i*3] = tmpNrm.x;
|
newNrm[i*3] = useNrm.x;
|
||||||
newNrm[i*3+1] = tmpNrm.y;
|
newNrm[i*3+1] = useNrm.y;
|
||||||
newNrm[i*3+2] = tmpNrm.z;
|
newNrm[i*3+2] = useNrm.z;
|
||||||
|
|
||||||
if (onProgress && i % REPORT_EVERY === 0) onProgress(i / count);
|
if (onProgress && i % REPORT_EVERY === 0) onProgress(i / count);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -245,7 +245,7 @@ async function handleSTL(file) {
|
|||||||
|
|
||||||
// Default edge length = 1/100 of the largest bounding box dimension
|
// Default edge length = 1/100 of the largest bounding box dimension
|
||||||
const maxDim = Math.max(bounds.size.x, bounds.size.y, bounds.size.z);
|
const maxDim = Math.max(bounds.size.x, bounds.size.y, bounds.size.z);
|
||||||
const defaultEdge = Math.max(0.1, Math.min(5.0, +(maxDim / 100).toFixed(2)));
|
const defaultEdge = Math.max(0.1, Math.min(5.0, +(maxDim / 200).toFixed(2)));
|
||||||
settings.refineLength = defaultEdge;
|
settings.refineLength = defaultEdge;
|
||||||
refineLenSlider.value = defaultEdge;
|
refineLenSlider.value = defaultEdge;
|
||||||
refineLenVal.textContent = `${defaultEdge.toFixed(2)} mm`;
|
refineLenVal.textContent = `${defaultEdge.toFixed(2)} mm`;
|
||||||
|
|||||||
+3
-3
@@ -78,9 +78,9 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
const az = Math.abs(normal.z);
|
const az = Math.abs(normal.z);
|
||||||
let uRaw, vRaw;
|
let uRaw, vRaw;
|
||||||
if (ax >= ay && ax >= az) {
|
if (ax >= ay && ax >= az) {
|
||||||
// ±X dominant → project onto YZ
|
// ±X dominant → project onto ZY (U=Z, V=Y keeps texture upright on side faces)
|
||||||
uRaw = (pos.y - min.y) / Math.max(size.y, 1e-6);
|
uRaw = (pos.z - min.z) / Math.max(size.z, 1e-6);
|
||||||
vRaw = (pos.z - min.z) / Math.max(size.z, 1e-6);
|
vRaw = (pos.y - min.y) / Math.max(size.y, 1e-6);
|
||||||
} else if (ay >= ax && ay >= az) {
|
} else if (ay >= ax && ay >= az) {
|
||||||
// ±Y dominant → project onto XZ
|
// ±Y dominant → project onto XZ
|
||||||
uRaw = (pos.x - min.x) / Math.max(size.x, 1e-6);
|
uRaw = (pos.x - min.x) / Math.max(size.x, 1e-6);
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ const fragmentShader = /* glsl */`
|
|||||||
// Picks the single planar projection whose axis is most aligned with the face normal.
|
// Picks the single planar projection whose axis is most aligned with the face normal.
|
||||||
vec3 absN = abs(MN);
|
vec3 absN = abs(MN);
|
||||||
if (absN.x >= absN.y && absN.x >= absN.z) {
|
if (absN.x >= absN.y && absN.x >= absN.z) {
|
||||||
// ±X dominant → project onto YZ plane
|
// ±X dominant → project onto ZY plane (U=Z, V=Y keeps texture upright on side faces)
|
||||||
return sampleMap((pos.yz - boundsMin.yz) / max(boundsSize.yz, vec2(1e-4)));
|
return sampleMap((pos.zy - boundsMin.zy) / max(boundsSize.zy, vec2(1e-4)));
|
||||||
} else if (absN.y >= absN.x && absN.y >= absN.z) {
|
} else if (absN.y >= absN.x && absN.y >= absN.z) {
|
||||||
// ±Y dominant → project onto XZ plane
|
// ±Y dominant → project onto XZ plane
|
||||||
return sampleMap((pos.xz - boundsMin.xz) / max(boundsSize.xz, vec2(1e-4)));
|
return sampleMap((pos.xz - boundsMin.xz) / max(boundsSize.xz, vec2(1e-4)));
|
||||||
|
|||||||
@@ -3,6 +3,57 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|||||||
|
|
||||||
let renderer, camera, scene, controls, meshGroup, ambientLight, dirLight1, dirLight2, grid;
|
let renderer, camera, scene, controls, meshGroup, ambientLight, dirLight1, dirLight2, grid;
|
||||||
let currentMesh = null;
|
let currentMesh = null;
|
||||||
|
let gizmoScene, gizmoCamera;
|
||||||
|
|
||||||
|
const GIZMO_PX = 90; // gizmo viewport size in CSS pixels
|
||||||
|
const GIZMO_MARGIN = 14;
|
||||||
|
|
||||||
|
function buildGizmo() {
|
||||||
|
gizmoScene = new THREE.Scene();
|
||||||
|
gizmoCamera = new THREE.OrthographicCamera(-1.6, 1.6, 1.6, -1.6, 0.1, 10);
|
||||||
|
gizmoCamera.position.set(0, 0, 3);
|
||||||
|
|
||||||
|
const addAxis = (dir, hex, label) => {
|
||||||
|
// Shaft line
|
||||||
|
const shaft = new THREE.BufferGeometry().setFromPoints([
|
||||||
|
new THREE.Vector3(0, 0, 0),
|
||||||
|
dir.clone().multiplyScalar(0.78),
|
||||||
|
]);
|
||||||
|
gizmoScene.add(new THREE.Line(
|
||||||
|
shaft,
|
||||||
|
new THREE.LineBasicMaterial({ color: hex, depthTest: false }),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Arrow-head cone
|
||||||
|
const cone = new THREE.Mesh(
|
||||||
|
new THREE.ConeGeometry(0.10, 0.24, 8),
|
||||||
|
new THREE.MeshBasicMaterial({ color: hex, depthTest: false }),
|
||||||
|
);
|
||||||
|
cone.position.copy(dir.clone().multiplyScalar(0.92));
|
||||||
|
cone.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||||
|
gizmoScene.add(cone);
|
||||||
|
|
||||||
|
// Text label sprite
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = c.height = 64;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
ctx.fillStyle = `#${hex.toString(16).padStart(6, '0')}`;
|
||||||
|
ctx.font = 'bold 46px Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(label, 32, 32);
|
||||||
|
const sprite = new THREE.Sprite(
|
||||||
|
new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(c), depthTest: false }),
|
||||||
|
);
|
||||||
|
sprite.position.copy(dir.clone().multiplyScalar(1.26));
|
||||||
|
sprite.scale.set(0.42, 0.42, 1);
|
||||||
|
gizmoScene.add(sprite);
|
||||||
|
};
|
||||||
|
|
||||||
|
addAxis(new THREE.Vector3(1, 0, 0), 0xff4040, 'X');
|
||||||
|
addAxis(new THREE.Vector3(0, 1, 0), 0x44dd44, 'Y');
|
||||||
|
addAxis(new THREE.Vector3(0, 0, 1), 0x5599ff, 'Z');
|
||||||
|
}
|
||||||
|
|
||||||
export function initViewer(canvas) {
|
export function initViewer(canvas) {
|
||||||
// Renderer
|
// Renderer
|
||||||
@@ -54,6 +105,8 @@ export function initViewer(canvas) {
|
|||||||
controls.maxDistance = 3000;
|
controls.maxDistance = 3000;
|
||||||
controls.screenSpacePanning = true;
|
controls.screenSpacePanning = true;
|
||||||
|
|
||||||
|
buildGizmo();
|
||||||
|
|
||||||
// Resize observer
|
// Resize observer
|
||||||
const resizeObserver = new ResizeObserver(() => onResize());
|
const resizeObserver = new ResizeObserver(() => onResize());
|
||||||
resizeObserver.observe(canvas.parentElement);
|
resizeObserver.observe(canvas.parentElement);
|
||||||
@@ -63,7 +116,29 @@ export function initViewer(canvas) {
|
|||||||
(function animate() {
|
(function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
controls.update();
|
controls.update();
|
||||||
|
|
||||||
|
const cw = renderer.domElement.clientWidth;
|
||||||
|
const ch = renderer.domElement.clientHeight;
|
||||||
|
|
||||||
|
// 1. Main scene — full viewport
|
||||||
|
renderer.setScissorTest(false);
|
||||||
|
renderer.setViewport(0, 0, cw, ch);
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
|
|
||||||
|
// 2. Gizmo overlay — upper-right corner
|
||||||
|
// WebGL y=0 is at bottom, so upper-right means large y.
|
||||||
|
const gx = cw - GIZMO_PX - GIZMO_MARGIN;
|
||||||
|
const gy = ch - GIZMO_PX - GIZMO_MARGIN;
|
||||||
|
gizmoCamera.quaternion.copy(camera.quaternion);
|
||||||
|
renderer.setScissorTest(true);
|
||||||
|
renderer.setScissor(gx, gy, GIZMO_PX, GIZMO_PX);
|
||||||
|
renderer.setViewport(gx, gy, GIZMO_PX, GIZMO_PX);
|
||||||
|
renderer.autoClear = false;
|
||||||
|
renderer.clearDepth();
|
||||||
|
renderer.render(gizmoScene, gizmoCamera);
|
||||||
|
renderer.autoClear = true;
|
||||||
|
renderer.setScissorTest(false);
|
||||||
|
renderer.setViewport(0, 0, cw, ch);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user