| @@ -3497,7 +3497,7 @@ Includes the following generator which inherit from [AGeometryGenerator](https:/ | |||
| Sample Usage: | |||
| ```typescript | |||
| import {ThreeViewer} from 'threepipe' | |||
| import {ThreeViewer, UnlitMaterial} from 'threepipe' | |||
| import {GeometryGeneratorPlugin} from '@threepipe/plugin-geometry-generator' | |||
| const viewer = new ThreeViewer({...}) | |||
| @@ -3510,8 +3510,14 @@ viewer.scene.addObject(sphere) | |||
| generator.updateGeometry(sphere.geometry, {radius: 4, widthSegments: 100}) | |||
| // to add a custom generator | |||
| generator.generators.custom = new CustomGenerator('custom') // Extend from AGeometryGenerator or implement GeometryGenerator | |||
| generator.generators.custom = new CustomGenerator('custom') // Extend from AGeometryGenerator or implement GeometryGenerator interface | |||
| // refresh the ui so the new generator is available to select. | |||
| generator.uiConfig.uiRefresh?.() | |||
| // change the material type for all objects | |||
| generator.defaultMaterialClass = UnlitMaterial // by default its PhysicalMaterial | |||
| viewer.scene.addObject(generator.generateObject('box', {width: 2, height: 2, depth: 2})) | |||
| ``` | |||
| ## @threepipe/plugin-gaussian-splatting | |||
| @@ -3555,3 +3561,56 @@ viewer.addPluginSync(GaussianSplattingPlugin) | |||
| const model = await viewer.load<GaussianSplatMesh>('path/to/file.splat') | |||
| ``` | |||
| ## @threepipe/plugin-svg-renderer | |||
| Exports [ThreeSVGRendererPlugin](https://threepipe.org/plugins/svg-renderer/docs/classes/ThreeSVGRendererPlugin.html) and [BasicSVGRendererPlugin](https://threepipe.org/plugins/svg-renderer/docs/classes/BasicSVGRendererPlugin.html) which provide support for rendering the 3d scene as [SVG(Scalable Vector Graphics)](https://developer.mozilla.org/en-US/docs/Web/SVG). The generated SVG is compatible with browser rendering and other software like figma, illustrator etc. | |||
| [Example](https://threepipe.org/examples/#three-svg-renderer/) — | |||
| [Source Code](./plugins/svg-renderer/src/index.ts) — | |||
| [API Reference](https://threepipe.org/plugins/svg-renderer/docs) — | |||
| [GPLV3 License](./plugins/svg-renderer/LICENSE) | |||
| NPM: `npm install @threepipe/plugin-svg-renderer` | |||
| Note: This is still a WIP. API might change slightly | |||
| `ThreeSVGRendererPlugin` uses [`three-svg-renderer`](./plugins/svg-renderer/src/three-svg-renderer), which is a modified version of [three-svg-renderer](https://www.npmjs.com/package/three-svg-renderer) (GPLV3 Licenced). | |||
| The plugin renderers meshes in the viewer scene to svg objects by computing polygons and contours of the geometry in view space. Check [LokiResearch/three-svg-renderer](https://github.com/LokiResearch/three-svg-renderer?tab=readme-ov-file#references) for more details. | |||
| In the modified version that is used here, support for some types of geometries is added and a rendered image in screen-space is used to create raster fill images for paths along with some other small changes. Check out the [Example](https://threepipe.org/examples/#three-svg-renderer/) for demo. See also [svg-geometry-playground example](https://threepipe.org/examples/#svg-geometry-playground/) for usage with other plugins `PickingPlugin`, `TransformControlsPlugin` and `GeometryGeneratorPlugin`. | |||
| Note that this does not support all the features of three.js and may not work with all types of materials and geometries. Check the examples for a list of sample models that do and don't work. | |||
| `BasicSVGRendererPlugin` is a sample plugin using [SVGRenderer](https://threejs.org/docs/index.html?q=svg#examples/en/renderers/SVGRenderer) from three.js addons. This renders all triangles in the scene to separate svg paths. Check the three.js docs for more details. Check out the [Example](https://threepipe.org/examples/#basic-svg-renderer/) for demo. | |||
| ```typescript | |||
| import {ThreeViewer} from 'threepipe' | |||
| import {ThreeSVGRendererPlugin} from '@threepipe/plugin-svg-renderer' | |||
| const viewer = new ThreeViewer({...}) | |||
| const svgRender = viewer.addPluginSync(ThreeSVGRendererPlugin) | |||
| svgRender.autoRender = true // automatically render when camera or any object changes. | |||
| svgRender.autoMakeSvgObjects = true // automatically create SVG objects for all meshes in the scene. | |||
| // svgRender.makeSVGObject(object) // manually create SVG object for an object. (if autoMakeSvgObjects is false) | |||
| // Now load or generate any 3d model. Make sure its not very big. And the meshes are optimized. | |||
| const model = await viewer.load<IOBject3D>('path/to/file.glb') | |||
| // clear the background of the viewer | |||
| // this is only required if rgbm = false in the viewer | |||
| viewer.scene.backgroundColor = null | |||
| // this is only required if rgbm = true in the viewer | |||
| viewer.renderManager.screenPass.clipBackground = true | |||
| // disable damping to get better experience. | |||
| viewer.scene.mainCamera.controls!.enableDamping = false | |||
| // hide the canvas to see the underlying svg node. | |||
| // note: do not set the display to none or remove the canvas as OrbitControls and other plugins might still be tracking the canvas. | |||
| viewer.canvas.style.opacity = '0' | |||
| // 3d pipeline can also be disabled like this if `drawImageFills` is `false` to get better performance. Do this only after loading the model. | |||
| // await viewer.doOnce('postFrame') // wait for the first frame to be rendered (for autoScale etc) | |||
| // viewer.renderManager.autoBuildPipeline = false | |||
| // viewer.renderManager.pipeline = [] // this will disable main viewer rendering | |||
| ``` | |||
| @@ -0,0 +1,38 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Basic SVG Renderer</title> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <!-- Import maps polyfill --> | |||
| <!-- Remove this when import maps will be widely supported --> | |||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||
| <script type="importmap"> | |||
| { | |||
| "imports": { | |||
| "threepipe": "./../../dist/index.mjs", | |||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs", | |||
| "@threepipe/plugin-svg-renderer": "./../../plugins/svg-renderer/dist/index.mjs" | |||
| } | |||
| } | |||
| </script> | |||
| <style id="example-style"> | |||
| html, body, #canvas-container, #mcanvas { | |||
| position: absolute; | |||
| width: 100%; | |||
| height: 100%; | |||
| margin: 0; | |||
| overflow: hidden; | |||
| } | |||
| </style> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,40 @@ | |||
| import {_testFinish, DirectionalLight2, ThreeViewer} from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| import {BasicSVGRendererPlugin} from '@threepipe/plugin-svg-renderer' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: false, | |||
| tonemap: false, | |||
| }) | |||
| viewer.scene.mainCamera.controls!.enableDamping = false | |||
| viewer.addPluginSync(new BasicSVGRendererPlugin(true)) | |||
| viewer.scene.addObject(new DirectionalLight2(0x0000ff, 1)) | |||
| const l = new DirectionalLight2(0xff0000, 1) | |||
| l.position.set(1, 1, 1) | |||
| viewer.scene.addObject(l) | |||
| await viewer.load('https://threejs.org/examples/models/gltf/ShadowmappableMesh.glb', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| await viewer.doOnce('postFrame') // wait for one frame | |||
| // disable rendering so canvas is transparent | |||
| viewer.renderManager.autoBuildPipeline = false | |||
| viewer.renderManager.pipeline = [] // this will disable main viewer rendering | |||
| // // make it invisible. | |||
| viewer.canvas.style.opacity = '0' | |||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| ui.setupPlugins(BasicSVGRendererPlugin) | |||
| ui.appendChild(l.uiConfig) | |||
| } | |||
| init().finally(_testFinish) | |||
| @@ -329,6 +329,8 @@ | |||
| <li><a href="./ssao-plugin/">SSAO Plugin </a></li> | |||
| <li><a href="./virtual-cameras-plugin/">Virtual Cameras Plugin </a></li> | |||
| <li><a href="./virtual-camera/">Virtual Camera (Animated) </a></li> | |||
| <li><a href="./basic-svg-renderer-plugin/">Basic SVG Renderer Plugin </a></li> | |||
| <li><a href="./three-svg-renderer-plugin/">Three SVG Renderer Plugin </a></li> | |||
| </ul> | |||
| <h2 class="category">Interaction</h2> | |||
| <ul> | |||
| @@ -368,6 +370,7 @@ | |||
| <li><a href="./render-target-export/">EXR, PNG, JPEG, WEBP Export<br/>(Render Target Export) </a></li> | |||
| <li><a href="./glb-export/">GLB Export </a></li> | |||
| <li><a href="./pmat-material-export/">PMAT Material export </a></li> | |||
| <li><a href="./svg-geometry-playground/">SVG Geometry Playground </a></li> | |||
| </ul> | |||
| <h2 class="category">UI Config</h2> | |||
| <ul> | |||
| @@ -0,0 +1,42 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>SVG Geometry Renderer</title> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <!-- Import maps polyfill --> | |||
| <!-- Remove this when import maps will be widely supported --> | |||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||
| <script type="importmap"> | |||
| { | |||
| "imports": { | |||
| "threepipe": "./../../dist/index.mjs", | |||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs", | |||
| "@threepipe/plugin-geometry-generator": "./../../plugins/geometry-generator/dist/index.mjs", | |||
| "@threepipe/plugin-svg-renderer": "./../../plugins/svg-renderer/dist/index.mjs" | |||
| } | |||
| } | |||
| </script> | |||
| <style id="example-style"> | |||
| html, body, #canvas-container, #mcanvas { | |||
| position: absolute; | |||
| width: 100%; | |||
| height: 100%; | |||
| margin: 0; | |||
| overflow: hidden; | |||
| } | |||
| body{ | |||
| background: #cccccc; | |||
| } | |||
| </style> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,91 @@ | |||
| import { | |||
| _testFinish, | |||
| EditorViewWidgetPlugin, | |||
| GBufferPlugin, | |||
| PickingPlugin, | |||
| ThreeViewer, | |||
| TransformControlsPlugin, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| import {ThreeSVGRendererPlugin} from '@threepipe/plugin-svg-renderer' | |||
| import {GeometryGeneratorPlugin} from '@threepipe/plugin-geometry-generator' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: false, | |||
| rgbm: false, | |||
| // zPrepass: true, | |||
| tonemap: false, | |||
| plugins: [GBufferPlugin, PickingPlugin, TransformControlsPlugin], | |||
| }) | |||
| viewer.addPluginSync(new EditorViewWidgetPlugin('bottom-left', 128)) | |||
| viewer.scene.backgroundColor = null | |||
| // viewer.renderManager.screenPass.clipBackground = true // required when rgbm: true | |||
| viewer.scene.mainCamera.controls!.enableDamping = false | |||
| viewer.renderEnabled = false | |||
| viewer.addPluginSync(new ThreeSVGRendererPlugin(true)) | |||
| // viewer.scene.addObject(new DirectionalLight2(0xffffff, 1).rotateZ(0.5).rotateX(0.5)) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| const generator = viewer.addPluginSync(GeometryGeneratorPlugin) | |||
| // generator.defaultMaterialClass = UnlitMaterial | |||
| console.log(generator.generators) | |||
| // Head (sphere) | |||
| const head = generator.generateObject('sphere', {radius: 0.5, widthSegments: 32, heightSegments: 32}) | |||
| head.translateY(1) | |||
| viewer.scene.addObject(head) | |||
| // Body (box) | |||
| const body = generator.generateObject('box', {width: 1.5, height: 1.5, depth: 1}) | |||
| body.material.color.set(0x00ffff) | |||
| viewer.scene.addObject(body) | |||
| // Legs (cylinders) | |||
| const leftLeg = generator.generateObject('cylinder', {radiusTop: 0.125, radiusBottom: 0.125, height: 1.5}) | |||
| leftLeg.material.color.set(0x00ff00) | |||
| leftLeg.translateX(-0.5) | |||
| leftLeg.translateY(-1) | |||
| viewer.scene.addObject(leftLeg) | |||
| const rightLeg = generator.generateObject('cylinder', {radiusTop: 0.125, radiusBottom: 0.125, height: 1.5}) | |||
| rightLeg.material.color.set(0x00ff00) | |||
| rightLeg.translateX(0.5) | |||
| rightLeg.translateY(-1) | |||
| viewer.scene.addObject(rightLeg) | |||
| // Arms (cylinders) | |||
| const leftArm = generator.generateObject('cylinder', {radiusTop: 0.125, radiusBottom: 0.125, height: 1}) | |||
| leftArm.material.color.set(0xff0000) | |||
| leftArm.translateX(-1) | |||
| leftArm.translateY(0.5) | |||
| leftArm.rotateZ(Math.PI / 2) | |||
| viewer.scene.addObject(leftArm) | |||
| const rightArm = generator.generateObject('cylinder', {radiusTop: 0.125, radiusBottom: 0.125, height: 1}) | |||
| rightArm.material.color.set(0xff0000) | |||
| rightArm.translateX(1) | |||
| rightArm.translateY(0.5) | |||
| rightArm.rotateZ(Math.PI / 2) | |||
| viewer.scene.addObject(rightArm) | |||
| viewer.renderEnabled = true | |||
| // waiting because we need to render pipeline once to autoscale | |||
| await viewer.doOnce('postFrame') | |||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| ui.setupPlugins(ThreeSVGRendererPlugin) | |||
| ui.setupPlugins(GeometryGeneratorPlugin) | |||
| ui.setupPlugins(PickingPlugin) | |||
| ui.setupPlugins(TransformControlsPlugin) | |||
| } | |||
| init().finally(_testFinish) | |||
| @@ -0,0 +1,41 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Three SVG Renderer</title> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||
| <!-- Import maps polyfill --> | |||
| <!-- Remove this when import maps will be widely supported --> | |||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||
| <script type="importmap"> | |||
| { | |||
| "imports": { | |||
| "threepipe": "./../../dist/index.mjs", | |||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs", | |||
| "@threepipe/plugin-svg-renderer": "./../../plugins/svg-renderer/dist/index.mjs" | |||
| } | |||
| } | |||
| </script> | |||
| <style id="example-style"> | |||
| html, body, #canvas-container, #mcanvas { | |||
| position: absolute; | |||
| width: 100%; | |||
| height: 100%; | |||
| margin: 0; | |||
| overflow: hidden; | |||
| } | |||
| body{ | |||
| background: #cccccc; | |||
| } | |||
| </style> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,83 @@ | |||
| import { | |||
| _testFinish, | |||
| EditorViewWidgetPlugin, | |||
| GBufferPlugin, | |||
| GLTFAnimationPlugin, | |||
| IObject3D, | |||
| PickingPlugin, | |||
| ThreeViewer, | |||
| TransformControlsPlugin, | |||
| } from 'threepipe' | |||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||
| import {ThreeSVGRendererPlugin} from '@threepipe/plugin-svg-renderer' | |||
| async function init() { | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| msaa: false, | |||
| rgbm: false, | |||
| // zPrepass: true, | |||
| tonemap: false, | |||
| plugins: [GBufferPlugin, PickingPlugin, TransformControlsPlugin], /* TransformControlsPlugin */ // todo: transform controls doesnt work when selected object is in a parent. | |||
| }) | |||
| viewer.addPluginSync(new EditorViewWidgetPlugin('bottom-left', 128)) | |||
| viewer.renderEnabled = false | |||
| viewer.addPluginSync(new ThreeSVGRendererPlugin(true)) | |||
| viewer.addPluginSync(GLTFAnimationPlugin)// .autoplayOnLoad = true | |||
| // viewer.scene.addObject(new DirectionalLight2(0xffffff, 1).rotateZ(0.5).rotateX(0.5)) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| const models = [ | |||
| // working/sort of working | |||
| 'https://threejs.org/examples/models/gltf/Horse.glb', | |||
| 'https://demo-assets.pixotronics.com/pixo/gltf/jewlr1.glb', | |||
| 'https://demo-assets.pixotronics.com/pixo/gltf/engagement_ring.glb', | |||
| 'https://threejs.org/examples/models/gltf/Flamingo.glb', | |||
| 'https://threejs.org/examples/models/gltf/ShadowmappableMesh.glb', | |||
| 'https://threejs.org/examples/models/gltf/BoomBox.glb', | |||
| 'https://cdn.jsdelivr.net/gh/LokiResearch/three-svg-renderer/resources/pig.gltf', | |||
| 'https://cdn.jsdelivr.net/gh/LokiResearch/three-svg-renderer/resources/vincent.gltf', // https://studio.blender.org/characters/5718a967c379cf04929a4247/v1/ | |||
| 'https://threejs.org/examples/models/fbx/Samba Dancing.fbx', | |||
| 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', | |||
| 'https://threejs.org/examples/models/obj/male02/male02.obj', | |||
| 'https://threejs.org/examples/models/gltf/kira.glb', // slow | |||
| // not working | |||
| 'https://threejs.org/examples/models/gltf/Soldier.glb', | |||
| 'https://threejs.org/examples/models/gltf/LittlestTokyo.glb', | |||
| 'https://threejs.org/examples/models/gltf/ferrari.glb', | |||
| ] | |||
| await viewer.load<IObject3D>(models[0], { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| viewer.scene.backgroundColor = null | |||
| viewer.scene.background = null | |||
| // viewer.renderManager.screenPass.clipBackground = true // required when rgbm: true | |||
| viewer.scene.mainCamera.controls!.enableDamping = false | |||
| viewer.renderEnabled = true | |||
| // waiting because we need to render pipeline once to autoscale? | |||
| await viewer.doOnce('postFrame') | |||
| // optionally disable rendering. but its required if drawImageFills option is enabled | |||
| // viewer.renderManager.autoBuildPipeline = false | |||
| // viewer.renderManager.pipeline = [] // this will disable main viewer rendering | |||
| // make canvas transparent to hide it. We still need pointer events so dont set display to none | |||
| // viewer.canvas.style.opacity = '0' | |||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||
| ui.setupPlugins(ThreeSVGRendererPlugin) | |||
| ui.setupPlugins(GLTFAnimationPlugin) | |||
| ui.setupPlugins(PickingPlugin) | |||
| } | |||
| init().finally(_testFinish) | |||
| @@ -0,0 +1,11 @@ | |||
| node_modules | |||
| dist | |||
| public | |||
| config | |||
| libs | |||
| docs | |||
| examples/**/*.js | |||
| examples/**/*.html | |||
| src/three-svg-renderer/ | |||
| src/three-mesh-halfedge/ | |||
| @@ -0,0 +1,674 @@ | |||
| GNU GENERAL PUBLIC LICENSE | |||
| Version 3, 29 June 2007 | |||
| Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | |||
| Everyone is permitted to copy and distribute verbatim copies | |||
| of this license document, but changing it is not allowed. | |||
| Preamble | |||
| The GNU General Public License is a free, copyleft license for | |||
| software and other kinds of works. | |||
| The licenses for most software and other practical works are designed | |||
| to take away your freedom to share and change the works. By contrast, | |||
| the GNU General Public License is intended to guarantee your freedom to | |||
| share and change all versions of a program--to make sure it remains free | |||
| software for all its users. We, the Free Software Foundation, use the | |||
| GNU General Public License for most of our software; it applies also to | |||
| any other work released this way by its authors. You can apply it to | |||
| your programs, too. | |||
| When we speak of free software, we are referring to freedom, not | |||
| price. Our General Public Licenses are designed to make sure that you | |||
| have the freedom to distribute copies of free software (and charge for | |||
| them if you wish), that you receive source code or can get it if you | |||
| want it, that you can change the software or use pieces of it in new | |||
| free programs, and that you know you can do these things. | |||
| To protect your rights, we need to prevent others from denying you | |||
| these rights or asking you to surrender the rights. Therefore, you have | |||
| certain responsibilities if you distribute copies of the software, or if | |||
| you modify it: responsibilities to respect the freedom of others. | |||
| For example, if you distribute copies of such a program, whether | |||
| gratis or for a fee, you must pass on to the recipients the same | |||
| freedoms that you received. You must make sure that they, too, receive | |||
| or can get the source code. And you must show them these terms so they | |||
| know their rights. | |||
| Developers that use the GNU GPL protect your rights with two steps: | |||
| (1) assert copyright on the software, and (2) offer you this License | |||
| giving you legal permission to copy, distribute and/or modify it. | |||
| For the developers' and authors' protection, the GPL clearly explains | |||
| that there is no warranty for this free software. For both users' and | |||
| authors' sake, the GPL requires that modified versions be marked as | |||
| changed, so that their problems will not be attributed erroneously to | |||
| authors of previous versions. | |||
| Some devices are designed to deny users access to install or run | |||
| modified versions of the software inside them, although the manufacturer | |||
| can do so. This is fundamentally incompatible with the aim of | |||
| protecting users' freedom to change the software. The systematic | |||
| pattern of such abuse occurs in the area of products for individuals to | |||
| use, which is precisely where it is most unacceptable. Therefore, we | |||
| have designed this version of the GPL to prohibit the practice for those | |||
| products. If such problems arise substantially in other domains, we | |||
| stand ready to extend this provision to those domains in future versions | |||
| of the GPL, as needed to protect the freedom of users. | |||
| Finally, every program is threatened constantly by software patents. | |||
| States should not allow patents to restrict development and use of | |||
| software on general-purpose computers, but in those that do, we wish to | |||
| avoid the special danger that patents applied to a free program could | |||
| make it effectively proprietary. To prevent this, the GPL assures that | |||
| patents cannot be used to render the program non-free. | |||
| The precise terms and conditions for copying, distribution and | |||
| modification follow. | |||
| TERMS AND CONDITIONS | |||
| 0. Definitions. | |||
| "This License" refers to version 3 of the GNU General Public License. | |||
| "Copyright" also means copyright-like laws that apply to other kinds of | |||
| works, such as semiconductor masks. | |||
| "The Program" refers to any copyrightable work licensed under this | |||
| License. Each licensee is addressed as "you". "Licensees" and | |||
| "recipients" may be individuals or organizations. | |||
| To "modify" a work means to copy from or adapt all or part of the work | |||
| in a fashion requiring copyright permission, other than the making of an | |||
| exact copy. The resulting work is called a "modified version" of the | |||
| earlier work or a work "based on" the earlier work. | |||
| A "covered work" means either the unmodified Program or a work based | |||
| on the Program. | |||
| To "propagate" a work means to do anything with it that, without | |||
| permission, would make you directly or secondarily liable for | |||
| infringement under applicable copyright law, except executing it on a | |||
| computer or modifying a private copy. Propagation includes copying, | |||
| distribution (with or without modification), making available to the | |||
| public, and in some countries other activities as well. | |||
| To "convey" a work means any kind of propagation that enables other | |||
| parties to make or receive copies. Mere interaction with a user through | |||
| a computer network, with no transfer of a copy, is not conveying. | |||
| An interactive user interface displays "Appropriate Legal Notices" | |||
| to the extent that it includes a convenient and prominently visible | |||
| feature that (1) displays an appropriate copyright notice, and (2) | |||
| tells the user that there is no warranty for the work (except to the | |||
| extent that warranties are provided), that licensees may convey the | |||
| work under this License, and how to view a copy of this License. If | |||
| the interface presents a list of user commands or options, such as a | |||
| menu, a prominent item in the list meets this criterion. | |||
| 1. Source Code. | |||
| The "source code" for a work means the preferred form of the work | |||
| for making modifications to it. "Object code" means any non-source | |||
| form of a work. | |||
| A "Standard Interface" means an interface that either is an official | |||
| standard defined by a recognized standards body, or, in the case of | |||
| interfaces specified for a particular programming language, one that | |||
| is widely used among developers working in that language. | |||
| The "System Libraries" of an executable work include anything, other | |||
| than the work as a whole, that (a) is included in the normal form of | |||
| packaging a Major Component, but which is not part of that Major | |||
| Component, and (b) serves only to enable use of the work with that | |||
| Major Component, or to implement a Standard Interface for which an | |||
| implementation is available to the public in source code form. A | |||
| "Major Component", in this context, means a major essential component | |||
| (kernel, window system, and so on) of the specific operating system | |||
| (if any) on which the executable work runs, or a compiler used to | |||
| produce the work, or an object code interpreter used to run it. | |||
| The "Corresponding Source" for a work in object code form means all | |||
| the source code needed to generate, install, and (for an executable | |||
| work) run the object code and to modify the work, including scripts to | |||
| control those activities. However, it does not include the work's | |||
| System Libraries, or general-purpose tools or generally available free | |||
| programs which are used unmodified in performing those activities but | |||
| which are not part of the work. For example, Corresponding Source | |||
| includes interface definition files associated with source files for | |||
| the work, and the source code for shared libraries and dynamically | |||
| linked subprograms that the work is specifically designed to require, | |||
| such as by intimate data communication or control flow between those | |||
| subprograms and other parts of the work. | |||
| The Corresponding Source need not include anything that users | |||
| can regenerate automatically from other parts of the Corresponding | |||
| Source. | |||
| The Corresponding Source for a work in source code form is that | |||
| same work. | |||
| 2. Basic Permissions. | |||
| All rights granted under this License are granted for the term of | |||
| copyright on the Program, and are irrevocable provided the stated | |||
| conditions are met. This License explicitly affirms your unlimited | |||
| permission to run the unmodified Program. The output from running a | |||
| covered work is covered by this License only if the output, given its | |||
| content, constitutes a covered work. This License acknowledges your | |||
| rights of fair use or other equivalent, as provided by copyright law. | |||
| You may make, run and propagate covered works that you do not | |||
| convey, without conditions so long as your license otherwise remains | |||
| in force. You may convey covered works to others for the sole purpose | |||
| of having them make modifications exclusively for you, or provide you | |||
| with facilities for running those works, provided that you comply with | |||
| the terms of this License in conveying all material for which you do | |||
| not control copyright. Those thus making or running the covered works | |||
| for you must do so exclusively on your behalf, under your direction | |||
| and control, on terms that prohibit them from making any copies of | |||
| your copyrighted material outside their relationship with you. | |||
| Conveying under any other circumstances is permitted solely under | |||
| the conditions stated below. Sublicensing is not allowed; section 10 | |||
| makes it unnecessary. | |||
| 3. Protecting Users' Legal Rights From Anti-Circumvention Law. | |||
| No covered work shall be deemed part of an effective technological | |||
| measure under any applicable law fulfilling obligations under article | |||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | |||
| similar laws prohibiting or restricting circumvention of such | |||
| measures. | |||
| When you convey a covered work, you waive any legal power to forbid | |||
| circumvention of technological measures to the extent such circumvention | |||
| is effected by exercising rights under this License with respect to | |||
| the covered work, and you disclaim any intention to limit operation or | |||
| modification of the work as a means of enforcing, against the work's | |||
| users, your or third parties' legal rights to forbid circumvention of | |||
| technological measures. | |||
| 4. Conveying Verbatim Copies. | |||
| You may convey verbatim copies of the Program's source code as you | |||
| receive it, in any medium, provided that you conspicuously and | |||
| appropriately publish on each copy an appropriate copyright notice; | |||
| keep intact all notices stating that this License and any | |||
| non-permissive terms added in accord with section 7 apply to the code; | |||
| keep intact all notices of the absence of any warranty; and give all | |||
| recipients a copy of this License along with the Program. | |||
| You may charge any price or no price for each copy that you convey, | |||
| and you may offer support or warranty protection for a fee. | |||
| 5. Conveying Modified Source Versions. | |||
| You may convey a work based on the Program, or the modifications to | |||
| produce it from the Program, in the form of source code under the | |||
| terms of section 4, provided that you also meet all of these conditions: | |||
| a) The work must carry prominent notices stating that you modified | |||
| it, and giving a relevant date. | |||
| b) The work must carry prominent notices stating that it is | |||
| released under this License and any conditions added under section | |||
| 7. This requirement modifies the requirement in section 4 to | |||
| "keep intact all notices". | |||
| c) You must license the entire work, as a whole, under this | |||
| License to anyone who comes into possession of a copy. This | |||
| License will therefore apply, along with any applicable section 7 | |||
| additional terms, to the whole of the work, and all its parts, | |||
| regardless of how they are packaged. This License gives no | |||
| permission to license the work in any other way, but it does not | |||
| invalidate such permission if you have separately received it. | |||
| d) If the work has interactive user interfaces, each must display | |||
| Appropriate Legal Notices; however, if the Program has interactive | |||
| interfaces that do not display Appropriate Legal Notices, your | |||
| work need not make them do so. | |||
| A compilation of a covered work with other separate and independent | |||
| works, which are not by their nature extensions of the covered work, | |||
| and which are not combined with it such as to form a larger program, | |||
| in or on a volume of a storage or distribution medium, is called an | |||
| "aggregate" if the compilation and its resulting copyright are not | |||
| used to limit the access or legal rights of the compilation's users | |||
| beyond what the individual works permit. Inclusion of a covered work | |||
| in an aggregate does not cause this License to apply to the other | |||
| parts of the aggregate. | |||
| 6. Conveying Non-Source Forms. | |||
| You may convey a covered work in object code form under the terms | |||
| of sections 4 and 5, provided that you also convey the | |||
| machine-readable Corresponding Source under the terms of this License, | |||
| in one of these ways: | |||
| a) Convey the object code in, or embodied in, a physical product | |||
| (including a physical distribution medium), accompanied by the | |||
| Corresponding Source fixed on a durable physical medium | |||
| customarily used for software interchange. | |||
| b) Convey the object code in, or embodied in, a physical product | |||
| (including a physical distribution medium), accompanied by a | |||
| written offer, valid for at least three years and valid for as | |||
| long as you offer spare parts or customer support for that product | |||
| model, to give anyone who possesses the object code either (1) a | |||
| copy of the Corresponding Source for all the software in the | |||
| product that is covered by this License, on a durable physical | |||
| medium customarily used for software interchange, for a price no | |||
| more than your reasonable cost of physically performing this | |||
| conveying of source, or (2) access to copy the | |||
| Corresponding Source from a network server at no charge. | |||
| c) Convey individual copies of the object code with a copy of the | |||
| written offer to provide the Corresponding Source. This | |||
| alternative is allowed only occasionally and noncommercially, and | |||
| only if you received the object code with such an offer, in accord | |||
| with subsection 6b. | |||
| d) Convey the object code by offering access from a designated | |||
| place (gratis or for a charge), and offer equivalent access to the | |||
| Corresponding Source in the same way through the same place at no | |||
| further charge. You need not require recipients to copy the | |||
| Corresponding Source along with the object code. If the place to | |||
| copy the object code is a network server, the Corresponding Source | |||
| may be on a different server (operated by you or a third party) | |||
| that supports equivalent copying facilities, provided you maintain | |||
| clear directions next to the object code saying where to find the | |||
| Corresponding Source. Regardless of what server hosts the | |||
| Corresponding Source, you remain obligated to ensure that it is | |||
| available for as long as needed to satisfy these requirements. | |||
| e) Convey the object code using peer-to-peer transmission, provided | |||
| you inform other peers where the object code and Corresponding | |||
| Source of the work are being offered to the general public at no | |||
| charge under subsection 6d. | |||
| A separable portion of the object code, whose source code is excluded | |||
| from the Corresponding Source as a System Library, need not be | |||
| included in conveying the object code work. | |||
| A "User Product" is either (1) a "consumer product", which means any | |||
| tangible personal property which is normally used for personal, family, | |||
| or household purposes, or (2) anything designed or sold for incorporation | |||
| into a dwelling. In determining whether a product is a consumer product, | |||
| doubtful cases shall be resolved in favor of coverage. For a particular | |||
| product received by a particular user, "normally used" refers to a | |||
| typical or common use of that class of product, regardless of the status | |||
| of the particular user or of the way in which the particular user | |||
| actually uses, or expects or is expected to use, the product. A product | |||
| is a consumer product regardless of whether the product has substantial | |||
| commercial, industrial or non-consumer uses, unless such uses represent | |||
| the only significant mode of use of the product. | |||
| "Installation Information" for a User Product means any methods, | |||
| procedures, authorization keys, or other information required to install | |||
| and execute modified versions of a covered work in that User Product from | |||
| a modified version of its Corresponding Source. The information must | |||
| suffice to ensure that the continued functioning of the modified object | |||
| code is in no case prevented or interfered with solely because | |||
| modification has been made. | |||
| If you convey an object code work under this section in, or with, or | |||
| specifically for use in, a User Product, and the conveying occurs as | |||
| part of a transaction in which the right of possession and use of the | |||
| User Product is transferred to the recipient in perpetuity or for a | |||
| fixed term (regardless of how the transaction is characterized), the | |||
| Corresponding Source conveyed under this section must be accompanied | |||
| by the Installation Information. But this requirement does not apply | |||
| if neither you nor any third party retains the ability to install | |||
| modified object code on the User Product (for example, the work has | |||
| been installed in ROM). | |||
| The requirement to provide Installation Information does not include a | |||
| requirement to continue to provide support service, warranty, or updates | |||
| for a work that has been modified or installed by the recipient, or for | |||
| the User Product in which it has been modified or installed. Access to a | |||
| network may be denied when the modification itself materially and | |||
| adversely affects the operation of the network or violates the rules and | |||
| protocols for communication across the network. | |||
| Corresponding Source conveyed, and Installation Information provided, | |||
| in accord with this section must be in a format that is publicly | |||
| documented (and with an implementation available to the public in | |||
| source code form), and must require no special password or key for | |||
| unpacking, reading or copying. | |||
| 7. Additional Terms. | |||
| "Additional permissions" are terms that supplement the terms of this | |||
| License by making exceptions from one or more of its conditions. | |||
| Additional permissions that are applicable to the entire Program shall | |||
| be treated as though they were included in this License, to the extent | |||
| that they are valid under applicable law. If additional permissions | |||
| apply only to part of the Program, that part may be used separately | |||
| under those permissions, but the entire Program remains governed by | |||
| this License without regard to the additional permissions. | |||
| When you convey a copy of a covered work, you may at your option | |||
| remove any additional permissions from that copy, or from any part of | |||
| it. (Additional permissions may be written to require their own | |||
| removal in certain cases when you modify the work.) You may place | |||
| additional permissions on material, added by you to a covered work, | |||
| for which you have or can give appropriate copyright permission. | |||
| Notwithstanding any other provision of this License, for material you | |||
| add to a covered work, you may (if authorized by the copyright holders of | |||
| that material) supplement the terms of this License with terms: | |||
| a) Disclaiming warranty or limiting liability differently from the | |||
| terms of sections 15 and 16 of this License; or | |||
| b) Requiring preservation of specified reasonable legal notices or | |||
| author attributions in that material or in the Appropriate Legal | |||
| Notices displayed by works containing it; or | |||
| c) Prohibiting misrepresentation of the origin of that material, or | |||
| requiring that modified versions of such material be marked in | |||
| reasonable ways as different from the original version; or | |||
| d) Limiting the use for publicity purposes of names of licensors or | |||
| authors of the material; or | |||
| e) Declining to grant rights under trademark law for use of some | |||
| trade names, trademarks, or service marks; or | |||
| f) Requiring indemnification of licensors and authors of that | |||
| material by anyone who conveys the material (or modified versions of | |||
| it) with contractual assumptions of liability to the recipient, for | |||
| any liability that these contractual assumptions directly impose on | |||
| those licensors and authors. | |||
| All other non-permissive additional terms are considered "further | |||
| restrictions" within the meaning of section 10. If the Program as you | |||
| received it, or any part of it, contains a notice stating that it is | |||
| governed by this License along with a term that is a further | |||
| restriction, you may remove that term. If a license document contains | |||
| a further restriction but permits relicensing or conveying under this | |||
| License, you may add to a covered work material governed by the terms | |||
| of that license document, provided that the further restriction does | |||
| not survive such relicensing or conveying. | |||
| If you add terms to a covered work in accord with this section, you | |||
| must place, in the relevant source files, a statement of the | |||
| additional terms that apply to those files, or a notice indicating | |||
| where to find the applicable terms. | |||
| Additional terms, permissive or non-permissive, may be stated in the | |||
| form of a separately written license, or stated as exceptions; | |||
| the above requirements apply either way. | |||
| 8. Termination. | |||
| You may not propagate or modify a covered work except as expressly | |||
| provided under this License. Any attempt otherwise to propagate or | |||
| modify it is void, and will automatically terminate your rights under | |||
| this License (including any patent licenses granted under the third | |||
| paragraph of section 11). | |||
| However, if you cease all violation of this License, then your | |||
| license from a particular copyright holder is reinstated (a) | |||
| provisionally, unless and until the copyright holder explicitly and | |||
| finally terminates your license, and (b) permanently, if the copyright | |||
| holder fails to notify you of the violation by some reasonable means | |||
| prior to 60 days after the cessation. | |||
| Moreover, your license from a particular copyright holder is | |||
| reinstated permanently if the copyright holder notifies you of the | |||
| violation by some reasonable means, this is the first time you have | |||
| received notice of violation of this License (for any work) from that | |||
| copyright holder, and you cure the violation prior to 30 days after | |||
| your receipt of the notice. | |||
| Termination of your rights under this section does not terminate the | |||
| licenses of parties who have received copies or rights from you under | |||
| this License. If your rights have been terminated and not permanently | |||
| reinstated, you do not qualify to receive new licenses for the same | |||
| material under section 10. | |||
| 9. Acceptance Not Required for Having Copies. | |||
| You are not required to accept this License in order to receive or | |||
| run a copy of the Program. Ancillary propagation of a covered work | |||
| occurring solely as a consequence of using peer-to-peer transmission | |||
| to receive a copy likewise does not require acceptance. However, | |||
| nothing other than this License grants you permission to propagate or | |||
| modify any covered work. These actions infringe copyright if you do | |||
| not accept this License. Therefore, by modifying or propagating a | |||
| covered work, you indicate your acceptance of this License to do so. | |||
| 10. Automatic Licensing of Downstream Recipients. | |||
| Each time you convey a covered work, the recipient automatically | |||
| receives a license from the original licensors, to run, modify and | |||
| propagate that work, subject to this License. You are not responsible | |||
| for enforcing compliance by third parties with this License. | |||
| An "entity transaction" is a transaction transferring control of an | |||
| organization, or substantially all assets of one, or subdividing an | |||
| organization, or merging organizations. If propagation of a covered | |||
| work results from an entity transaction, each party to that | |||
| transaction who receives a copy of the work also receives whatever | |||
| licenses to the work the party's predecessor in interest had or could | |||
| give under the previous paragraph, plus a right to possession of the | |||
| Corresponding Source of the work from the predecessor in interest, if | |||
| the predecessor has it or can get it with reasonable efforts. | |||
| You may not impose any further restrictions on the exercise of the | |||
| rights granted or affirmed under this License. For example, you may | |||
| not impose a license fee, royalty, or other charge for exercise of | |||
| rights granted under this License, and you may not initiate litigation | |||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | |||
| any patent claim is infringed by making, using, selling, offering for | |||
| sale, or importing the Program or any portion of it. | |||
| 11. Patents. | |||
| A "contributor" is a copyright holder who authorizes use under this | |||
| License of the Program or a work on which the Program is based. The | |||
| work thus licensed is called the contributor's "contributor version". | |||
| A contributor's "essential patent claims" are all patent claims | |||
| owned or controlled by the contributor, whether already acquired or | |||
| hereafter acquired, that would be infringed by some manner, permitted | |||
| by this License, of making, using, or selling its contributor version, | |||
| but do not include claims that would be infringed only as a | |||
| consequence of further modification of the contributor version. For | |||
| purposes of this definition, "control" includes the right to grant | |||
| patent sublicenses in a manner consistent with the requirements of | |||
| this License. | |||
| Each contributor grants you a non-exclusive, worldwide, royalty-free | |||
| patent license under the contributor's essential patent claims, to | |||
| make, use, sell, offer for sale, import and otherwise run, modify and | |||
| propagate the contents of its contributor version. | |||
| In the following three paragraphs, a "patent license" is any express | |||
| agreement or commitment, however denominated, not to enforce a patent | |||
| (such as an express permission to practice a patent or covenant not to | |||
| sue for patent infringement). To "grant" such a patent license to a | |||
| party means to make such an agreement or commitment not to enforce a | |||
| patent against the party. | |||
| If you convey a covered work, knowingly relying on a patent license, | |||
| and the Corresponding Source of the work is not available for anyone | |||
| to copy, free of charge and under the terms of this License, through a | |||
| publicly available network server or other readily accessible means, | |||
| then you must either (1) cause the Corresponding Source to be so | |||
| available, or (2) arrange to deprive yourself of the benefit of the | |||
| patent license for this particular work, or (3) arrange, in a manner | |||
| consistent with the requirements of this License, to extend the patent | |||
| license to downstream recipients. "Knowingly relying" means you have | |||
| actual knowledge that, but for the patent license, your conveying the | |||
| covered work in a country, or your recipient's use of the covered work | |||
| in a country, would infringe one or more identifiable patents in that | |||
| country that you have reason to believe are valid. | |||
| If, pursuant to or in connection with a single transaction or | |||
| arrangement, you convey, or propagate by procuring conveyance of, a | |||
| covered work, and grant a patent license to some of the parties | |||
| receiving the covered work authorizing them to use, propagate, modify | |||
| or convey a specific copy of the covered work, then the patent license | |||
| you grant is automatically extended to all recipients of the covered | |||
| work and works based on it. | |||
| A patent license is "discriminatory" if it does not include within | |||
| the scope of its coverage, prohibits the exercise of, or is | |||
| conditioned on the non-exercise of one or more of the rights that are | |||
| specifically granted under this License. You may not convey a covered | |||
| work if you are a party to an arrangement with a third party that is | |||
| in the business of distributing software, under which you make payment | |||
| to the third party based on the extent of your activity of conveying | |||
| the work, and under which the third party grants, to any of the | |||
| parties who would receive the covered work from you, a discriminatory | |||
| patent license (a) in connection with copies of the covered work | |||
| conveyed by you (or copies made from those copies), or (b) primarily | |||
| for and in connection with specific products or compilations that | |||
| contain the covered work, unless you entered into that arrangement, | |||
| or that patent license was granted, prior to 28 March 2007. | |||
| Nothing in this License shall be construed as excluding or limiting | |||
| any implied license or other defenses to infringement that may | |||
| otherwise be available to you under applicable patent law. | |||
| 12. No Surrender of Others' Freedom. | |||
| If conditions are imposed on you (whether by court order, agreement or | |||
| otherwise) that contradict the conditions of this License, they do not | |||
| excuse you from the conditions of this License. If you cannot convey a | |||
| covered work so as to satisfy simultaneously your obligations under this | |||
| License and any other pertinent obligations, then as a consequence you may | |||
| not convey it at all. For example, if you agree to terms that obligate you | |||
| to collect a royalty for further conveying from those to whom you convey | |||
| the Program, the only way you could satisfy both those terms and this | |||
| License would be to refrain entirely from conveying the Program. | |||
| 13. Use with the GNU Affero General Public License. | |||
| Notwithstanding any other provision of this License, you have | |||
| permission to link or combine any covered work with a work licensed | |||
| under version 3 of the GNU Affero General Public License into a single | |||
| combined work, and to convey the resulting work. The terms of this | |||
| License will continue to apply to the part which is the covered work, | |||
| but the special requirements of the GNU Affero General Public License, | |||
| section 13, concerning interaction through a network will apply to the | |||
| combination as such. | |||
| 14. Revised Versions of this License. | |||
| The Free Software Foundation may publish revised and/or new versions of | |||
| the GNU General Public License from time to time. Such new versions will | |||
| be similar in spirit to the present version, but may differ in detail to | |||
| address new problems or concerns. | |||
| Each version is given a distinguishing version number. If the | |||
| Program specifies that a certain numbered version of the GNU General | |||
| Public License "or any later version" applies to it, you have the | |||
| option of following the terms and conditions either of that numbered | |||
| version or of any later version published by the Free Software | |||
| Foundation. If the Program does not specify a version number of the | |||
| GNU General Public License, you may choose any version ever published | |||
| by the Free Software Foundation. | |||
| If the Program specifies that a proxy can decide which future | |||
| versions of the GNU General Public License can be used, that proxy's | |||
| public statement of acceptance of a version permanently authorizes you | |||
| to choose that version for the Program. | |||
| Later license versions may give you additional or different | |||
| permissions. However, no additional obligations are imposed on any | |||
| author or copyright holder as a result of your choosing to follow a | |||
| later version. | |||
| 15. Disclaimer of Warranty. | |||
| THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | |||
| APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | |||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | |||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | |||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |||
| PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | |||
| IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | |||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | |||
| 16. Limitation of Liability. | |||
| IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | |||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | |||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | |||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | |||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | |||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | |||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | |||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | |||
| SUCH DAMAGES. | |||
| 17. Interpretation of Sections 15 and 16. | |||
| If the disclaimer of warranty and limitation of liability provided | |||
| above cannot be given local legal effect according to their terms, | |||
| reviewing courts shall apply local law that most closely approximates | |||
| an absolute waiver of all civil liability in connection with the | |||
| Program, unless a warranty or assumption of liability accompanies a | |||
| copy of the Program in return for a fee. | |||
| END OF TERMS AND CONDITIONS | |||
| How to Apply These Terms to Your New Programs | |||
| If you develop a new program, and you want it to be of the greatest | |||
| possible use to the public, the best way to achieve this is to make it | |||
| free software which everyone can redistribute and change under these terms. | |||
| To do so, attach the following notices to the program. It is safest | |||
| to attach them to the start of each source file to most effectively | |||
| state the exclusion of warranty; and each file should have at least | |||
| the "copyright" line and a pointer to where the full notice is found. | |||
| <one line to give the program's name and a brief idea of what it does.> | |||
| Copyright (C) <year> <name of author> | |||
| This program is free software: you can redistribute it and/or modify | |||
| it under the terms of the GNU General Public License as published by | |||
| the Free Software Foundation, either version 3 of the License, or | |||
| (at your option) any later version. | |||
| This program is distributed in the hope that it will be useful, | |||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| GNU General Public License for more details. | |||
| You should have received a copy of the GNU General Public License | |||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
| Also add information on how to contact you by electronic and paper mail. | |||
| If the program does terminal interaction, make it output a short | |||
| notice like this when it starts in an interactive mode: | |||
| <program> Copyright (C) <year> <name of author> | |||
| This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | |||
| This is free software, and you are welcome to redistribute it | |||
| under certain conditions; type `show c' for details. | |||
| The hypothetical commands `show w' and `show c' should show the appropriate | |||
| parts of the General Public License. Of course, your program's commands | |||
| might be different; for a GUI interface, you would use an "about box". | |||
| You should also get your employer (if you work as a programmer) or school, | |||
| if any, to sign a "copyright disclaimer" for the program, if necessary. | |||
| For more information on this, and how to apply and follow the GNU GPL, see | |||
| <https://www.gnu.org/licenses/>. | |||
| The GNU General Public License does not permit incorporating your program | |||
| into proprietary programs. If your program is a subroutine library, you | |||
| may consider it more useful to permit linking proprietary applications with | |||
| the library. If this is what you want to do, use the GNU Lesser General | |||
| Public License instead of this License. But first, please read | |||
| <https://www.gnu.org/licenses/why-not-lgpl.html>. | |||
| @@ -0,0 +1,137 @@ | |||
| { | |||
| "name": "@threepipe/plugin-svg-renderer", | |||
| "version": "0.1.0", | |||
| "lockfileVersion": 3, | |||
| "requires": true, | |||
| "packages": { | |||
| "": { | |||
| "name": "@threepipe/plugin-svg-renderer", | |||
| "version": "0.1.0", | |||
| "license": "Apache-2.0", | |||
| "dependencies": { | |||
| "threepipe": "file:./../../src/" | |||
| }, | |||
| "devDependencies": { | |||
| "@svgdotjs/svg.js": "^3.2.0", | |||
| "@svgdotjs/svg.topath.js": "^2.0.3", | |||
| "arrangement-2d-js": "github:LokiResearch/arrangement-2d-js", | |||
| "fast-triangle-triangle-intersection": "^1.0.7", | |||
| "flatbush": "^4.4.0", | |||
| "isect": "^3.0.2", | |||
| "three-mesh-bvh": "^0.7.4", | |||
| "xml-formatter": "^2.6.1" | |||
| } | |||
| }, | |||
| "../../src": {}, | |||
| "node_modules/@svgdotjs/svg.js": { | |||
| "version": "3.2.0", | |||
| "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.0.tgz", | |||
| "integrity": "sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA==", | |||
| "dev": true, | |||
| "funding": { | |||
| "type": "github", | |||
| "url": "https://github.com/sponsors/Fuzzyma" | |||
| } | |||
| }, | |||
| "node_modules/@svgdotjs/svg.topath.js": { | |||
| "version": "2.0.3", | |||
| "resolved": "https://registry.npmjs.org/@svgdotjs/svg.topath.js/-/svg.topath.js-2.0.3.tgz", | |||
| "integrity": "sha512-vZG+4DpzjhkjCT9LJqg+cMCekJIGSUTpOEfbQN/wueSaiP7N04JFXYW4X3lj1MlZDNU9IcE1GsWEuwdhLJJGuA==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "@svgdotjs/svg.js": "^3.0.10" | |||
| }, | |||
| "engines": { | |||
| "node": ">= 0.8.0" | |||
| } | |||
| }, | |||
| "node_modules/arrangement-2d-js": { | |||
| "version": "1.0.0", | |||
| "resolved": "git+ssh://git@github.com/LokiResearch/arrangement-2d-js.git#0d61b1eec52c252af24c56e02a4c8214e22f443c", | |||
| "dev": true | |||
| }, | |||
| "node_modules/fast-triangle-triangle-intersection": { | |||
| "version": "1.0.7", | |||
| "resolved": "https://registry.npmjs.org/fast-triangle-triangle-intersection/-/fast-triangle-triangle-intersection-1.0.7.tgz", | |||
| "integrity": "sha512-IdWIfknFUBSd8u1KG3mgFCbsJfE2QKzRe3rFmFUmGWq/ZylbpryoMBoRyx4Rcc0MxsNG+f8p9CKI8QeP8QYa3w==", | |||
| "dev": true, | |||
| "peerDependencies": { | |||
| "three": ">= 0.123.0" | |||
| } | |||
| }, | |||
| "node_modules/flatbush": { | |||
| "version": "4.4.0", | |||
| "resolved": "https://registry.npmjs.org/flatbush/-/flatbush-4.4.0.tgz", | |||
| "integrity": "sha512-cf6G+sfy/+/FLH7Ls1URQ5GCRlXgwgqUZiEsMNrMZqb3Us3EkKmzUlKbnyoBy/4wI4oLJ+8cyCQoKJIVm92Fmg==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "flatqueue": "^2.0.3" | |||
| } | |||
| }, | |||
| "node_modules/flatqueue": { | |||
| "version": "2.0.3", | |||
| "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-2.0.3.tgz", | |||
| "integrity": "sha512-RZCWZNkmxzUOh8jqEcEGZCycb3B8KAfpPwg3H//cURasunYxsg1eIvE+QDSjX+ZPHTIVfINfK1aLTrVKKO0i4g==", | |||
| "dev": true, | |||
| "engines": { | |||
| "node": ">= 12.17.0" | |||
| } | |||
| }, | |||
| "node_modules/isect": { | |||
| "version": "3.0.2", | |||
| "resolved": "https://registry.npmjs.org/isect/-/isect-3.0.2.tgz", | |||
| "integrity": "sha512-HMzl1S9rnhjhenLDKCmS1y4UPqFuVfVD9VM96mmBiiowrDuy7LNEuBNzTU2UHpFov4EK83f/WF2imktZhoP9Nw==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "splaytree": "^2.0.2" | |||
| } | |||
| }, | |||
| "node_modules/splaytree": { | |||
| "version": "2.0.3", | |||
| "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-2.0.3.tgz", | |||
| "integrity": "sha512-IziTvWQv9F1EiKq9XveosQRGTLrdUW0jLokpmAXz0+hnLgBZitvU0j4gUvCGASKwUQvCZaofhff1H8OmE2LRdA==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/three": { | |||
| "version": "0.164.1", | |||
| "resolved": "https://registry.npmjs.org/three/-/three-0.164.1.tgz", | |||
| "integrity": "sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==", | |||
| "dev": true, | |||
| "peer": true | |||
| }, | |||
| "node_modules/three-mesh-bvh": { | |||
| "version": "0.7.4", | |||
| "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.4.tgz", | |||
| "integrity": "sha512-flxe0A4uflTPR6elgq/Y8VrLoljDNS899i422SxQcU3EtMj6o8z4kZRyqZqGWzR0qMf1InTZzY1/0xZl/rnvVw==", | |||
| "dev": true, | |||
| "peerDependencies": { | |||
| "three": ">= 0.151.0" | |||
| } | |||
| }, | |||
| "node_modules/threepipe": { | |||
| "resolved": "../../src", | |||
| "link": true | |||
| }, | |||
| "node_modules/xml-formatter": { | |||
| "version": "2.6.1", | |||
| "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.6.1.tgz", | |||
| "integrity": "sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw==", | |||
| "dev": true, | |||
| "dependencies": { | |||
| "xml-parser-xo": "^3.2.0" | |||
| }, | |||
| "engines": { | |||
| "node": ">= 10" | |||
| } | |||
| }, | |||
| "node_modules/xml-parser-xo": { | |||
| "version": "3.2.0", | |||
| "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz", | |||
| "integrity": "sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==", | |||
| "dev": true, | |||
| "engines": { | |||
| "node": ">= 10" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,66 @@ | |||
| { | |||
| "name": "@threepipe/plugin-svg-renderer", | |||
| "description": "Plugins for SVG Rendering of 3d objects for Threepipe", | |||
| "version": "0.1.0", | |||
| "devDependencies": { | |||
| "@svgdotjs/svg.js": "^3.2.0", | |||
| "@svgdotjs/svg.topath.js": "^2.0.3", | |||
| "arrangement-2d-js": "github:LokiResearch/arrangement-2d-js", | |||
| "fast-triangle-triangle-intersection": "^1.0.7", | |||
| "flatbush": "^4.4.0", | |||
| "isect": "^3.0.2", | |||
| "three-mesh-bvh": "^0.7.4", | |||
| "xml-formatter": "^2.6.1" | |||
| }, | |||
| "dependencies": { | |||
| "threepipe": "file:./../../src/" | |||
| }, | |||
| "clean-package": { | |||
| "remove": [ | |||
| "clean-package", | |||
| "scripts", | |||
| "devDependencies", | |||
| "//", | |||
| "markdown-to-html" | |||
| ], | |||
| "replace": { | |||
| "dependencies": { | |||
| "threepipe": "^0.0.26" | |||
| } | |||
| } | |||
| }, | |||
| "type": "module", | |||
| "main": "dist/index.js", | |||
| "module": "dist/index.mjs", | |||
| "types": "dist/index.d.ts", | |||
| "files": [ | |||
| "dist", | |||
| "src" | |||
| ], | |||
| "scripts": { | |||
| "new:pack": "npm run prepare && clean-package && npm pack && clean-package restore", | |||
| "new:publish": "npm run prepare && clean-package && npm publish --access public && clean-package restore", | |||
| "prepare": "npm run build", | |||
| "build": "rimraf dist && vite build", | |||
| "dev": "NODE_ENV=development vite build --watch", | |||
| "docs": "rimraf docs && npx typedoc" | |||
| }, | |||
| "author": "repalash <palash@shaders.app>", | |||
| "license": "GPLV3", | |||
| "keywords": [ | |||
| "three", | |||
| "three.js", | |||
| "threepipe", | |||
| "svg", | |||
| "rendering", | |||
| "vector-graphics" | |||
| ], | |||
| "bugs": { | |||
| "url": "https://github.com/repalash/threepipe/issues" | |||
| }, | |||
| "homepage": "https://github.com/repalash/threepipe#readme", | |||
| "repository": { | |||
| "type": "git", | |||
| "url": "git://github.com/repalash/threepipe.git" | |||
| } | |||
| } | |||
| @@ -0,0 +1,113 @@ | |||
| import { | |||
| AViewerPluginSync, | |||
| type IViewerEvent, | |||
| ThreeViewer, | |||
| uiButton, | |||
| uiConfig, | |||
| uiFolderContainer, | |||
| uiToggle, | |||
| } from 'threepipe' | |||
| import {BasicSVGRenderer} from './basic/BasicSVGRenderer' | |||
| /** | |||
| * SVG rendering of 3d objects using SVGRenderer from three/addons | |||
| */ | |||
| @uiFolderContainer('SVG Renderer') | |||
| export class BasicSVGRendererPlugin extends AViewerPluginSync<''> { | |||
| static readonly PluginType = 'BasicSVGRendererPlugin' | |||
| @uiToggle() | |||
| enabled = true | |||
| @uiConfig() | |||
| readonly renderer = new BasicSVGRenderer() | |||
| /** | |||
| * @param enabled | |||
| * @param autoAddToContainer - automatically add the svg to the viewer container and style it same as the viewer is position is absolute | |||
| */ | |||
| constructor(enabled = true, readonly autoAddToContainer = true) { | |||
| super() | |||
| this._onResize = this._onResize.bind(this) | |||
| this.enabled = enabled | |||
| this.renderer.domElement.style.position = 'absolute' | |||
| this.renderer.domElement.style.display = 'none' | |||
| } | |||
| protected _lastStyles?: string = undefined | |||
| onAdded(viewer: ThreeViewer) { | |||
| super.onAdded(viewer) | |||
| this.renderer.setSize(viewer.canvas.clientWidth, viewer.canvas.clientHeight) | |||
| this._refreshParams() | |||
| if (this.autoAddToContainer) { | |||
| viewer.container.prepend(this.renderer.domElement) // behind the canvas so that we get pointer events | |||
| const element = this.renderer.domElement | |||
| element.style.pointerEvents = 'none' | |||
| const canvasStyles = getComputedStyle(viewer.canvas) | |||
| if (canvasStyles.position === 'absolute') { | |||
| this._lastStyles = element.style.cssText | |||
| // copy styles from canvas to svg so it looks the same. | |||
| element.style.top = canvasStyles.top | |||
| element.style.left = canvasStyles.left | |||
| element.style.width = canvasStyles.width | |||
| element.style.height = canvasStyles.height | |||
| // element.style.zIndex = '999999' // svg should be behind the canvas | |||
| } else { | |||
| this._viewer?.console.warn('BasicSVGRendererPlugin: canvas position should be absolute for proper rendering') | |||
| } | |||
| viewer.renderManager.addEventListener('resize', this._onResize) | |||
| } | |||
| this.renderer.domElement.style.display = this.enabled ? '' : 'none' | |||
| } | |||
| onRemove(viewer: ThreeViewer) { | |||
| super.onRemove(viewer) | |||
| if (this.autoAddToContainer) { | |||
| viewer.container.removeChild(this.renderer.domElement) | |||
| } | |||
| if (this._lastStyles !== undefined) { | |||
| this.renderer.domElement.style.cssText = this._lastStyles | |||
| this._lastStyles = undefined | |||
| } | |||
| viewer.renderManager.removeEventListener('resize', this._onResize) | |||
| this.renderer.domElement.style.display = 'none' | |||
| } | |||
| @uiToggle() | |||
| autoRender = true | |||
| @uiButton() | |||
| render() { | |||
| if (!this._viewer) return | |||
| if (this.isDisabled()) return | |||
| this.renderer.render(this._viewer.scene, this._viewer.scene.mainCamera) | |||
| } | |||
| @uiButton() | |||
| download() { | |||
| const svg = this.renderer.domElement.outerHTML | |||
| const blob = new Blob([svg], {type: 'image/svg+xml'}) | |||
| this._viewer?.exportBlob(blob, 'scene.svg') | |||
| } | |||
| protected _viewerListeners = { | |||
| postRender: (_: IViewerEvent)=>{ | |||
| if (this.autoRender) this.render() | |||
| }, | |||
| } | |||
| get svgNode() { | |||
| return this.renderer.domElement | |||
| } | |||
| protected _refreshParams() { | |||
| if (this.isDisabled()) return | |||
| this.renderer.setQuality('medium') | |||
| } | |||
| protected _onResize() { | |||
| if (!this._viewer) return | |||
| this.renderer.setSize(this._viewer.canvas.clientWidth, this._viewer.canvas.clientHeight) | |||
| } | |||
| } | |||
| @@ -0,0 +1,405 @@ | |||
| import { | |||
| AViewerPluginSync, | |||
| IObject3D, | |||
| type IViewerEvent, | |||
| Mesh, | |||
| onChange, | |||
| PerspectiveCamera2, | |||
| ThreeViewer, | |||
| timeout, | |||
| uiButton, | |||
| uiFolderContainer, | |||
| uiToggle, | |||
| uiVector, | |||
| Vector2, | |||
| } from 'threepipe' | |||
| import {FillPass, HiddenChainPass, SVGMesh, SVGRenderer, VisibleChainPass} from './three-svg-renderer' | |||
| import {Vertex} from './three-mesh-halfedge' | |||
| /** | |||
| * SVG Rendering from 3d scenes helper plugin using [three-svg-renderer](https://www.npmjs.com/package/three-svg-renderer) (GPLV3 Licenced) | |||
| */ | |||
| @uiFolderContainer('SVG Renderer') | |||
| export class ThreeSVGRendererPlugin extends AViewerPluginSync<''> { | |||
| static readonly PluginType = 'ThreeSVGRendererPlugin' | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| enabled = true | |||
| /** | |||
| * Automatically render when camera or any object changes. | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| autoRender = true | |||
| /** | |||
| * Use the fill pass to draw polygons.(both fills and strokes) | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| drawPolygons = true | |||
| /** | |||
| * Draw polygon fills. (fill color from material.color) | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| drawPolygonFills = true | |||
| /** | |||
| * Draw polygon strokes. (stroke color from material.color) | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| drawPolygonStrokes = true | |||
| /** | |||
| * Draw image fills. (fill image from rendered canvas image). | |||
| * Make sure canvas is rendered(and render pipeline has a render pass) before calling this. | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| drawImageFills = false | |||
| /** | |||
| * Draw visible contours of meshes. | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| drawVisibleContours = true | |||
| /** | |||
| * Draw hidden contours of meshes. | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| drawHiddenContours = true | |||
| /** | |||
| * Update meshes on every render. If this is false, meshes will only be updated when they change. (tracked using objectUpdate event) | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| alwaysUpdateMeshes = true | |||
| /** | |||
| * Min and Max Crease angle for mesh edges. | |||
| */ | |||
| @uiVector() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| creaseAngle = new Vector2(80, 100) | |||
| /** | |||
| * Automatically create SVG objects for all meshes in the scene. | |||
| * If this is false, you will have to manually create SVG objects for meshes using `makeSVGObject` method. | |||
| */ | |||
| @uiToggle() | |||
| @onChange(ThreeSVGRendererPlugin.prototype.setDirty) | |||
| autoMakeSvgObjects = true | |||
| readonly renderer = new SVGRenderer() | |||
| readonly svgNodeContainer = document.createElement('div') | |||
| protected readonly _fillPass: FillPass | |||
| setDirty(...args: any[]): any { | |||
| if (args[0] === 'enabled') { | |||
| const last = args[2] | |||
| const current = args[1] | |||
| if (last !== current && this._meshes?.size) { | |||
| this._toggleMaterialRendering([...this._meshes.values()], !current) | |||
| } | |||
| if (this.svgNodeContainer) { | |||
| this.svgNodeContainer.style.display = current ? '' : 'none' | |||
| } | |||
| } | |||
| this._viewer?.setDirty() | |||
| } | |||
| /** | |||
| * @param enabled | |||
| * @param autoAddToContainer - automatically add the svg to the viewer container and style it same as the viewer is position is absolute | |||
| */ | |||
| constructor(enabled = true, readonly autoAddToContainer = true) { | |||
| super() | |||
| this._onResize = this._onResize.bind(this) | |||
| this._onMeshDispose = this._onMeshDispose.bind(this) | |||
| this._onMeshUpdate = this._onMeshUpdate.bind(this) | |||
| this.updateMeshes = this.updateMeshes.bind(this) | |||
| this.enabled = enabled | |||
| this.svgNodeContainer.style.position = 'absolute' | |||
| this.svgNodeContainer.style.display = 'none' | |||
| // This pass will draw fills for meshes using the three.js material color | |||
| this._fillPass = new FillPass() | |||
| // This pass will draw visible contours of meshes on top of fills | |||
| // using black color and solid line of width 1 | |||
| const visibleChainPass = new VisibleChainPass({ | |||
| // color: '#000000', | |||
| // width: 1, | |||
| }) | |||
| // This pass will draw hidden contours on top of visible and fills | |||
| // using red color, dash line of width 1 | |||
| const hiddenChainPass = new HiddenChainPass({ | |||
| // color: '#FF0000', | |||
| // width: 1, | |||
| // dasharray: '2,2', | |||
| }) | |||
| this.renderer.addPass(this._fillPass) | |||
| this.renderer.addPass(visibleChainPass) | |||
| this.renderer.addPass(hiddenChainPass) | |||
| Vertex.MAX_LOOP = 10000 // todo; this is for large models that get stuck. | |||
| // this.renderer.addPass(new SingularityPointPass()) | |||
| // this.renderer.addPass(new TexturePass()) | |||
| } | |||
| protected _lastStyles?: string = undefined | |||
| onAdded(viewer: ThreeViewer) { | |||
| super.onAdded(viewer) | |||
| // this.renderer.setSize(viewer.canvas.clientWidth, viewer.canvas.clientHeight) | |||
| // this._refreshParams() // this is done before rendering | |||
| if (this.autoAddToContainer) { | |||
| viewer.container.prepend(this.svgNodeContainer) // behind the canvas so that we get pointer events and see other stuff | |||
| this.svgNodeContainer.style.pointerEvents = 'none' | |||
| const canvasStyles = getComputedStyle(viewer.canvas) | |||
| if (canvasStyles.position === 'absolute') { | |||
| this._lastStyles = this.svgNodeContainer.style.cssText | |||
| // copy styles from canvas to svg so it looks the same. | |||
| this.svgNodeContainer.style.top = canvasStyles.top | |||
| this.svgNodeContainer.style.left = canvasStyles.left | |||
| this.svgNodeContainer.style.width = canvasStyles.width | |||
| this.svgNodeContainer.style.height = canvasStyles.height | |||
| // this.svgNodeContainer.style.zIndex = '999999' // svg should be behind the canvas | |||
| } else { | |||
| this._viewer?.console.warn('ThreeSVGRendererPlugin: canvas position should be absolute for proper rendering') | |||
| } | |||
| viewer.renderManager.addEventListener('resize', this._onResize) | |||
| } | |||
| this.svgNodeContainer.style.display = this.enabled ? '' : 'none' | |||
| viewer.scene.modelRoot.addEventListener('objectUpdate', this.updateMeshes) | |||
| } | |||
| private _meshesNeedsUpdate = true | |||
| updateMeshes() { | |||
| console.log('updateMeshes') | |||
| this._meshesNeedsUpdate = true | |||
| } | |||
| onRemove(viewer: ThreeViewer) { | |||
| super.onRemove(viewer) | |||
| if (this.autoAddToContainer) { | |||
| viewer.container.removeChild(this.svgNodeContainer) | |||
| } | |||
| if (this._lastStyles !== undefined) { | |||
| this.svgNodeContainer.style.cssText = this._lastStyles | |||
| this._lastStyles = undefined | |||
| } | |||
| viewer.renderManager.removeEventListener('resize', this._onResize) | |||
| this._meshes.clear() // ? | |||
| this.svgNodeContainer.style.display = 'none' | |||
| viewer.scene.modelRoot.removeEventListener('objectUpdate', this.updateMeshes) | |||
| } | |||
| protected _onMeshDispose(ev: any) { | |||
| if (!ev.target) return | |||
| const mesh = ev.target as Mesh | |||
| const svgMesh = this._meshes.get(mesh.uuid) | |||
| if (!svgMesh) return | |||
| svgMesh.dispose() | |||
| this._meshes.delete(mesh.uuid) | |||
| mesh.removeEventListener('dispose', this._onMeshDispose) | |||
| mesh.removeEventListener('objectUpdate', this._onMeshUpdate) | |||
| } | |||
| protected _onMeshUpdate(ev: any) { | |||
| if (!ev.target) return | |||
| const mesh = ev.target as Mesh | |||
| const svgMesh = this._meshes.get(mesh.uuid) | |||
| if (!svgMesh) return | |||
| svgMesh.updateObject() | |||
| } | |||
| protected _meshes = new Map<string, SVGMesh>() | |||
| protected _refreshMeshes(root?: IObject3D) { | |||
| if (!this.autoMakeSvgObjects) return [] | |||
| if (!root && this._viewer) root = this._viewer.scene.modelRoot | |||
| if (!root) return [] | |||
| root.traverse(o=>{ | |||
| this.makeSVGObject(o) | |||
| }) | |||
| } | |||
| makeSVGObject(o: IObject3D) { | |||
| if (!(o.isMesh && !this._meshes.has(o.uuid))) return | |||
| const ud = o.userData | |||
| o.userData = {} | |||
| const svgMesh = new SVGMesh(o as any as Mesh) | |||
| o.userData = ud | |||
| this._meshes.set(o.uuid, svgMesh) | |||
| this._toggleMaterialRendering([svgMesh], !this.enabled) | |||
| o.addEventListener('dispose', this._onMeshDispose) | |||
| o.addEventListener('objectUpdate', this._onMeshUpdate) // todo: check if we need to do object update everytime and what actions specifically. | |||
| this._meshesNeedsUpdate = true // because we have a new mesh | |||
| } | |||
| private _rendering = false | |||
| static SVG_RENDER_TIMEOUT = 2000 | |||
| protected _toggleMaterialRendering(meshes: SVGMesh[], enable: boolean) { | |||
| const materials = [] | |||
| for (const mesh of meshes) { | |||
| materials.push(...Array.isArray(mesh.material) ? mesh.material : [mesh.material]) | |||
| } | |||
| // enable rendering of material colors | |||
| for (const mat of materials) { | |||
| if (mat.colorWrite !== undefined) { | |||
| if (enable && !mat.colorWrite) { | |||
| mat.colorWrite = true | |||
| delete mat.userData.forcedLinearDepth | |||
| // mat.userData._colorWriteSet = true | |||
| } else if (!enable /* && mat.userData._colorWriteSet*/) { | |||
| mat.colorWrite = false | |||
| mat.userData.forcedLinearDepth = 1 // for gbuffer plugin | |||
| // delete mat.userData._colorWriteSet | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @uiButton() | |||
| async render() { | |||
| if (!this._viewer || !this._viewer.renderEnabled) return | |||
| if (this.isDisabled()) return | |||
| if (this._rendering) return | |||
| this._rendering = true | |||
| this._refreshParams() | |||
| this._refreshMeshes() | |||
| const meshes = [...this._meshes.values()] | |||
| // todo: make sure all meshes are in the scene | |||
| // todo only use meshes that should be rendered. | |||
| if (!meshes.length) { | |||
| this._rendering = false | |||
| return '' | |||
| } | |||
| const camera = this._viewer.scene.mainCamera as PerspectiveCamera2 | |||
| try { | |||
| if (this.drawImageFills) { | |||
| this._toggleMaterialRendering(meshes, true) | |||
| this._viewer.setDirty() | |||
| this._viewer.canvas.style.opacity = '0' | |||
| await this._viewer.doOnce('preFrame') // because we are already in postRender or postFrame. | |||
| await this._viewer.doOnce('postFrame') // todo wait for progressive also maybe | |||
| await this._viewer.doOnce('postFrame') // once more | |||
| this._fillPass.options.fillImage = this._viewer.canvas.toDataURL('image/png') | |||
| // disable rendering of material colors | |||
| this._toggleMaterialRendering(meshes, false) | |||
| this._viewer.setDirty() | |||
| await this._viewer.doOnce('preFrame') // already in postFrame | |||
| await this._viewer.doOnce('postFrame') | |||
| this._viewer.canvas.style.opacity = '1' | |||
| } | |||
| // this._fillPass.options.fillImage = this._viewer.canvas.toDataURL('image/png') | |||
| // this._viewer.renderEnabled = false | |||
| this.renderer.viewmap.skipActions = false | |||
| const svgPromise = this.renderer.generateSVG(meshes, camera, { | |||
| w: this._viewer.canvas.width, | |||
| h: this._viewer.canvas.height, | |||
| }) | |||
| let svgResolved = false | |||
| timeout(ThreeSVGRendererPlugin.SVG_RENDER_TIMEOUT).then(()=>{ // todo: make support in libs to cancel the promise. this will just wait for an action to complete. | |||
| if (!svgResolved) { | |||
| console.warn('timeout') | |||
| this.renderer.viewmap.skipActions = true | |||
| } | |||
| }) | |||
| const svg = await svgPromise | |||
| svgResolved = true | |||
| this.renderer.viewmap.skipActions = false | |||
| // this._viewer.renderEnabled = true | |||
| this.svgNodeContainer.innerHTML = svg.node.outerHTML | |||
| this._rendering = false | |||
| return svg.svg() | |||
| } catch (e) { | |||
| console.error(e) | |||
| } | |||
| this._rendering = false | |||
| return '' | |||
| } | |||
| @uiButton() | |||
| download() { | |||
| const svg = this.svgNodeContainer.innerHTML | |||
| const blob = new Blob([svg], {type: 'image/svg+xml'}) | |||
| this._viewer?.exportBlob(blob, 'scene.svg') | |||
| } | |||
| protected _viewerListeners = { | |||
| postRender: (_: IViewerEvent)=>{ | |||
| if (this.autoRender) this.render() | |||
| }, | |||
| } | |||
| get svgNode() { | |||
| if (!this.svgNodeContainer.children.length) return undefined | |||
| if (this.svgNodeContainer.children.length > 1) { | |||
| this._viewer?.console.warn('ThreeSVGRenderer: Multiple svg nodes in container, should not be possible') | |||
| } | |||
| return this.svgNodeContainer.children[0] | |||
| } | |||
| protected _refreshParams() { | |||
| if (this.isDisabled()) return | |||
| if (this._meshesNeedsUpdate) { | |||
| this.renderer.viewmap.options.updateMeshes = true | |||
| this._meshesNeedsUpdate = false | |||
| } else this.renderer.viewmap.options.updateMeshes = this.alwaysUpdateMeshes | |||
| this.renderer.viewmap.options.creaseAngle.min = this.creaseAngle.x | |||
| this.renderer.viewmap.options.creaseAngle.max = this.creaseAngle.y | |||
| const passes = this.renderer.drawHandler.passes | |||
| const fillPass = passes.find(p=>p instanceof FillPass) as FillPass | |||
| const visibleContourPass = passes.find(p=>p instanceof VisibleChainPass) as VisibleChainPass | |||
| const hiddenContourPass = passes.find(p=>p instanceof HiddenChainPass) as HiddenChainPass | |||
| if (fillPass) { | |||
| fillPass.enabled = this.drawPolygons && (this.drawPolygonFills || this.drawPolygonStrokes || this.drawImageFills) | |||
| fillPass.drawFills = this.drawPolygonFills | |||
| fillPass.drawStrokes = this.drawPolygonStrokes | |||
| fillPass.drawImageFills = this.drawImageFills | |||
| } | |||
| if (visibleContourPass) { | |||
| visibleContourPass.enabled = this.drawVisibleContours | |||
| } | |||
| if (hiddenContourPass) { | |||
| hiddenContourPass.enabled = this.drawHiddenContours | |||
| } | |||
| } | |||
| protected _onResize() { | |||
| if (!this._viewer) return | |||
| // this.renderer.setSize(this._viewer.canvas.clientWidth, this._viewer.canvas.clientHeight) | |||
| } | |||
| } | |||
| // adding here since they dont show up in dependencies.txt somehow | |||
| /** | |||
| * @license | |||
| * three-svg-renderer | |||
| * | |||
| * GNU GENERAL PUBLIC LICENSE | |||
| * Version 3, 29 June 2007 | |||
| * | |||
| * Copyright (c) 2022 Axel Antoine | |||
| */ | |||
| /** | |||
| * @license | |||
| * three-mesh-halfedge | |||
| * | |||
| * MIT License | |||
| * | |||
| * Copyright (c) 2022 Axel Antoine | |||
| */ | |||
| @@ -0,0 +1,23 @@ | |||
| import {SVGRenderer} from 'three/examples/jsm/renderers/SVGRenderer.js' | |||
| import {uiDropdown, uiFolderContainer, uiNumber, uiToggle} from 'uiconfig.js' | |||
| import {onChange2} from 'ts-browser-helpers' | |||
| @uiFolderContainer('Basic SVG Renderer') | |||
| export class BasicSVGRenderer extends SVGRenderer { | |||
| @uiToggle() | |||
| autoClear: boolean | |||
| @uiToggle() | |||
| sortObjects: boolean | |||
| @uiToggle() | |||
| sortElements: boolean | |||
| @uiNumber() | |||
| overdraw: number | |||
| @uiDropdown(undefined, ['low', 'high']) | |||
| @onChange2(BasicSVGRenderer.prototype._refresh) | |||
| quality: 'low' | 'high' = 'high' | |||
| private _refresh() { | |||
| this.setQuality(this.quality) | |||
| } | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| declare module '*.txt' { | |||
| const content: string | |||
| export default content | |||
| } | |||
| declare module '*.glsl' { | |||
| const content: string | |||
| export default content | |||
| } | |||
| declare module '*.vert' { | |||
| const content: string | |||
| export default content | |||
| } | |||
| declare module '*.frag' { | |||
| const content: string | |||
| export default content | |||
| } | |||
| declare module '*.module.scss' { | |||
| const content: any | |||
| export default content | |||
| export const stylesheet: string | |||
| } | |||
| declare module '*.module.css' { | |||
| const content: any | |||
| export default content | |||
| export const stylesheet: string | |||
| } | |||
| declare module '*.css' { | |||
| const content: string | |||
| export default content | |||
| } | |||
| declare module '*.css?inline' { // for vite | |||
| const content: string | |||
| export default content | |||
| } | |||
| // export {} | |||
| // hack for typedoc | |||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||
| // declare type OffscreenCanvas = HTMLCanvasElement | |||
| @@ -0,0 +1,5 @@ | |||
| export {BasicSVGRendererPlugin} from './BasicSVGRendererPlugin' | |||
| export {ThreeSVGRendererPlugin} from './ThreeSVGRendererPlugin' | |||
| export * from './basic/BasicSVGRenderer' | |||
| export {FillPass, SVGMesh, SVGRenderer, DrawPass, SVGRenderInfo, SVGDrawInfo, ViewmapBuildInfo, Viewmap, SVGDrawHandler} from './three-svg-renderer' | |||
| export type {SVGMeshOptions, SVGDrawOptions, ViewmapOptions, FillPassOptions, SVGTexture} from './three-svg-renderer' | |||
| @@ -0,0 +1,21 @@ | |||
| MIT License | |||
| Copyright (c) 2022 Axel Antoine | |||
| Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| of this software and associated documentation files (the "Software"), to deal | |||
| in the Software without restriction, including without limitation the rights | |||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| copies of the Software, and to permit persons to whom the Software is | |||
| furnished to do so, subject to the following conditions: | |||
| The above copyright notice and this permission notice shall be included in all | |||
| copies or substantial portions of the Software. | |||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| SOFTWARE. | |||
| @@ -0,0 +1,35 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 24/05/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 | |||
| declare global { | |||
| interface Array<T> { | |||
| clear(): Array<T>; | |||
| remove(t: T): boolean; | |||
| } | |||
| } | |||
| Array.prototype.clear = function() { | |||
| this.splice(0, this.length); | |||
| return this; | |||
| } | |||
| Array.prototype.remove = function<T>(t: T) { | |||
| const idx = this.indexOf(t); | |||
| if (idx === -1) { | |||
| return false; | |||
| } | |||
| this.splice(idx, 1); | |||
| return true; | |||
| } | |||
| export {} | |||
| @@ -0,0 +1,122 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 17/03/2021 | |||
| // 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 { Vector3, Triangle } from 'three'; | |||
| import { Vertex } from './Vertex'; | |||
| import { Halfedge } from './Halfedge'; | |||
| const _viewVector = new Vector3(); | |||
| const _normal = new Vector3(); | |||
| const _triangle = new Triangle(); | |||
| const _vec = new Vector3(); | |||
| export class Face { | |||
| halfedge: Halfedge; | |||
| constructor(halfEdge: Halfedge) { | |||
| this.halfedge = halfEdge; | |||
| } | |||
| getNormal(target: Vector3) { | |||
| _triangle.set( | |||
| this.halfedge.prev.vertex.position, | |||
| this.halfedge.vertex.position, | |||
| this.halfedge.next.vertex.position | |||
| ); | |||
| _triangle.getNormal(target); | |||
| } | |||
| getMidpoint(target: Vector3) { | |||
| _triangle.set( | |||
| this.halfedge.prev.vertex.position, | |||
| this.halfedge.vertex.position, | |||
| this.halfedge.next.vertex.position | |||
| ); | |||
| _triangle.getNormal(target); | |||
| } | |||
| /** | |||
| * Returns wether the face facing the given position | |||
| * | |||
| * @param position The position | |||
| * @return `true` if face is front facing, `false` otherwise. | |||
| */ | |||
| isFront(position: Vector3) { | |||
| this.getNormal(_normal); | |||
| return _viewVector | |||
| .subVectors(position, this.halfedge.vertex.position) | |||
| .normalize() | |||
| .dot(_normal) >= 0; | |||
| } | |||
| /** | |||
| * Returns the face halfedge containing the given position. | |||
| * @param position Target position | |||
| * @param tolerance Tolerance | |||
| * @returns `HalfEdge` if found, `null` otherwise | |||
| */ | |||
| halfedgeFromPosition(position: Vector3, tolerance = 1e-10): Halfedge | null { | |||
| for (const he of this.halfedge.nextLoop()) { | |||
| if (he.containsPoint(position, tolerance)) { | |||
| return he; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * Returns the face vertex that matches the given position within the tolerance | |||
| * @param position | |||
| * @param tolerance | |||
| * @returns | |||
| */ | |||
| vertexFromPosition(position: Vector3, tolerance = 1e-10): Vertex | null { | |||
| for (const he of this.halfedge.nextLoop()) { | |||
| // Check if position is close enough to the vertex position within the | |||
| // provided tolerance | |||
| _vec.subVectors(he.vertex.position, position); | |||
| if (_vec.length() < tolerance) { | |||
| return he.vertex; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * Returns the face halfedge starting from the given vertex | |||
| * @param vertex | |||
| * @returns | |||
| */ | |||
| halfedgeFromVertex(vertex: Vertex) { | |||
| for (const he of this.halfedge.nextLoop()) { | |||
| if (he.vertex === vertex) { | |||
| return he; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| hasVertex(vertex: Vertex) { | |||
| for (const he of this.halfedge.nextLoop()) { | |||
| if (he.vertex === vertex) { | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,109 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 23/02/2021 | |||
| // 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 { Line3, Vector3 } from 'three'; | |||
| import { Face } from './Face'; | |||
| import { Vertex } from './Vertex'; | |||
| import { frontSide } from '../utils/geometry'; | |||
| const _u = new Vector3(); | |||
| const _v = new Vector3(); | |||
| const _line = new Line3(); | |||
| export class Halfedge { | |||
| vertex: Vertex; | |||
| // Set during the stucture build phase | |||
| face: Face | null = null; | |||
| declare twin: Halfedge; | |||
| declare prev: Halfedge; | |||
| declare next: Halfedge; | |||
| constructor(vertex: Vertex) { | |||
| this.vertex = vertex; | |||
| } | |||
| get id() { | |||
| return this.vertex.id + '-'+ this.twin.vertex.id; | |||
| } | |||
| containsPoint(point: Vector3, tolerance = 1e-10): boolean { | |||
| _u.subVectors(this.vertex.position, point) | |||
| _v.subVectors(this.next.vertex.position, point) | |||
| _line.set(this.vertex.position, this.next.vertex.position); | |||
| _line.closestPointToPoint(point, true, _u); | |||
| return _u.distanceTo(point) < tolerance; | |||
| } | |||
| /** | |||
| * Indicates whether the halfedge is free (i.e. no connected face) | |||
| * | |||
| * @type {boolean} | |||
| */ | |||
| isFree() { | |||
| return this.face === null; | |||
| } | |||
| /** | |||
| * Indicated wetcher the halfedge is a boundary (i.e. no connected face but | |||
| * twin has a face) | |||
| */ | |||
| isBoundary() { | |||
| return this.face === null && this.twin.face !== null; | |||
| } | |||
| /** | |||
| * Returns true if the halfedge is concave, false if convexe. | |||
| * IMPORTANT: Returns false if halfedge has no twin. | |||
| * | |||
| * @type {boolean} | |||
| */ | |||
| get isConcave() { | |||
| if (this.twin) { | |||
| return frontSide( | |||
| this.vertex.position, | |||
| this.next.vertex.position, | |||
| this.prev.vertex.position, | |||
| this.twin.prev.vertex.position) > 0; | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * Returns a generator looping over all the next halfedges | |||
| */ | |||
| *nextLoop() { | |||
| const start: Halfedge = this; | |||
| let curr: Halfedge = start; | |||
| do { | |||
| yield curr; | |||
| curr = curr.next; | |||
| } while(curr !== start); | |||
| return null; | |||
| } | |||
| /** | |||
| * Returns a generator looping over all the previous halfedges | |||
| */ | |||
| *prevLoop() { | |||
| const start: Halfedge = this; | |||
| let curr: Halfedge = start; | |||
| do { | |||
| yield curr; | |||
| curr = curr.next; | |||
| } while(curr !== start); | |||
| return null; | |||
| } | |||
| } | |||
| @@ -0,0 +1,192 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 23/02/2021 | |||
| // 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 | |||
| // LICENCE: Licence.md | |||
| import {BufferGeometry, Vector3} from 'three'; | |||
| import {Face} from './Face'; | |||
| import {Vertex} from './Vertex'; | |||
| import {Halfedge} from './Halfedge'; | |||
| import {addEdge} from '../operations/addEdge'; | |||
| import {addFace} from '../operations/addFace'; | |||
| import {addVertex} from '../operations/addVertex'; | |||
| import {removeVertex} from '../operations/removeVertex'; | |||
| import {removeEdge} from '../operations/removeEdge'; | |||
| import {removeFace} from '../operations/removeFace'; | |||
| import {cutFace} from '../operations/cutFace'; | |||
| import {splitEdge} from '../operations/splitEdge'; | |||
| import {setFromGeometry} from '../operations/setFromGeometry'; | |||
| /** | |||
| * Class representing an Halfedge Data Structure | |||
| */ | |||
| export class HalfedgeDS { | |||
| /** @readonly Faces */ | |||
| readonly faces = new Array<Face>(); | |||
| /** @readonly Vertices */ | |||
| readonly vertices = new Array<Vertex>(); | |||
| /** @readonly Halfedges */ | |||
| readonly halfedges = new Array<Halfedge>(); | |||
| /** | |||
| * Sets the halfedge structure from a BufferGeometry. | |||
| * @param geometry BufferGeometry to read | |||
| * @param tolerance Tolerance distance from which positions are considered equal | |||
| */ | |||
| setFromGeometry(geometry: BufferGeometry, tolerance = 1e-10) { | |||
| return setFromGeometry(this, geometry, tolerance); | |||
| } | |||
| /** | |||
| * Returns an array of all the halfedge loops in the structure. | |||
| * | |||
| * *Note: Actually returns an array of halfedges from which loop generator | |||
| * can be called* | |||
| * | |||
| * @returns | |||
| */ | |||
| loops() { | |||
| const loops = new Array<Halfedge>(); | |||
| const handled = new Set<Halfedge>(); | |||
| for (const halfedge of this.halfedges) { | |||
| if (!handled.has(halfedge)) { | |||
| for (const he of halfedge.nextLoop()) { | |||
| handled.add(he); | |||
| } | |||
| loops.push(halfedge); | |||
| } | |||
| } | |||
| return loops; | |||
| } | |||
| /** | |||
| * Clear the structure data | |||
| */ | |||
| clear() { | |||
| this.faces.clear(); | |||
| this.vertices.clear(); | |||
| this.halfedges.clear(); | |||
| } | |||
| /** | |||
| * Adds a new vertex to the structure at the given position and returns it. | |||
| * If checkDuplicates is true, returns any existing vertex that matches the | |||
| * given position. | |||
| * | |||
| * @param position New vertex position | |||
| * @param checkDuplicates Enable/disable existing vertex matching, default false | |||
| * @param tolerance Tolerance used for vertices position comparison | |||
| * @returns | |||
| */ | |||
| addVertex( | |||
| position: Vector3, | |||
| checkDuplicates = false, | |||
| tolerance = 1e-10) { | |||
| return addVertex(this, position, checkDuplicates, tolerance); | |||
| } | |||
| /** | |||
| * Adds an edge (i.e. a pair of halfedges) between the given vertices. | |||
| * Requires vertices to be free, i.e., there is at least one free halfedge | |||
| * (i.e. without face) in their neighborhood. | |||
| * | |||
| * @param v1 First vertex to link | |||
| * @param v2 Second vertex to link | |||
| * @param allowParallels Allows multiple pair of halfedges between vertices, default false | |||
| * @returns Existing or new halfedge | |||
| */ | |||
| addEdge(v1: Vertex, v2: Vertex, allowParallels = false) { | |||
| return addEdge(this, v1, v2, allowParallels) | |||
| } | |||
| /** | |||
| * Adds a face to an existing halfedge loop | |||
| * @param halfedge | |||
| * @returns | |||
| */ | |||
| addFace(halfedges: Halfedge[]) { | |||
| return addFace(this, halfedges); | |||
| } | |||
| /** | |||
| * Removes a vertex from the structure | |||
| * @param vertex Vertex to remove | |||
| * @param mergeFaces If true, merges connected faces if any, otherwise removes them. Default true | |||
| */ | |||
| removeVertex(vertex: Vertex, mergeFaces = true) { | |||
| return removeVertex(this, vertex, mergeFaces); | |||
| } | |||
| /** | |||
| * Removes an edge from the structrure | |||
| * @param halfedge Halfedge to remove | |||
| * @param mergeFaces If true, merges connected faces if any, otherwise removes them. Default true | |||
| */ | |||
| removeEdge(halfedge: Halfedge, mergeFaces = true) { | |||
| return removeEdge(this, halfedge, mergeFaces); | |||
| } | |||
| /** | |||
| * Removes a face from the structure. | |||
| * @param face Face to remove | |||
| */ | |||
| removeFace(face: Face) { | |||
| return removeFace(this, face); | |||
| } | |||
| /**ts | |||
| * Cuts the `face` between the vertices `v1` and `v2`. | |||
| * v1 and v2 must either be vertices of the face or isolated vertices. | |||
| * | |||
| * To test if a new face is created, simply do | |||
| * ``` | |||
| * const halfedge = struct.cutFace(face, v1, v2, true); | |||
| * if (halfedge.face !== halfedge.twin.face) { | |||
| * // Halfedge are on different faces / loops | |||
| * const existingFace = halfedge.face; | |||
| * const newFace = halfedge.twin.face; | |||
| * } | |||
| * ``` | |||
| * | |||
| * | |||
| * @param face Face to cut | |||
| * @param v1 1st vertex | |||
| * @param v2 2nd vertex | |||
| * @param createNewFace wether to create a new face or not when cutting | |||
| * @returns the cutting halfedge | |||
| */ | |||
| cutFace(face: Face, v1: Vertex, v2: Vertex, createNewFace = true) { | |||
| return cutFace(this, face, v1, v2, createNewFace); | |||
| } | |||
| /** | |||
| * Splits the halfedge at position and returns the new vertex | |||
| * @param halfEdge The HalfEdge to be splitted | |||
| * @param position Position of the split vertex | |||
| * @returns the new created vertex | |||
| */ | |||
| splitEdge(halfedge: Halfedge, position: Vector3, tolerance = 1e-10) { | |||
| return splitEdge(this, halfedge, position, tolerance); | |||
| } | |||
| } | |||
| @@ -0,0 +1,185 @@ | |||
| // /* | |||
| // * Author: Axel Antoine | |||
| // * mail: ax.antoine@gmail.com | |||
| // * website: http://axantoine.com | |||
| // * Created on Mon Nov 14 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 { Vector3 } from "three"; | |||
| // import { addEdge, } from "../operations/addEdge"; | |||
| // import { addVertex } from "../operations/addVertex"; | |||
| // import { removeEdge } from "../operations/removeEdge"; | |||
| // import { HalfedgeDS } from "./HalfedgeDS"; | |||
| // import { Vertex } from "./Vertex"; | |||
| // import { generatorToArray } from "../utils/testutils"; | |||
| // import { addFace } from "../operations/addFace"; | |||
| // | |||
| // const vec_ = new Vector3(); | |||
| // let v1: Vertex, v2: Vertex, v3: Vertex, v4: Vertex; | |||
| // const struct = new HalfedgeDS(); | |||
| // | |||
| // beforeEach(() => { | |||
| // struct.clear(); | |||
| // v1 = addVertex(struct, vec_.set(1,1,1)); | |||
| // v2 = addVertex(struct, vec_.set(2,2,2)); | |||
| // v3 = addVertex(struct, vec_.set(3,3,3)); | |||
| // v4 = addVertex(struct, vec_.set(4,4,4)); | |||
| // }); | |||
| // | |||
| // test('Vertex is isolated', () => { | |||
| // | |||
| // expect(v1.isIsolated()).toBe(true); | |||
| // expect(v2.isIsolated()).toBe(true); | |||
| // | |||
| // const half = addEdge(struct, v1, v2); | |||
| // | |||
| // expect(v1.isIsolated()).toBe(false); | |||
| // expect(v2.isIsolated()).toBe(false); | |||
| // | |||
| // removeEdge(struct, half); | |||
| // | |||
| // expect(v1.isIsolated()).toBe(true); | |||
| // expect(v2.isIsolated()).toBe(true); | |||
| // | |||
| // }); | |||
| // | |||
| // test('Vertex is connected to another vertex', () => { | |||
| // | |||
| // expect(v1.isConnectedToVertex(v2)).toBe(false); | |||
| // expect(v2.isConnectedToVertex(v1)).toBe(false); | |||
| // | |||
| // const half = addEdge(struct, v1, v2); | |||
| // | |||
| // expect(v1.isConnectedToVertex(v2)).toBe(true); | |||
| // expect(v2.isConnectedToVertex(v1)).toBe(true); | |||
| // | |||
| // removeEdge(struct, half); | |||
| // | |||
| // expect(v1.isConnectedToVertex(v2)).toBe(false); | |||
| // expect(v2.isConnectedToVertex(v1)).toBe(false); | |||
| // | |||
| // }); | |||
| // | |||
| // test('Vertex loop CW', () => { | |||
| // | |||
| // let array = generatorToArray(v1.loopCW()); | |||
| // expect(array).toHaveLength(0); | |||
| // | |||
| // const v1v2 = addEdge(struct, v1, v2); | |||
| // const v1v3 = addEdge(struct, v1, v3); | |||
| // const v1v4 = addEdge(struct, v1, v4); | |||
| // | |||
| // array = generatorToArray(v1.loopCW()); | |||
| // expect(array).toHaveLength(3); | |||
| // expect(array).toContain(v1v2); | |||
| // expect(array).toContain(v1v3); | |||
| // expect(array).toContain(v1v4); | |||
| // | |||
| // removeEdge(struct, v1v2); | |||
| // | |||
| // array = generatorToArray(v1.loopCW()); | |||
| // expect(array).toHaveLength(2); | |||
| // expect(array).toContain(v1v3); | |||
| // expect(array).toContain(v1v4); | |||
| // | |||
| // }); | |||
| // | |||
| // test('Vertex loop CCW', () => { | |||
| // | |||
| // let array = generatorToArray(v1.loopCCW()); | |||
| // expect(array).toHaveLength(0); | |||
| // | |||
| // const v1v2 = addEdge(struct, v1, v2); | |||
| // const v1v3 = addEdge(struct, v1, v3); | |||
| // const v1v4 = addEdge(struct, v1, v4); | |||
| // | |||
| // array = generatorToArray(v1.loopCCW()); | |||
| // expect(array).toHaveLength(3); | |||
| // expect(array).toContain(v1v2); | |||
| // expect(array).toContain(v1v3); | |||
| // expect(array).toContain(v1v4); | |||
| // | |||
| // removeEdge(struct, v1v2); | |||
| // | |||
| // array = generatorToArray(v1.loopCCW()); | |||
| // expect(array).toHaveLength(2); | |||
| // expect(array).toContain(v1v3); | |||
| // expect(array).toContain(v1v4); | |||
| // | |||
| // }); | |||
| // | |||
| // test('Boundary in halfedges loop', () => { | |||
| // | |||
| // let array = generatorToArray(v1.freeHalfedgesInLoop()); | |||
| // expect(array).toHaveLength(0); | |||
| // | |||
| // const v1v2 = addEdge(struct, v1, v2); | |||
| // const v1v3 = addEdge(struct, v1, v3); | |||
| // const v1v4 = addEdge(struct, v1, v4); | |||
| // | |||
| // array = generatorToArray(v1.freeHalfedgesInLoop()); | |||
| // expect(array).toHaveLength(3); | |||
| // expect(array).toContain(v1v2.twin); | |||
| // expect(array).toContain(v1v3.twin); | |||
| // expect(array).toContain(v1v4.twin); | |||
| // | |||
| // // Close 1-2-3 triangles | |||
| // const v2v3 = addEdge(struct, v2, v3); | |||
| // addFace(struct, [v1v2, v2v3, v1v3.twin]); | |||
| // array = generatorToArray(v1.freeHalfedgesInLoop()); | |||
| // expect(array).toHaveLength(2); | |||
| // expect(array).toContain(v1v2.twin); | |||
| // expect(array).toContain(v1v4.twin); | |||
| // | |||
| // // Close 1-3-4 and 1-4-2 triangles | |||
| // const v3v4 = addEdge(struct, v3, v4); | |||
| // addFace(struct, [v3v4, v1v4.twin, v1v3]); | |||
| // | |||
| // const v4v2 = addEdge(struct, v4, v2); | |||
| // addFace(struct, [v4v2, v1v2.twin, v1v4]); | |||
| // | |||
| // array = generatorToArray(v1.freeHalfedgesInLoop()); | |||
| // expect(array).toHaveLength(0); | |||
| // }); | |||
| // | |||
| // test('Boundary out halfedges loop', () => { | |||
| // | |||
| // let array = generatorToArray(v1.freeHalfedgesOutLoop()); | |||
| // expect(array).toHaveLength(0); | |||
| // | |||
| // const v1v2 = addEdge(struct, v1, v2); | |||
| // const v1v3 = addEdge(struct, v1, v3); | |||
| // const v1v4 = addEdge(struct, v1, v4); | |||
| // | |||
| // array = generatorToArray(v1.freeHalfedgesOutLoop()); | |||
| // expect(array).toHaveLength(3); | |||
| // expect(array).toContain(v1v2); | |||
| // expect(array).toContain(v1v3); | |||
| // expect(array).toContain(v1v4); | |||
| // | |||
| // // Close 1-2-3 triangles | |||
| // const v2v3 = addEdge(struct, v2, v3); | |||
| // addFace(struct, [v1v2, v2v3, v1v3.twin]); | |||
| // array = generatorToArray(v1.freeHalfedgesOutLoop()); | |||
| // expect(array).toHaveLength(2); | |||
| // expect(array).toContain(v1v3); | |||
| // expect(array).toContain(v1v4); | |||
| // | |||
| // // Close 1-3-4 and 1-4-2 triangles | |||
| // const v3v4 = addEdge(struct, v3, v4); | |||
| // addFace(struct, [v3v4, v1v4.twin, v1v3]); | |||
| // | |||
| // const v4v2 = addEdge(struct, v4, v2); | |||
| // addFace(struct, [v4v2, v1v2.twin, v1v4]); | |||
| // | |||
| // array = generatorToArray(v1.freeHalfedgesOutLoop()); | |||
| // expect(array).toHaveLength(0); | |||
| // }); | |||
| // | |||
| @@ -0,0 +1,192 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 17/03/2021 | |||
| // 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 {Vector3} from 'three'; | |||
| import type {Face} from './Face'; | |||
| import {Halfedge} from './Halfedge'; | |||
| const _u = new Vector3(); | |||
| let _idCount = 0; | |||
| export class Vertex { | |||
| /** Vertex position */ | |||
| readonly position: Vector3 = new Vector3(); | |||
| /** Reference to one halfedge starting from the vertex */ | |||
| halfedge: Halfedge | null = null; | |||
| id: number; | |||
| constructor() { | |||
| this.id = _idCount; | |||
| _idCount++; | |||
| } | |||
| /** | |||
| * Returns a generator of free halfedges starting from this vertex. | |||
| * @param start The halfedge to start, default is vertex halfedge | |||
| */ | |||
| *freeHalfedgesOutLoop(start = this.halfedge) { | |||
| for (const halfedge of this.loopCW(start)) { | |||
| if (halfedge.isFree()) { | |||
| yield halfedge; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * Returns a generator of free halfedges arriving to this vertex. | |||
| * @param start The halfedge to start, default is vertex halfedge | |||
| */ | |||
| *freeHalfedgesInLoop(start = this.halfedge) { | |||
| for (const halfedge of this.loopCW(start)) { | |||
| if (halfedge.twin.isFree()) { | |||
| yield halfedge.twin; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * Returns a generator of boundary halfedges starting from this vertex. | |||
| * @param start The halfedge to start, default is vertex halfedge | |||
| */ | |||
| *boundaryHalfedgesOutLoop(start = this.halfedge) { | |||
| for (const halfedge of this.loopCW(start)) { | |||
| if (halfedge.isBoundary()) { | |||
| yield halfedge; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * Returns a generator of boundary halfedges arriving to this vertex. | |||
| * @param start The halfedge to start, default is vertex halfedge | |||
| */ | |||
| *boundaryHalfedgesInLoop(start = this.halfedge) { | |||
| for (const halfedge of this.loopCW(start)) { | |||
| if (halfedge.twin.isBoundary()) { | |||
| yield halfedge.twin; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * Returns whether the vertex is free, i.e. on of its ongoing halfedge has no | |||
| * face. | |||
| * | |||
| * @ref https://kaba.hilvi.org/homepage/blog/halfedge/halfedge.htm | |||
| * | |||
| * @returns `true` if free, `false` otherwise | |||
| */ | |||
| isFree() { | |||
| if (this.isIsolated()) { | |||
| return true; | |||
| } | |||
| for (const halfEdge of this.loopCW()) { | |||
| if (halfEdge.isFree()) { | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| isIsolated() { | |||
| return this.halfedge === null; | |||
| } | |||
| commonFacesWithVertex(other: Vertex) { | |||
| const faces = new Array<Face>(); | |||
| for (const halfedge of this.loopCW()) { | |||
| if (halfedge.face && halfedge.face.hasVertex(other)) { | |||
| faces.push(halfedge.face); | |||
| } | |||
| } | |||
| return faces; | |||
| } | |||
| /** | |||
| * Checkes whether the vertex matches the given position | |||
| * | |||
| * @param {Vector3} position The position | |||
| * @param {number} [tolerance=1e-10] The tolerance | |||
| * @return {boolean} | |||
| */ | |||
| matchesPosition(position: Vector3, tolerance = 1e-10): boolean { | |||
| _u.subVectors(position, this.position); | |||
| return _u.length() < tolerance; | |||
| } | |||
| /** | |||
| * Returns the halfedge going from *this* vertex to *other* vertex if any. | |||
| * @param other The other vertex | |||
| * @returns `HalfEdge` if found, `null` otherwise. | |||
| */ | |||
| getHalfedgeToVertex(other: Vertex): Halfedge | null { | |||
| for (const halfEdge of this.loopCW()) { | |||
| if (halfEdge.twin.vertex === other) { | |||
| return halfEdge; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| isConnectedToVertex(other: Vertex) { | |||
| return this.getHalfedgeToVertex(other) !== null; | |||
| } | |||
| static MAX_LOOP = Infinity; | |||
| /** | |||
| * Returns a generator of halfedges starting from this vertex in CW order. | |||
| * @param start The halfedge to start looping, default is vertex halfedge | |||
| */ | |||
| *loopCW(start = this.halfedge, maxLoop?: number) { | |||
| let i = 0 | |||
| if (start && start.vertex === this) { | |||
| let curr: Halfedge = start; | |||
| do { | |||
| yield curr; | |||
| curr = curr.twin.next; | |||
| i++; | |||
| if(i>(maxLoop||Vertex.MAX_LOOP)){ | |||
| break; | |||
| } | |||
| } while(curr != start); | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * Returns a generator of halfedges starting from this vertex in CCW order. | |||
| * @param start The halfedge to start, default is vertex halfedge | |||
| */ | |||
| *loopCCW(start = this.halfedge, maxLoop?: number) { | |||
| let i = 0 | |||
| if (start && start.vertex === this) { | |||
| let curr: Halfedge = start; | |||
| do { | |||
| yield curr; | |||
| curr = curr.prev.twin; | |||
| i++; | |||
| if(i>(maxLoop||Vertex.MAX_LOOP)){ | |||
| break; | |||
| } | |||
| } while(curr != start); | |||
| } | |||
| return null; | |||
| } | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| export {HalfedgeDS} from './core/HalfedgeDS'; | |||
| export {Face} from './core/Face'; | |||
| export {Vertex} from './core/Vertex'; | |||
| export {Halfedge} from './core/Halfedge'; | |||
| import './augments'; | |||
| @@ -0,0 +1,119 @@ | |||
| // /* | |||
| // * Author: Axel Antoine | |||
| // * mail: ax.antoine@gmail.com | |||
| // * website: http://axantoine.com | |||
| // * Created on Wed Nov 09 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 { Vector3 } from "three"; | |||
| // import { Halfedge } from "../core/Halfedge"; | |||
| // import { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| // | |||
| // const position = new Vector3(); | |||
| // const struct = new HalfedgeDS(); | |||
| // | |||
| // /* | |||
| // * v2 | |||
| // * | \ | |||
| // * | \ | |||
| // * | \ | |||
| // * v0 ----- v1 | |||
| // */ | |||
| // | |||
| // const v0 = struct.addVertex(position.set(0,0,0)); | |||
| // const v1 = struct.addVertex(position.set(2,0,0)); | |||
| // const v2 = struct.addVertex(position.set(0,2,0)); | |||
| // | |||
| // let v0v1: Halfedge, v1v2: Halfedge, v2v0: Halfedge; | |||
| // let v1v0: Halfedge, v2v1: Halfedge, v0v2: Halfedge; | |||
| // | |||
| // test("Link isolated vertices", () => { | |||
| // v0v1 = struct.addEdge(v0, v1); | |||
| // v1v0 = v0v1.twin; | |||
| // expect(v0v1.next).toBeHalfedge(v1v0); | |||
| // expect(v0v1.prev).toBeHalfedge(v1v0); | |||
| // expect(v1v0.next).toBeHalfedge(v0v1); | |||
| // expect(v1v0.prev).toBeHalfedge(v0v1); | |||
| // expect(v0.halfedge).toBeHalfedge(v0v1); | |||
| // expect(v1.halfedge).toBeHalfedge(v1v0); | |||
| // }); | |||
| // | |||
| // test("Link to another edge", () => { | |||
| // v1v2 = struct.addEdge(v1, v2); | |||
| // v2v1 = v1v2.twin; | |||
| // expect(v1v2.next).toBeHalfedge(v2v1); | |||
| // expect(v1v2.prev).toBeHalfedge(v0v1); | |||
| // expect(v0v1.next).toBeHalfedge(v1v2); | |||
| // expect(v2v1.next).toBeHalfedge(v1v0); | |||
| // expect(v2v1.prev).toBeHalfedge(v1v2); | |||
| // expect(v1v0.prev).toBeHalfedge(v2v1); | |||
| // }); | |||
| // | |||
| // test("Closing a loop", () => { | |||
| // v2v0 = struct.addEdge(v2, v0); | |||
| // v0v2 = v2v0.twin; | |||
| // expect(v2v0.next).toBeHalfedge(v0v1); | |||
| // expect(v2v0.prev).toBeHalfedge(v1v2); | |||
| // expect(v0v1.prev).toBeHalfedge(v2v0); | |||
| // expect(v1v2.next).toBeHalfedge(v2v0); | |||
| // | |||
| // expect(v0v2.next).toBeHalfedge(v2v1); | |||
| // expect(v0v2.prev).toBeHalfedge(v1v0); | |||
| // expect(v1v0.next).toBeHalfedge(v0v2); | |||
| // expect(v2v1.prev).toBeHalfedge(v0v2); | |||
| // }); | |||
| // | |||
| // | |||
| // /** | |||
| // * v2 v3 | |||
| // * | \ | \ | |||
| // * | \ | \ | |||
| // * | \ | \ | |||
| // * v0 ---- v1 ---- v4 | |||
| // */ | |||
| // | |||
| // const v3 = struct.addVertex(position.set(2,2,0)); | |||
| // const v4 = struct.addVertex(position.set(4,2,0)); | |||
| // | |||
| // let v3v1: Halfedge, v1v4: Halfedge, v4v3: Halfedge; | |||
| // let v1v3: Halfedge, v4v1: Halfedge, v3v4: Halfedge; | |||
| // | |||
| // test("Connect to face", () => { | |||
| // struct.addFace([v0v1, v1v2, v2v0]); | |||
| // | |||
| // v3v1 = struct.addEdge(v3, v1); | |||
| // v1v3 = v3v1.twin; | |||
| // | |||
| // expect(v3v1.next).toBeHalfedge(v1v0); | |||
| // expect(v3v1.prev).toBeHalfedge(v1v3); | |||
| // expect(v1v3.next).toBeHalfedge(v3v1); | |||
| // expect(v1v3.prev).toBeHalfedge(v2v1); | |||
| // | |||
| // v1v4 = struct.addEdge(v1, v4); | |||
| // v4v1 = v1v4.twin; | |||
| // | |||
| // expect(v1v4.next).toBeHalfedge(v4v1); | |||
| // expect(v4v1.prev).toBeHalfedge(v1v4); | |||
| // expect(v4v1.next).toBeOneOfHalfedges([v1v0, v1v3]); | |||
| // expect(v1v4.prev).toBeOneOfHalfedges([v2v1, v3v1]); | |||
| // | |||
| // v4v3 = struct.addEdge(v4, v3); | |||
| // v3v4 = v4v3.twin; | |||
| // expect(v4v3.next).toBeHalfedge(v3v1); | |||
| // expect(v4v3.prev).toBeHalfedge(v1v4); | |||
| // expect(v3v4.next).toBeHalfedge(v4v1); | |||
| // expect(v3v4.prev).toBeHalfedge(v1v3); | |||
| // | |||
| // struct.addFace([v1v4, v4v3, v3v1]); | |||
| // | |||
| // expect(v1v4.prev).toBeHalfedge(v3v1); | |||
| // expect(v3v1.next).toBeHalfedge(v1v4); | |||
| // | |||
| // }); | |||
| @@ -0,0 +1,100 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Oct 25 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 {Halfedge} from "../core/Halfedge"; | |||
| import {HalfedgeDS} from "../core/HalfedgeDS"; | |||
| import {Vertex} from "../core/Vertex"; | |||
| export function addEdge( | |||
| struct: HalfedgeDS, | |||
| v1: Vertex, | |||
| v2: Vertex, | |||
| allowParallels = false) { | |||
| if (v1 === v2) { | |||
| throw new Error('Vertices v1 and v2 should be different'); | |||
| } | |||
| if (!allowParallels) { | |||
| // Check if v1 and v2 are already connected | |||
| const currentHalfEdge = v1.getHalfedgeToVertex(v2); | |||
| if (currentHalfEdge) { | |||
| return currentHalfEdge; | |||
| } | |||
| } | |||
| if (!v1.isFree() || !v2.isFree()) { | |||
| throw new Error('Vertices v1 and v2 are not free'); | |||
| } | |||
| // Create new halfedges, by default twin halfedges are connected together | |||
| // as prev/next in case vertices are isolated | |||
| const h1 = new Halfedge(v1); | |||
| const h2 = new Halfedge(v2); | |||
| h1.twin = h2; | |||
| h1.next = h2; | |||
| h1.prev = h2; | |||
| h2.twin = h1; | |||
| h2.next = h1; | |||
| h2.prev = h1; | |||
| /* | |||
| * ↖ ↙ | |||
| * out2 ↖ ↙ in2 | |||
| * v2 | |||
| * ⇅ | |||
| * ⇅ | |||
| * h1 ⇅ h2 | |||
| * ⇅ | |||
| * ⇅ | |||
| * v1 | |||
| * in1 ↗ ↘ out1 | |||
| * ↗ ↘ | |||
| * | |||
| */ | |||
| // Update refs around v1 if not isolated | |||
| const in1 = v1.freeHalfedgesInLoop().next().value; | |||
| if (in1) { | |||
| const out1 = in1.next; | |||
| h1.prev = in1; | |||
| in1.next = h1; | |||
| h2.next = out1; | |||
| out1.prev = h2; | |||
| } else { | |||
| v1.halfedge = h1; | |||
| } | |||
| // Update refs around v2 if not isolated | |||
| const in2 = v2.freeHalfedgesInLoop().next().value; | |||
| if (in2) { | |||
| const out2 = in2.next; | |||
| h2.prev = in2; | |||
| in2.next = h2; | |||
| h1.next = out2; | |||
| out2.prev = h1; | |||
| } else { | |||
| v2.halfedge = h2; | |||
| } | |||
| struct.halfedges.push(h1); | |||
| struct.halfedges.push(h2); | |||
| return h1; | |||
| } | |||
| @@ -0,0 +1,108 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Fri Nov 04 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 {Face} from "../core/Face"; | |||
| import {Halfedge} from "../core/Halfedge"; | |||
| import {HalfedgeDS} from "../core/HalfedgeDS"; | |||
| export function addFace(struct: HalfedgeDS, halfedges: Halfedge[]) { | |||
| const size = halfedges.length; | |||
| if (size < 2) { | |||
| throw new Error("At least 3 halfedges required to build a face."); | |||
| } | |||
| // Make some checks before changing topology | |||
| for (let i=0; i<size; i++) { | |||
| const curr = halfedges[i]; | |||
| const next = halfedges[(i+1) % size]; | |||
| if (curr.face) { | |||
| throw new Error("Halfedge already has a face"); | |||
| } | |||
| if (curr.twin.vertex !== next.vertex) { | |||
| throw new Error("Halfedges do not form a chain"); | |||
| } | |||
| } | |||
| // Add the face | |||
| for (let i = 0; i<size; i++) { | |||
| const curr = halfedges[i]; | |||
| const next = halfedges[(i+1) % size]; | |||
| if (!makeHalfedgesAdjacent(curr, next)) { | |||
| throw new Error('Face cannot be created: mesh would be non manifold.'); | |||
| } | |||
| } | |||
| const face = new Face(halfedges[0]); | |||
| for (const halfedge of halfedges) { | |||
| halfedge.face = face; | |||
| } | |||
| struct.faces.push(face); | |||
| return face; | |||
| } | |||
| /** | |||
| * | |||
| * | |||
| * @see https://kaba.hilvi.org/homepage/blog/halfedge/halfedge.htm | |||
| * | |||
| * @param | |||
| * @param out | |||
| * @returns | |||
| */ | |||
| function makeHalfedgesAdjacent( | |||
| halfIn: Halfedge, | |||
| halfOut: Halfedge): boolean { | |||
| if (halfIn.next === halfOut) { | |||
| // Adjacency is alrady correct | |||
| return true; | |||
| } | |||
| // Find a boundary halfedge different from out.twin and in | |||
| let g: Halfedge | null = null; | |||
| const loop = halfOut.vertex.freeHalfedgesInLoop(halfOut); | |||
| let he = loop.next(); | |||
| while (!g && !he.done) { | |||
| if (he.value !== halfIn) { | |||
| g = he.value; | |||
| } | |||
| he = loop.next(); | |||
| } | |||
| if (!g) { | |||
| return false; | |||
| } | |||
| const b = halfIn.next; | |||
| const d = halfOut.prev; | |||
| const h = g.next; | |||
| halfIn.next = halfOut; | |||
| halfOut.prev = halfIn; | |||
| g.next = b; | |||
| b.prev = g; | |||
| d.next = h; | |||
| h.prev = d; | |||
| return true; | |||
| } | |||
| @@ -0,0 +1,60 @@ | |||
| // /* | |||
| // * Author: Axel Antoine | |||
| // * mail: ax.antoine@gmail.com | |||
| // * website: http://axantoine.com | |||
| // * Created on Wed Nov 09 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 { Vector3 } from "three"; | |||
| // import { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| // import { Vertex } from "../core/Vertex"; | |||
| // | |||
| // const v1 = new Vertex(); | |||
| // v1.position.set(2, 3, 4); | |||
| // const position = new Vector3(); | |||
| // const struct = new HalfedgeDS(); | |||
| // | |||
| // beforeEach(() => { | |||
| // struct.clear(); | |||
| // struct.vertices.push(v1); | |||
| // }); | |||
| // | |||
| // test("Add vertex new position", () => { | |||
| // | |||
| // position.set(1,2,3); | |||
| // const v = struct.addVertex(position); | |||
| // | |||
| // expect(struct.vertices).toHaveLength(2); | |||
| // expect(struct.vertices.includes(v)).toBeTruthy(); | |||
| // | |||
| // }); | |||
| // | |||
| // describe ("Add vertex existing position", () => { | |||
| // | |||
| // test("duplicates not allowed", () => { | |||
| // position.set(2, 3, 4); | |||
| // const v = struct.addVertex(position, true); | |||
| // | |||
| // expect(struct.vertices).toHaveLength(1); | |||
| // expect(v).toBe(v1); | |||
| // }); | |||
| // | |||
| // test("duplicates allowed", () => { | |||
| // position.set(2, 3, 4); | |||
| // const v = struct.addVertex(position); | |||
| // | |||
| // expect(struct.vertices).toHaveLength(2); | |||
| // expect(struct.vertices.includes(v)).toBeTruthy(); | |||
| // expect(v).not.toBe(v1); | |||
| // }); | |||
| // | |||
| // | |||
| // }); | |||
| // | |||
| @@ -0,0 +1,38 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Oct 25 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 { Vector3 } from "three"; | |||
| import { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| import { Vertex } from "../core/Vertex"; | |||
| export function addVertex( | |||
| struct: HalfedgeDS, | |||
| position: Vector3, | |||
| checkDuplicates = false, | |||
| tolerance = 1e-10) { | |||
| // Check if position matches one face vertex and returns it | |||
| if (checkDuplicates) { | |||
| for (const vertex of struct.vertices) { | |||
| if (vertex.matchesPosition(position, tolerance)) { | |||
| return vertex; | |||
| } | |||
| } | |||
| } | |||
| const v = new Vertex(); | |||
| v.position.copy(position); | |||
| struct.vertices.push(v); | |||
| return v; | |||
| } | |||
| @@ -0,0 +1,157 @@ | |||
| // /* | |||
| // * Author: Axel Antoine | |||
| // * mail: ax.antoine@gmail.com | |||
| // * website: http://axantoine.com | |||
| // * Created on Thu Nov 17 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 { Vector3 } from "three"; | |||
| // import { Face } from "../core/Face"; | |||
| // import { Halfedge } from "../core/Halfedge"; | |||
| // import { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| // import { Vertex } from "../core/Vertex"; | |||
| // | |||
| // /** | |||
| // * This is the global topology we are going to use. | |||
| // * Before each test, all vertices exist, and the polygon v0-v3-v1-v7-v2 | |||
| // * is set | |||
| // * | |||
| // * v2 | |||
| // * | \ | |||
| // * | \ | |||
| // * | v7 | |||
| // * | | \ | |||
| // * | v6 \ | |||
| // * | \ \ | |||
| // * | \ \ | |||
| // * | v5 \ | |||
| // * | \ \ | |||
| // * | f0 v4 \ | |||
| // * | \ f1 \ | |||
| // * | \ \ | |||
| // * v0 --------- v3 --------- v1 | |||
| // */ | |||
| // | |||
| // const vec = new Vector3(); | |||
| // const struct = new HalfedgeDS(); | |||
| // let v0: Vertex, v1: Vertex, v2: Vertex, v3: Vertex; | |||
| // let v4: Vertex, v5: Vertex, v6: Vertex, v7: Vertex; | |||
| // let f0: Face; | |||
| // let v4v5: Halfedge; | |||
| // | |||
| // beforeEach(() => { | |||
| // struct.clear(); | |||
| // v0 = struct.addVertex(vec.set(0,0,0)); | |||
| // v1 = struct.addVertex(vec.set(1,1,1)); | |||
| // v2 = struct.addVertex(vec.set(2,2,2)); | |||
| // | |||
| // const v0v1 = struct.addEdge(v0, v1); | |||
| // const v1v2 = struct.addEdge(v1, v2); | |||
| // const v2v0 = struct.addEdge(v2, v0); | |||
| // | |||
| // f0 = struct.addFace([v0v1, v1v2, v2v0]); | |||
| // | |||
| // v3 = struct.splitEdge(v0v1, vec.set(3,3,3)); | |||
| // | |||
| // v4 = struct.addVertex(vec.set(4,4,4)); | |||
| // v5 = struct.addVertex(vec.set(5,5,5)); | |||
| // v6 = struct.addVertex(vec.set(6,6,6)); | |||
| // | |||
| // v7 = struct.splitEdge(v1v2, vec.set(7,7,7)); | |||
| // | |||
| // v4v5 = struct.addEdge(v4, v5); | |||
| // }); | |||
| // | |||
| // | |||
| // test("Cut from center", () => { | |||
| // const v5v6 = struct.addEdge(v5, v6); | |||
| // | |||
| // // v4v5 already exist, v5v6 should be connected to it | |||
| // expect(v5v6.next).toBeHalfedge(v5v6.twin); | |||
| // expect(v5v6.prev).toBeHalfedge(v4v5); | |||
| // expect(v4v5.next).toBeHalfedge(v5v6); | |||
| // expect(v4v5.prev).toBeHalfedge(v4v5.twin); | |||
| // expect(v5v6.twin.next).toBeHalfedge(v4v5.twin); | |||
| // | |||
| // // Both halfedges should not be connected to the face f0 | |||
| // expect(v5v6.face).toBeNull(); | |||
| // expect(v5v6.twin.face).toBeNull(); | |||
| // expect(v4v5.face).toBeNull(); | |||
| // expect(v4v5.twin.face).toBeNull(); | |||
| // | |||
| // }); | |||
| // | |||
| // test("Connect from side", () => { | |||
| // | |||
| // const v7v2 = f0.halfedgeFromVertex(v7); | |||
| // expect(v7v2).not.toBeNull(); | |||
| // | |||
| // if (v7v2) { | |||
| // | |||
| // const v1v7 = v7v2.prev; | |||
| // const v6v7 = struct.cutFace(f0, v6, v7); | |||
| // | |||
| // expect(v6v7.next).toBeHalfedge(v7v2); | |||
| // expect(v6v7.prev).toBeHalfedge(v6v7.twin); | |||
| // expect(v6v7.twin.next).toBeHalfedge(v6v7); | |||
| // expect(v6v7.twin.prev).toBeHalfedge(v1v7); | |||
| // | |||
| // expect(v6v7.face).toBe(f0); | |||
| // expect(v6v7.twin.face).toBe(f0); | |||
| // | |||
| // } | |||
| // | |||
| // }); | |||
| // | |||
| // test("Connect the two cuts", () => { | |||
| // | |||
| // const v7v2 = f0.halfedgeFromVertex(v7); | |||
| // const v3v1 = f0.halfedgeFromVertex(v3); | |||
| // expect(v7v2).not.toBeNull(); | |||
| // expect(v3v1).not.toBeNull(); | |||
| // | |||
| // if (v7v2 && v3v1) { | |||
| // | |||
| // const v0v3 = v3v1.prev; | |||
| // const v1v7 = v7v2.prev; | |||
| // const v3v7 = struct.cutFace(f0, v3, v7); | |||
| // | |||
| // // We expect a new face | |||
| // const f1 = v3v7.twin.face; | |||
| // expect(v3v7.face).toBe(f0); | |||
| // expect(f1).not.toBe(f0) | |||
| // | |||
| // // f0 next / prev | |||
| // expect(v0v3.next).toBeHalfedge(v3v7); | |||
| // expect(v3v7.next).toBeHalfedge(v7v2); | |||
| // expect(v7v2.prev).toBeHalfedge(v3v7); | |||
| // expect(v3v7.prev).toBeHalfedge(v0v3); | |||
| // | |||
| // // f1 next / prev | |||
| // expect(v1v7.next).toBeHalfedge(v3v7.twin); | |||
| // expect(v3v7.twin.next).toBeHalfedge(v3v1); | |||
| // expect(v3v1.prev).toBeHalfedge(v3v7.twin); | |||
| // expect(v3v7.twin.prev).toBeHalfedge(v1v7); | |||
| // | |||
| // // Check f0 loop | |||
| // for (const h of v3v7.nextLoop()) { | |||
| // expect(h.face).toBe(f0); | |||
| // } | |||
| // | |||
| // // Check f1 loop | |||
| // for (const h of v3v7.twin.nextLoop()) { | |||
| // expect(h.face).toBe(f1); | |||
| // } | |||
| // } | |||
| // | |||
| // }); | |||
| // | |||
| // | |||
| // | |||
| @@ -0,0 +1,162 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Thu Nov 03 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 { Face } from "../core/Face"; | |||
| import { Halfedge } from "../core/Halfedge"; | |||
| import { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| import { Vertex } from "../core/Vertex"; | |||
| export function cutFace( | |||
| struct: HalfedgeDS, | |||
| face: Face, | |||
| v1: Vertex, | |||
| v2: Vertex, | |||
| createNewFace = true){ | |||
| if (v1 === v2) { | |||
| throw new Error('Vertices v1 and v2 should be different'); | |||
| } | |||
| let out1 = face.halfedgeFromVertex(v1); | |||
| if (!out1 && !v1.isFree()) { | |||
| throw new Error('Vertices v1 does not belong to face nor is free'); | |||
| } | |||
| let out2 = face.halfedgeFromVertex(v2); | |||
| if (!out2 && !v2.isFree()) { | |||
| throw new Error('Vertices v2 does not belong to face nor is free'); | |||
| } | |||
| // Check if v1 is already connected to v2 in the face | |||
| if ((out1 && out1.next.vertex === v2) || (out2 && out2.next.vertex === v1)) { | |||
| throw new Error("Vertices v1 and v2 are already connected"); | |||
| } | |||
| /* | |||
| * From To | |||
| * | |||
| * o → → → v1 → → → o o → → → v1 → → → o | |||
| * ↖ ↙ ↖ f ↓↑ f' ↙ | |||
| * ↖ f ↙ ↖ ↓↑ ↙ | |||
| * ↖ ↙ ↖ ↓↑ ↙ | |||
| * v2 v2 | |||
| * | |||
| * or | |||
| * | |||
| * o → → → o → → → o o → → → v1 → → → o | |||
| * ↖ f ↓↑ ↙ ↖ f ↓↑ f' ↙ | |||
| * ↖ v1 ↙ ↖ ↓↑ ↙ | |||
| * ↖ ↙ ↖ ↓↑ ↙ | |||
| * v2 v2 | |||
| * | |||
| * -------------------------------------- | |||
| * | |||
| * ↖ ↙ | |||
| * out2 ↖ ↙ in2 | |||
| * v2 | |||
| * ⇅ | |||
| * ⇅ | |||
| * h1 ⇅ h2 | |||
| * ⇅ | |||
| * ⇅ | |||
| * v1 | |||
| * in1 ↗ ↘ out1 | |||
| * ↗ ↘ | |||
| * | |||
| */ | |||
| // Create new halfedges | |||
| const h1 = new Halfedge(v1); | |||
| const h2 = new Halfedge(v2); | |||
| h1.face = face; | |||
| h2.face = face; | |||
| h1.twin = h2; | |||
| h1.next = h2; | |||
| h1.prev = h2; | |||
| h2.twin = h1; | |||
| h2.next = h1; | |||
| h2.prev = h1; | |||
| // If v1 is not part of face, get any outgoing halfedge | |||
| out1 = out1 ?? v1.freeHalfedgesOutLoop().next().value; | |||
| // Update refs around v1 if not isolated | |||
| if (out1) { | |||
| const in1 = out1.prev; | |||
| h1.prev = in1; | |||
| in1.next = h1; | |||
| h2.next = out1; | |||
| out1.prev = h2; | |||
| } else { | |||
| v1.halfedge = h1; | |||
| } | |||
| // If v2 is not part of face, get any outgoing halfedge | |||
| out2 = out2 ?? v2.freeHalfedgesOutLoop().next().value; | |||
| // Update refs around v2 if not isolated | |||
| if (out2) { | |||
| const in2 = out2.prev; | |||
| h2.prev = in2; | |||
| in2.next = h2; | |||
| h1.next = out2; | |||
| out2.prev = h1; | |||
| } else { | |||
| v2.halfedge = h2; | |||
| } | |||
| struct.halfedges.push(h1); | |||
| struct.halfedges.push(h2); | |||
| // In the case where we connect isolated halfedge (without face) to this face, | |||
| // We update face ref loop | |||
| for (const he of face.halfedge.nextLoop()){ | |||
| he.face = face; | |||
| } | |||
| // Check if h1 and h2 (twin halfedges) are on the same loop, if there aren't, | |||
| // it means we created a new halfedges loop, i.e. new face | |||
| let found = false; | |||
| const loop = h1.nextLoop(); | |||
| let h = loop.next(); | |||
| while(!found && !h.done) { | |||
| found = h.value === h2; | |||
| h = loop.next(); | |||
| } | |||
| if (!found) { | |||
| // h2 is on a different loop than h1 | |||
| // Update initial face halfedge reference in case it changed loop | |||
| face.halfedge = h1; | |||
| let newFace = null; | |||
| if (createNewFace) { | |||
| newFace = new Face(h2); | |||
| struct.faces.push(newFace); | |||
| } | |||
| // Update the face ref for each halfedge of the new loop either a new face | |||
| // or null | |||
| for (const h of h2.nextLoop()) { | |||
| h.face = newFace; | |||
| } | |||
| } | |||
| return h1; | |||
| } | |||
| @@ -0,0 +1,84 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Thu Nov 03 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 { Halfedge } from "../core/Halfedge"; | |||
| import { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| import { removeFace } from "./removeFace"; | |||
| export function removeEdge( | |||
| struct: HalfedgeDS, | |||
| halfedge: Halfedge, | |||
| mergeFaces = true) { | |||
| /* | |||
| * ↖ ↙ | |||
| * ↖ ↙ | |||
| * ↖ ↙ | |||
| * v2 | |||
| * ⇅ | |||
| * ⇅ | |||
| * he ⇅ twin | |||
| * ⇅ | |||
| * v1 | |||
| * ↗ ↘ | |||
| * ↗ ↘ | |||
| * ↗ ↘ | |||
| * | |||
| */ | |||
| const twin = halfedge.twin; | |||
| if (mergeFaces && halfedge.face && twin.face) { | |||
| // Keep only one face in both faces for halfedge and twin exist, and update | |||
| // ref | |||
| removeFace(struct, twin.face); | |||
| halfedge.face.halfedge = halfedge.prev; | |||
| } else { | |||
| // Remove both faces | |||
| if (halfedge.face) { | |||
| removeFace(struct, halfedge.face); | |||
| } | |||
| if (twin.face) { | |||
| removeFace(struct, twin.face); | |||
| } | |||
| } | |||
| // Update topology around v1 | |||
| const v1 = halfedge.vertex; | |||
| if (twin.next === halfedge) { | |||
| // v1 is now isolated | |||
| v1.halfedge = null; | |||
| } else { | |||
| v1.halfedge = twin.next; | |||
| halfedge.prev.next = twin.next; | |||
| twin.next.prev = halfedge.prev; | |||
| } | |||
| // Update topology around v2 | |||
| const v2 = twin.vertex; | |||
| if (halfedge.next === twin) { | |||
| // v2 is now isolated | |||
| v2.halfedge = null; | |||
| } else { | |||
| v2.halfedge = halfedge.next; | |||
| halfedge.next.prev = twin.prev; | |||
| twin.prev.next = halfedge.next | |||
| } | |||
| // Remove halfedges from struct | |||
| struct.halfedges.remove(halfedge); | |||
| struct.halfedges.remove(twin); | |||
| } | |||
| @@ -0,0 +1,30 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Fri Nov 04 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 { Face } from "../core/Face"; | |||
| import { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| export function removeFace( | |||
| struct: HalfedgeDS, | |||
| face: Face) { | |||
| if (!struct.faces.remove(face)) { | |||
| return; | |||
| } | |||
| // Remove face ref from halfedges loop | |||
| for (const halfedge of face.halfedge.nextLoop()) { | |||
| halfedge.face = null; | |||
| } | |||
| } | |||
| @@ -0,0 +1,53 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Oct 25 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 { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| import { Vertex } from "../core/Vertex"; | |||
| import { removeEdge } from "./removeEdge"; | |||
| /* | |||
| * From To | |||
| * | |||
| * | |||
| * o o | |||
| * ↙ ⇅ ↖ ↙ ↖ | |||
| * ↙ ⇅ ↖ ↙ ↖ | |||
| * ↙ f1 ⇅ f4 ↖ ↙ ↖ | |||
| * ↙ ⇅ ↖ ↙ ↖ | |||
| * o ⇄ ⇄ ⇄ ⇄ v ⇄ ⇄ ⇄ ⇄ o o f o | |||
| * ↘ ⇅ ↗ ↘ ↗ | |||
| * ↘ f2 ⇅ f3 ↗ ↘ ↗ | |||
| * ↘ ⇅ ↗ ↘ ↗ | |||
| * ↘ ⇅ ↗ ↘ ↗ | |||
| * o o | |||
| * | |||
| * If all halfedges starting from vertex v to delete are connected to a face, | |||
| * then we create a new face v. | |||
| * If some of the halfedges starting from v are boundaries (i.e. no face), | |||
| * then we can't create a new face. | |||
| * | |||
| */ | |||
| export function removeVertex( | |||
| struct: HalfedgeDS, | |||
| vertex: Vertex, | |||
| mergeFaces = true) { | |||
| for (const halfedge of vertex.loopCW()) { | |||
| removeEdge(struct, halfedge, mergeFaces); | |||
| } | |||
| struct.vertices.remove(vertex); | |||
| } | |||
| @@ -0,0 +1,216 @@ | |||
| // // Author: Axel Antoine | |||
| // // mail: ax.antoine@gmail.com | |||
| // // website: https://axantoine.com | |||
| // // 09/12/2021 | |||
| // | |||
| // // 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 { computeVerticesIndexArray } from './setFromGeometry'; | |||
| // import { | |||
| // CylinderGeometry, | |||
| // BoxGeometry, | |||
| // BufferAttribute, BufferGeometry} from 'three'; | |||
| // import { generatorSize } from '../utils/testutils'; | |||
| // import { HalfedgeDS } from '../core/HalfedgeDS'; | |||
| // import { Halfedge } from '../core/Halfedge'; | |||
| // | |||
| // const struct = new HalfedgeDS(); | |||
| // | |||
| // function runCommonTests( | |||
| // struct: HalfedgeDS, | |||
| // nFace: number, | |||
| // nEdges: number, | |||
| // nVertices: number) { | |||
| // | |||
| // describe("Base Tests", () => { | |||
| // | |||
| // test('Test sets size', () => { | |||
| // expect(struct.faces).toHaveLength(nFace); | |||
| // expect(struct.halfedges).toHaveLength(nEdges*2); | |||
| // expect(struct.vertices).toHaveLength(nVertices); | |||
| // }); | |||
| // | |||
| // test("Test halfedge prev/next references", () => { | |||
| // for (const halfedge of struct.halfedges) { | |||
| // expect(halfedge.next.prev).toBe(halfedge); | |||
| // expect(halfedge.prev.next).toBe(halfedge); | |||
| // } | |||
| // }); | |||
| // | |||
| // test("Test halfedges pairs", () => { | |||
| // for (const halfEdge of struct.halfedges) { | |||
| // expect(halfEdge.twin.twin).toBe(halfEdge); | |||
| // } | |||
| // }); | |||
| // | |||
| // test('Test face loops size', () => { | |||
| // for (const face of struct.faces) { | |||
| // expect(generatorSize(face.halfedge.nextLoop())).toBe(3); | |||
| // expect(generatorSize(face.halfedge.prevLoop())).toBe(3); | |||
| // } | |||
| // }); | |||
| // | |||
| // test('Test face reference', () => { | |||
| // for (const face of struct.faces) { | |||
| // for (const halfedge of face.halfedge.nextLoop()) { | |||
| // expect(halfedge.face).toBe(face); | |||
| // } | |||
| // } | |||
| // }); | |||
| // }); | |||
| // } | |||
| // | |||
| // describe("Triangle topology", () => { | |||
| // | |||
| // const array = new Int8Array([0,0,0, 0,2,0, 2,0,0]); | |||
| // const buffer = new BufferAttribute(array, 3); | |||
| // const geometry = new BufferGeometry(); | |||
| // geometry.setAttribute('position', buffer); | |||
| // | |||
| // beforeAll(() => { | |||
| // struct.setFromGeometry(geometry); | |||
| // }); | |||
| // | |||
| // runCommonTests(struct, 1, 3, 3); | |||
| // | |||
| // test("Test number of boundary halfedges", () => { | |||
| // let boundaries = 0; | |||
| // for (const halfedge of struct.halfedges) { | |||
| // if (!halfedge.face) { | |||
| // boundaries += 1; | |||
| // } | |||
| // } | |||
| // expect(boundaries).toBe(3); | |||
| // }); | |||
| // | |||
| // }); | |||
| // | |||
| // describe("Double triangles topology", () => { | |||
| // | |||
| // const array = new Int8Array([0,0,0, 0,2,0, 2,0,0, 2,0,0, 4,0,0, 4,2,0]); | |||
| // const buffer = new BufferAttribute(array, 3); | |||
| // const geometry = new BufferGeometry(); | |||
| // geometry.setAttribute('position', buffer); | |||
| // | |||
| // beforeAll(() => { | |||
| // struct.setFromGeometry(geometry); | |||
| // }); | |||
| // | |||
| // runCommonTests(struct, 2, 6, 5); | |||
| // | |||
| // test('Test number of loops', () => { | |||
| // let boundaryLoops = 0; | |||
| // let faceLoops = 0; | |||
| // for (const loop of struct.loops()) { | |||
| // if (!loop.face) { | |||
| // boundaryLoops += 1; | |||
| // } else { | |||
| // faceLoops += 1; | |||
| // } | |||
| // } | |||
| // expect(boundaryLoops).toBe(1); | |||
| // expect(faceLoops).toBe(2); | |||
| // }); | |||
| // | |||
| // test("Test number of boundary halfedges", () => { | |||
| // let boundaries = 0; | |||
| // for (const halfedge of struct.halfedges) { | |||
| // if (!halfedge.face) { | |||
| // boundaries += 1; | |||
| // } | |||
| // } | |||
| // expect(boundaries).toBe(6); | |||
| // }); | |||
| // | |||
| // }); | |||
| // | |||
| // describe("Cylinder topology", () => { | |||
| // | |||
| // // https://threejs.org/docs/scenes/geometry-browser.html#CylinderGeometry | |||
| // const geometry = new CylinderGeometry(2, 2, 1, 6, 1, true); | |||
| // | |||
| // beforeAll(() => { | |||
| // struct.setFromGeometry(geometry); | |||
| // }); | |||
| // | |||
| // runCommonTests(struct, 12, 24, 12); | |||
| // | |||
| // test("Test boundary loops", () => { | |||
| // const loops = struct.loops(); | |||
| // const boundaryLoops = new Array<Halfedge>(); | |||
| // for (const he of loops) { | |||
| // if (!he.face) { | |||
| // boundaryLoops.push(he); | |||
| // } | |||
| // } | |||
| // expect(boundaryLoops).toHaveLength(2); | |||
| // for (const bloop of boundaryLoops) { | |||
| // expect(generatorSize(bloop.nextLoop())).toBe(6); | |||
| // } | |||
| // }); | |||
| // | |||
| // }); | |||
| // | |||
| // describe("Cube topology", () => { | |||
| // | |||
| // const geometry = new BoxGeometry(1, 1, 1); | |||
| // | |||
| // beforeAll(() => { | |||
| // struct.setFromGeometry(geometry); | |||
| // }); | |||
| // | |||
| // runCommonTests(struct, 12, 18, 8); | |||
| // | |||
| // }); | |||
| // | |||
| // describe("Degenerated geometries", () => { | |||
| // | |||
| // test("No positions attribute", () => { | |||
| // const geometry = new BufferGeometry(); | |||
| // | |||
| // expect(() => {struct.setFromGeometry(geometry);}).toThrow(Error); | |||
| // }); | |||
| // }); | |||
| // | |||
| // | |||
| // describe("Check merge of vertices", () => { | |||
| // | |||
| // test("Expect position indices to be merged", () => { | |||
| // const array = new Int8Array([1,2,3,4,5,6,7,8,9,1,2,3,4,5,6]); | |||
| // const buffer = new BufferAttribute(array, 3); | |||
| // const idxArray = computeVerticesIndexArray(buffer, 1); | |||
| // expect(idxArray).toHaveLength(5); | |||
| // expect(idxArray[0]).toBe(0); | |||
| // expect(idxArray[1]).toBe(1); | |||
| // expect(idxArray[2]).toBe(2); | |||
| // expect(idxArray[3]).toBe(0); | |||
| // expect(idxArray[4]).toBe(1); | |||
| // }); | |||
| // | |||
| // test("Expect decimals to be trunked when precision changes", () => { | |||
| // const array = new Float32Array([1.110,2.220,3.330,1.111,2.222,3.333]); | |||
| // const buffer = new BufferAttribute(array, 3); | |||
| // let idxArray = computeVerticesIndexArray(buffer, 1E-1); | |||
| // expect(idxArray).toHaveLength(2); | |||
| // expect(idxArray[0]).toBe(0); | |||
| // expect(idxArray[1]).toBe(0); | |||
| // | |||
| // idxArray = computeVerticesIndexArray(buffer, 1E-2); | |||
| // expect(idxArray).toHaveLength(2); | |||
| // expect(idxArray[0]).toBe(0); | |||
| // expect(idxArray[1]).toBe(0); | |||
| // | |||
| // idxArray = computeVerticesIndexArray(buffer, 1E-3); | |||
| // expect(idxArray).toHaveLength(2); | |||
| // expect(idxArray[0]).toBe(0); | |||
| // expect(idxArray[1]).toBe(1); | |||
| // }); | |||
| // | |||
| // }); | |||
| // | |||
| // | |||
| @@ -0,0 +1,174 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Fri Nov 18 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 {BufferAttribute, BufferGeometry, InterleavedBufferAttribute, Vector3} from "three"; | |||
| import {Halfedge} from "../core/Halfedge"; | |||
| import {HalfedgeDS} from "../core/HalfedgeDS"; | |||
| import {Vertex} from "../core/Vertex"; | |||
| const pos_ = new Vector3(); | |||
| export function setFromGeometry( | |||
| struct: HalfedgeDS, | |||
| geometry: BufferGeometry, | |||
| tolerance= 1e-10) { | |||
| struct.clear(); | |||
| // Check position and normal attributes | |||
| if (!geometry.hasAttribute("position")) { | |||
| throw new Error("BufferGeometry does not have a position BufferAttribute."); | |||
| } | |||
| // console.log(geometry) | |||
| const positions = geometry.getAttribute('position'); | |||
| // Get the merged vertices Array | |||
| const indexVertexArray = computeVerticesIndexArray(positions, tolerance); | |||
| // If the geometry is not indexed, we get the indexes of faces vertices from | |||
| // the position buffer attribute directly in group of 3 | |||
| let nbOfFaces = positions.count/3; | |||
| let getVertexIndex = function(bufferIndex: number) { | |||
| return indexVertexArray[bufferIndex]; | |||
| } | |||
| // Otherwise, if the geometry is indexed, we get the index of faces vertices | |||
| // from the index buffer in group of 3 | |||
| const indexBuffer = geometry.getIndex(); | |||
| if (indexBuffer) { | |||
| nbOfFaces = indexBuffer.count/3; | |||
| getVertexIndex = function(bufferIndex: number) { | |||
| return indexVertexArray[indexBuffer.array[bufferIndex]]; | |||
| } | |||
| } | |||
| // Save halfedges in a map where with a hash <src-vertex-id> | |||
| // their hash is index1-index2, so that it is easier to find the twin | |||
| const halfedgeMap = new Map<string, Halfedge>(); | |||
| const vertexMap = new Map<number, Vertex>(); | |||
| for (let faceIndex = 0; faceIndex < nbOfFaces; faceIndex++) { | |||
| let loopHalfedges = [] as Halfedge[] | |||
| let addedVertex = [] as Vertex[] | |||
| let addedEdges = [] as Halfedge[] | |||
| for (let i=0; i<3; i++) { | |||
| // Get the source vertex v1 | |||
| const i1 = getVertexIndex(faceIndex*3 + i); | |||
| let v1 = vertexMap.get(i1); | |||
| // if(!v1?.isFree()) break | |||
| if (!v1) { | |||
| pos_.fromBufferAttribute(positions, i1); | |||
| v1 = struct.addVertex(pos_); | |||
| addedVertex.push(v1); | |||
| vertexMap.set(i1, v1); | |||
| } | |||
| // if(!v1.isFree()) break | |||
| // Get the destitation vertex | |||
| const i2 = getVertexIndex(faceIndex*3 + (i+1)%3); | |||
| let v2 = vertexMap.get(i2); | |||
| // if(!v2?.isFree()) break | |||
| if (!v2) { | |||
| pos_.fromBufferAttribute(positions, i2); | |||
| v2 = struct.addVertex(pos_); | |||
| addedVertex.push(v1); | |||
| vertexMap.set(i2, v2); | |||
| } | |||
| // if(!v2.isFree()) break | |||
| // Get the halfedge from v1 to v2 | |||
| const hash1 = i1+'-'+i2; | |||
| let h1 = halfedgeMap.get(hash1); | |||
| if(h1?.face) h1 = undefined | |||
| if (!h1) { | |||
| try { | |||
| // console.log(h1) | |||
| h1 = struct.addEdge(v1, v2); | |||
| addedEdges.push(h1); | |||
| const h2 = h1.twin; | |||
| const hash2 = i2 + '-' + i1; | |||
| halfedgeMap.set(hash1, h1); | |||
| halfedgeMap.set(hash2, h2); | |||
| // console.log(h1.face) | |||
| }catch (e){ | |||
| // console.error(e); | |||
| } | |||
| } | |||
| if(h1) loopHalfedges.push(h1); | |||
| else break; | |||
| } | |||
| try { | |||
| if(loopHalfedges.length < 3) throw 'need 3 for face' | |||
| struct.addFace(loopHalfedges); | |||
| }catch (e){ | |||
| // console.error(e); | |||
| // for (const addedEdge of addedEdges) { | |||
| // struct.removeEdge(addedEdge); | |||
| // } | |||
| // if(!addedEdges.length) | |||
| // for (const addedVert of addedVertex) { | |||
| // struct.removeVertex(addedVert); | |||
| // } | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Returns an array where each index points to its new index in the buffer | |||
| * attribute | |||
| * | |||
| * @param positions Vertices positions buffer | |||
| * @param tolerance Distance tolerance of the vertices to merge | |||
| * @returns | |||
| */ | |||
| export function computeVerticesIndexArray( | |||
| positions: BufferAttribute | InterleavedBufferAttribute, | |||
| tolerance = 1e-10){ | |||
| const decimalShift = Math.log10(1 / tolerance); | |||
| const shiftMultiplier = Math.pow(10, decimalShift); | |||
| const hashMap = new Map<string, number>(); | |||
| const indexArray = new Array<number>(); | |||
| for (let i=0; i < positions.count; i++) { | |||
| // Compute a hash based on the vertex position rounded to a given precision | |||
| let hash = ""; | |||
| for (let j=0; j<3; j++) { | |||
| hash += `${Math.round(positions.array[i*3+j] * shiftMultiplier)}`; | |||
| } | |||
| // If hash already exist, then set the buffer index to the existing vertex, | |||
| // otherwise, create it | |||
| let vertexIndex = hashMap.get(hash); | |||
| if (vertexIndex === undefined) { | |||
| vertexIndex = i; | |||
| hashMap.set(hash, i); | |||
| } | |||
| indexArray.push(vertexIndex); | |||
| } | |||
| return indexArray; | |||
| } | |||
| @@ -0,0 +1,80 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Oct 25 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 { Vector3 } from "three"; | |||
| import { Halfedge } from "../core/Halfedge"; | |||
| import { HalfedgeDS } from "../core/HalfedgeDS"; | |||
| import { Vertex } from "../core/Vertex"; | |||
| export function splitEdge( | |||
| struct: HalfedgeDS, | |||
| halfedge: Halfedge, | |||
| position: Vector3, | |||
| tolerance = 1e-10) { | |||
| /** | |||
| * From | |||
| * A -------------- he -------------> B | |||
| * A <------------ twin ------------- B | |||
| * To | |||
| * A ---- he ----> v ---- newhe ----> B | |||
| * A <--- twin --- v <--- newtwin --- B | |||
| */ | |||
| const twin = halfedge.twin; | |||
| const A = halfedge.vertex; | |||
| const B = twin.vertex; | |||
| // No need to split if position matches A or B | |||
| if (A.matchesPosition(position, tolerance)) { | |||
| return A; | |||
| } | |||
| if (B.matchesPosition(position, tolerance)) { | |||
| return B; | |||
| } | |||
| const newVertex = new Vertex(); | |||
| newVertex.position.copy(position); | |||
| // Create the new halfegdes | |||
| const newHalfedge = new Halfedge(newVertex); | |||
| const newTwin = new Halfedge(B); | |||
| newHalfedge.twin = newTwin; | |||
| newTwin.twin = newHalfedge; | |||
| // Update vertices halfedge refs | |||
| A.halfedge = halfedge; | |||
| newVertex.halfedge = newHalfedge; | |||
| B.halfedge = newTwin; | |||
| // Copy the face refs | |||
| newHalfedge.face = halfedge.face; | |||
| newTwin.face = twin.face; | |||
| // Update next and prev refs | |||
| newHalfedge.next = halfedge.next; | |||
| newHalfedge.prev = halfedge; | |||
| halfedge.next = newHalfedge; | |||
| newTwin.next = twin; | |||
| newTwin.prev = twin.prev; | |||
| twin.prev = newTwin; | |||
| // Update structure | |||
| struct.vertices.push(newVertex); | |||
| struct.halfedges.push(newHalfedge); | |||
| struct.halfedges.push(newTwin); | |||
| return newVertex; | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Fri Nov 18 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 './augments'; | |||
| import './utils/testutils'; | |||
| @@ -0,0 +1,44 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 06/09/2021 | |||
| // 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 {Matrix4, Vector3} from 'three'; | |||
| const EPSILON = 1e-10; | |||
| // See https://hal.inria.fr/hal-02189483 appendix C.2 Orientation test | |||
| const _matrix = new Matrix4(); | |||
| export function orient3D(a: Vector3, b: Vector3, c: Vector3, d: Vector3) { | |||
| _matrix.set( | |||
| a.x, a.y, a.z, 1, | |||
| b.x, b.y, b.z, 1, | |||
| c.x, c.y, c.z, 1, | |||
| d.x, d.y, d.z, 1 | |||
| ); | |||
| const det = _matrix.determinant(); | |||
| if (det > EPSILON) { | |||
| return 1; | |||
| } else if (det < -EPSILON) { | |||
| return -1; | |||
| } | |||
| return 0; | |||
| } | |||
| // See https://hal.inria.fr/hal-02189483 appendix C.2 Orientation test | |||
| export function frontSide(a: Vector3, b: Vector3, c: Vector3, d: Vector3) { | |||
| return orient3D(d, b, c, a); | |||
| } | |||
| // See https://hal.inria.fr/hal-02189483 appendix C.2 Orientation test | |||
| export function sameSide(a: Vector3, b: Vector3, c: Vector3, d: Vector3, e: Vector3) { | |||
| return (orient3D(a,b,c,d) > 0) === (orient3D(a,b,c,e) > 0); | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Thu Nov 10 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 | |||
| */ | |||
| // declare global { | |||
| // namespace jest { | |||
| // interface Matchers<R> { | |||
| // toBeHalfedge(expected: Halfedge): CustomMatcherResult; | |||
| // toBeVertex(expected: Vertex): CustomMatcherResult; | |||
| // toBeOneOfHalfedges(expected: Halfedge[]): CustomMatcherResult; | |||
| // } | |||
| // } | |||
| // } | |||
| // expect.extend({ | |||
| // | |||
| // toBeHalfedge(received: Halfedge, expected: Halfedge) { | |||
| // const pass = received === expected; | |||
| // | |||
| // return { | |||
| // message: () => | |||
| // `Expected Halfedges ${pass? 'not ': ''}to be equal`+ | |||
| // '\nReceived: '+ received.id + | |||
| // '\nExpected: '+ expected.id, | |||
| // pass: pass, | |||
| // }; | |||
| // }, | |||
| // | |||
| // toBeOneOfHalfedges(received: Halfedge, expected: Halfedge[]) { | |||
| // const pass = expected.indexOf(received) !== -1; | |||
| // | |||
| // return { | |||
| // message: () => | |||
| // `Expected Halfedges ${pass? 'not ': ''}to be in the list`+ | |||
| // '\nReceived: '+ received.id + | |||
| // '\nExpected list: '+ expected.map(e => e.id).join(', '), | |||
| // pass: pass, | |||
| // }; | |||
| // }, | |||
| // | |||
| // toBeVertex(received: Vertex, expected: Vertex) { | |||
| // const pass = received === expected; | |||
| // | |||
| // return { | |||
| // message: () => | |||
| // `Expected Vertices ${pass? 'not ': ''}to be equal`+ | |||
| // '\nReceived: '+ received.id + | |||
| // '\nExpected: '+ expected.id, | |||
| // pass: pass, | |||
| // }; | |||
| // }, | |||
| // | |||
| // }); | |||
| export function generatorSize(g: Generator) { | |||
| let cpt = 0; | |||
| let v = g.next(); | |||
| while(!v.done) { | |||
| cpt += 1; | |||
| v = g.next(); | |||
| } | |||
| return cpt; | |||
| } | |||
| export function generatorToArray<T>(g: Generator<T>) { | |||
| const array = new Array<T>(); | |||
| let v = g.next(); | |||
| while(!v.done) { | |||
| array.push(v.value); | |||
| v = g.next(); | |||
| } | |||
| return array; | |||
| } | |||
| @@ -0,0 +1,674 @@ | |||
| GNU GENERAL PUBLIC LICENSE | |||
| Version 3, 29 June 2007 | |||
| Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | |||
| Everyone is permitted to copy and distribute verbatim copies | |||
| of this license document, but changing it is not allowed. | |||
| Preamble | |||
| The GNU General Public License is a free, copyleft license for | |||
| software and other kinds of works. | |||
| The licenses for most software and other practical works are designed | |||
| to take away your freedom to share and change the works. By contrast, | |||
| the GNU General Public License is intended to guarantee your freedom to | |||
| share and change all versions of a program--to make sure it remains free | |||
| software for all its users. We, the Free Software Foundation, use the | |||
| GNU General Public License for most of our software; it applies also to | |||
| any other work released this way by its authors. You can apply it to | |||
| your programs, too. | |||
| When we speak of free software, we are referring to freedom, not | |||
| price. Our General Public Licenses are designed to make sure that you | |||
| have the freedom to distribute copies of free software (and charge for | |||
| them if you wish), that you receive source code or can get it if you | |||
| want it, that you can change the software or use pieces of it in new | |||
| free programs, and that you know you can do these things. | |||
| To protect your rights, we need to prevent others from denying you | |||
| these rights or asking you to surrender the rights. Therefore, you have | |||
| certain responsibilities if you distribute copies of the software, or if | |||
| you modify it: responsibilities to respect the freedom of others. | |||
| For example, if you distribute copies of such a program, whether | |||
| gratis or for a fee, you must pass on to the recipients the same | |||
| freedoms that you received. You must make sure that they, too, receive | |||
| or can get the source code. And you must show them these terms so they | |||
| know their rights. | |||
| Developers that use the GNU GPL protect your rights with two steps: | |||
| (1) assert copyright on the software, and (2) offer you this License | |||
| giving you legal permission to copy, distribute and/or modify it. | |||
| For the developers' and authors' protection, the GPL clearly explains | |||
| that there is no warranty for this free software. For both users' and | |||
| authors' sake, the GPL requires that modified versions be marked as | |||
| changed, so that their problems will not be attributed erroneously to | |||
| authors of previous versions. | |||
| Some devices are designed to deny users access to install or run | |||
| modified versions of the software inside them, although the manufacturer | |||
| can do so. This is fundamentally incompatible with the aim of | |||
| protecting users' freedom to change the software. The systematic | |||
| pattern of such abuse occurs in the area of products for individuals to | |||
| use, which is precisely where it is most unacceptable. Therefore, we | |||
| have designed this version of the GPL to prohibit the practice for those | |||
| products. If such problems arise substantially in other domains, we | |||
| stand ready to extend this provision to those domains in future versions | |||
| of the GPL, as needed to protect the freedom of users. | |||
| Finally, every program is threatened constantly by software patents. | |||
| States should not allow patents to restrict development and use of | |||
| software on general-purpose computers, but in those that do, we wish to | |||
| avoid the special danger that patents applied to a free program could | |||
| make it effectively proprietary. To prevent this, the GPL assures that | |||
| patents cannot be used to render the program non-free. | |||
| The precise terms and conditions for copying, distribution and | |||
| modification follow. | |||
| TERMS AND CONDITIONS | |||
| 0. Definitions. | |||
| "This License" refers to version 3 of the GNU General Public License. | |||
| "Copyright" also means copyright-like laws that apply to other kinds of | |||
| works, such as semiconductor masks. | |||
| "The Program" refers to any copyrightable work licensed under this | |||
| License. Each licensee is addressed as "you". "Licensees" and | |||
| "recipients" may be individuals or organizations. | |||
| To "modify" a work means to copy from or adapt all or part of the work | |||
| in a fashion requiring copyright permission, other than the making of an | |||
| exact copy. The resulting work is called a "modified version" of the | |||
| earlier work or a work "based on" the earlier work. | |||
| A "covered work" means either the unmodified Program or a work based | |||
| on the Program. | |||
| To "propagate" a work means to do anything with it that, without | |||
| permission, would make you directly or secondarily liable for | |||
| infringement under applicable copyright law, except executing it on a | |||
| computer or modifying a private copy. Propagation includes copying, | |||
| distribution (with or without modification), making available to the | |||
| public, and in some countries other activities as well. | |||
| To "convey" a work means any kind of propagation that enables other | |||
| parties to make or receive copies. Mere interaction with a user through | |||
| a computer network, with no transfer of a copy, is not conveying. | |||
| An interactive user interface displays "Appropriate Legal Notices" | |||
| to the extent that it includes a convenient and prominently visible | |||
| feature that (1) displays an appropriate copyright notice, and (2) | |||
| tells the user that there is no warranty for the work (except to the | |||
| extent that warranties are provided), that licensees may convey the | |||
| work under this License, and how to view a copy of this License. If | |||
| the interface presents a list of user commands or options, such as a | |||
| menu, a prominent item in the list meets this criterion. | |||
| 1. Source Code. | |||
| The "source code" for a work means the preferred form of the work | |||
| for making modifications to it. "Object code" means any non-source | |||
| form of a work. | |||
| A "Standard Interface" means an interface that either is an official | |||
| standard defined by a recognized standards body, or, in the case of | |||
| interfaces specified for a particular programming language, one that | |||
| is widely used among developers working in that language. | |||
| The "System Libraries" of an executable work include anything, other | |||
| than the work as a whole, that (a) is included in the normal form of | |||
| packaging a Major Component, but which is not part of that Major | |||
| Component, and (b) serves only to enable use of the work with that | |||
| Major Component, or to implement a Standard Interface for which an | |||
| implementation is available to the public in source code form. A | |||
| "Major Component", in this context, means a major essential component | |||
| (kernel, window system, and so on) of the specific operating system | |||
| (if any) on which the executable work runs, or a compiler used to | |||
| produce the work, or an object code interpreter used to run it. | |||
| The "Corresponding Source" for a work in object code form means all | |||
| the source code needed to generate, install, and (for an executable | |||
| work) run the object code and to modify the work, including scripts to | |||
| control those activities. However, it does not include the work's | |||
| System Libraries, or general-purpose tools or generally available free | |||
| programs which are used unmodified in performing those activities but | |||
| which are not part of the work. For example, Corresponding Source | |||
| includes interface definition files associated with source files for | |||
| the work, and the source code for shared libraries and dynamically | |||
| linked subprograms that the work is specifically designed to require, | |||
| such as by intimate data communication or control flow between those | |||
| subprograms and other parts of the work. | |||
| The Corresponding Source need not include anything that users | |||
| can regenerate automatically from other parts of the Corresponding | |||
| Source. | |||
| The Corresponding Source for a work in source code form is that | |||
| same work. | |||
| 2. Basic Permissions. | |||
| All rights granted under this License are granted for the term of | |||
| copyright on the Program, and are irrevocable provided the stated | |||
| conditions are met. This License explicitly affirms your unlimited | |||
| permission to run the unmodified Program. The output from running a | |||
| covered work is covered by this License only if the output, given its | |||
| content, constitutes a covered work. This License acknowledges your | |||
| rights of fair use or other equivalent, as provided by copyright law. | |||
| You may make, run and propagate covered works that you do not | |||
| convey, without conditions so long as your license otherwise remains | |||
| in force. You may convey covered works to others for the sole purpose | |||
| of having them make modifications exclusively for you, or provide you | |||
| with facilities for running those works, provided that you comply with | |||
| the terms of this License in conveying all material for which you do | |||
| not control copyright. Those thus making or running the covered works | |||
| for you must do so exclusively on your behalf, under your direction | |||
| and control, on terms that prohibit them from making any copies of | |||
| your copyrighted material outside their relationship with you. | |||
| Conveying under any other circumstances is permitted solely under | |||
| the conditions stated below. Sublicensing is not allowed; section 10 | |||
| makes it unnecessary. | |||
| 3. Protecting Users' Legal Rights From Anti-Circumvention Law. | |||
| No covered work shall be deemed part of an effective technological | |||
| measure under any applicable law fulfilling obligations under article | |||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | |||
| similar laws prohibiting or restricting circumvention of such | |||
| measures. | |||
| When you convey a covered work, you waive any legal power to forbid | |||
| circumvention of technological measures to the extent such circumvention | |||
| is effected by exercising rights under this License with respect to | |||
| the covered work, and you disclaim any intention to limit operation or | |||
| modification of the work as a means of enforcing, against the work's | |||
| users, your or third parties' legal rights to forbid circumvention of | |||
| technological measures. | |||
| 4. Conveying Verbatim Copies. | |||
| You may convey verbatim copies of the Program's source code as you | |||
| receive it, in any medium, provided that you conspicuously and | |||
| appropriately publish on each copy an appropriate copyright notice; | |||
| keep intact all notices stating that this License and any | |||
| non-permissive terms added in accord with section 7 apply to the code; | |||
| keep intact all notices of the absence of any warranty; and give all | |||
| recipients a copy of this License along with the Program. | |||
| You may charge any price or no price for each copy that you convey, | |||
| and you may offer support or warranty protection for a fee. | |||
| 5. Conveying Modified Source Versions. | |||
| You may convey a work based on the Program, or the modifications to | |||
| produce it from the Program, in the form of source code under the | |||
| terms of section 4, provided that you also meet all of these conditions: | |||
| a) The work must carry prominent notices stating that you modified | |||
| it, and giving a relevant date. | |||
| b) The work must carry prominent notices stating that it is | |||
| released under this License and any conditions added under section | |||
| 7. This requirement modifies the requirement in section 4 to | |||
| "keep intact all notices". | |||
| c) You must license the entire work, as a whole, under this | |||
| License to anyone who comes into possession of a copy. This | |||
| License will therefore apply, along with any applicable section 7 | |||
| additional terms, to the whole of the work, and all its parts, | |||
| regardless of how they are packaged. This License gives no | |||
| permission to license the work in any other way, but it does not | |||
| invalidate such permission if you have separately received it. | |||
| d) If the work has interactive user interfaces, each must display | |||
| Appropriate Legal Notices; however, if the Program has interactive | |||
| interfaces that do not display Appropriate Legal Notices, your | |||
| work need not make them do so. | |||
| A compilation of a covered work with other separate and independent | |||
| works, which are not by their nature extensions of the covered work, | |||
| and which are not combined with it such as to form a larger program, | |||
| in or on a volume of a storage or distribution medium, is called an | |||
| "aggregate" if the compilation and its resulting copyright are not | |||
| used to limit the access or legal rights of the compilation's users | |||
| beyond what the individual works permit. Inclusion of a covered work | |||
| in an aggregate does not cause this License to apply to the other | |||
| parts of the aggregate. | |||
| 6. Conveying Non-Source Forms. | |||
| You may convey a covered work in object code form under the terms | |||
| of sections 4 and 5, provided that you also convey the | |||
| machine-readable Corresponding Source under the terms of this License, | |||
| in one of these ways: | |||
| a) Convey the object code in, or embodied in, a physical product | |||
| (including a physical distribution medium), accompanied by the | |||
| Corresponding Source fixed on a durable physical medium | |||
| customarily used for software interchange. | |||
| b) Convey the object code in, or embodied in, a physical product | |||
| (including a physical distribution medium), accompanied by a | |||
| written offer, valid for at least three years and valid for as | |||
| long as you offer spare parts or customer support for that product | |||
| model, to give anyone who possesses the object code either (1) a | |||
| copy of the Corresponding Source for all the software in the | |||
| product that is covered by this License, on a durable physical | |||
| medium customarily used for software interchange, for a price no | |||
| more than your reasonable cost of physically performing this | |||
| conveying of source, or (2) access to copy the | |||
| Corresponding Source from a network server at no charge. | |||
| c) Convey individual copies of the object code with a copy of the | |||
| written offer to provide the Corresponding Source. This | |||
| alternative is allowed only occasionally and noncommercially, and | |||
| only if you received the object code with such an offer, in accord | |||
| with subsection 6b. | |||
| d) Convey the object code by offering access from a designated | |||
| place (gratis or for a charge), and offer equivalent access to the | |||
| Corresponding Source in the same way through the same place at no | |||
| further charge. You need not require recipients to copy the | |||
| Corresponding Source along with the object code. If the place to | |||
| copy the object code is a network server, the Corresponding Source | |||
| may be on a different server (operated by you or a third party) | |||
| that supports equivalent copying facilities, provided you maintain | |||
| clear directions next to the object code saying where to find the | |||
| Corresponding Source. Regardless of what server hosts the | |||
| Corresponding Source, you remain obligated to ensure that it is | |||
| available for as long as needed to satisfy these requirements. | |||
| e) Convey the object code using peer-to-peer transmission, provided | |||
| you inform other peers where the object code and Corresponding | |||
| Source of the work are being offered to the general public at no | |||
| charge under subsection 6d. | |||
| A separable portion of the object code, whose source code is excluded | |||
| from the Corresponding Source as a System Library, need not be | |||
| included in conveying the object code work. | |||
| A "User Product" is either (1) a "consumer product", which means any | |||
| tangible personal property which is normally used for personal, family, | |||
| or household purposes, or (2) anything designed or sold for incorporation | |||
| into a dwelling. In determining whether a product is a consumer product, | |||
| doubtful cases shall be resolved in favor of coverage. For a particular | |||
| product received by a particular user, "normally used" refers to a | |||
| typical or common use of that class of product, regardless of the status | |||
| of the particular user or of the way in which the particular user | |||
| actually uses, or expects or is expected to use, the product. A product | |||
| is a consumer product regardless of whether the product has substantial | |||
| commercial, industrial or non-consumer uses, unless such uses represent | |||
| the only significant mode of use of the product. | |||
| "Installation Information" for a User Product means any methods, | |||
| procedures, authorization keys, or other information required to install | |||
| and execute modified versions of a covered work in that User Product from | |||
| a modified version of its Corresponding Source. The information must | |||
| suffice to ensure that the continued functioning of the modified object | |||
| code is in no case prevented or interfered with solely because | |||
| modification has been made. | |||
| If you convey an object code work under this section in, or with, or | |||
| specifically for use in, a User Product, and the conveying occurs as | |||
| part of a transaction in which the right of possession and use of the | |||
| User Product is transferred to the recipient in perpetuity or for a | |||
| fixed term (regardless of how the transaction is characterized), the | |||
| Corresponding Source conveyed under this section must be accompanied | |||
| by the Installation Information. But this requirement does not apply | |||
| if neither you nor any third party retains the ability to install | |||
| modified object code on the User Product (for example, the work has | |||
| been installed in ROM). | |||
| The requirement to provide Installation Information does not include a | |||
| requirement to continue to provide support service, warranty, or updates | |||
| for a work that has been modified or installed by the recipient, or for | |||
| the User Product in which it has been modified or installed. Access to a | |||
| network may be denied when the modification itself materially and | |||
| adversely affects the operation of the network or violates the rules and | |||
| protocols for communication across the network. | |||
| Corresponding Source conveyed, and Installation Information provided, | |||
| in accord with this section must be in a format that is publicly | |||
| documented (and with an implementation available to the public in | |||
| source code form), and must require no special password or key for | |||
| unpacking, reading or copying. | |||
| 7. Additional Terms. | |||
| "Additional permissions" are terms that supplement the terms of this | |||
| License by making exceptions from one or more of its conditions. | |||
| Additional permissions that are applicable to the entire Program shall | |||
| be treated as though they were included in this License, to the extent | |||
| that they are valid under applicable law. If additional permissions | |||
| apply only to part of the Program, that part may be used separately | |||
| under those permissions, but the entire Program remains governed by | |||
| this License without regard to the additional permissions. | |||
| When you convey a copy of a covered work, you may at your option | |||
| remove any additional permissions from that copy, or from any part of | |||
| it. (Additional permissions may be written to require their own | |||
| removal in certain cases when you modify the work.) You may place | |||
| additional permissions on material, added by you to a covered work, | |||
| for which you have or can give appropriate copyright permission. | |||
| Notwithstanding any other provision of this License, for material you | |||
| add to a covered work, you may (if authorized by the copyright holders of | |||
| that material) supplement the terms of this License with terms: | |||
| a) Disclaiming warranty or limiting liability differently from the | |||
| terms of sections 15 and 16 of this License; or | |||
| b) Requiring preservation of specified reasonable legal notices or | |||
| author attributions in that material or in the Appropriate Legal | |||
| Notices displayed by works containing it; or | |||
| c) Prohibiting misrepresentation of the origin of that material, or | |||
| requiring that modified versions of such material be marked in | |||
| reasonable ways as different from the original version; or | |||
| d) Limiting the use for publicity purposes of names of licensors or | |||
| authors of the material; or | |||
| e) Declining to grant rights under trademark law for use of some | |||
| trade names, trademarks, or service marks; or | |||
| f) Requiring indemnification of licensors and authors of that | |||
| material by anyone who conveys the material (or modified versions of | |||
| it) with contractual assumptions of liability to the recipient, for | |||
| any liability that these contractual assumptions directly impose on | |||
| those licensors and authors. | |||
| All other non-permissive additional terms are considered "further | |||
| restrictions" within the meaning of section 10. If the Program as you | |||
| received it, or any part of it, contains a notice stating that it is | |||
| governed by this License along with a term that is a further | |||
| restriction, you may remove that term. If a license document contains | |||
| a further restriction but permits relicensing or conveying under this | |||
| License, you may add to a covered work material governed by the terms | |||
| of that license document, provided that the further restriction does | |||
| not survive such relicensing or conveying. | |||
| If you add terms to a covered work in accord with this section, you | |||
| must place, in the relevant source files, a statement of the | |||
| additional terms that apply to those files, or a notice indicating | |||
| where to find the applicable terms. | |||
| Additional terms, permissive or non-permissive, may be stated in the | |||
| form of a separately written license, or stated as exceptions; | |||
| the above requirements apply either way. | |||
| 8. Termination. | |||
| You may not propagate or modify a covered work except as expressly | |||
| provided under this License. Any attempt otherwise to propagate or | |||
| modify it is void, and will automatically terminate your rights under | |||
| this License (including any patent licenses granted under the third | |||
| paragraph of section 11). | |||
| However, if you cease all violation of this License, then your | |||
| license from a particular copyright holder is reinstated (a) | |||
| provisionally, unless and until the copyright holder explicitly and | |||
| finally terminates your license, and (b) permanently, if the copyright | |||
| holder fails to notify you of the violation by some reasonable means | |||
| prior to 60 days after the cessation. | |||
| Moreover, your license from a particular copyright holder is | |||
| reinstated permanently if the copyright holder notifies you of the | |||
| violation by some reasonable means, this is the first time you have | |||
| received notice of violation of this License (for any work) from that | |||
| copyright holder, and you cure the violation prior to 30 days after | |||
| your receipt of the notice. | |||
| Termination of your rights under this section does not terminate the | |||
| licenses of parties who have received copies or rights from you under | |||
| this License. If your rights have been terminated and not permanently | |||
| reinstated, you do not qualify to receive new licenses for the same | |||
| material under section 10. | |||
| 9. Acceptance Not Required for Having Copies. | |||
| You are not required to accept this License in order to receive or | |||
| run a copy of the Program. Ancillary propagation of a covered work | |||
| occurring solely as a consequence of using peer-to-peer transmission | |||
| to receive a copy likewise does not require acceptance. However, | |||
| nothing other than this License grants you permission to propagate or | |||
| modify any covered work. These actions infringe copyright if you do | |||
| not accept this License. Therefore, by modifying or propagating a | |||
| covered work, you indicate your acceptance of this License to do so. | |||
| 10. Automatic Licensing of Downstream Recipients. | |||
| Each time you convey a covered work, the recipient automatically | |||
| receives a license from the original licensors, to run, modify and | |||
| propagate that work, subject to this License. You are not responsible | |||
| for enforcing compliance by third parties with this License. | |||
| An "entity transaction" is a transaction transferring control of an | |||
| organization, or substantially all assets of one, or subdividing an | |||
| organization, or merging organizations. If propagation of a covered | |||
| work results from an entity transaction, each party to that | |||
| transaction who receives a copy of the work also receives whatever | |||
| licenses to the work the party's predecessor in interest had or could | |||
| give under the previous paragraph, plus a right to possession of the | |||
| Corresponding Source of the work from the predecessor in interest, if | |||
| the predecessor has it or can get it with reasonable efforts. | |||
| You may not impose any further restrictions on the exercise of the | |||
| rights granted or affirmed under this License. For example, you may | |||
| not impose a license fee, royalty, or other charge for exercise of | |||
| rights granted under this License, and you may not initiate litigation | |||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | |||
| any patent claim is infringed by making, using, selling, offering for | |||
| sale, or importing the Program or any portion of it. | |||
| 11. Patents. | |||
| A "contributor" is a copyright holder who authorizes use under this | |||
| License of the Program or a work on which the Program is based. The | |||
| work thus licensed is called the contributor's "contributor version". | |||
| A contributor's "essential patent claims" are all patent claims | |||
| owned or controlled by the contributor, whether already acquired or | |||
| hereafter acquired, that would be infringed by some manner, permitted | |||
| by this License, of making, using, or selling its contributor version, | |||
| but do not include claims that would be infringed only as a | |||
| consequence of further modification of the contributor version. For | |||
| purposes of this definition, "control" includes the right to grant | |||
| patent sublicenses in a manner consistent with the requirements of | |||
| this License. | |||
| Each contributor grants you a non-exclusive, worldwide, royalty-free | |||
| patent license under the contributor's essential patent claims, to | |||
| make, use, sell, offer for sale, import and otherwise run, modify and | |||
| propagate the contents of its contributor version. | |||
| In the following three paragraphs, a "patent license" is any express | |||
| agreement or commitment, however denominated, not to enforce a patent | |||
| (such as an express permission to practice a patent or covenant not to | |||
| sue for patent infringement). To "grant" such a patent license to a | |||
| party means to make such an agreement or commitment not to enforce a | |||
| patent against the party. | |||
| If you convey a covered work, knowingly relying on a patent license, | |||
| and the Corresponding Source of the work is not available for anyone | |||
| to copy, free of charge and under the terms of this License, through a | |||
| publicly available network server or other readily accessible means, | |||
| then you must either (1) cause the Corresponding Source to be so | |||
| available, or (2) arrange to deprive yourself of the benefit of the | |||
| patent license for this particular work, or (3) arrange, in a manner | |||
| consistent with the requirements of this License, to extend the patent | |||
| license to downstream recipients. "Knowingly relying" means you have | |||
| actual knowledge that, but for the patent license, your conveying the | |||
| covered work in a country, or your recipient's use of the covered work | |||
| in a country, would infringe one or more identifiable patents in that | |||
| country that you have reason to believe are valid. | |||
| If, pursuant to or in connection with a single transaction or | |||
| arrangement, you convey, or propagate by procuring conveyance of, a | |||
| covered work, and grant a patent license to some of the parties | |||
| receiving the covered work authorizing them to use, propagate, modify | |||
| or convey a specific copy of the covered work, then the patent license | |||
| you grant is automatically extended to all recipients of the covered | |||
| work and works based on it. | |||
| A patent license is "discriminatory" if it does not include within | |||
| the scope of its coverage, prohibits the exercise of, or is | |||
| conditioned on the non-exercise of one or more of the rights that are | |||
| specifically granted under this License. You may not convey a covered | |||
| work if you are a party to an arrangement with a third party that is | |||
| in the business of distributing software, under which you make payment | |||
| to the third party based on the extent of your activity of conveying | |||
| the work, and under which the third party grants, to any of the | |||
| parties who would receive the covered work from you, a discriminatory | |||
| patent license (a) in connection with copies of the covered work | |||
| conveyed by you (or copies made from those copies), or (b) primarily | |||
| for and in connection with specific products or compilations that | |||
| contain the covered work, unless you entered into that arrangement, | |||
| or that patent license was granted, prior to 28 March 2007. | |||
| Nothing in this License shall be construed as excluding or limiting | |||
| any implied license or other defenses to infringement that may | |||
| otherwise be available to you under applicable patent law. | |||
| 12. No Surrender of Others' Freedom. | |||
| If conditions are imposed on you (whether by court order, agreement or | |||
| otherwise) that contradict the conditions of this License, they do not | |||
| excuse you from the conditions of this License. If you cannot convey a | |||
| covered work so as to satisfy simultaneously your obligations under this | |||
| License and any other pertinent obligations, then as a consequence you may | |||
| not convey it at all. For example, if you agree to terms that obligate you | |||
| to collect a royalty for further conveying from those to whom you convey | |||
| the Program, the only way you could satisfy both those terms and this | |||
| License would be to refrain entirely from conveying the Program. | |||
| 13. Use with the GNU Affero General Public License. | |||
| Notwithstanding any other provision of this License, you have | |||
| permission to link or combine any covered work with a work licensed | |||
| under version 3 of the GNU Affero General Public License into a single | |||
| combined work, and to convey the resulting work. The terms of this | |||
| License will continue to apply to the part which is the covered work, | |||
| but the special requirements of the GNU Affero General Public License, | |||
| section 13, concerning interaction through a network will apply to the | |||
| combination as such. | |||
| 14. Revised Versions of this License. | |||
| The Free Software Foundation may publish revised and/or new versions of | |||
| the GNU General Public License from time to time. Such new versions will | |||
| be similar in spirit to the present version, but may differ in detail to | |||
| address new problems or concerns. | |||
| Each version is given a distinguishing version number. If the | |||
| Program specifies that a certain numbered version of the GNU General | |||
| Public License "or any later version" applies to it, you have the | |||
| option of following the terms and conditions either of that numbered | |||
| version or of any later version published by the Free Software | |||
| Foundation. If the Program does not specify a version number of the | |||
| GNU General Public License, you may choose any version ever published | |||
| by the Free Software Foundation. | |||
| If the Program specifies that a proxy can decide which future | |||
| versions of the GNU General Public License can be used, that proxy's | |||
| public statement of acceptance of a version permanently authorizes you | |||
| to choose that version for the Program. | |||
| Later license versions may give you additional or different | |||
| permissions. However, no additional obligations are imposed on any | |||
| author or copyright holder as a result of your choosing to follow a | |||
| later version. | |||
| 15. Disclaimer of Warranty. | |||
| THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | |||
| APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | |||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | |||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | |||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |||
| PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | |||
| IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | |||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | |||
| 16. Limitation of Liability. | |||
| IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | |||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | |||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | |||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | |||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | |||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | |||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | |||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | |||
| SUCH DAMAGES. | |||
| 17. Interpretation of Sections 15 and 16. | |||
| If the disclaimer of warranty and limitation of liability provided | |||
| above cannot be given local legal effect according to their terms, | |||
| reviewing courts shall apply local law that most closely approximates | |||
| an absolute waiver of all civil liability in connection with the | |||
| Program, unless a warranty or assumption of liability accompanies a | |||
| copy of the Program in return for a fee. | |||
| END OF TERMS AND CONDITIONS | |||
| How to Apply These Terms to Your New Programs | |||
| If you develop a new program, and you want it to be of the greatest | |||
| possible use to the public, the best way to achieve this is to make it | |||
| free software which everyone can redistribute and change under these terms. | |||
| To do so, attach the following notices to the program. It is safest | |||
| to attach them to the start of each source file to most effectively | |||
| state the exclusion of warranty; and each file should have at least | |||
| the "copyright" line and a pointer to where the full notice is found. | |||
| <one line to give the program's name and a brief idea of what it does.> | |||
| Copyright (C) <year> <name of author> | |||
| This program is free software: you can redistribute it and/or modify | |||
| it under the terms of the GNU General Public License as published by | |||
| the Free Software Foundation, either version 3 of the License, or | |||
| (at your option) any later version. | |||
| This program is distributed in the hope that it will be useful, | |||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| GNU General Public License for more details. | |||
| You should have received a copy of the GNU General Public License | |||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
| Also add information on how to contact you by electronic and paper mail. | |||
| If the program does terminal interaction, make it output a short | |||
| notice like this when it starts in an interactive mode: | |||
| <program> Copyright (C) <year> <name of author> | |||
| This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | |||
| This is free software, and you are welcome to redistribute it | |||
| under certain conditions; type `show c' for details. | |||
| The hypothetical commands `show w' and `show c' should show the appropriate | |||
| parts of the General Public License. Of course, your program's commands | |||
| might be different; for a GUI interface, you would use an "about box". | |||
| You should also get your employer (if you work as a programmer) or school, | |||
| if any, to sign a "copyright disclaimer" for the program, if necessary. | |||
| For more information on this, and how to apply and follow the GNU GPL, see | |||
| <https://www.gnu.org/licenses/>. | |||
| The GNU General Public License does not permit incorporating your program | |||
| into proprietary programs. If your program is a subroutine library, you | |||
| may consider it more useful to permit linking proprietary applications with | |||
| the library. If this is what you want to do, use the GNU Lesser General | |||
| Public License instead of this License. But first, please read | |||
| <https://www.gnu.org/licenses/why-not-lgpl.html>. | |||
| @@ -0,0 +1,141 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 14/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 {PerspectiveCamera} from 'three'; | |||
| import {DrawPass, SVGDrawHandler, SVGDrawInfo, SVGDrawOptions, Viewmap, ViewmapBuildInfo, ViewmapOptions} from './core'; | |||
| import {SVGMesh} from './core/SVGMesh'; | |||
| import {Svg} from '@svgdotjs/svg.js'; | |||
| // import format from 'xml-formatter'; | |||
| export interface ExportOptions { | |||
| prettify?: boolean; | |||
| } | |||
| export class SVGRenderInfo { | |||
| resolution = {w: Infinity, h: Infinity}; | |||
| renderingTime = Infinity; | |||
| readonly svgDrawInfo = new SVGDrawInfo(); | |||
| readonly viewmapInfo = new ViewmapBuildInfo(); | |||
| } | |||
| export interface ProgressInfo { | |||
| currentStepName: string; | |||
| currentStep: number; | |||
| totalSteps: number; | |||
| } | |||
| /** | |||
| * | |||
| */ | |||
| export class SVGRenderer { | |||
| readonly viewmap; | |||
| readonly drawHandler; | |||
| constructor(vOptions?: ViewmapOptions, sOptions?: SVGDrawOptions) { | |||
| this.viewmap = new Viewmap(vOptions); | |||
| this.drawHandler = new SVGDrawHandler(sOptions); | |||
| } | |||
| /** | |||
| * Render a SVG file from the given meshes and returns it. | |||
| * @param meshes Mehses to render | |||
| * @param camera Camera used to compute the perspective | |||
| * @param size Size of the render (will be scaled by camera aspect ratio) | |||
| * @param options Options to customize the render | |||
| * @param info Object containing info (e.g. times) on the rendering process | |||
| * @returns SVG object from the Svgdotjs lib | |||
| */ | |||
| async generateSVG( | |||
| meshes: Array<SVGMesh>, | |||
| camera: PerspectiveCamera, | |||
| size: {w: number, h: number}, | |||
| info = new SVGRenderInfo()): Promise<Svg> { | |||
| const renderStartTime = Date.now(); | |||
| // Setup camera keeping | |||
| const renderSize = {w: size.w, h: size.w/camera.aspect}; | |||
| info.resolution = renderSize; | |||
| // Viewmap Build | |||
| await this.viewmap.build( | |||
| meshes, camera, renderSize, info.viewmapInfo | |||
| ); | |||
| // SVG Buid | |||
| const svg = await this.drawHandler.drawSVG( | |||
| this.viewmap, renderSize, info.svgDrawInfo | |||
| ); | |||
| info.renderingTime = Date.now() - renderStartTime; | |||
| // console.log(JSON.parse(JSON.stringify(info))); | |||
| return svg; | |||
| } | |||
| /** | |||
| * Adds a pass to the SVG rendering pipeline. | |||
| * @param pass | |||
| */ | |||
| addPass(pass: DrawPass) { | |||
| if (!this.drawHandler.passes.includes(pass)) { | |||
| this.drawHandler.passes.push(pass); | |||
| } | |||
| } | |||
| /** | |||
| * Removes a pass from the SVG rendering pipeline | |||
| * @param pass | |||
| */ | |||
| removePass(pass: DrawPass) { | |||
| this.drawHandler.passes.remove(pass); | |||
| } | |||
| /** | |||
| * Removes all the passes from the SVG rendering pipeline. | |||
| */ | |||
| clearPasses() { | |||
| this.drawHandler.passes.clear(); | |||
| } | |||
| // static exportSVG(svg: Svg, filename: string, options?: ExportOptions) { | |||
| // | |||
| // const opt = { | |||
| // prettify: false, | |||
| // ...options, | |||
| // } | |||
| // | |||
| // let text = svg.svg(); | |||
| // if (opt.prettify) { | |||
| // text = (text, {}); | |||
| // } | |||
| // const svgBlob = new Blob([text], {type:"image/svg+xml;charset=utf-8"}); | |||
| // const svgUrl = URL.createObjectURL(svgBlob); | |||
| // const downloadLink = document.createElement("a"); | |||
| // downloadLink.href = svgUrl; | |||
| // downloadLink.download = filename; | |||
| // document.body.appendChild(downloadLink); | |||
| // downloadLink.click(); | |||
| // document.body.removeChild(downloadLink); | |||
| // } | |||
| } | |||
| @@ -0,0 +1,172 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 09/12/2021 | |||
| // 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 {Color, Material, Mesh, Vector3} from 'three'; | |||
| import {HalfedgeDS} from '../../three-mesh-halfedge'; | |||
| import {acceleratedRaycast, CENTER, MeshBVH, MeshBVHOptions} from 'three-mesh-bvh'; | |||
| import {computeMorphedGeometry, disposeMesh} from '../utils/buffergeometry'; | |||
| type ColorMaterial = Material & {color: Color}; | |||
| export interface SVGMeshOptions { | |||
| bvhOptions?: MeshBVHOptions; | |||
| } | |||
| /** | |||
| * SVGTexture allows to add a texture to a SVGMesh. | |||
| * Raster image (.jpeg, .png) or vector graphics (.svg) are supported. | |||
| */ | |||
| export interface SVGTexture { | |||
| /** | |||
| * Name of the texture | |||
| */ | |||
| name: string; | |||
| /** | |||
| * DataUrl to the image and vector graphics texture | |||
| */ | |||
| url: string; | |||
| } | |||
| /** | |||
| * Mesh object that can be rendered as SVG. | |||
| * Wrapper class around three mesh object that duplicates geometry if needed (i.e. | |||
| * for SkinnedMesh) and computes BVH and HalfEdgeStructure on demand) | |||
| */ | |||
| export class SVGMesh { | |||
| readonly sourceMesh: Mesh; | |||
| readonly threeMesh = new Mesh(); | |||
| readonly hes: HalfedgeDS; | |||
| readonly bvhOptions: MeshBVHOptions; | |||
| bvh: MeshBVH; | |||
| drawFills = true; | |||
| drawVisibleContours = true; | |||
| drawHiddenContours = true; | |||
| isUsingBVHForRaycasting = false; | |||
| texture?: SVGTexture; | |||
| constructor(mesh: Mesh, options: SVGMeshOptions = {}) { | |||
| this.sourceMesh = mesh; | |||
| this.threeMesh.copy(mesh); | |||
| // if(this.sourceMesh.geometry.index){ | |||
| // this.threeMesh.geometry = this.sourceMesh.geometry.toNonIndexed(); | |||
| // }else { | |||
| this.threeMesh.geometry = this.sourceMesh.geometry.clone(); | |||
| // } | |||
| // this.threeMesh.geometry = toIndexedGeometry(this.sourceMesh.geometry, 1); | |||
| // Setup HES | |||
| this.hes = new HalfedgeDS(); | |||
| // const t = this.hes.addEdge | |||
| // this.hes.addEdge = (...args)=>{ | |||
| // try{ | |||
| // return t.call(this.hes, ...args) | |||
| // }catch(e){ | |||
| // console.error(e) | |||
| // console.log('args', args) | |||
| // } | |||
| // } | |||
| // Setup BVH | |||
| const bvhOptions = { | |||
| maxLeafTris: 1, | |||
| strategy: CENTER, | |||
| ...options?.bvhOptions | |||
| } | |||
| this.bvhOptions = bvhOptions | |||
| this.bvh = new MeshBVH(this.threeMesh.geometry, bvhOptions); | |||
| this.threeMesh.geometry.boundsTree = this.bvh; | |||
| this.threeMesh.raycast = acceleratedRaycast; | |||
| } | |||
| /** | |||
| * Adds a SVGtexture to the mesh. | |||
| * | |||
| * @param texture The image or vector graphics texture to use. | |||
| */ | |||
| addTexture(texture: SVGTexture) { | |||
| this.texture = texture; | |||
| } | |||
| updateMorphGeometry() { | |||
| computeMorphedGeometry(this.sourceMesh, this.threeMesh.geometry); | |||
| } | |||
| // private _i= 0 | |||
| updateBVH(updateMorphGeometry = true) { | |||
| // if(this._i) return | |||
| // this._i++ | |||
| updateMorphGeometry && this.updateMorphGeometry(); | |||
| this.bvh.refit(); | |||
| } | |||
| updateHES(updateMorphGeometry = true) { | |||
| // if(!force && this.hes.faces.length) return | |||
| updateMorphGeometry && this.updateMorphGeometry(); | |||
| this.hes.setFromGeometry(this.threeMesh.geometry, 1e-10); | |||
| } | |||
| localToWorld(target: Vector3): Vector3 { | |||
| return this.threeMesh.localToWorld(target); | |||
| } | |||
| colorForFaceIndex(faceIndex: number): null | Color { | |||
| if (Array.isArray(this.material)) { | |||
| for (const group of this.threeMesh.geometry.groups) { | |||
| if (group.start <= faceIndex && | |||
| faceIndex < (group.start + group.count) && | |||
| group.materialIndex != undefined && | |||
| group.materialIndex < this.material.length) { | |||
| return colorForMaterial(this.material[group.materialIndex]); | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| return colorForMaterial(this.material); | |||
| } | |||
| dispose() { | |||
| disposeMesh(this.threeMesh); | |||
| } | |||
| get material() { return this.threeMesh.material; } | |||
| get matrixWorld() { return this.threeMesh.matrixWorld; } | |||
| get name() { return this.threeMesh.name; } | |||
| set name(name: string) { this.threeMesh.name = name; } | |||
| updateObject(){ | |||
| // const g = this.sourceMesh.geometry; | |||
| // const ud = this.sourceMesh.userData; | |||
| // this.sourceMesh.userData = {}; | |||
| // this.threeMesh.copy(this.sourceMesh, false); | |||
| // this.sourceMesh.userData = ud; | |||
| // this.threeMesh.geometry = g; | |||
| this.threeMesh.position.copy(this.sourceMesh.position); | |||
| this.threeMesh.quaternion.copy(this.sourceMesh.quaternion); | |||
| this.threeMesh.scale.copy(this.sourceMesh.scale); | |||
| this.threeMesh.updateMatrix(); | |||
| this.threeMesh.updateMatrixWorld(); | |||
| } | |||
| remakeBVH(){ | |||
| this.bvh = new MeshBVH(this.threeMesh.geometry, this.bvhOptions); | |||
| this.threeMesh.geometry.boundsTree = this.bvh; | |||
| } | |||
| } | |||
| function colorForMaterial(material: Material) { | |||
| const colorMaterial = material as ColorMaterial; | |||
| return colorMaterial.color; | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Mon Oct 17 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 | |||
| */ | |||
| export * from "./viewmap"; | |||
| export * from "./svg"; | |||
| export { SVGMesh, type SVGMeshOptions, type SVGTexture } from "./SVGMesh"; | |||
| @@ -0,0 +1,78 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 23/02/2021 | |||
| // 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 {Viewmap} from '../viewmap/Viewmap'; | |||
| import {SizeLike} from '../../utils/geometry'; | |||
| import {Svg} from '@svgdotjs/svg.js'; | |||
| import '@svgdotjs/svg.topath.js'; | |||
| import {DrawPass} from './passes/DrawPass'; | |||
| export interface SVGDrawPassInfo { | |||
| name: string; | |||
| order: number; | |||
| time: number; | |||
| } | |||
| export class SVGDrawInfo { | |||
| totalTime = Infinity; | |||
| passesInfo = new Array<SVGDrawPassInfo>(); | |||
| } | |||
| export interface SVGDrawOptions { | |||
| prettifySVG?: boolean; | |||
| } | |||
| export class SVGDrawHandler { | |||
| readonly options: Required<SVGDrawOptions> = { | |||
| prettifySVG: false, | |||
| }; | |||
| readonly passes = new Array<DrawPass>(); | |||
| constructor(options?: SVGDrawOptions) { | |||
| Object.assign(this.options, options); | |||
| } | |||
| async drawSVG( | |||
| viewmap: Viewmap, | |||
| size: SizeLike, | |||
| info = new SVGDrawInfo() | |||
| ): Promise<Svg> { | |||
| const buildStartTime = Date.now(); | |||
| const svg = new Svg(); | |||
| svg.width(size.w); | |||
| svg.height(size.h); | |||
| // Call the draw passes | |||
| for (let i=0; i<this.passes.length; i++) { | |||
| const pass = this.passes[i]; | |||
| if (pass.enabled) { | |||
| const passStartTime = Date.now(); | |||
| await pass.draw(svg, viewmap); | |||
| info.passesInfo.push({ | |||
| name: pass.name, | |||
| order: i, | |||
| time: Date.now() - passStartTime, | |||
| }); | |||
| } | |||
| } | |||
| info.totalTime = Date.now() - buildStartTime; | |||
| return svg; | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Thu Oct 20 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 | |||
| */ | |||
| export { | |||
| SVGDrawHandler, | |||
| type SVGDrawOptions, | |||
| type SVGDrawPassInfo, | |||
| SVGDrawInfo | |||
| } from './SVGDrawHandler'; | |||
| export * from './passes'; | |||
| @@ -0,0 +1,303 @@ | |||
| // 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; | |||
| } | |||
| @@ -0,0 +1,37 @@ | |||
| // 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 {Svg} from '@svgdotjs/svg.js'; | |||
| import {Viewmap} from '../../viewmap/Viewmap'; | |||
| export abstract class DrawPass { | |||
| /** | |||
| * Name of the draw pass | |||
| */ | |||
| readonly name: string; | |||
| /** | |||
| * Enables/Disables draw pass. | |||
| * @defaultValue `true` | |||
| */ | |||
| enabled = true; | |||
| constructor() { | |||
| this.name = this.constructor.name; | |||
| } | |||
| /** | |||
| * Function automatically called by the `SVGDrawHandler` | |||
| * @param svg The svg tree being built | |||
| * @param viewmap The viewmap data structure | |||
| */ | |||
| abstract draw(svg: Svg, viewmap: Viewmap): Promise<void>; | |||
| } | |||
| @@ -0,0 +1,227 @@ | |||
| // 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 | |||
| // Modified by repalash <palash@shaders.app>. Added support for cropping and overlaying rendered image, added parameters and other small changes. | |||
| import {DrawPass} from './DrawPass'; | |||
| import {Viewmap} from '../../viewmap/Viewmap'; | |||
| import {Color as SVGColor, Element as SVGElement, G as SVGGroup, Image, Pattern, Svg} from '@svgdotjs/svg.js'; | |||
| import {getSVGCircle, getSVGPath} from '../svgutils'; | |||
| import {Polygon} from '../../viewmap/Polygon'; | |||
| import {mergeOptions} from '../../../utils/objects'; | |||
| import {Box2, Vector2} from 'three' | |||
| export interface FillPassOptions { | |||
| drawRaycastPoint?: boolean; | |||
| /** | |||
| * Use a random color for each polygon in the svg. Overwrites | |||
| * {@link useFixedStyle} if `true`. | |||
| * @defaultValue `false` | |||
| */ | |||
| useRandomColors?: boolean; | |||
| /** | |||
| * Use a fixed style ()`color` and/or `opacity`) provided by {@link fillStyle} | |||
| * instead of mesh material. | |||
| * @defaultValue `false` | |||
| */ | |||
| useFixedStyle?: boolean; | |||
| /** | |||
| * Fixed style to apply to polygons | |||
| */ | |||
| fillStyle?: FillStyle; | |||
| strokeStyle?: FillStyle; | |||
| fillImage?: string; | |||
| } | |||
| export interface FillStyle { | |||
| /** | |||
| * Color of the polygons. | |||
| * @defaultValue `"#333333"` | |||
| */ | |||
| color?: string; | |||
| /** | |||
| * Opacity of the polygons. | |||
| * @defaultValue `1` | |||
| */ | |||
| opacity?: number; | |||
| } | |||
| export class FillPass extends DrawPass { | |||
| drawFills = true; | |||
| drawStrokes = true; | |||
| drawImageFills = false; | |||
| readonly options: Required<FillPassOptions> = { | |||
| drawRaycastPoint: false, | |||
| useRandomColors: false, | |||
| useFixedStyle: false, | |||
| fillStyle: { | |||
| color: "#333333", | |||
| opacity: 1, | |||
| }, | |||
| strokeStyle: { | |||
| color: "#111111", | |||
| opacity: 1, | |||
| }, | |||
| fillImage: '', | |||
| }; | |||
| constructor(options: FillPassOptions = {}) { | |||
| super(); | |||
| mergeOptions(this.options, options); | |||
| } | |||
| async draw(svg: Svg, viewmap: Viewmap) { | |||
| let img = this.options.fillImage && this.drawImageFills ? new window.Image() : undefined | |||
| if(img) { | |||
| await new Promise<void>((res, rej) => { | |||
| img!.onload = () => { | |||
| res() | |||
| } | |||
| img!.onerror = (e) => { | |||
| console.error('error loading image', e) | |||
| rej() | |||
| } | |||
| img!.src = this.options.fillImage! | |||
| }).catch((e) => { | |||
| img = undefined | |||
| console.error('error loading image', e) | |||
| }) | |||
| } | |||
| // debugger | |||
| const group = new SVGGroup({id: "fills"}); | |||
| svg.add(group); | |||
| for (const mesh of viewmap.meshes) { | |||
| if (mesh.drawFills) { | |||
| const polygons = viewmap.polygons.filter(p => p.mesh === mesh); | |||
| const objectGroup = new SVGGroup({id: mesh.name}); | |||
| group.add(objectGroup); | |||
| for (const polygon of polygons) { | |||
| let img1 = undefined | |||
| if(this.options.fillImage && img){ // todo this can be optimized | |||
| const polygonBounds = new Box2().setFromPoints(polygon.contour); | |||
| const rootSize = new Vector2(svg.width() as number, svg.height() as number) | |||
| const polygonSize = polygonBounds.getSize(new Vector2()) | |||
| const polygonCenter = polygonBounds.getCenter(new Vector2()) | |||
| if(polygonSize.length() > rootSize.length()){ | |||
| // use the full image directly. | |||
| await new Promise<void>((res) => { | |||
| img1 = svg.image(img!.src, () => { | |||
| // this._svgFillImage?.size(20, 20) | |||
| res(); | |||
| // debugger | |||
| }); | |||
| }) | |||
| }else { | |||
| if (img.width !== rootSize.x || img.height !== rootSize.y) { | |||
| console.error('image size does not match svg size') | |||
| } else { | |||
| const canvas = document.createElement('canvas') | |||
| canvas.width = Math.floor(polygonSize.width) | |||
| canvas.height = Math.floor(polygonSize.height) | |||
| const context = canvas.getContext('2d') | |||
| if (context && canvas.width > 0 && canvas.height > 0) { | |||
| context.drawImage(img, | |||
| Math.floor(polygonCenter.x - polygonSize.width / 2), | |||
| Math.floor(polygonCenter.y - polygonSize.height / 2), | |||
| Math.floor(polygonSize.width), Math.floor(polygonSize.height), | |||
| 0, 0, Math.floor(polygonSize.width), Math.floor(polygonSize.height), | |||
| ) | |||
| // canvas.style.position = 'absolute' | |||
| // canvas.style.top = '0' | |||
| // canvas.style.left = '0' | |||
| // document.body.appendChild(canvas) | |||
| const croppedImgData = canvas.toDataURL('image/png') // use jpeg if not transparent? | |||
| await new Promise<void>((res) => { | |||
| img1 = svg.image(croppedImgData, () => { | |||
| // this._svgFillImage?.size(20, 20) | |||
| res(); | |||
| // debugger | |||
| }); | |||
| }) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| drawPolygon(group, polygon, this.options, img1, this.drawFills, this.drawStrokes); | |||
| } | |||
| } | |||
| } | |||
| if(img){ | |||
| // loop through defs | |||
| // size w, h for all patterns. | |||
| // move image out of the pattern | |||
| // <use xlink:href="#SvgjsImage1000"/> | |||
| const patternAndImages = svg.defs().children().filter(c=>c instanceof Pattern && c.children().length === 1).map(c=>[c,c.children()[0]]) | |||
| // console.log(patternAndImages) | |||
| patternAndImages.forEach(([pattern, image])=>{ | |||
| pattern.x(0) | |||
| pattern.y(0) | |||
| pattern.width(1) | |||
| pattern.height(1) | |||
| pattern.attr('patternUnits', 'objectBoundingBox') | |||
| svg.defs().add(image) | |||
| pattern.add(svg.use(image)) | |||
| }) | |||
| } | |||
| // this._svgFillImage = undefined | |||
| } | |||
| } | |||
| function drawPolygon( | |||
| parent: SVGElement, | |||
| polygon: Polygon, | |||
| options: FillPassOptions, | |||
| fillImage?: Image, drawFills = true, drawStroke = true) { | |||
| // Make a copy of the style so we can modify it | |||
| const style = {...options.fillStyle}; | |||
| const strokeStyle = {...options.strokeStyle}; | |||
| // If not using fixed color through the style, use the object color | |||
| if (!options.useFixedStyle) { | |||
| style.color = '#'+polygon.color.getHexString(); | |||
| strokeStyle.color = '#'+polygon.color.getHexString(); | |||
| } | |||
| if (options.useRandomColors) { | |||
| style.color = SVGColor.random().toString(); | |||
| strokeStyle.color = SVGColor.random().toString(); | |||
| } | |||
| const path = getSVGPath(polygon.contour, polygon.holes, true, drawStroke ? strokeStyle : undefined, drawFills ? style : undefined, fillImage, parent); | |||
| path.id("fill-"+polygon.id); | |||
| // parent.add(path); | |||
| if (options.drawRaycastPoint) { | |||
| drawPolygonRaycastPoint(parent, polygon); | |||
| } | |||
| } | |||
| function drawPolygonRaycastPoint(parent: SVGElement, polygon: Polygon) { | |||
| const strokeStyle = {color: "black"}; | |||
| const fillStyle = {color: "white"}; | |||
| const cx = polygon.insidePoint.x; | |||
| const cy = polygon.insidePoint.y; | |||
| const point = getSVGCircle(cx, cy, 2, strokeStyle, fillStyle); | |||
| point.id('raycast-point'); | |||
| parent.add(point); | |||
| } | |||
| @@ -0,0 +1,126 @@ | |||
| // 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} from '@svgdotjs/svg.js'; | |||
| import {ChainVisibility} from '../../viewmap/Chain'; | |||
| import {getSVGCircle, getSVGText} from '../svgutils'; | |||
| import { ViewVertexSingularity } from '../../viewmap/ViewVertex'; | |||
| const ViewVertexSingularities = Object.values(ViewVertexSingularity) | |||
| .filter(singularity => singularity !== ViewVertexSingularity.None); | |||
| const ViewVertexSingularityColor = { | |||
| [ViewVertexSingularity.None]: "", | |||
| [ViewVertexSingularity.ImageIntersection]: "green", | |||
| [ViewVertexSingularity.MeshIntersection]: "red", | |||
| [ViewVertexSingularity.CurtainFold]: "blue", | |||
| [ViewVertexSingularity.Bifurcation]: "orange", | |||
| } | |||
| export interface SingularityPointPassOptions { | |||
| drawLegend?: boolean; | |||
| pointSize?: number; | |||
| drawVisiblePoints?: boolean; | |||
| drawHiddenPoints?: boolean; | |||
| } | |||
| export class SingularityPointPass extends DrawPass { | |||
| readonly options: Required<SingularityPointPassOptions> = { | |||
| drawVisiblePoints: true, | |||
| drawHiddenPoints: false, | |||
| drawLegend: true, | |||
| pointSize: 2, | |||
| }; | |||
| constructor(options: SingularityPointPassOptions = {}) { | |||
| super(); | |||
| Object.assign(this.options, options); | |||
| } | |||
| async draw(svg: Svg, viewmap: Viewmap) { | |||
| // Update point visibility to avoid drawing point on hidden chains if only | |||
| // visible chains are drawn | |||
| for (const chain of viewmap.chains) { | |||
| for (const p of chain.vertices) { | |||
| p.visible = p.visible || chain.visibility === ChainVisibility.Visible; | |||
| } | |||
| } | |||
| const visibilities = []; | |||
| if (this.options.drawVisiblePoints) { | |||
| visibilities.push(true); | |||
| } | |||
| if (this.options.drawHiddenPoints) { | |||
| visibilities.push(false); | |||
| } | |||
| const group = new SVGGroup({id: "singularity-points"}); | |||
| svg.add(group); | |||
| const strokeStyle = { | |||
| color: 'black' | |||
| }; | |||
| const fillStyle = { | |||
| color: "", | |||
| }; | |||
| const singularityPoints = Array.from(viewmap.viewVertexMap.values()) | |||
| .filter(p => p.singularity != ViewVertexSingularity.None); | |||
| for (const visibility of visibilities) { | |||
| const visibilityGroup = new SVGGroup({id: visibility? "visible" : "hidden"}) | |||
| group.add(visibilityGroup); | |||
| for (const singularity of ViewVertexSingularities) { | |||
| const points = singularityPoints | |||
| .filter(p => p.singularity === singularity && p.visible === visibility); | |||
| const singularityGroup = new SVGGroup({id: singularity}); | |||
| visibilityGroup.add(singularityGroup); | |||
| fillStyle.color = ViewVertexSingularityColor[singularity]; | |||
| for (const p of points) { | |||
| const svgPoint = getSVGCircle(p.pos2d.x, p.pos2d.y, this.options.pointSize, strokeStyle, fillStyle); | |||
| singularityGroup.add(svgPoint); | |||
| } | |||
| } | |||
| } | |||
| if (this.options.drawLegend) { | |||
| group.add(getLegend()); | |||
| } | |||
| } | |||
| } | |||
| function getLegend() { | |||
| const legend = new SVGGroup({id: "singularity-legend"}); | |||
| legend.add(getSVGText("Singularities", 10, 10, {size: 15, anchor: 'start'})) | |||
| let y = 40; | |||
| for (const singularity of ViewVertexSingularities) { | |||
| const fillColor = ViewVertexSingularityColor[singularity]; | |||
| legend.add(getSVGCircle(15, y, 8, {color: "black"}, {color: fillColor})); | |||
| legend.add(getSVGText(singularity, 30, y-10, {size: 15, anchor: 'start'})); | |||
| y += 20; | |||
| } | |||
| return legend; | |||
| } | |||
| @@ -0,0 +1,440 @@ | |||
| // // 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]}; | |||
| // } | |||
| @@ -0,0 +1,19 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Thu Oct 20 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 | |||
| */ | |||
| export { DrawPass } from "./DrawPass"; | |||
| export { type FillStyle, FillPass, type FillPassOptions } from "./FillPass"; | |||
| export { SingularityPointPass, type SingularityPointPassOptions } from "./SingularityPointPass"; | |||
| // export { TexturePass } from "./TexturePass"; | |||
| export { type StrokeStyle, VisibleChainPass, HiddenChainPass, type ChainPassOptions, ChainPass} from "./ChainPass"; | |||
| @@ -0,0 +1,152 @@ | |||
| // 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 { | |||
| Circle as SVGCircle, | |||
| Dom, | |||
| Ellipse as SVGEllipse, | |||
| FillData, | |||
| FontData, | |||
| Image, | |||
| Image as SVGImage, | |||
| Number as SVGNumber, | |||
| NumberAlias as SVGNumberAlias, | |||
| Path as SVGPath, | |||
| PathArray as SVGPathArray, | |||
| PathCommand as SVGPathCommand, | |||
| Polygon as SVGPolygon, | |||
| Rect as SVGRect, | |||
| StrokeData, | |||
| Text as SVGText | |||
| } from '@svgdotjs/svg.js'; | |||
| import {PointLike, RectLike, round} from '../../utils'; | |||
| export function getSVGImage(url: string, rect: RectLike) { | |||
| const svgImage = new SVGImage(); | |||
| svgImage.load(url); | |||
| svgImage.x(rect.x); | |||
| svgImage.y(rect.y); | |||
| svgImage.width(rect.w); | |||
| svgImage.height(rect.h) | |||
| return svgImage; | |||
| } | |||
| export function getSVGText( | |||
| text: string, | |||
| x: number, | |||
| y: number, | |||
| fontStyle: FontData = {}, | |||
| strokeStyle: StrokeData = {}, | |||
| fillStyle: FillData = {}, | |||
| ): SVGText { | |||
| const svgText = new SVGText(); | |||
| svgText.text(text); | |||
| svgText.x(x); | |||
| svgText.y(y); | |||
| svgText.font(fontStyle); | |||
| svgText.stroke(strokeStyle); | |||
| svgText.fill(fillStyle); | |||
| return svgText; | |||
| } | |||
| export function getSVGPath( | |||
| contour: PointLike[], | |||
| holes: PointLike[][], | |||
| closed: boolean, | |||
| strokeStyle?: StrokeData, | |||
| fillStyle?: FillData, | |||
| fillImage?: Image, | |||
| parent?: Dom, | |||
| ): SVGPath { | |||
| const path = new SVGPath(); | |||
| let cmds = getSVGPathCommands(contour, closed); | |||
| for (const hole of holes) { | |||
| cmds = cmds.concat(getSVGPathCommands(hole, closed)); | |||
| } | |||
| path.plot(new SVGPathArray(cmds)); | |||
| if (strokeStyle) { | |||
| path.stroke(strokeStyle); | |||
| } else { | |||
| path.stroke('none'); | |||
| } | |||
| if(parent){ | |||
| parent.add(path); // required for fillImage | |||
| } | |||
| if(fillImage){ | |||
| path.fill(fillImage); | |||
| } else if (fillStyle) { | |||
| path.fill({...fillStyle, rule: "evenodd"}); | |||
| } else { | |||
| path.fill('none'); | |||
| } | |||
| return path; | |||
| } | |||
| function getSVGPathCommands(points: PointLike[], closed = true): SVGPathCommand[] { | |||
| const cmds = new Array<SVGPathCommand>(); | |||
| let p; | |||
| if (points.length > 0) { | |||
| p = points[0]; | |||
| cmds.push(['M', round(p.x), round(p.y)]) | |||
| for (let i=1; i<points.length; i++) { | |||
| p = points[i]; | |||
| cmds.push(['L', round(p.x), round(p.y)]); | |||
| } | |||
| if (closed) { | |||
| cmds.push(['Z']); | |||
| } | |||
| } | |||
| return cmds; | |||
| } | |||
| export function getSVGCircle( | |||
| cx: number, | |||
| cy: number, | |||
| radius: number, | |||
| strokeStyle: StrokeData = {}, | |||
| fillStyle: FillData = {} | |||
| ) { | |||
| const circle = new SVGCircle(); | |||
| circle.center(cx, cy); | |||
| circle.radius(radius); | |||
| circle.stroke(strokeStyle); | |||
| circle.fill(fillStyle); | |||
| return circle; | |||
| } | |||
| const _ignoredAttributes = ["x","y","width","height","viewbox","cx","cy","rw","rx","points"]; | |||
| export function replaceShapeByPath( | |||
| shape: SVGPolygon | SVGRect | SVGEllipse | SVGCircle | |||
| ): SVGPath { | |||
| const path = shape.toPath(true); | |||
| const attributes = shape.attr(); | |||
| for(const attribute in attributes) { | |||
| if(!_ignoredAttributes.includes(attribute)) { | |||
| path.attr(attribute, attributes[attribute]); | |||
| } | |||
| } | |||
| return path; | |||
| } | |||
| export function NumberAliasToNumber(n: SVGNumberAlias): number { | |||
| switch (typeof n) { | |||
| case "number": | |||
| return n as number; | |||
| case "string": | |||
| return Number(n); | |||
| case typeof SVGNumber: | |||
| return (n as SVGNumber).value; | |||
| } | |||
| return 0; | |||
| } | |||
| @@ -0,0 +1,82 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 23/02/2021 | |||
| // 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 {Vector2} from 'three'; | |||
| import {ViewEdge} from './ViewEdge'; | |||
| import { SVGMesh } from '../SVGMesh'; | |||
| import { ViewVertex } from './ViewVertex'; | |||
| export enum ChainVisibility { | |||
| Unknown = "Unknown", | |||
| Hidden = "Hidden", | |||
| Visible = "Visible", | |||
| } | |||
| export class Chain { | |||
| id: number; | |||
| object: SVGMesh; | |||
| raycastPoint = new Vector2(); | |||
| edges = new Array<ViewEdge>(); | |||
| vertices = new Array<ViewVertex>(); | |||
| visibility: ChainVisibility = ChainVisibility.Unknown; | |||
| constructor(id: number, object: SVGMesh) { | |||
| this.id = id; | |||
| this.object = object; | |||
| } | |||
| get head(): ViewVertex { | |||
| return this.vertices[0]; | |||
| } | |||
| get tail(): ViewVertex { | |||
| return this.vertices[this.vertices.length -1]; | |||
| } | |||
| get size() { | |||
| return this.vertices.length; | |||
| } | |||
| get nature() { | |||
| return this.edges[0].nature; | |||
| } | |||
| middlePoint(): ViewVertex { | |||
| return this.vertices[Math.floor(this.vertices.length/2)]; | |||
| } | |||
| middleEdge(): ViewEdge | null { | |||
| if (this.edges.length === 0) { | |||
| return null; | |||
| } else { | |||
| return this.edges[Math.floor(this.edges.length/2)] | |||
| } | |||
| } | |||
| addEdge(edge: ViewEdge): void { | |||
| if (this.edges.length == 0) { | |||
| this.edges.push(edge); | |||
| this.vertices.push(edge.a); | |||
| this.vertices.push(edge.b); | |||
| } else { | |||
| if (edge.hasVertex(this.head)) { | |||
| // Put vertex and segment in the head of the lists | |||
| this.vertices.unshift(edge.otherVertex(this.head)); | |||
| this.edges.unshift(edge); | |||
| } else if (edge.hasVertex(this.tail)) { | |||
| // Put vertex and segment in the tail of the lists | |||
| this.vertices.push(edge.otherVertex(this.tail)); | |||
| this.edges.push(edge); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 23/02/2021 | |||
| // 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 {Vector2, Color} from 'three'; | |||
| import { SVGMesh } from '../SVGMesh'; | |||
| export class Polygon { | |||
| id: number; | |||
| mesh?: SVGMesh; | |||
| color = new Color(); | |||
| insidePoint: Vector2 = new Vector2(); | |||
| contour: Vector2[]; | |||
| holes: Vector2[][]; | |||
| constructor( | |||
| id: number, | |||
| contour: Vector2[], | |||
| holes: Vector2[][]) { | |||
| this.id = id; | |||
| this.contour = contour; | |||
| this.holes = holes; | |||
| } | |||
| } | |||
| @@ -0,0 +1,143 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 09/12/2021 | |||
| // 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 {Vector2} from 'three'; | |||
| import {Face, Halfedge} from '../../../three-mesh-halfedge'; | |||
| import {SVGMesh} from '../SVGMesh'; | |||
| // import { ViewPoint } from './ViewPoint_'; | |||
| import {ViewVertex} from './ViewVertex'; | |||
| /** | |||
| * Possible values for the edge nature in the viemap. | |||
| */ | |||
| export enum ViewEdgeNature { | |||
| // /** Edge is standard */ | |||
| // None = "None", | |||
| /** Edge is connected to front-facing and a back-facing face */ | |||
| Silhouette = "Silhouette", | |||
| /** Edge is only connected to one face */ | |||
| Boundary = "Boundary", | |||
| /** Edge is on the intersection between two meshes */ | |||
| MeshIntersection = "MeshIntersection", | |||
| /** Edge is connected to two faces where the angle between normals is acute */ | |||
| Crease = "Crease", | |||
| /** Edge is connected to two faces using a different material/vertex color */ | |||
| Material = "Material", | |||
| } | |||
| export const VisibilityIndicatingNatures = new Set([ | |||
| ViewEdgeNature.Silhouette, | |||
| ViewEdgeNature.Boundary, | |||
| ViewEdgeNature.MeshIntersection, | |||
| ]); | |||
| export class ViewEdge { | |||
| /** | |||
| * Halfedge on which the edge is based on | |||
| * @defaultValue null | |||
| */ | |||
| halfedge?: Halfedge; | |||
| /** | |||
| * List of the meshes the Edge belongs to | |||
| */ | |||
| readonly meshes = new Array<SVGMesh>(); | |||
| /** | |||
| * Nature of the edge | |||
| * @defautValue EdgeNature.None | |||
| */ | |||
| nature: ViewEdgeNature; | |||
| /** | |||
| * Angle between to the connected faces. | |||
| * @defaultValue Infinity */ | |||
| faceAngle = Infinity; | |||
| /** | |||
| * Indicates whether the edge is connected to back-facing faces only | |||
| * *Note: this makes only sense with 2 connected faces.* | |||
| * @defaultValue false | |||
| */ | |||
| isBack = false; | |||
| /** | |||
| * Indicates wheter the edge is concave. | |||
| * *Note: this makes only sense with 2 connected faces.* | |||
| * @defaultValue false | |||
| */ | |||
| isConcave = false; | |||
| faces = new Array<Face>(); | |||
| a: ViewVertex; | |||
| b: ViewVertex; | |||
| constructor(a: ViewVertex, b: ViewVertex, nature: ViewEdgeNature, halfedge?: Halfedge) { | |||
| this.a = a; | |||
| this.b = b; | |||
| this.nature = nature; | |||
| this.halfedge = halfedge; | |||
| } | |||
| get vertices() { | |||
| return [this.a, this.b]; | |||
| } | |||
| get from(): Vector2 { | |||
| return this.a.pos2d; | |||
| } | |||
| get to(): Vector2 { | |||
| return this.b.pos2d; | |||
| } | |||
| toJSON() { | |||
| return { | |||
| id: [...this.a.vertices].map(v => v.id).join(',') + '-' + | |||
| [...this.b.vertices].map(v => v.id).join(','), | |||
| } | |||
| } | |||
| clone() { | |||
| const edge = new ViewEdge(this.a, this.b, this.nature, this.halfedge); | |||
| edge.faceAngle = this.faceAngle; | |||
| edge.isBack = this.isBack; | |||
| edge.isConcave = this.isConcave; | |||
| edge.meshes.push(...this.meshes); | |||
| edge.faces.push(...this.faces); | |||
| return edge; | |||
| } | |||
| otherVertex(vertex: ViewVertex) { | |||
| if (vertex === this.a) { | |||
| return this.b; | |||
| } else { | |||
| return this.a; | |||
| } | |||
| } | |||
| hasVertex(vertex: ViewVertex) { | |||
| return this.a === vertex || this.b === vertex; | |||
| } | |||
| isConnectedTo(edge: ViewEdge) { | |||
| return this.hasVertex(edge.a) || this.hasVertex(edge.b); | |||
| } | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Fri Dec 09 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 {Vector2, Vector3} from "three"; | |||
| import {Vertex} from "../../../three-mesh-halfedge"; | |||
| import {vectors2Equal, vectors3Equal} from "../../utils"; | |||
| import {ViewEdge} from "./ViewEdge"; | |||
| export enum ViewVertexSingularity { | |||
| None = "None", | |||
| ImageIntersection = "ImageIntersection", | |||
| MeshIntersection = "MeshIntersection", | |||
| CurtainFold = "CurtainFold", | |||
| Bifurcation = "Bifurcation", | |||
| } | |||
| export class ViewVertex { | |||
| hash3d = ""; | |||
| hash2d = ""; | |||
| singularity = ViewVertexSingularity.None; | |||
| readonly vertices = new Set<Vertex>(); | |||
| readonly pos3d = new Vector3(); | |||
| readonly pos2d = new Vector2(); | |||
| readonly viewEdges = new Array<ViewEdge>(); | |||
| visible = false; | |||
| commonViewEdgeWith(other: ViewVertex) { | |||
| for (const viewEdge of this.viewEdges) { | |||
| if (other.viewEdges.includes(viewEdge)) { | |||
| return viewEdge; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| isConnectedTo(other: ViewVertex) { | |||
| return this.commonViewEdgeWith(other) != null; | |||
| } | |||
| matches3dPosition(position: Vector3, tolerance = 1e-4) { | |||
| return vectors3Equal(this.pos3d, position, tolerance); | |||
| } | |||
| matches2dPosition(position: Vector2, tolerance = 1e-4) { | |||
| return vectors2Equal(this.pos2d, position, tolerance); | |||
| } | |||
| get x() { | |||
| return this.pos2d.x; | |||
| } | |||
| get y() { | |||
| return this.pos2d.y; | |||
| } | |||
| } | |||
| @@ -0,0 +1,389 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 23/02/2021 | |||
| // 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 | |||
| // Modified by repalash <palash@shaders.app>. Minor changes, added parameters and error handling. | |||
| import {ColorRepresentation, PerspectiveCamera} from 'three'; | |||
| import {SizeLike} from '../../utils'; | |||
| import {SVGMesh} from '../SVGMesh'; | |||
| import {Chain, ChainVisibility} from './Chain'; | |||
| import {ViewEdge} from './ViewEdge'; | |||
| import {computePolygons, PolygonsInfo} from './operations/computePolygons'; | |||
| import {setupEdges} from './operations/setupEdges'; // import { ViewPoint } from './ViewPoint_'; | |||
| import {Polygon} from './Polygon'; | |||
| import {AssignPolygonInfo, assignPolygons} from './operations/assignPolygons'; | |||
| import {ChainVisibilityInfo, computeChainsVisibility} from './operations/computeChainsVisibility'; // import { setupPoints } from './operations/setupPoints_'; | |||
| import {computeMeshIntersections, MeshIntersectionInfo} from './operations/computeMeshIntersections'; | |||
| import {ViewVertex} from './ViewVertex'; | |||
| import {createChains} from './operations/createChains' | |||
| import {find2dSingularities} from './operations/find2dSingularities' | |||
| import {find3dSingularities} from './operations/find3dSingularities' | |||
| declare module '../../../three-mesh-halfedge' { | |||
| export interface Face { | |||
| viewEdges: ViewEdge[]; | |||
| } | |||
| export interface Vertex { | |||
| viewVertex: ViewVertex; | |||
| } | |||
| } | |||
| export interface ViewmapOptions { | |||
| updateMeshes?: boolean; | |||
| ignoreVisibility?: boolean; | |||
| defaultMeshColor?: ColorRepresentation; | |||
| creaseAngle?: { | |||
| min: number, | |||
| max: number, | |||
| } | |||
| } | |||
| export class ViewmapBuildInfo { | |||
| totalTime = Infinity; | |||
| /** Record or times in ms */ | |||
| times = { | |||
| updateGeometries: Infinity, | |||
| updateBVH: Infinity, | |||
| updateHES: Infinity, | |||
| setupEdges: Infinity, | |||
| find3dSingularities: Infinity, | |||
| find2dSingularities: Infinity, | |||
| computeChains: Infinity, | |||
| visibility: Infinity, | |||
| computePolygons: Infinity, | |||
| assignPolygons: Infinity, | |||
| worldTransform: Infinity, | |||
| meshIntersections: Infinity, | |||
| setupPoints: Infinity, | |||
| setupFaceMap: Infinity, | |||
| }; | |||
| intersections = new MeshIntersectionInfo(); | |||
| visibility = { | |||
| nbTests: Infinity, | |||
| nbRaycasts: Infinity, | |||
| }; | |||
| polygons = { | |||
| smallAreaIgnored: Infinity, | |||
| insidePointErrors: Infinity, | |||
| assigned: Infinity, | |||
| nonAssigned: Infinity, | |||
| }; | |||
| } | |||
| export interface ProgressInfo { | |||
| currentStepName: string; | |||
| currentStep: number; | |||
| totalSteps: number; | |||
| } | |||
| interface ViewmapAction { | |||
| name: string; | |||
| process: () => Promise<void>; | |||
| } | |||
| export class Viewmap { | |||
| readonly meshes = new Array<SVGMesh>(); | |||
| readonly viewEdges = new Array<ViewEdge>(); | |||
| // readonly viewPointMap = new Map<string, ViewPoint>(); | |||
| readonly viewVertexMap = new Map<string, ViewVertex>(); | |||
| readonly chains = new Array<Chain>(); | |||
| readonly polygons = new Array<Polygon>(); | |||
| readonly camera = new PerspectiveCamera(); | |||
| readonly renderSize = {w: 500, h: 500}; | |||
| readonly options: Required<ViewmapOptions> = { | |||
| updateMeshes: true, | |||
| ignoreVisibility: false, | |||
| defaultMeshColor: 0x555555, | |||
| creaseAngle: { | |||
| min: 80, | |||
| max: 100, | |||
| } | |||
| } | |||
| constructor(options?: ViewmapOptions) { | |||
| Object.assign(this.options, options); | |||
| } | |||
| clear() { | |||
| this.meshes.clear(); | |||
| this.viewEdges.clear(); | |||
| // this.viewPointMap.clear(); | |||
| this.viewVertexMap.clear(); | |||
| this.chains.clear(); | |||
| this.polygons.clear(); | |||
| } | |||
| build( | |||
| meshes: SVGMesh[], | |||
| camera: PerspectiveCamera, | |||
| renderSize: SizeLike, | |||
| info = new ViewmapBuildInfo(), | |||
| progressCallback?: (progress: ProgressInfo) => void) { | |||
| this.clear(); | |||
| this.meshes.push(...meshes); | |||
| const ud = camera.userData | |||
| camera.userData = {}; | |||
| this.camera.copy(camera); | |||
| camera.userData = ud; | |||
| this.camera.getWorldPosition(camera.position); | |||
| this.renderSize.w = renderSize.w; | |||
| this.renderSize.h = renderSize.h; | |||
| const actions = this.setupActions(info); | |||
| info.totalTime = Date.now(); | |||
| return this.buildAsync(0, actions, info, progressCallback); | |||
| } | |||
| skipActions = false | |||
| private buildAsync( | |||
| idx: number, | |||
| actions: ViewmapAction[], | |||
| info: ViewmapBuildInfo, | |||
| progressCallback?: (progress: ProgressInfo) => void) { | |||
| info.totalTime = Date.now(); | |||
| return new Promise<void>((resolve) => { | |||
| if (idx < actions.length) { | |||
| progressCallback && progressCallback({ | |||
| totalSteps: actions.length, | |||
| currentStep: idx+1, | |||
| currentStepName: actions[idx].name | |||
| }); | |||
| const res = () => { | |||
| resolve(this.buildAsync(idx+1, actions, info, progressCallback)); | |||
| } | |||
| if(this.skipActions) res() | |||
| else { | |||
| // console.info(`Viewmap step ${idx+1}/${actions.length} : ${actions[idx].name}`); | |||
| actions[idx].process().catch(_ => { | |||
| // todo handle errors properly depending on the step. | |||
| // console.error(`Viewmap step ${idx+1}/${actions.length} : ${actions[idx].name} failed`, e); | |||
| }).then(res) | |||
| } | |||
| } else { | |||
| info.totalTime = Date.now() - info.totalTime; | |||
| resolve(); | |||
| } | |||
| }); | |||
| } | |||
| private setupActions(info = new ViewmapBuildInfo()): Array<ViewmapAction> { | |||
| const actions = new Array<ViewmapAction>(); | |||
| if (this.options.updateMeshes) { | |||
| /** | |||
| * Update Morphed Geometries | |||
| */ | |||
| actions.push({ | |||
| name: "Update Morphed Geometries", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| for (const mesh of this.meshes) { | |||
| mesh.updateMorphGeometry(); | |||
| } | |||
| info.times.updateGeometries = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Update BVH structs | |||
| */ | |||
| actions.push({ | |||
| name: "Update BVH Structures", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| for (const mesh of this.meshes) { | |||
| mesh.updateBVH(false); | |||
| } | |||
| info.times.updateBVH = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Update Halfedge structs | |||
| */ | |||
| actions.push({ | |||
| name: "Update Halfedge Structures", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| for (const mesh of this.meshes) { | |||
| mesh.updateHES(false); | |||
| } | |||
| info.times.updateHES = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Update Halfedge structures to world positions | |||
| */ | |||
| actions.push({ | |||
| name: "Transform local 3d points into world", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| for (const mesh of this.meshes) { | |||
| for (const vertex of mesh.hes.vertices) { | |||
| vertex.position.applyMatrix4(mesh.matrixWorld); | |||
| } | |||
| } | |||
| info.times.worldTransform = Date.now() - startTime; | |||
| } | |||
| }); | |||
| } | |||
| /** | |||
| * Setup edges | |||
| */ | |||
| actions.push({ | |||
| name: "Setup viewmap edges", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| setupEdges(this, this.options); | |||
| info.times.setupEdges = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Compute Meshes Intersections | |||
| */ | |||
| actions.push({ | |||
| name: "Compute meshes intersections", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| computeMeshIntersections(this, info.intersections); | |||
| info.times.meshIntersections = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Find singularities in the 3D space | |||
| */ | |||
| actions.push({ | |||
| name: "Find singularities in the 3d space", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| // this.singularityPoints = | |||
| find3dSingularities(this); | |||
| info.times.find3dSingularities = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Find singularity points in the 2d space (image place intersections) | |||
| * This step creates new points and segments on-the-fly | |||
| */ | |||
| actions.push({ | |||
| name: "Find singularities in the 2d space", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| find2dSingularities(this); | |||
| info.times.find2dSingularities = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Compute chains from the set of segments: link segments depending | |||
| * of their connexity and nature | |||
| */ | |||
| actions.push({ | |||
| name: "Create chains", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| createChains(this); | |||
| info.times.computeChains = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Compute contours visibility using geometry's topology or raycasting if | |||
| * need. | |||
| * If ignore visibility is set, set all contours to be visible | |||
| */ | |||
| actions.push({ | |||
| name: "Compute chains visibility", | |||
| process: async() => { | |||
| if (!this.options.ignoreVisibility) { | |||
| const startTime = Date.now(); | |||
| const visInfo = new ChainVisibilityInfo(); | |||
| computeChainsVisibility(this, visInfo); | |||
| info.visibility.nbRaycasts = visInfo.nbRaycasts; | |||
| info.visibility.nbTests = visInfo.nbTests; | |||
| info.times.visibility = Date.now() - startTime; | |||
| } else { | |||
| this.chains.map(chain => chain.visibility = ChainVisibility.Visible); | |||
| } | |||
| } | |||
| }); | |||
| /** | |||
| * Compute the polygons formed by the visible subset of contours | |||
| */ | |||
| actions.push({ | |||
| name: "Compute polygons", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| const polyInfo = new PolygonsInfo(); | |||
| await computePolygons(this, polyInfo); | |||
| info.polygons.smallAreaIgnored = polyInfo.smallAreaIgnored; | |||
| info.polygons.insidePointErrors = polyInfo.insidePointErrors; | |||
| info.times.computePolygons = Date.now() - startTime; | |||
| } | |||
| }); | |||
| /** | |||
| * Assign polygons to their corresponding object with raycasting | |||
| */ | |||
| actions.push({ | |||
| name: "Assign Polygons", | |||
| process: async() => { | |||
| const startTime = Date.now(); | |||
| const assignInfo = new AssignPolygonInfo(); | |||
| assignPolygons(this, this.options, assignInfo); | |||
| info.polygons.assigned = assignInfo.assigned; | |||
| info.polygons.nonAssigned = assignInfo.nonAssigned; | |||
| info.times.assignPolygons = Date.now() - startTime; | |||
| } | |||
| }); | |||
| return actions; | |||
| } | |||
| visibleChains() { | |||
| return this.chains.filter(c => c.visibility === ChainVisibility.Visible); | |||
| } | |||
| hiddenChains() { | |||
| return this.chains.filter(c => c.visibility === ChainVisibility.Hidden); | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Thu Oct 20 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 | |||
| */ | |||
| export { ViewEdge, ViewEdgeNature } from './ViewEdge'; | |||
| export { ViewVertex, ViewVertexSingularity } from './ViewVertex'; | |||
| export { Chain, ChainVisibility } from './Chain'; | |||
| export { Polygon } from './Polygon'; | |||
| export { | |||
| Viewmap, | |||
| ViewmapBuildInfo, | |||
| type ProgressInfo, | |||
| type ViewmapOptions, | |||
| } from './Viewmap'; | |||
| @@ -0,0 +1,85 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Nov 22 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 { Color, ColorRepresentation, Mesh, Raycaster, Vector2 } from "three"; | |||
| import { imagePointToNDC } from "../../../utils"; | |||
| import { SVGMesh } from "../../SVGMesh"; | |||
| import { Viewmap } from "../Viewmap"; | |||
| export interface AssignPolygonOptions { | |||
| defaultMeshColor: ColorRepresentation; | |||
| } | |||
| export class AssignPolygonInfo { | |||
| assigned = Infinity; | |||
| nonAssigned = Infinity; | |||
| } | |||
| const _color = new Color(); | |||
| const _raycaster = new Raycaster(); | |||
| const _vec2 = new Vector2(); | |||
| export function assignPolygons( | |||
| viewmap: Viewmap, | |||
| options?: AssignPolygonOptions, | |||
| info = new AssignPolygonInfo()) { | |||
| options = { | |||
| defaultMeshColor: 0x333333, | |||
| ...options, | |||
| } | |||
| _color.set(options.defaultMeshColor); | |||
| info.assigned = 0; | |||
| info.nonAssigned = 0; | |||
| const {meshes, renderSize, camera, polygons} = viewmap; | |||
| const svgMeshesMap = new Map<Mesh, SVGMesh>(); | |||
| const threeMeshes = new Array<Mesh>(); | |||
| for (const mesh of meshes) { | |||
| svgMeshesMap.set(mesh.threeMesh, mesh); | |||
| threeMeshes.push(mesh.threeMesh); | |||
| } | |||
| for (const polygon of polygons) { | |||
| imagePointToNDC(polygon.insidePoint, _vec2, renderSize); | |||
| _raycaster.setFromCamera(_vec2, camera); | |||
| _raycaster.firstHitOnly = true; | |||
| const intersections = _raycaster.intersectObjects(threeMeshes, false); | |||
| if (intersections.length > 0) { | |||
| const intersection = intersections[0]; | |||
| const faceIndex = intersection.faceIndex; | |||
| if (faceIndex !== undefined) { | |||
| const intersectionMesh = intersection.object as Mesh; | |||
| polygon.mesh = svgMeshesMap.get(intersectionMesh); | |||
| if (polygon.mesh) { | |||
| polygon.color.copy(polygon.mesh.colorForFaceIndex(faceIndex) || _color); | |||
| info.assigned += 1; | |||
| } else { | |||
| console.error(`Could not associate SVG mesh to polygon ${polygon.id}`); | |||
| } | |||
| } else { | |||
| console.error(`Polygon ${polygon.id} intersection has no face index`,intersection); | |||
| } | |||
| } | |||
| } | |||
| info.nonAssigned = polygons.length - info.assigned; | |||
| } | |||
| @@ -0,0 +1,147 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Nov 22 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 { DoubleSide, Material, Mesh, PerspectiveCamera, Raycaster, Side, Vector3 } from "three"; | |||
| import { Chain, ChainVisibility } from "../Chain"; | |||
| import { Viewmap } from "../Viewmap"; | |||
| const _raycaster = new Raycaster(); | |||
| const _rayDirection = new Vector3(); | |||
| const _rayOrigin = new Vector3(); | |||
| export class ChainVisibilityInfo { | |||
| nbTests = Infinity; | |||
| nbRaycasts = Infinity; | |||
| } | |||
| export function computeChainsVisibility( | |||
| viewmap: Viewmap, | |||
| info = new ChainVisibilityInfo()) { | |||
| const {chains, meshes, camera} = viewmap; | |||
| const threeMeshes = meshes.map(obj => obj.threeMesh); | |||
| info.nbRaycasts = 0; | |||
| info.nbTests = 0; | |||
| // As we cast rays from object to the camera, we want rays to intersect only | |||
| // on the backside face. So we need to change material sideness | |||
| const materialSidenessMap = new Map<Material, Side>(); | |||
| for (const mesh of meshes) { | |||
| if (Array.isArray(mesh.material)) { | |||
| for (const material of mesh.material) { | |||
| materialSidenessMap.set(material, material.side); | |||
| material.side = DoubleSide; | |||
| } | |||
| } else { | |||
| materialSidenessMap.set(mesh.material, mesh.material.side); | |||
| mesh.material.side = DoubleSide; | |||
| } | |||
| } | |||
| // Compute chain visibility | |||
| for (const chain of chains) { | |||
| info.nbTests += 1; | |||
| // if (!chainVisibilityWithGeometry(chain)) { | |||
| chainVisibilityWithRaycasting(chain, camera, threeMeshes); | |||
| info.nbRaycasts += 1; | |||
| // } | |||
| } | |||
| // Restaure the sideness of material | |||
| for (const mesh of meshes) { | |||
| if (Array.isArray(mesh.material)) { | |||
| for (const material of mesh.material) { | |||
| material.side = materialSidenessMap.get(material) ?? material.side; | |||
| } | |||
| } else { | |||
| mesh.material.side = materialSidenessMap.get(mesh.material) ?? mesh.material.side; | |||
| } | |||
| } | |||
| } | |||
| export function chainVisibilityWithGeometry(chain: Chain) { | |||
| // Search for an edge that is not obvisouly hidden by geometry | |||
| // (i.e. not back and not concave | |||
| // see paper https://hal.inria.fr/hal-02189483) | |||
| let i = 0; | |||
| let hiddenByGeometry = false; | |||
| do { | |||
| hiddenByGeometry = chain.edges[i].isConcave || chain.edges[i].isBack; | |||
| i += 1; | |||
| } while(!hiddenByGeometry && i < chain.edges.length); | |||
| for (const edge of chain.edges) { | |||
| if (edge.isConcave || edge.isBack) { | |||
| chain.visibility = ChainVisibility.Hidden; | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * Determines chain visibility via casting a rayfrom the chain to the camera | |||
| * @param contour | |||
| * @param camera | |||
| * @param objects | |||
| * @param tolerance | |||
| * @returns | |||
| */ | |||
| export function chainVisibilityWithRaycasting( | |||
| chain: Chain, | |||
| camera: PerspectiveCamera, | |||
| objects: Array<Mesh>, | |||
| tolerance = 1e-5) { | |||
| const edge = chain.middleEdge(); | |||
| if (!edge) { | |||
| console.error("Contour has no edges"); | |||
| chain.visibility = ChainVisibility.Visible; | |||
| return; | |||
| } | |||
| // Cast a ray from the middle of the segment to the camera | |||
| _rayOrigin.lerpVectors(edge.a.pos3d, edge.b.pos3d, 0.5); | |||
| _rayDirection.subVectors(camera.position, _rayOrigin).normalize(); | |||
| _raycaster.firstHitOnly = false; | |||
| _raycaster.set(_rayOrigin, _rayDirection); | |||
| // Get the projection of the origin of the ray cast | |||
| chain.raycastPoint.lerpVectors(edge.a.pos2d, edge.b.pos2d, 0.5); | |||
| // Compute total distance in case of mathematical imprecision | |||
| const intersections = _raycaster.intersectObjects(objects, false); | |||
| let totalDistance = 0; | |||
| for (const intersection of intersections) { | |||
| totalDistance += intersection.distance; | |||
| } | |||
| if (totalDistance < tolerance) { | |||
| chain.visibility = ChainVisibility.Visible; | |||
| } else { | |||
| chain.visibility = ChainVisibility.Hidden; | |||
| } | |||
| } | |||
| @@ -0,0 +1,129 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Nov 29 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 {Line3, Vector3} from "three"; | |||
| import {Face} from "../../../../three-mesh-halfedge"; | |||
| import {intersectLines} from "../../../utils"; | |||
| import {SVGMesh} from "../../SVGMesh"; | |||
| import {ViewEdge, ViewEdgeNature} from "../ViewEdge"; | |||
| import {Viewmap} from "../Viewmap"; | |||
| import {createViewVertex} from "./createViewVertex"; | |||
| import {meshIntersectionCb, TriIntersectionInfo} from "./meshIntersectionCb"; | |||
| import {splitViewEdge3d} from "./splitEdge"; | |||
| const _line = new Line3(); | |||
| const _inter = new Vector3(); | |||
| const _lineDir = new Vector3(); | |||
| const _dir = new Vector3(); | |||
| export class MeshIntersectionInfo { | |||
| details = new Array<TriIntersectionInfo>(); | |||
| nbTests = Infinity; | |||
| nbIntersections = Infinity; | |||
| nbMeshesTested = Infinity; | |||
| nbEdgesAdded = Infinity; | |||
| } | |||
| export function computeMeshIntersections( | |||
| viewmap: Viewmap, | |||
| info = new MeshIntersectionInfo()) { | |||
| const {meshes} = viewmap; | |||
| info.nbMeshesTested = 0; | |||
| info.nbIntersections = 0; | |||
| info.nbTests = 0; | |||
| info.nbEdgesAdded = 0; | |||
| const intersectCallback = ( | |||
| meshA: SVGMesh, meshB: SVGMesh, line: Line3, | |||
| faceA: Face, faceB: Face) => { | |||
| if(!faceA || !faceB) { | |||
| // console.error("No face found", faceA, faceB); | |||
| return; | |||
| } | |||
| // Create vertices for line ends | |||
| const v1 = createViewVertex(viewmap, line.start); | |||
| const v2 = createViewVertex(viewmap, line.end); | |||
| const intersectionViewVertices = [v1, v2]; | |||
| // Gather all the viewEdges that lie on faceA and faceB and check if | |||
| // they intersect with the line | |||
| const faceViewEdges = new Set([...faceA.viewEdges, ...faceB.viewEdges]); | |||
| for (const e of faceViewEdges) { | |||
| _line.set(e.a.pos3d, e.b.pos3d); | |||
| if (intersectLines(_line, line, _inter)) { | |||
| const splitResult = splitViewEdge3d(viewmap, e, _inter); | |||
| if (splitResult) { | |||
| if (!intersectionViewVertices.includes(splitResult.viewVertex)) { | |||
| intersectionViewVertices.push(splitResult.viewVertex); | |||
| } | |||
| } else { | |||
| console.error("Intersection but split failed"); | |||
| } | |||
| } | |||
| } | |||
| // Sort point along the line | |||
| _dir.subVectors(line.end, line.start); | |||
| intersectionViewVertices.sort((a,b) => { | |||
| _dir.subVectors(b.pos3d, a.pos3d); | |||
| return _dir.dot(_lineDir) | |||
| }); | |||
| // Create new edges | |||
| for (let i = 0; i<intersectionViewVertices.length-1; i++) { | |||
| const v1 = intersectionViewVertices[i]; | |||
| const v2 = intersectionViewVertices[i+1]; | |||
| const viewEdge = new ViewEdge(v1, v2, ViewEdgeNature.MeshIntersection); | |||
| viewEdge.meshes.push(meshA, meshB); | |||
| viewEdge.faces.push(faceA, faceB); | |||
| v1.viewEdges.push(viewEdge); | |||
| v2.viewEdges.push(viewEdge); | |||
| faceA.viewEdges.push(viewEdge); | |||
| faceB.viewEdges.push(viewEdge); | |||
| viewmap.viewEdges.push(viewEdge); | |||
| } | |||
| } | |||
| // Apply the callback for every pair of meshes | |||
| // TODO: Need to run that for self-intersections as well | |||
| for (let i=0; i<meshes.length-1; i++) { | |||
| for (let j=i+1; j<meshes.length; j++) { | |||
| const meshA = meshes[i]; | |||
| const meshB = meshes[j]; | |||
| const triInfo = new TriIntersectionInfo(); | |||
| meshIntersectionCb(meshA, meshB, intersectCallback, triInfo); | |||
| info.nbIntersections += triInfo.nbIntersections; | |||
| info.nbTests += triInfo.nbTests; | |||
| info.nbMeshesTested += 1; | |||
| info.details.push(triInfo); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,112 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Nov 22 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 Arrangement2D from 'arrangement-2d-js'; | |||
| import {Vector2} from 'three'; | |||
| import {ChainVisibility} from '../Chain'; | |||
| import {Polygon} from '../Polygon'; | |||
| import {Viewmap} from '../Viewmap'; | |||
| // Make the wrapper a global promise so it is load once | |||
| const Arr2DPromise = Arrangement2D(); | |||
| export class PolygonsInfo{ | |||
| smallAreaIgnored = Infinity; | |||
| insidePointErrors = Infinity; | |||
| } | |||
| /** | |||
| * Computes the polygons formed by the projection of the ViewEdges on the image | |||
| * plane | |||
| * @param viewmap | |||
| * @param info | |||
| */ | |||
| export async function computePolygons( | |||
| viewmap: Viewmap, | |||
| info = new PolygonsInfo()) { | |||
| const {chains, polygons} = viewmap; | |||
| const Arr2D = await Arr2DPromise; | |||
| const visibleChains = chains.filter(c => c.visibility === ChainVisibility.Visible); | |||
| const points = new Arr2D.PointList(); | |||
| let a, b; | |||
| for (const chain of visibleChains) { | |||
| a = new Arr2D.Point(chain.vertices[0].pos2d.x, chain.vertices[0].pos2d.y); | |||
| for (let i=1; i<chain.vertices.length; i++) { | |||
| b = new Arr2D.Point(chain.vertices[i].pos2d.x, chain.vertices[i].pos2d.y); | |||
| points.push_back(a); | |||
| points.push_back(b); | |||
| a = b; | |||
| } | |||
| } | |||
| const builder = new Arr2D.ArrangementBuilder(); | |||
| // todo: this gets stuck in infinite loop sometimes. either clamp or run it in a worker with timeout? | |||
| const arr2DPolygonlist = builder.getPolygons(points); | |||
| const p = new Arr2D.Point(); | |||
| info.smallAreaIgnored = 0; | |||
| info.insidePointErrors = 0; | |||
| for (let i=0; i<arr2DPolygonlist.size(); i++) { | |||
| const arr2DPolygon = arr2DPolygonlist.at(i); | |||
| const area = arr2DPolygon.getPolyTristripArea(); | |||
| if (area > 1e-10) { | |||
| // Transform types from the Arrangement2D to more friendly three types | |||
| const contour = convertContour(arr2DPolygon.contour); | |||
| const holes = convertContourList(arr2DPolygon.holes); | |||
| const polygon = new Polygon(i, contour, holes); | |||
| if (arr2DPolygon.getInsidePoint(p)) { | |||
| polygon.insidePoint.set(p.x, p.y); | |||
| polygons.push(polygon); | |||
| } else { | |||
| info.insidePointErrors += 1; | |||
| } | |||
| } else { | |||
| info.smallAreaIgnored += 1; | |||
| } | |||
| Arr2D.destroy(arr2DPolygon); | |||
| } | |||
| Arr2D.destroy(arr2DPolygonlist); | |||
| Arr2D.destroy(p); | |||
| } | |||
| export function convertContourList( | |||
| vector: Arrangement2D.ContourList) : Array<Array<Vector2>> { | |||
| const array = new Array<Array<Vector2>>(); | |||
| for (let i=0; i<vector.size(); i++) { | |||
| array.push(convertContour(vector.at(i))); | |||
| } | |||
| return array; | |||
| } | |||
| export function convertContour( | |||
| contour: Arrangement2D.Contour) : Array<Vector2> { | |||
| const array = new Array<Vector2>(); | |||
| for (let i=0; i<contour.size(); i++) { | |||
| const p = contour.at(i); | |||
| array.push(new Vector2(p.x, p.y)); | |||
| } | |||
| return array; | |||
| } | |||
| @@ -0,0 +1,82 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Nov 22 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 {SVGMesh} from "../../SVGMesh"; | |||
| import {Chain} from "../Chain"; | |||
| import {ViewEdge} from "../ViewEdge"; | |||
| import {Viewmap} from "../Viewmap"; | |||
| import {ViewVertex, ViewVertexSingularity} from "../ViewVertex"; | |||
| // See chaining section of https://hal.inria.fr/hal-02189483 | |||
| export function createChains(viewmap: Viewmap, maxChains = 10000) { | |||
| const {viewEdges, chains} = viewmap; | |||
| const remainingEdges = new Set(viewEdges); | |||
| let chainId = 0; | |||
| while(remainingEdges.size > 0 && chainId < maxChains) { | |||
| const [startEdge] = remainingEdges; | |||
| const currentObject = startEdge.meshes[0]; | |||
| const chain = new Chain(chainId, currentObject); | |||
| remainingEdges.delete(startEdge); | |||
| chain.addEdge(startEdge); | |||
| // Search for connected edges from one direction | |||
| for (const startViewVertex of startEdge.vertices) { | |||
| let current = startViewVertex; | |||
| let edge = nextChainEdge(startEdge, current, remainingEdges, currentObject); | |||
| while (edge) { | |||
| remainingEdges.delete(edge); | |||
| chain.addEdge(edge); | |||
| current = edge.otherVertex(current); | |||
| edge = nextChainEdge(edge, current, remainingEdges, currentObject); | |||
| } | |||
| } | |||
| chains.push(chain); | |||
| chainId += 1; | |||
| } | |||
| } | |||
| export function nextChainEdge( | |||
| currentEdge: ViewEdge, | |||
| viewVertex: ViewVertex, | |||
| remainingEdges: Set<ViewEdge>, | |||
| obj: SVGMesh) : ViewEdge | null { | |||
| // If point is a singularity, chaining stops | |||
| if (viewVertex.singularity !== ViewVertexSingularity.None) { | |||
| return null; | |||
| } | |||
| // TODO: Taking into account the nature of the current segment and geometric | |||
| // properties to build longer chains | |||
| for (const viewEdge of viewVertex.viewEdges) { | |||
| const takeEdge = | |||
| // Take edge only if it has not been assigned yet | |||
| remainingEdges.has(viewEdge) && | |||
| // Next edge must have the same nature of the current edge | |||
| viewEdge.nature === currentEdge.nature && | |||
| // Next edge must be part of the same object | |||
| viewEdge.meshes.includes(obj); | |||
| if (takeEdge) { | |||
| return viewEdge; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| @@ -0,0 +1,42 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Mon Dec 12 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 { Vector3 } from "three"; | |||
| import { hashVector2, hashVector3, projectPoint } from "../../../utils"; | |||
| import { Viewmap } from "../Viewmap"; | |||
| import { ViewVertex } from "../ViewVertex"; | |||
| /** | |||
| * Creates a ViewVertex at the given position if no one already exist | |||
| * @param viewmap | |||
| * @param pos3d | |||
| * @returns | |||
| */ | |||
| export function createViewVertex(viewmap: Viewmap, pos3d: Vector3) { | |||
| const {camera, viewVertexMap, renderSize} = viewmap; | |||
| const hash3d = hashVector3(pos3d); | |||
| let viewVertex = viewVertexMap.get(hash3d); | |||
| if (!viewVertex) { | |||
| viewVertex = new ViewVertex(); | |||
| viewVertex.pos3d.copy(pos3d); | |||
| projectPoint(pos3d, viewVertex.pos2d, camera, renderSize); | |||
| viewVertex.hash2d = hashVector2(viewVertex.pos2d); | |||
| viewVertex.hash3d = hash3d; | |||
| viewVertexMap.set(hash3d, viewVertex); | |||
| } | |||
| return viewVertex; | |||
| } | |||
| @@ -0,0 +1,126 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Nov 22 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 {ViewEdge, VisibilityIndicatingNatures} from "../ViewEdge"; | |||
| import {bush} from 'isect'; | |||
| import {Vector2} from "three"; | |||
| import {Viewmap} from "../Viewmap"; | |||
| import {splitViewEdge2d} from "./splitEdge"; | |||
| import {ViewVertexSingularity} from "../ViewVertex"; | |||
| import {hashVector2} from "../../../utils"; | |||
| const _vec = new Vector2(); | |||
| /** | |||
| * Finds the 2d singularities in the viewmap and mark them. | |||
| * (Computes the intersection of ViewEdges in the image plane) | |||
| * | |||
| * @param viewmap | |||
| */ | |||
| export function find2dSingularities(viewmap: Viewmap) { | |||
| const {viewEdges} = viewmap; | |||
| const interAlgorithm = bush([...viewEdges]); | |||
| let intersections = interAlgorithm.run() as Array<{ | |||
| segments: ViewEdge[], | |||
| point: {x: number, y: number} | |||
| }>; | |||
| // Keep intersections of non connected edges with at least one visibility | |||
| // indicating ViewEdgeNature | |||
| intersections = intersections.filter(({segments: [a,b]}) => { | |||
| return !(a).isConnectedTo(b) && | |||
| (VisibilityIndicatingNatures.has(a.nature) || | |||
| VisibilityIndicatingNatures.has(b.nature)); | |||
| }); | |||
| // As we will cut viewEdge recursively in small viewEdge, we store the current | |||
| // cuts in a map | |||
| const cutMap = new Map<ViewEdge, ViewEdge[]>(); | |||
| for (const intersection of intersections) { | |||
| const splitViewVertices = []; | |||
| _vec.set(intersection.point.x, intersection.point.y); | |||
| const hash = hashVector2(_vec); | |||
| for (const viewEdge of intersection.segments) { | |||
| // Setup edge cuts if needed | |||
| let cuts = cutMap.get(viewEdge); | |||
| if (!cuts) { | |||
| cuts = [viewEdge]; | |||
| cutMap.set(viewEdge, cuts); | |||
| } | |||
| // Test the cuts to find the intersection point | |||
| let i = 0; | |||
| let splitResult = null; | |||
| while(i < cuts.length && splitResult === null) { | |||
| splitResult = splitViewEdge2d(viewmap, cuts[i], _vec); | |||
| i += 1; | |||
| } | |||
| if (splitResult) { | |||
| splitViewVertices.push(splitResult.viewVertex); | |||
| /* | |||
| * Overwrite position and hash so we are sure the vertices have the | |||
| * exact same 2D position from the camera which is CRUCIAL for the | |||
| * CGAL step | |||
| */ | |||
| splitResult.viewVertex.pos2d.copy(_vec); | |||
| splitResult.viewVertex.hash2d = hash; | |||
| if (splitResult.viewEdge) { | |||
| cuts.push(splitResult.viewEdge); | |||
| } | |||
| } else { | |||
| // console.error("Image intersection -- Edge could not be splitted", cuts, _vec); | |||
| } | |||
| } | |||
| if (splitViewVertices.length === 0) { | |||
| // console.error("Image intersection -- Should have 2 split vertices"); | |||
| } else if (splitViewVertices.length === 1) { | |||
| const v = splitViewVertices[0]; | |||
| v.singularity = ViewVertexSingularity.ImageIntersection; | |||
| } else { | |||
| const v1 = splitViewVertices[0]; | |||
| const v2 = splitViewVertices[1]; | |||
| // Compute the distance between the vertices and the camera. | |||
| // We only need to insert a singularity point at the farest vertex | |||
| // If equal, both vertices get a singularity | |||
| // See https://hal.inria.fr/hal-02189483, image intersections of type T-cusp | |||
| const d1 = v1.pos3d.distanceTo(viewmap.camera.position); | |||
| const d2 = v2.pos3d.distanceTo(viewmap.camera.position); | |||
| if (d1 > d2 + 1e-10) { | |||
| v1.singularity = ViewVertexSingularity.ImageIntersection; | |||
| } else if (d2 > d1 + 1e-10) { | |||
| v2.singularity = ViewVertexSingularity.ImageIntersection; | |||
| } else { | |||
| v1.singularity = ViewVertexSingularity.ImageIntersection; | |||
| v2.singularity = ViewVertexSingularity.ImageIntersection; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,176 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Nov 22 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 {PerspectiveCamera} from "three"; | |||
| import {Vertex} from "../../../../three-mesh-halfedge"; | |||
| import {sameSide} from "../../../utils"; | |||
| import {ViewEdgeNature} from "../ViewEdge"; | |||
| import {Viewmap} from "../Viewmap"; | |||
| import {ViewVertex, ViewVertexSingularity} from "../ViewVertex"; | |||
| export function find3dSingularities(viewmap: Viewmap) { | |||
| const {viewVertexMap, camera} = viewmap; | |||
| for (const [, viewVertex] of viewVertexMap) { | |||
| viewVertex.singularity = singularityForPoint(viewVertex, camera); | |||
| } | |||
| } | |||
| /** | |||
| * | |||
| * @ref https://hal.inria.fr/hal-02189483/file/contour_tutorial.pdf Section 4.3 | |||
| * | |||
| * @param point | |||
| * @param camera | |||
| * @returns | |||
| */ | |||
| export function singularityForPoint( | |||
| viewVertex: ViewVertex, camera: PerspectiveCamera) { | |||
| const natures = new Set<ViewEdgeNature>(); | |||
| let concaveSilhouetteEdgeFound = false; | |||
| let convexSilhouetteEdgeFound = false; | |||
| // Count the number of different natures connected to the vertex | |||
| for (const edge of viewVertex.viewEdges) { | |||
| natures.add(edge.nature); | |||
| if (edge.faces.length > 1 && edge.nature === ViewEdgeNature.Silhouette) { | |||
| concaveSilhouetteEdgeFound ||= edge.isConcave; | |||
| convexSilhouetteEdgeFound ||= !edge.isConcave; | |||
| } | |||
| } | |||
| if (natures.size === 0) { | |||
| console.error("No natures found around vertex", viewVertex); | |||
| return ViewVertexSingularity.None; | |||
| } | |||
| // If the number of segment natures is 1 and there is more than 2 segments | |||
| // connected to the point, then there is a bifurcation singularity | |||
| if (natures.size === 1) { | |||
| if(viewVertex.viewEdges.length > 2 && ( | |||
| natures.has(ViewEdgeNature.Silhouette) || natures.has(ViewEdgeNature.Boundary) | |||
| )) { | |||
| return ViewVertexSingularity.Bifurcation; | |||
| } | |||
| } | |||
| // If there are at least 2 edges of different natures connected to the vertex, | |||
| // then there is a mesh intersection singularity | |||
| if (natures.size > 1) { | |||
| if (natures.has(ViewEdgeNature.Silhouette) || | |||
| natures.has(ViewEdgeNature.Boundary) || | |||
| natures.has(ViewEdgeNature.MeshIntersection)) { | |||
| return ViewVertexSingularity.MeshIntersection; | |||
| } | |||
| } | |||
| // Curtains folds: | |||
| // Curtain fold singularity can occur on a non-boundary segment where | |||
| // there are at least one concave and one convex edges connected | |||
| // if (!natures.has(EdgeNature.Boundary) && | |||
| if (concaveSilhouetteEdgeFound && convexSilhouetteEdgeFound) { | |||
| return ViewVertexSingularity.CurtainFold; | |||
| } | |||
| // Curtain fold singularity can also occur on a Boundary edge where | |||
| // one of the connected face overlaps the boundary edge | |||
| // Note that at this stage of the pipeline, each point should only have | |||
| // one associated vertex, hence the index 0 | |||
| if (natures.has(ViewEdgeNature.Boundary)) { | |||
| if (isAnyFaceOverlappingBoundary(viewVertex, camera)) { | |||
| return ViewVertexSingularity.CurtainFold; | |||
| } | |||
| } | |||
| return ViewVertexSingularity.None; | |||
| } | |||
| export function *listBoundaryHalfedgesInOut(vertex: Vertex) { | |||
| yield* vertex.boundaryHalfedgesInLoop(); | |||
| yield* vertex.boundaryHalfedgesOutLoop(); | |||
| } | |||
| /** | |||
| * Checks if face adjacent to a boundary vertex overlap in image-space. | |||
| * | |||
| * @ref https://hal.inria.fr/hal-02189483/file/contour_tutorial.pdf Appendix C.2.1 | |||
| * | |||
| * @param vertex | |||
| * @param camera | |||
| * @returns | |||
| */ | |||
| export function isAnyFaceOverlappingBoundary(viewVertex: ViewVertex, camera: PerspectiveCamera) { | |||
| for (const vertex of viewVertex.vertices) { | |||
| // Get the farthest boundary halfedge from the camera and connected to the | |||
| // vertex | |||
| let farthestHalfedge = null; | |||
| let otherVertex = null; | |||
| let distance = -Infinity; | |||
| for (const halfedge of listBoundaryHalfedgesInOut(vertex)) { | |||
| let other; | |||
| if (halfedge.vertex === vertex) { | |||
| // Halfedge is starting from vertex | |||
| other = halfedge.next.vertex; | |||
| } else { | |||
| // Halfedge is arriving to vertex | |||
| other = halfedge.vertex; | |||
| } | |||
| const d = other.position.distanceTo(camera.position); | |||
| if (d > distance) { | |||
| distance = d; | |||
| farthestHalfedge = halfedge; | |||
| otherVertex = other; | |||
| } | |||
| } | |||
| if (farthestHalfedge && otherVertex) { | |||
| // Iterate on each connected faces to vertex and check if it overlaps | |||
| // the farthest halfedge | |||
| const c = camera.position; | |||
| const p = vertex.position; | |||
| const e = otherVertex.position; | |||
| const boundaryFace = farthestHalfedge.twin.face; | |||
| if (boundaryFace) { | |||
| for (const halfedge of vertex.loopCW()) { | |||
| if (halfedge.face !== boundaryFace) { | |||
| const q = halfedge.next.vertex.position; | |||
| const r = halfedge.next.vertex.position; | |||
| if (!sameSide(p,q,r,c,e) && sameSide(c,p,q,e,r) && sameSide(c,p,r,e,q)) { | |||
| return true; | |||
| } | |||
| } | |||
| } | |||
| } else { | |||
| console.error("Boundary halfedge twin has no connected face"); | |||
| } | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| @@ -0,0 +1,86 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Wed Nov 30 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 {trianglesIntersect} from "fast-triangle-triangle-intersection"; | |||
| import {Line3, Matrix4, Triangle, Vector3} from "three"; | |||
| import {Face} from "../../../../three-mesh-halfedge"; | |||
| import {SVGMesh} from "../../SVGMesh"; | |||
| const _matrix = new Matrix4(); | |||
| const _line = new Line3(); | |||
| const _points = new Array<Vector3>(); | |||
| export class TriIntersectionInfo { | |||
| name = ""; | |||
| nbTests = Infinity; | |||
| nbIntersections = Infinity; | |||
| time = Infinity; | |||
| } | |||
| /** | |||
| * Run the specify callback for all | |||
| * @param meshA | |||
| * @param meshB | |||
| * @param callback | |||
| * @param info | |||
| */ | |||
| export function meshIntersectionCb( | |||
| meshA: SVGMesh, | |||
| meshB: SVGMesh, | |||
| callback: (meshA: SVGMesh, meshB: SVGMesh, line: Line3, faceA: Face, faceB: Face) => void, | |||
| info = new TriIntersectionInfo()) { | |||
| const startTime = Date.now(); | |||
| info.name = meshA.name + ' ∩ ' + meshB.name; | |||
| info.nbTests = 0; | |||
| info.nbIntersections = 0; | |||
| _matrix.copy(meshA.matrixWorld).invert().multiply(meshB.matrixWorld); | |||
| meshA.bvh.bvhcast(meshB.bvh, _matrix, { | |||
| intersectsTriangles: (t1: Triangle, t2: Triangle, idx1: number, idx2: number) => { | |||
| info.nbTests += 1; | |||
| if (trianglesIntersect(t1, t2, _points) !== null) { | |||
| info.nbIntersections += 1; | |||
| // Ignore intersection on a single point | |||
| if (_points.length === 1) { | |||
| return false; | |||
| } | |||
| else if (_points.length > 2) { | |||
| _points.push(_points[0]); | |||
| } | |||
| for (let i=0; i<_points.length-1; i++) { | |||
| _line.start.copy(_points[i]); | |||
| _line.end.copy(_points[i+1]); | |||
| if (_line.distance() > 1e-10) { | |||
| _line.applyMatrix4(meshA.matrixWorld); | |||
| callback(meshA, meshB, _line, meshA.hes.faces[idx1], meshB.hes.faces[idx2]); | |||
| } | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| }); | |||
| info.time = Date.now() - startTime; | |||
| } | |||
| @@ -0,0 +1,152 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Wed Nov 16 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 {Halfedge} from "../../../../three-mesh-halfedge"; | |||
| import {frontSide} from "../../../utils"; | |||
| import {ViewEdge, ViewEdgeNature} from "../ViewEdge"; | |||
| import {Viewmap} from "../Viewmap"; | |||
| import {PerspectiveCamera, Vector3} from "three"; | |||
| import {createViewVertex} from "./createViewVertex"; | |||
| export interface ViewEdgeNatureOptions { | |||
| creaseAngle?: {min: number, max: number}; | |||
| } | |||
| const _u = new Vector3(); | |||
| const _v = new Vector3(); | |||
| /** | |||
| * Returns the list | |||
| * @param meshes | |||
| * @param camera | |||
| * @param options | |||
| * @returns | |||
| */ | |||
| export function setupEdges( | |||
| viewmap: Viewmap, | |||
| options: ViewEdgeNatureOptions) { | |||
| const {viewEdges, camera, meshes} = viewmap; | |||
| const handledHalfedges = new Set<Halfedge>(); | |||
| for (const mesh of meshes) { | |||
| for (const face of mesh.hes.faces) { | |||
| face.viewEdges = new Array<ViewEdge>(); | |||
| } | |||
| for (const halfedge of mesh.hes.halfedges) { | |||
| if (!handledHalfedges.has(halfedge.twin)) { | |||
| handledHalfedges.add(halfedge); | |||
| const props = propsForViewEdge(halfedge, camera, options); | |||
| if (props) { | |||
| const meshv1 = halfedge.vertex; | |||
| const meshv2 = halfedge.twin.vertex; | |||
| // Get the viewmap points from the vertices or create them | |||
| const v1 = createViewVertex(viewmap, meshv1.position); | |||
| const v2 = createViewVertex(viewmap, meshv2.position); | |||
| meshv1.viewVertex = v1; | |||
| meshv2.viewVertex = v2; | |||
| // Point stores a set of vertices, so unicity is guaranted | |||
| v1.vertices.add(meshv1); | |||
| v2.vertices.add(meshv2); | |||
| const viewEdge = new ViewEdge(v1, v2, props.nature, halfedge); | |||
| viewEdge.faceAngle = props.faceAngle; | |||
| viewEdge.isConcave = props.isConcave; | |||
| viewEdge.isBack = props.isBack; | |||
| viewEdge.meshes.push(mesh); | |||
| v1.viewEdges.push(viewEdge); | |||
| v2.viewEdges.push(viewEdge); | |||
| if (halfedge.face) { | |||
| halfedge.face.viewEdges.push(viewEdge); | |||
| viewEdge.faces.push(halfedge.face); | |||
| } | |||
| if (halfedge.twin.face) { | |||
| halfedge.twin.face.viewEdges.push(viewEdge); | |||
| viewEdge.faces.push(halfedge.twin.face); | |||
| } | |||
| viewEdges.push(viewEdge); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| export function propsForViewEdge( | |||
| halfedge: Halfedge, | |||
| camera: PerspectiveCamera, | |||
| options?: ViewEdgeNatureOptions) { | |||
| const props = { | |||
| nature: ViewEdgeNature.Silhouette, | |||
| faceAngle: 0, | |||
| isConcave: false, | |||
| isBack: false, | |||
| } | |||
| const opt = { | |||
| creaseAngle: {min: 80, max: 100}, | |||
| ...options | |||
| } | |||
| // If halfedge only has one connected face, then it is a boundary | |||
| if (!halfedge.face || !halfedge.twin.face) { | |||
| props.nature = ViewEdgeNature.Boundary; | |||
| return props; | |||
| } else { | |||
| const faceAFront = halfedge.face.isFront(camera.position); | |||
| const faceBFront = halfedge.twin.face.isFront(camera.position); | |||
| // If edge is between two back faces, then it is a back edge | |||
| props.isBack = !faceAFront && !faceBFront; | |||
| // Compute the angle between the 2 connected face | |||
| halfedge.face.getNormal(_u); | |||
| halfedge.twin.face.getNormal(_v); | |||
| props.faceAngle = Math.acos(_u.dot(_v)) * 180 / Math.PI; | |||
| // Concavity is determined by an orientation test | |||
| props.isConcave = frontSide( | |||
| halfedge.prev.vertex.position, | |||
| halfedge.vertex.position, | |||
| halfedge.next.vertex.position, | |||
| halfedge.twin.prev.vertex.position); | |||
| // If edge is between front and back face, then it is a silhouette edge | |||
| if (faceAFront !== faceBFront) { | |||
| props.nature = ViewEdgeNature.Silhouette; | |||
| return props; | |||
| } else if(opt.creaseAngle.min <= props.faceAngle && | |||
| props.faceAngle <= opt.creaseAngle.max) { | |||
| props.nature = ViewEdgeNature.Crease; | |||
| return props; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| @@ -0,0 +1,197 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Nov 29 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 {Vector2, Vector3} from "three"; | |||
| // import { Vertex } from "three-mesh-halfedge"; | |||
| // import {hashVector2, hashVector3} from "../../../utils"; | |||
| import {ViewEdge} from "../ViewEdge"; | |||
| import {Viewmap} from "../Viewmap"; | |||
| import {ViewVertex} from "../ViewVertex"; | |||
| import {createViewVertex} from "./createViewVertex"; | |||
| const _u = new Vector3(); | |||
| const _v = new Vector3(); | |||
| const _vec3 = new Vector3(); | |||
| const _u2 = new Vector2(); | |||
| const _v2 = new Vector2(); | |||
| export function splitViewEdge3d( | |||
| viewmap: Viewmap, | |||
| edge: ViewEdge, | |||
| position: Vector3) { | |||
| /** | |||
| * We consider that position is on the infinite line formed by a and b | |||
| * | |||
| * p? p? p? | |||
| * x--a--------x---------b--x | |||
| * edge | |||
| */ | |||
| // const hash = hashVector3(position); | |||
| // if (edge.a.hash3d === hash) { | |||
| if (edge.a.matches3dPosition(position)) { | |||
| // if (edge.a.hash3d !== hash) { | |||
| // console.log("Different hash", edge.a, position, hash); | |||
| // } | |||
| return { | |||
| viewVertex: edge.a, | |||
| viewEdge: null | |||
| }; | |||
| } | |||
| // if (edge.b.hash3d === hash) { | |||
| if (edge.b.matches3dPosition(position)) { | |||
| // if (edge.b.hash3d !== hash) { | |||
| // console.log("Different hash", edge.b, position, hash); | |||
| // } | |||
| return { | |||
| viewVertex: edge.b, | |||
| viewEdge: null | |||
| }; | |||
| } | |||
| _u.subVectors(position, edge.a.pos3d); | |||
| _v.subVectors(edge.b.pos3d, edge.a.pos3d); | |||
| const cross = _u.cross(_v); | |||
| const v = cross.x + cross.y + cross.z; | |||
| if (v > 1e-10 || v < -1e-10) { | |||
| return null; | |||
| } | |||
| if (_u.dot(_v) < -1e-10) { | |||
| return null; | |||
| } | |||
| const lengthU = _u.length(); | |||
| const lengthV = _v.length(); | |||
| if (lengthU > lengthV) { | |||
| return null; | |||
| } | |||
| const viewVertex = createViewVertex(viewmap, position); | |||
| const viewEdge = splitViewEdgeWithViewVertex(viewmap, edge, viewVertex); | |||
| return { | |||
| viewVertex: viewVertex, | |||
| viewEdge: viewEdge | |||
| }; | |||
| } | |||
| export function splitViewEdge2d( | |||
| viewmap: Viewmap, | |||
| edge: ViewEdge, | |||
| position: Vector2) { | |||
| // tolerance = 1e-10) { | |||
| // const hash = hashVector2(position); | |||
| // if (edge.a.hash2d === hash) { | |||
| if (edge.a.matches2dPosition(position)) { | |||
| // if (edge.a.hash2d !== hash) { | |||
| // console.log("Different hash", edge.a, position, hash); | |||
| // } | |||
| return { | |||
| viewVertex: edge.a, | |||
| viewEdge: null | |||
| }; | |||
| } | |||
| // if (edge.b.hash2d === hash) { | |||
| if (edge.b.matches2dPosition(position)) { | |||
| // if (edge.b.hash2d !== hash) { | |||
| // console.log("Different hash", edge.b, position, hash); | |||
| // } | |||
| return { | |||
| viewVertex: edge.b, | |||
| viewEdge: null | |||
| }; | |||
| } | |||
| _u2.subVectors(position, edge.a.pos2d); | |||
| _v2.subVectors(edge.b.pos2d, edge.a.pos2d); | |||
| // Check points are aligned | |||
| const cross = _u2.cross(_v2); | |||
| if (cross > 1e-10 || cross < -1e-10) { | |||
| return null; | |||
| } | |||
| const lengthU = _u2.length(); | |||
| const lengthV = _v2.length(); | |||
| if (lengthU > lengthV) { | |||
| return null; | |||
| } | |||
| // Check points order | |||
| if (_u.dot(_v) < -1e10) { | |||
| return null; | |||
| } | |||
| _vec3.lerpVectors(edge.a.pos3d, edge.b.pos3d, lengthU/lengthV); | |||
| const viewVertex = createViewVertex(viewmap, _vec3); | |||
| const viewEdge = splitViewEdgeWithViewVertex(viewmap, edge, viewVertex); | |||
| return { | |||
| viewVertex: viewVertex, | |||
| viewEdge: viewEdge | |||
| }; | |||
| } | |||
| export function splitViewEdgeWithViewVertex( | |||
| viewmap: Viewmap, | |||
| edge: ViewEdge, | |||
| vertex: ViewVertex) { | |||
| /** | |||
| * Update the references around the new vertex | |||
| * | |||
| * vertex | |||
| * ---a--------x---------b-- | |||
| * edge newedge | |||
| */ | |||
| const b = edge.b; | |||
| const newEdge = edge.clone(); | |||
| edge.b = vertex; | |||
| newEdge.a = vertex; | |||
| newEdge.b = b; | |||
| vertex.viewEdges.push(edge); | |||
| vertex.viewEdges.push(newEdge); | |||
| b.viewEdges.remove(edge); | |||
| b.viewEdges.push(newEdge); | |||
| for (const face of newEdge.faces) { | |||
| face.viewEdges.push(newEdge); | |||
| } | |||
| viewmap.viewEdges.push(newEdge); | |||
| return newEdge; | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Mon Oct 17 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 | |||
| */ | |||
| export { SVGRenderer, SVGRenderInfo } from "./SVGRenderer"; | |||
| export * from './core'; | |||
| export type {PointLike, SizeLike, RectLike} from './utils'; | |||
| @@ -0,0 +1,15 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Tue Dec 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 './utils/testutils'; | |||
| @@ -0,0 +1,65 @@ | |||
| // Author: Axel Antoine | |||
| // mail: ax.antoine@gmail.com | |||
| // website: https://axantoine.com | |||
| // 04/05/2021 | |||
| // 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 | |||
| declare module 'isect' { | |||
| interface Point { | |||
| x: number; | |||
| y: number; | |||
| } | |||
| interface Segment { | |||
| from: Point; | |||
| to: Point; | |||
| } | |||
| interface ISectResults { | |||
| run: ()=>void; | |||
| step: ()=>void; | |||
| } | |||
| interface Intersection { | |||
| point: Point; | |||
| segments: Segment[]; | |||
| } | |||
| interface Options { | |||
| onFound?: (result: ISectResults) => boolean; | |||
| } | |||
| interface Detector { | |||
| results: Intersection[]; | |||
| /** | |||
| * Find all intersections synchronously. | |||
| * | |||
| * @returns array of found intersections. | |||
| */ | |||
| run(): Intersection[]; | |||
| /** | |||
| * Performs a single step in the sweep line algorithm | |||
| * | |||
| * @returns true if there was something to process; False if no more work to do | |||
| */ | |||
| step(): boolean; | |||
| /** | |||
| * Add segment | |||
| */ | |||
| addSegment(segment: Segment): void; | |||
| } | |||
| export function brute(segments: Segment[], options?: Options): Detector; | |||
| export function bush(segments: Segment[], options?: Options): Detector; | |||
| export function sweep(segments: Segment[], options?: Options): Detector; | |||
| } | |||
| @@ -0,0 +1,66 @@ | |||
| import {BufferAttribute, BufferGeometry, Material, Mesh} from 'three'; | |||
| import {computeMorphedAttributes} from 'threepipe'; | |||
| /** | |||
| * Types definitions are not up to date | |||
| */ | |||
| declare module 'threepipe' { | |||
| export function computeMorphedAttributes(object: Mesh): { | |||
| positionAttribute: BufferAttribute, | |||
| normalAttribute: BufferAttribute, | |||
| morphedPositionAttribute: BufferAttribute, | |||
| morphedNormalAttribute: BufferAttribute | |||
| } | |||
| } | |||
| export function triangleGeometry(size: number) { | |||
| const vertices = new Float32Array([ | |||
| -size, 0, -size, | |||
| size, 0, -size, | |||
| 0, 0, size | |||
| ]); | |||
| const geometry = new BufferGeometry(); | |||
| geometry.setAttribute('position', new BufferAttribute(vertices, 3)); | |||
| return geometry; | |||
| } | |||
| export function disposeMesh(mesh: Mesh) { | |||
| mesh.geometry.dispose(); | |||
| if (mesh.material instanceof Array) { | |||
| const materials = mesh.material as Array<Material>; | |||
| for (const material of materials) { | |||
| material.dispose(); | |||
| } | |||
| } else { | |||
| mesh.material.dispose(); | |||
| } | |||
| } | |||
| export function disposeGeometry(geometry: BufferGeometry) { | |||
| geometry.dispose(); | |||
| for (const attribute in geometry.attributes) { | |||
| geometry.deleteAttribute(attribute); | |||
| } | |||
| } | |||
| export function computeMorphedGeometry(source: Mesh, target: BufferGeometry) { | |||
| if (!source.geometry.hasAttribute("normal")) { | |||
| source.geometry.computeVertexNormals(); | |||
| } | |||
| const { | |||
| morphedPositionAttribute, | |||
| morphedNormalAttribute | |||
| } = computeMorphedAttributes(source); | |||
| target.groups = [...source.geometry.groups]; | |||
| if (source.geometry.index) { | |||
| target.index = source.geometry.index.clone(); | |||
| } | |||
| target.deleteAttribute('position'); | |||
| target.deleteAttribute('normal'); | |||
| target.setAttribute('position', morphedPositionAttribute); | |||
| target.setAttribute('normal', morphedNormalAttribute); | |||
| } | |||
| @@ -0,0 +1,95 @@ | |||
| // /* | |||
| // * Author: Axel Antoine | |||
| // * mail: ax.antoine@gmail.com | |||
| // * website: http://axantoine.com | |||
| // * Created on Mon Dec 05 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 { Line3, Vector3 } from "three"; | |||
| // import { intersectLines } from "./geometry"; | |||
| // | |||
| // describe('intersectLines intersectLines', () => { | |||
| // | |||
| // const a = new Line3(); | |||
| // const b = new Line3(); | |||
| // const target = new Vector3(); | |||
| // | |||
| // test ('Intersecting Lines in 2d', () => { | |||
| // | |||
| // a.start.set(1,1,0); | |||
| // a.end.set(1,3,0); | |||
| // | |||
| // b.start.set(0,1,0); | |||
| // b.end.set(2,1,0); | |||
| // | |||
| // expect(intersectLines(a, b, target)).toBeTruthy(); | |||
| // expect(target.x).toBeCloseTo(1); | |||
| // expect(target.y).toBeCloseTo(1); | |||
| // expect(target.z).toBeCloseTo(0); | |||
| // | |||
| // }); | |||
| // | |||
| // test ('Intersecting Lines in 3d', () => { | |||
| // | |||
| // a.start.set(0,0,0); | |||
| // a.end.set(2,2,2); | |||
| // | |||
| // b.start.set(2,0,0); | |||
| // b.end.set(0,2,2); | |||
| // | |||
| // expect(intersectLines(a, b, target)).toBeTruthy(); | |||
| // expect(target.x).toBeCloseTo(1); | |||
| // expect(target.y).toBeCloseTo(1); | |||
| // expect(target.z).toBeCloseTo(1); | |||
| // }); | |||
| // | |||
| // test ('Intersect on point', () => { | |||
| // | |||
| // a.start.set(0,0,0); | |||
| // a.end.set(1.345678912,2.456789123,3.5678912345); | |||
| // | |||
| // b.start.set(1.345678912,2.456789123,3.5678912345); | |||
| // b.end.set(9,9,9); | |||
| // | |||
| // expect(intersectLines(a, b, target)).toBeTruthy(); | |||
| // expect(target.x).toBeCloseTo(1.345678912, 9); | |||
| // expect(target.y).toBeCloseTo(2.456789123, 9); | |||
| // expect(target.z).toBeCloseTo(3.567891234, 9); | |||
| // }); | |||
| // | |||
| // test ('Intersect T shape', () => { | |||
| // | |||
| // a.start.set(1.456789,0,0); | |||
| // a.end.set(1.456789,2,0); | |||
| // | |||
| // b.start.set(1.456789,1,0); | |||
| // b.end.set(3,2,0); | |||
| // | |||
| // expect(intersectLines(a, b, target)).toBeTruthy(); | |||
| // expect(target.x).toBeCloseTo(1.456789, 9); | |||
| // expect(target.y).toBeCloseTo(1); | |||
| // expect(target.z).toBeCloseTo(0); | |||
| // }); | |||
| // | |||
| // test ('Non Intersecting Lines', () => { | |||
| // | |||
| // a.start.set(0,0,0); | |||
| // a.end.set(0,2,0); | |||
| // | |||
| // b.start.set(1,1,0); | |||
| // b.end.set(2,1,0); | |||
| // | |||
| // expect(intersectLines(a, b, target)).toBeFalsy(); | |||
| // }); | |||
| // | |||
| // | |||
| // | |||
| // | |||
| // }) | |||
| @@ -0,0 +1,156 @@ | |||
| import {Vector2, Vector3, PerspectiveCamera, Line3} from 'three'; | |||
| const _u = new Vector3(); | |||
| export interface PointLike { | |||
| x: number; | |||
| y: number; | |||
| } | |||
| export interface SizeLike { | |||
| w: number; | |||
| h: number; | |||
| } | |||
| export interface RectLike extends PointLike, SizeLike {} | |||
| export function projectPointNDC( | |||
| point: Vector3, | |||
| target: Vector2, | |||
| camera: PerspectiveCamera | |||
| ): Vector2 { | |||
| _u.copy(point).project(camera); | |||
| return target.set(_u.x, _u.y); | |||
| } | |||
| export function projectPoint( | |||
| point: Vector3, | |||
| target: Vector2, | |||
| camera: PerspectiveCamera, | |||
| renderSize: SizeLike): Vector2 { | |||
| projectPointNDC(point, target, camera); | |||
| NDCPointToImage(target, target, renderSize); | |||
| return target; | |||
| } | |||
| /** | |||
| * Converts a point from the NDC coordinates to the image coordinates | |||
| * @param point Point in NDC to be converted | |||
| * @param size Size of the render | |||
| * @returns | |||
| */ | |||
| export function NDCPointToImage(point: Vector2, target: Vector2, size: SizeLike): Vector2 { | |||
| return target.set( | |||
| (point.x + 1)/2 * size.w, | |||
| (1 - point.y)/2 * size.h | |||
| ); | |||
| } | |||
| /** | |||
| * Converts a point from the image coordinates to the NDC coordinates | |||
| * @param point Point in the image coordinates | |||
| * @param size Size of the render | |||
| * @returns | |||
| */ | |||
| export function imagePointToNDC(point: Vector2, target: Vector2, size: SizeLike): Vector2 { | |||
| return target.set( | |||
| 2/size.w*point.x - 1, | |||
| 1 - 2/size.h*point.y | |||
| ); | |||
| } | |||
| export function hashVector3(vec: Vector3, multiplier = 1e10) { | |||
| const gap = 1e-3/multiplier; | |||
| return `${hashNumber(vec.x+gap, multiplier)},` + | |||
| `${hashNumber(vec.y+gap, multiplier)},` + | |||
| `${hashNumber(vec.z+gap, multiplier)}`; | |||
| } | |||
| export function hashVector2(vec: Vector2, multiplier = 1e10) { | |||
| const gap = 1e-3/multiplier; | |||
| return `${hashNumber(vec.x+gap, multiplier)},` + | |||
| `${hashNumber(vec.y+gap, multiplier)}`; | |||
| } | |||
| function hashNumber(value: number, multiplier = 1e10) { | |||
| // return (~ ~ (value*multiplier)); | |||
| return Math.trunc(value*multiplier); | |||
| } | |||
| /** | |||
| * Checks wether lines intersect and computes the intersection point. | |||
| * | |||
| * Adapted from mathjs | |||
| * | |||
| * @param line1 First segment/line | |||
| * @param line2 Second segment/line | |||
| * @param target Destination of the intersection point | |||
| * @param infiniteLine Wether to consider segments as infinite lines. Default, false | |||
| * @param tolerance Tolerance from which points are considred equal | |||
| * @returns true if lines intersect, false otherwise | |||
| */ | |||
| export function intersectLines( | |||
| line1: Line3, | |||
| line2: Line3, | |||
| target: Vector3, | |||
| infiniteLine = false, | |||
| tolerance = 1e-10) { | |||
| const {x : x1, y : y1, z : z1} = line1.start; | |||
| const {x : x2, y : y2, z : z2} = line1.end; | |||
| const {x : x3, y : y3, z : z3} = line2.start; | |||
| const {x : x4, y : y4, z : z4} = line2.end; | |||
| // (a - b)*(c - d) + (e - f)*(g - h) + (i - j)*(k - l) | |||
| const d1343 = (x1 - x3)*(x4 - x3) + (y1 - y3)*(y4 - y3) + (z1 - z3)*(z4 - z3); | |||
| const d4321 = (x4 - x3)*(x2 - x1) + (y4 - y3)*(y2 - y1) + (z4 - z3)*(z2 - z1); | |||
| const d1321 = (x1 - x3)*(x2 - x1) + (y1 - y3)*(y2 - y1) + (z1 - z3)*(z2 - z1); | |||
| const d4343 = (x4 - x3)*(x4 - x3) + (y4 - y3)*(y4 - y3) + (z4 - z3)*(z4 - z3); | |||
| const d2121 = (x2 - x1)*(x2 - x1) + (y2 - y1)*(y2 - y1) + (z2 - z1)*(z2 - z1); | |||
| const numerator = (d1343 * d4321) - (d1321 * d4343); | |||
| const denominator = (d2121 * d4343) - (d4321 * d4321); | |||
| if (denominator < tolerance) { | |||
| return false; | |||
| } | |||
| const ta = numerator / denominator; | |||
| const tb = ((d1343 + (ta * d4321)) / d4343); | |||
| if (!infiniteLine && (ta < 0 || ta > 1 || tb < 0 || tb > 1)) { | |||
| return false; | |||
| } | |||
| const pax = x1 + (ta * (x2 - x1)); | |||
| const pay = y1 + (ta * (y2 - y1)); | |||
| const paz = z1 + (ta * (z2 - z1)); | |||
| const pbx = x3 + (tb * (x4 - x3)); | |||
| const pby = y3 + (tb * (y4 - y3)); | |||
| const pbz = z3 + (tb * (z4 - z3)); | |||
| if (Math.abs(pax - pbx) < tolerance && | |||
| Math.abs(pay - pby) < tolerance && | |||
| Math.abs(paz - pbz) < tolerance) { | |||
| target.set(pax, pay, paz); | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| export function vectors3Equal(a: Vector3, b: Vector3, tolerance = 1e-10) { | |||
| return ( | |||
| Math.abs(a.x - b.x) < tolerance && | |||
| Math.abs(a.y - b.y) < tolerance && | |||
| Math.abs(a.z - b.z) < tolerance | |||
| ); | |||
| } | |||
| export function vectors2Equal(a: Vector2, b: Vector2, tolerance = 1e-10) { | |||
| return ( | |||
| Math.abs(a.x - b.x) < tolerance && | |||
| Math.abs(a.y - b.y) < tolerance | |||
| ); | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Thu Oct 20 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 | |||
| */ | |||
| export * from './buffergeometry'; | |||
| export * from './geometry'; | |||
| export * from './orientationtests'; | |||
| @@ -0,0 +1,24 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Wed Dec 14 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 | |||
| */ | |||
| export function mergeOptions (target: any, source: any) { | |||
| // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties | |||
| for (const key of Object.keys(source)) { | |||
| if (source[key] instanceof Object) Object.assign(source[key], mergeOptions(target[key], source[key])) | |||
| } | |||
| // Join `target` and modified `source` | |||
| Object.assign(target || {}, source) | |||
| return target | |||
| } | |||
| @@ -0,0 +1,108 @@ | |||
| /* | |||
| * Author: Axel Antoine | |||
| * mail: ax.antoine@gmail.com | |||
| * website: http://axantoine.com | |||
| * Created on Thu Oct 20 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 { Matrix4, Triangle, Vector3 } from "three"; | |||
| const _matrix = new Matrix4(); | |||
| /** | |||
| * Determines whether the point `d` is to the left of, to the right of, or on | |||
| * the oriented plane defined by triangle `abc` appearing in counter-clockwise | |||
| * order when viewed from above the plane. | |||
| * | |||
| * See https://hal.inria.fr/hal-02189483 Appendix C.2 Orientation test | |||
| * | |||
| * @param a Triangle point | |||
| * @param b Triangle point | |||
| * @param c Triangle point | |||
| * @param d Test point | |||
| * @param epsilon Precision, default to `1e-10` | |||
| * @returns `1` if on the right side, `-1` if left, `0` if coplanar | |||
| */ | |||
| export function orient3D(a: Vector3, b: Vector3, c: Vector3, d: Vector3, epsilon = 1e-10): 1|-1|0{ | |||
| _matrix.set( | |||
| a.x, a.y, a.z, 1, | |||
| b.x, b.y, b.z, 1, | |||
| c.x, c.y, c.z, 1, | |||
| d.x, d.y, d.z, 1 | |||
| ); | |||
| const det = _matrix.determinant(); | |||
| if (det > epsilon) { | |||
| return 1; | |||
| } else if (det < -epsilon) { | |||
| return -1; | |||
| } | |||
| return 0; | |||
| } | |||
| /** | |||
| * | |||
| * Determines whether the point `d` is to the left of, to the right of, or on | |||
| * the oriented plane defined by triangle `abc` appearing in counter-clockwise | |||
| * order when viewed from above the plane. | |||
| * | |||
| * See https://hal.inria.fr/hal-02189483 Appendix C.2 Orientation test | |||
| * | |||
| * @param tri Triangle | |||
| * @param p Test point | |||
| * @param epsilon Precision, default to `1e-10` | |||
| * @returns `1` if on the right side, `-1` if left, `0` if coplanar | |||
| */ | |||
| export function triOrient3D(tri: Triangle, p: Vector3, epsilon = 1e-10) { | |||
| return orient3D(tri.a, tri.b, tri.c, p, epsilon); | |||
| } | |||
| /** | |||
| * Returns whether the point `d` is front facing the triangle `abc`. | |||
| * | |||
| * See https://hal.inria.fr/hal-02189483 Appendix C.2 Orientation test | |||
| * | |||
| * @param a Triangle point | |||
| * @param b Triangle point | |||
| * @param c Triangle point | |||
| * @param d Camera position | |||
| * @param epsilon Precision, default to `1e-10` | |||
| * @returns `True` if triangle if front facing, `False` otherwise | |||
| */ | |||
| export function frontSide(a: Vector3, b: Vector3, c: Vector3, d: Vector3, epsilon = 1e-10) { | |||
| return orient3D(d, b, c, a, epsilon) > 0; | |||
| } | |||
| /** | |||
| * Returns whether the points `d` and `e` are on the same side of the triangle `abc`. | |||
| * | |||
| * See https://hal.inria.fr/hal-02189483 Appendix C.2 Orientation test | |||
| * | |||
| * @param a Triangle point | |||
| * @param b Triangle point | |||
| * @param c Triangle point | |||
| * @param d Test point | |||
| * @param e Test point | |||
| * @param epsilon Precision, default to `1e-10` | |||
| * @returns `True` if points are on the same side, `False` otherwise | |||
| */ | |||
| export function sameSide(a: Vector3, b: Vector3, c: Vector3, d: Vector3, e: Vector3, epsilon = 1e-10) { | |||
| return (orient3D(a,b,c,d,epsilon) > 0) === (orient3D(a,b,c,e,epsilon) > 0); | |||
| } | |||
| /** | |||
| * Rounds the number `num` with the given `divider`. | |||
| * @param num Number to round | |||
| * @param divider Value of the divider, default `100`. | |||
| * @returns Rounded number | |||
| */ | |||
| export function round(num: number, divider = 100) { | |||
| return Math.round(num * divider)/divider; | |||
| } | |||
| @@ -0,0 +1,64 @@ | |||
| // /* | |||
| // * Author: Axel Antoine | |||
| // * mail: ax.antoine@gmail.com | |||
| // * website: http://axantoine.com | |||
| // * Created on Tue Dec 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 {Vector3} from 'three'; | |||
| // import { Vertex } from 'three-mesh-halfedge'; | |||
| // | |||
| // declare global { | |||
| // namespace jest { | |||
| // interface Matchers<R> { | |||
| // toBeVertex(expected: Vertex): CustomMatcherResult; | |||
| // } | |||
| // } | |||
| // } | |||
| // | |||
| // expect.extend({ | |||
| // | |||
| // toBeVertex(received: Vertex, expected: Vertex) { | |||
| // const pass = received === expected; | |||
| // | |||
| // return { | |||
| // message: () => | |||
| // `Expected Vertices ${pass? 'not ': ''}to be equal`+ | |||
| // `\nReceived: Vertex ${received.id} ${vecToStr(received.position)}`+ | |||
| // `\nExpected: Vertex ${expected.id} ${vecToStr(expected.position)}`, | |||
| // pass: pass, | |||
| // }; | |||
| // }, | |||
| // | |||
| // }); | |||
| // | |||
| // export function vecToStr(v: Vector3) { | |||
| // return `(${v.x.toFixed(3)},${v.y.toFixed(3)},${v.z.toFixed(3)})`; | |||
| // } | |||
| // | |||
| // export function generatorSize(g: Generator) { | |||
| // let cpt = 0; | |||
| // let v = g.next(); | |||
| // while(!v.done) { | |||
| // cpt += 1; | |||
| // v = g.next(); | |||
| // } | |||
| // return cpt; | |||
| // } | |||
| // | |||
| // export function generatorToArray<T>(g: Generator<T>) { | |||
| // const array = new Array<T>(); | |||
| // let v = g.next(); | |||
| // while(!v.done) { | |||
| // array.push(v.value); | |||
| // v = g.next(); | |||
| // } | |||
| // return array; | |||
| // } | |||
| @@ -0,0 +1,45 @@ | |||
| { | |||
| "compilerOptions": { | |||
| "baseUrl": "./src", | |||
| "rootDir": "./src", | |||
| "allowJs": true, | |||
| "checkJs": false, | |||
| "skipLibCheck": true, | |||
| "allowSyntheticDefaultImports": true, | |||
| "experimentalDecorators": true, | |||
| "isolatedModules": true, | |||
| "module": "es2020", | |||
| "noImplicitAny": true, | |||
| "declaration": true, | |||
| "declarationMap": true, | |||
| "declarationDir": "dist", | |||
| "outDir": "dist", | |||
| "noImplicitThis": true, | |||
| "noUnusedLocals": true, | |||
| "noUnusedParameters": true, | |||
| "removeComments": false, | |||
| "preserveConstEnums": true, | |||
| "moduleResolution": "node", | |||
| "emitDecoratorMetadata": false, | |||
| "sourceMap": true, | |||
| "target": "ES2021", | |||
| "strictNullChecks": true, | |||
| "paths": { | |||
| "three-mesh-halfedge": ["./src/three-mesh-halfedge"], | |||
| "three": ["threepipe"], | |||
| }, | |||
| "lib": [ | |||
| "es2020", | |||
| "esnext", | |||
| "dom" | |||
| ] | |||
| }, | |||
| "include": [ | |||
| "src/**/*" | |||
| ], | |||
| "exclude": [ | |||
| "node_modules", | |||
| "**/*.spec.ts", | |||
| "dist" | |||
| ] | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| { | |||
| "extends": [ | |||
| "../../typedoc.json" | |||
| ], | |||
| "entryPoints": [ | |||
| "src/index.ts" | |||
| ], | |||
| "name": "Threepipe Gaussian Splatting Plugin", | |||
| "readme": "none" | |||
| } | |||
| @@ -0,0 +1,91 @@ | |||
| import {defineConfig} from 'vite' | |||
| import json from '@rollup/plugin-json'; | |||
| import dts from 'vite-plugin-dts' | |||
| import packageJson from './package.json'; | |||
| import license from 'rollup-plugin-license'; | |||
| import glsl from 'rollup-plugin-glsl'; | |||
| import path from 'node:path'; | |||
| import replace from '@rollup/plugin-replace'; | |||
| const isProd = process.env.NODE_ENV === 'production' | |||
| const { name, version, author } = packageJson | |||
| const {main, module, browser} = packageJson | |||
| const globals = { | |||
| 'three': 'threepipe', // just incase someone uses three | |||
| 'threepipe': 'threepipe', | |||
| '@threepipe/plugin-tweakpane': '@threepipe/plugin-tweakpane', | |||
| } | |||
| export default defineConfig({ | |||
| optimizeDeps: { | |||
| exclude: ['uiconfig.js', 'ts-browser-helpers', 'three-mesh-bvh'], | |||
| }, | |||
| base: '', | |||
| // define: { | |||
| // 'process.env': process.env | |||
| // }, | |||
| build: { | |||
| sourcemap: true, | |||
| minify: false, | |||
| cssMinify: isProd, | |||
| cssCodeSplit: false, | |||
| watch: !isProd ? { | |||
| buildDelay: 1000, | |||
| } : null, | |||
| lib: { | |||
| entry: 'src/index.ts', | |||
| formats: isProd ? ['es', 'umd'] : ['es'], | |||
| name: name, | |||
| fileName: (format) => (format === 'umd' ? main : module).replace('dist/', ''), | |||
| }, | |||
| outDir: 'dist', | |||
| emptyOutDir: isProd, | |||
| commonjsOptions: { | |||
| exclude: [/uiconfig.js/, /ts-browser-helpers/, /three-mesh-bvh/], | |||
| }, | |||
| rollupOptions: { | |||
| output: { | |||
| // inlineDynamicImports: false, | |||
| globals, | |||
| }, | |||
| external: Object.keys(globals), | |||
| }, | |||
| }, | |||
| plugins: [ | |||
| isProd ? dts({tsconfigPath: './tsconfig.json'}) : null, | |||
| replace({ | |||
| 'from \'three\'': 'from \'threepipe\'', | |||
| delimiters: ['', ''], | |||
| }), | |||
| // replace({ | |||
| // 'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development'), | |||
| // preventAssignment: true, | |||
| // }), | |||
| glsl({ // todo: minify glsl. | |||
| include: 'src/**/*.glsl', | |||
| }), | |||
| json(), | |||
| // postcss({ | |||
| // modules: false, | |||
| // autoModules: true, // todo; issues with typescript import css, because inject is false | |||
| // inject: false, | |||
| // minimize: isProduction, | |||
| // // Or with custom options for `postcss-modules` | |||
| // }), | |||
| license({ | |||
| banner: ` | |||
| @license | |||
| ${name} v${version} | |||
| Copyright 2022<%= moment().format('YYYY') > 2022 ? '-' + moment().format('YYYY') : null %> ${author} | |||
| ${packageJson.license} License | |||
| See ./dependencies.txt for any bundled third-party dependencies and licenses. | |||
| `, | |||
| thirdParty: { | |||
| output: path.join(__dirname, 'dist', 'dependencies.txt'), | |||
| includePrivate: true, // Default is false. | |||
| }, | |||
| }), | |||
| ], | |||
| }) | |||