UNPKG

@threepipe/plugin-svg-renderer

Version:

Plugins for SVG Rendering of 3d objects for Threepipe

441 lines (440 loc) 14.3 kB
// // Author: Axel Antoine // // mail: ax.antoine@gmail.com // // website: https://axantoine.com // // 17/06/2022 // // // Loki, Inria project-team with Université de Lille // // within the Joint Research Unit UMR 9189 CNRS-Centrale // // Lille-Université de Lille, CRIStAL. // // https://loki.lille.inria.fr // // // LICENCE: Licence.md // // import {DrawPass} from "./DrawPass"; // import cv, {Mat as CVMat} from "opencv-ts"; // import {PointLike, RectLike, round} from '../../../utils'; // import {SVGMesh, SVGTexture} from "../../SVGMesh"; // // import { // Circle as SVGCircle, // ClipPath as SVGClipPath, // Element as SVGElement, // Ellipse as SVGEllipse, // G as SVGGroup, // Image as SVGImage, // Path as SVGPath, // PathArray as SVGPathArray, // PathCommand as SVGPathCommand, // Polygon as SVGPolygon, // Rect as SVGRect, // Svg, // SVG, // } from '@svgdotjs/svg.js'; // import '@svgdotjs/svg.topath.js'; // import {Polygon} from "../../viewmap/Polygon"; // import {Viewmap} from "../../viewmap/Viewmap"; // import {getSVGImage, getSVGPath, NumberAliasToNumber, replaceShapeByPath} from '../svgutils'; // // // let _cvVectorIn: CVMat; // let _cvVectorOut: CVMat; // // // Make a promise to know when opencv module is available and init the two buffers // const cvPromise = new Promise<void>(resolve => { // cv.onRuntimeInitialized = () => { // _cvVectorIn= cv.matFromArray(1, 1, cv.CV_32FC2, [0, 0]) // _cvVectorOut = cv.matFromArray(1, 1, cv.CV_32FC2, [0, 0]); // resolve(); // } // }); // // /* // TODO: support all types of geometries // // * Texture is an image // // - Idea 1: // Draw only image on gpu in the framebuffer, the image will have the correct // shape, get back the framebuffer and draw the image in SVG in the correct layer // // * Texture is a svg // // - Idea 1: // Add UV attribute to meshes and, for each triangle UV, cut the SVG shapes. // Then we project each shapes point using the triangle coordinates in world // space and draw them. // // Good luck have fun! ;) // */ // // // type SVGMeshWithTexture = SVGMesh & {texture: SVGTexture}; // // /** // * SVGTexturePass used to draw image or vector graphics textures on mesh in the // * final SVG. // * // * Note that only `PlaneGeometry` is supported for now. Textures set on // * geometries other than plane will be ignored. // */ // export class TexturePass extends DrawPass { // // async draw(svg: Svg, viewmap: Viewmap) { // // const {meshes, polygons} = viewmap; // // /** // * Gather meshes with texture // */ // const textureMeshes = new Array<SVGMeshWithTexture>(); // for (const mesh of meshes) { // if (mesh.texture) { // /** // * We only can handle Plane Geometry for now // * // * Probably a bit rough, but we consider tthat if the mesh's // * HalfEdgeStructure has 4 vertices and 2 faces, it is a plane // * // */ // if (mesh.hes && mesh.hes.vertices.length === 4 // && mesh.hes.faces.length === 2) { // textureMeshes.push(mesh as SVGMeshWithTexture); // } else { // console.warn(`Mesh "${mesh.name}": Texture ignored, not a plane geometry.`); // } // } // } // // /** // * Exit if there is no mesh to handle // */ // if (textureMeshes.length === 0) { // return; // } // // /** // * Wait OpenCV to be loaded, as we need the module to compute the // * perspective transform matrix and image perspective transform // */ // await cvPromise; // // const group = new SVGGroup({id: "textures"}); // svg.add(group); // // /** // * Get the viewmap polygons for each mesh so they can be used as svg clipping // * path for the texture // */ // const meshPolygonsMap = new Map<SVGMesh, Polygon[]>(); // for (const mesh of textureMeshes) { // meshPolygonsMap.set(mesh, []); // } // // for (const polygon of polygons) { // if (polygon.mesh && meshPolygonsMap.has(polygon.mesh)) { // meshPolygonsMap.get(polygon.mesh)?.push(polygon); // } // } // // /** // * Draw each mesh texture // */ // for (const mesh of textureMeshes) { // // let svgTexture: SVGElement; // if (mesh.texture.url.startsWith('data:image/svg+xml;base64,')) { // svgTexture = await getSVGTexture(mesh); // } else { // svgTexture = await getImageTexture(mesh); // } // // // Draw a clipping path using the polygons // const clipPath = new SVGClipPath(); // const polygons = meshPolygonsMap.get(mesh) ?? []; // for (const polygon of polygons) { // const svgPath = getSVGPath(polygon.contour, polygon.holes, true); // clipPath.add(svgPath); // } // group.add(clipPath); // svgTexture.clipWith(clipPath); // group.add(svgTexture); // // } // } // } // // async function getImageTexture(mesh: SVGMeshWithTexture) { // // const imgEl = document.createElement('img'); // imgEl.src = mesh.texture.url; // const srcImageMatrix = cv.imread(imgEl); // // // Get the transformation matrix and the output size; // const imgRect = {x: 0, y: 0, w: srcImageMatrix.cols, h: srcImageMatrix.rows}; // const {matrix, outRect} = getCVTransformMatrix(imgRect, mesh); // // const dstImageMatrix = new cv.Mat(); // const dSize = new cv.Size(outRect.w, outRect.h); // cv.warpPerspective(srcImageMatrix, dstImageMatrix, matrix, dSize, cv.INTER_LINEAR); // // // OpenCV needs a canvas to draw the transformed image // const canvas = document.createElement('canvas'); // cv.imshow(canvas, dstImageMatrix); // srcImageMatrix.delete(); // dstImageMatrix.delete(); // // return new Promise<SVGImage>((resolve, reject) => { // canvas.toBlob((blob) => { // if (blob) { // const reader = new FileReader(); // reader.onloadend = () => { // const url = reader.result as string; // const svgImage = getSVGImage(url, outRect); // resolve(svgImage); // } // reader.readAsDataURL(blob); // } else { // reject("Error blob conversion from opencv canvas") // } // }); // }); // } // // async function getSVGTexture(mesh: SVGMeshWithTexture) { // // return new Promise<SVGGroup>((resolve, reject) => { // // svgContentFromDataURL(mesh.texture.url) // .then(content => { // // // As SVG.js gets an extra <svg> div around the svg for internal // // computations, we only take the children // // See first question in the FAQ: https://svgjs.dev/docs/3.0/faq/ // // const svg = SVG().svg(content); // // const group = new SVGGroup({id:"svg-interface-"+mesh.name}); // for(const child of svg.children()) { // try { // const ignoredElements = new Array<SVGElement>(); // transformSVG(child, mesh, undefined, ignoredElements); // // console.info(`SVG Transform: ${ignoredElements.length} elements ignored.`, ignoredElements); // // } catch(e) { // console.error("Error while transforming SVG", e); // } // group.add(child) // } // resolve(group); // }) // .catch(reason => { // reject("Couldn't retrieved svg content from base64 dataURL: "+reason); // }); // }); // } // // function svgContentFromDataURL(dataUrl: string) { // return new Promise<string>((resolve, reject) => { // // if (dataUrl.startsWith('data:image/svg+xml;base64,')) { // // fetch(dataUrl).then(value => { // value.blob() // .then(blob => { // const reader = new FileReader(); // // reader.onloadend = () => { // resolve(reader.result as string); // } // // reader.onerror = () => { // reject("Couldn't read content"); // } // // reader.readAsText(blob); // }) // .catch(() => { // reject("Couldn't create blob"); // }); // }).catch(() => { // reject("Couldn't fetch data "); // }); // } else { // reject("Data not svg xml based"); // } // }); // } // // function getCVTransformMatrix(srcRect: RectLike, mesh: SVGMesh) { // // let minX = Infinity; // let minY = Infinity; // let maxX = -Infinity; // let maxY = -Infinity; // // // Setup initial points with the size of the input SVG/image // const srcPointsArray = [ // srcRect.x, srcRect.y, // srcRect.x, srcRect.y+srcRect.h, // srcRect.x+srcRect.w, srcRect.y, // srcRect.x+srcRect.w, srcRect.y+srcRect.h]; // const dstPointsArray = new Array<number>(); // // // Get the coordinates in pixels of the four screen corners // const vertices = Array.from(mesh.hes.vertices); // const viewVertices = vertices.map(vertex => vertex.viewVertex); // // for (const vertex of viewVertices) { // minX = Math.min(minX, vertex.x); // minY = Math.min(minY, vertex.y); // maxX = Math.max(maxX, vertex.x); // maxY = Math.max(maxY, vertex.y); // dstPointsArray.push(vertex.x); // dstPointsArray.push(vertex.y); // } // // // Recenter the projection on top left corner of the object // for (let i=0; i<8; i+=2) { // dstPointsArray[i] -= minX; // dstPointsArray[i+1] -= minY; // } // // const srcMat = cv.matFromArray(4, 1, cv.CV_32FC2, srcPointsArray); // const dstMat = cv.matFromArray(4, 1, cv.CV_32FC2, dstPointsArray); // // const matrix = cv.getPerspectiveTransform(srcMat, dstMat, cv.DECOMP_LU) // // srcMat.delete(); // dstMat.delete(); // // return { // matrix: matrix, // outRect: {x: minX, y: minY, w: maxX - minX, h: maxY - minY} // }; // } // // function transformSVG( // element: SVGElement, // mesh: SVGMesh, // transformMatrix?: CVMat, // ignoredElements?: SVGElement[], // ){ // if (element.type === "svg") { // const svg = element as Svg; // let inRect = { // x: NumberAliasToNumber(svg.x()), y: NumberAliasToNumber(svg.y()), // w: NumberAliasToNumber(svg.width()), h: NumberAliasToNumber(svg.height())}; // const viewBox = svg.viewbox(); // if (viewBox.height !== 0 && viewBox.width !==0) { // inRect = {x: viewBox.x, y: viewBox.y, w: viewBox.width, h: viewBox.height}; // } // // if (inRect.w === 0 || inRect.h === 0) { // throw("Embedded SVG has no visible dimension: i.e no width/height or viewbox properties."); // } // // const {matrix, outRect} = getCVTransformMatrix(inRect, mesh); // svg.x(outRect.x); // svg.y(outRect.y); // svg.width(outRect.w); // svg.height(outRect.h); // svg.attr('viewBox', null); // transformMatrix = matrix; // } else if (element.type === "polygon") { // element = replaceShapeByPath(element as SVGPolygon); // } else if (element.type === "rect") { // element = replaceShapeByPath(element as SVGRect); // } else if (element.type === "ellipse") { // element = replaceShapeByPath(element as SVGEllipse); // } else if (element.type === "circle") { // element = replaceShapeByPath(element as SVGCircle); // } else if (element.type !== "path" && element.type !== "g") { // ignoredElements?.push(element); // } // // if (element.type !== 'svg' && !transformMatrix) { // throw('There is no perspective transform matrix or it hasn\'t been initialized.'); // } // // // Convert path elements // if (transformMatrix && element.type === 'path') { // const path = element as SVGPath; // transformSVGPath(path, transformMatrix); // } // // for (const child of element.children()) { // transformSVG(child, mesh, transformMatrix, ignoredElements); // } // // // Delete OpenCV Matrix if the top element has finished its transform // if(element.type === 'svg' && transformMatrix) { // transformMatrix.delete(); // } // } // // function transformSVGPath(path: SVGPath, matrix: CVMat) { // const array = path.array(); // const newCmds = new Array<SVGPathCommand>(); // const lastP = {x: 0, y:0}; // let p: PointLike, p1: PointLike, p2: PointLike; // for (let i=0; i<array.length; i++) { // const cmd = array[i]; // const op = cmd[0]; // switch(op) { // // Horizontal line from the last point // case 'H': // p = transformCoords(cmd[1], lastP.y, matrix); // newCmds.push(['L', round(p.x), round(p.y)]); // lastP.x = cmd[1]; // break; // // vertical line from the last point // case 'V': // p = transformCoords(lastP.x, cmd[1], matrix); // newCmds.push(['L', round(p.x), round(p.y)]); // lastP.y = cmd[1]; // break; // // Move to | Line to // case 'M': // case 'L': // p = transformCoords(cmd[1], cmd[2], matrix); // newCmds.push([op, round(p.x), round(p.y)]); // lastP.x = cmd[1] // lastP.y = cmd[2]; // break; // // Curve to // case 'C': // p = transformCoords(cmd[1], cmd[2], matrix); // p1 = transformCoords(cmd[3], cmd[4], matrix); // p2 = transformCoords(cmd[5], cmd[6], matrix); // newCmds.push([op, round(p.x), round(p.y), // round(p1.x), round(p1.y), round(p2.x), round(p2.y)]); // lastP.x = cmd[5] // lastP.y = cmd[6]; // break; // // Close path // case 'Z': // newCmds.push(['Z']) // break; // // default: // console.info("Unsupported SVG path command", op); // } // } // path.plot(new SVGPathArray(newCmds)); // } // // /** // * Transform x and y coords with an OpenCV [perspective] matrix // * // * @param {number} x { parameter_description } // * @param {number} y { parameter_description } // * @param {CVMat} matrix The matrix // * @return {Object} { description_of_the_return_value } // */ // function transformCoords(x: number, y: number, matrix: CVMat) { // _cvVectorIn.data32F[0] = x; // _cvVectorIn.data32F[1] = y; // cv.perspectiveTransform(_cvVectorIn, _cvVectorOut, matrix); // return {x: _cvVectorOut.data32F[0], y: _cvVectorOut.data32F[1]}; // }