@threepipe/plugin-svg-renderer
Version:
Plugins for SVG Rendering of 3d objects for Threepipe
303 lines (245 loc) • 7.61 kB
text/typescript
// Author: Axel Antoine
// mail: ax.antoine@gmail.com
// website: https://axantoine.com
// 16/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 {Viewmap} from '../../viewmap/Viewmap';
import {Svg, G as SVGGroup, Element as SVGElement, Color as SVGColor,
} from '@svgdotjs/svg.js';
import {Chain, ChainVisibility} from '../../viewmap/Chain';
import {ViewEdgeNature} from '../../viewmap/ViewEdge';
import {getSVGPath, getSVGCircle, getSVGText} from '../svgutils';
import { SVGMesh } from '../../SVGMesh';
import { mergeOptions } from '../../../utils/objects';
const ViewEdgesNatures = Object.values(ViewEdgeNature);
export interface StrokeNatureOptions {
enable?: boolean;
renderOrder?: number;
}
export interface ChainPassOptions {
/**
* Draw each chains in the svg with random colors.
* @defaultValue `false`
*/
useRandomColors?: boolean;
/**
* Draw the raycasting point used to determine visibility in the svg.
* @defaultValue `false`
*/
drawRaycastPoint?: boolean;
/**
* Draw the legend showing the mapping between color and nature for chains.
* Useful only if {@link colorByNature} is true.
*/
drawLegend?: boolean;
/**
* Default style applied to strokes
*/
defaultStyle?: StrokeStyle,
/**
* Customize stroke styles depending on their nature, if value are set,
* they overide default style
*/
styles?: {
[ViewEdgeNature.Silhouette]: PassStrokeStyle,
[ViewEdgeNature.MeshIntersection]: PassStrokeStyle,
[ViewEdgeNature.Crease]: PassStrokeStyle,
[ViewEdgeNature.Boundary]: PassStrokeStyle,
[ViewEdgeNature.Material]: PassStrokeStyle,
};
}
export interface StrokeStyle {
/**
* Color of the stroke in hex format.
* @defaultValue `"#000000"'
*/
color?: string;
/**
* Width of the stroke
* @defaultValue `1`
*/
width?: number;
/**
* Opacity of the stroke
* @defaultValue `1`
*/
opacity?: number;
/**
* Pattern of dashes and gaps used for the stroke e.g. `"2,2"`
* @defaultValue `""`
*/
dasharray?: string;
/**
* Shape to be used at the ends of stroke
* @defaultValue `"butt"`
*/
linecap?: 'butt' | 'round' | 'square';
/**
* Shape to use at the corners of stroke
* @defaultValue `"miter"`
*/
linejoin?: 'arcs' | 'bevel' | 'miter' | 'miter-clip' | 'round';
/**
* Offset to use before starting dash-array
* @defaultValue `0`
*/
dashoffset?: number;
}
/**
* Stroke Style interface with options specific to the Chain Pass
*/
export interface PassStrokeStyle extends StrokeStyle {
/**
* Draw order of the stroke in the svg. High order are drawn on top
* @defaultValue Silhouette 5, Boundary 4, MeshIntersection 3, Crease 2, Material 1
*
*/
drawOrder?: number;
/**
* Enable the edge nature type to be drawn in the svg
*/
enabled?: boolean;
}
export abstract class ChainPass extends DrawPass {
/** Options of the draw pass */
readonly options : Required<ChainPassOptions> = {
drawRaycastPoint: false,
useRandomColors: false,
drawLegend: false,
defaultStyle: {
color: "#000000",
width: 1,
dasharray: "",
linecap: "butt",
linejoin: "miter",
opacity: 1,
dashoffset: 0,
},
styles: {
[ViewEdgeNature.Silhouette]: {enabled: true, drawOrder: 5},
[ViewEdgeNature.Boundary]: {enabled: true, drawOrder: 4},
[ViewEdgeNature.MeshIntersection]: {enabled: true, drawOrder: 3},
[ViewEdgeNature.Crease]: {enabled: true, drawOrder: 2},
[ViewEdgeNature.Material]: {enabled: true, drawOrder: 1}
}
}
/**
*
* @param strokeStyle Default style applied to the strokes
* @param options
*/
constructor(options: ChainPassOptions = {}) {
super();
mergeOptions(this.options, options);
}
}
export class VisibleChainPass extends ChainPass {
constructor(options: Partial<ChainPassOptions> = {}) {
super(options);
}
async draw(svg: Svg, viewmap: Viewmap) {
const chains = viewmap.chains
.filter(c => c.visibility === ChainVisibility.Visible);
const meshes = Array.from(viewmap.meshes).filter(m => m.drawVisibleContours);
const group = new SVGGroup({id: "visible-contours"});
drawChains(group, meshes, chains, this.options);
svg.add(group);
}
}
export class HiddenChainPass extends ChainPass {
constructor(options: Partial<ChainPassOptions> = {}) {
const {defaultStyle, ...otherOptions} = options;
options = {
defaultStyle: {
color: "#FF0000",
dasharray: "2,2",
...defaultStyle,
},
...otherOptions
}
super(options);
}
async draw(svg: Svg, viewmap: Viewmap) {
const chains = viewmap.chains.filter(
c => c.visibility === ChainVisibility.Hidden
);
const meshes = Array.from(viewmap.meshes).filter(m => m.drawHiddenContours);
const group = new SVGGroup({id: "hidden-contours"});
svg.add(group);
drawChains(group, meshes, chains, this.options);
}
}
function drawChains(
parent: SVGElement,
meshes: SVGMesh[],
chains: Chain[],
options: Required<ChainPassOptions>) {
const {defaultStyle, styles} = options;
// Order natures depending on the draw order
ViewEdgesNatures.sort(
(n1, n2) => (styles[n1].drawOrder ?? 0) - (styles[n2].drawOrder ?? 0)
);
// Group the contours by mesh
for (const mesh of meshes) {
const objectChains = chains.filter(c => c.object === mesh);
const objectGroup = new SVGGroup({id: mesh.name});
parent.add(objectGroup);
for (const nature of ViewEdgesNatures) {
if (styles[nature]?.enabled) {
const strokeStyle = {...defaultStyle, ...styles[nature]};
const natureChains = objectChains.filter(c => c.nature === nature);
const natureGroup = new SVGGroup({id: nature});
objectGroup.add(natureGroup);
for (const chain of natureChains) {
drawChain(natureGroup, chain, options, strokeStyle);
}
}
}
}
if (options.drawLegend) {
parent.add(getLegend(options));
}
}
function drawChain(
parent: SVGElement,
chain: Chain,
options: Required<ChainPassOptions>,
style: StrokeStyle = {}
) {
// Make a copy of the style so we can modify it
style = {...style};
if (options.useRandomColors) {
style.color = SVGColor.random().toString();
}
const path = getSVGPath(chain.vertices, [], false, style);
parent.add(path);
if (options.drawRaycastPoint) {
drawContourRaycastPoint(parent, chain);
}
}
function drawContourRaycastPoint(parent: SVGElement, chain: Chain) {
const strokeStyle = {color: "black"};
const fillStyle = {color: "white"};
const cx = chain.raycastPoint.x;
const cy = chain.raycastPoint.y;
const point = getSVGCircle(cx, cy, 2, strokeStyle, fillStyle);
point.id('raycast-point');
parent.add(point);
}
function getLegend(options: Required<ChainPassOptions>) {
const legend = new SVGGroup({id: "edges-nature-legend"});
legend.add(getSVGText("Natures", 10, 140, {size: 15, anchor: 'start'}))
let y = 170;
for (const nature of ViewEdgesNatures) {
const fillColor = options.styles[nature].color ?? 'black';
legend.add(getSVGCircle(15, y, 8, {color: "black"}, {color: fillColor}));
legend.add(getSVGText(nature, 30, y-10, {size: 15, anchor: 'start'}));
y += 20;
}
return legend;
}