From c9e8bde9ca2319e4e7704aa4dcfcfad07199c41e Mon Sep 17 00:00:00 2001 From: Yeicor <4929005+Yeicor@users.noreply.github.com> Date: Mon, 19 Feb 2024 20:53:10 +0100 Subject: [PATCH] several fixes to tessellation and extremely fast (in comparison) initial load of CAD objects --- poetry.lock | 115 +++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + src/misc/scene.ts | 29 +++------- src/misc/settings.ts | 4 +- src/models/glb/glbs.ts | 69 ----------------------- src/models/glb/merge.ts | 2 +- yacv_server/gltf.py | 44 +++++++++------ yacv_server/logo.py | 2 +- yacv_server/tessellate.py | 69 ++++++++++------------- 9 files changed, 183 insertions(+), 152 deletions(-) delete mode 100644 src/models/glb/glbs.ts diff --git a/poetry.lock b/poetry.lock index 29cae33..64a7338 100644 --- a/poetry.lock +++ b/poetry.lock @@ -235,6 +235,17 @@ svgpathtools = ">=1.5.1,<2" trianglesolver = "*" typing-extensions = ">=4.6.0,<5" +[[package]] +name = "cachetools" +version = "5.2.1" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = "~=3.7" +files = [ + {file = "cachetools-5.2.1-py3-none-any.whl", hash = "sha256:8462eebf3a6c15d25430a8c27c56ac61340b2ecf60c9ce57afc2b97e450e47da"}, + {file = "cachetools-5.2.1.tar.gz", hash = "sha256:5991bc0e08a1319bb618d3195ca5b6bc76646a49c21d55962977197b301cc1fe"}, +] + [[package]] name = "cadquery-ocp" version = "7.7.2" @@ -843,6 +854,73 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +[[package]] +name = "numpy-quaternion" +version = "2023.0.2" +description = "Add a quaternion dtype to NumPy" +optional = false +python-versions = "*" +files = [ + {file = "numpy-quaternion-2023.0.2.tar.gz", hash = "sha256:37f73d7f84c645bd9be95cb4862bd900b7f99bd2f801232006dde00641bf2fd7"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf487d6b56883895ddf22307a0cf8e9949604465154d0cd9b78250d800d07a0d"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac5e37ed57c0e2ff938c88d4462a126b16c98581dde0c003eba05741188b7f38"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b144be3dca3330f8ad5866c561cebbfe3273a5b228ece058c014cdbf8916630d"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48bb1fc03b580a9bb89da9d4f8916f87101bc75682611c423bafa031b6d96176"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:713e4357868ebd8e4f3500435fcb49a997a8a9a5f8514e3a79d51f46abcdf2ae"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c829f58ebc908f07487d3351a13ba99c3e39eb5e04aea389ca5175642cfdab15"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5dd15141aecbf32cdb6bf96bdc13df7dd2f31833011a7f0ef51ecc86872cf8f0"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0560b12235aaab7aee56e94c2df2f7879e0c965b8aea3c6bccaad7f2b4fb031a"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-win32.whl", hash = "sha256:e033eef943a904b9c34c1d9e66570a07fa2c3d4a311a357d1aeb305493092c08"}, + {file = "numpy_quaternion-2023.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6c7e82014a51c93fe76322654d9c59f03b2e5cd19d0d6535d606bf7a119d4394"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86d46c5f220ed2289d7d53c918b0e2432d6ddeae20c5ca232f3dab6fafe6c340"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c2ddf7e16a611f1c07a170d9464d69291eeb734ade2ce50b7f4eb38d9620f007"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57d99cee91c7356c62d70817d32432db3da58f4d5f3bd29757c5696f56fa2e86"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c4536fdb7f22733631b7953e2db82b27964d96f97423901e749c971cb7f6f2"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fdbc31fdac812ed2ff0287a2d51e1b87d5ec6d2aeea4a667adb14f4b6198bc5"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f994628b10bf29461fb50cf3ce022d0a610e173068414942a9efd746b35b38b"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:32e34d2ebeeed25b238df22eba0030ba8db4a4e82a7eb6f5e32fda45768990ee"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98bfb77597ea56462be3f94e002640ebc6ecf9d2eeea140f5d1c13145af56a31"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-win32.whl", hash = "sha256:e6dcfec4c7f615e6c46411c2034631e0a1934ffc3509e7bd61c3aacce4ecb181"}, + {file = "numpy_quaternion-2023.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:86f931da5893db57c4da4142045b605cc99d469fb3e6238ae487e080dcd7227e"}, + {file = "numpy_quaternion-2023.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:bca80ceef24364eb4dc07026e3d5c7cc9932b844888a3a15f27941f0ee6ba5c3"}, + {file = "numpy_quaternion-2023.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bf6a99191d1d0b3289eb256c1eaf7e290d80d4a306bb31d04121bf9a7eb88701"}, + {file = "numpy_quaternion-2023.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b26f4961fef053d552f5dcea0957b1eb34c99fea92efe1544044013d04e1407"}, + {file = "numpy_quaternion-2023.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c5b7dfb7412b582101ae4e576f15bc6af904f66b24b832aa1fafa3a846c71da"}, + {file = "numpy_quaternion-2023.0.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c058ee103024dc15b3232e57204934a53be080d5c75246cdec9eb92e9f56c5f"}, + {file = "numpy_quaternion-2023.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:02b93874723c38ad1e684d0862899d9266bf9855fd5a5bdcba8793169e672c31"}, + {file = "numpy_quaternion-2023.0.2-cp312-cp312-win32.whl", hash = "sha256:449ba07ec505dd757aa4ba6df8ef086bdd06c85f4681529ddaecd4ce7d62e792"}, + {file = "numpy_quaternion-2023.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e218a5207be1a983d3fd54d710067a6638d324015ba695c0509082a29086284"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:333dea61c9628707223dc062e4a6e0a72bbb4fffd58a84231ea24b959e694bde"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b343649600eb9f30275380b47ee4430f4393ed3370e5fa3fbb1db0ebbd908228"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b4df8ffdcab6f773eec518ed09abb81e233afd9a38534e3a1db0cb0bfc54b370"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04671ea098c0fe879eb07a24ec80dc09efc674e178f9b58a427f9d2368b2c009"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d11f6f030d1cc7b58afe83fa849422a1c8c3a742b7af30232b98acbe32cd2be6"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c1e9f9997205949c770702307451eeffd96f3a2824f4dc49ed42336bd698e2"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6dd56641fddad6c35d86a6d9f3cee4a786d0a4c6b41ed74d60dad97741835280"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:681aaa2cf4d59fc412ee00188dcdc551c8ff91ea63d54d06f37ec66dd383633d"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-win32.whl", hash = "sha256:e6b4dd4797e6e77fcdd8b3487893f8af3fe934f1f26839d1605f771f700dded6"}, + {file = "numpy_quaternion-2023.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4b9421d46d56fbec0dd625c9909550c66bb81265a76efaecc5621166f18069bf"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:41968027811fa81157c9bc9f2bf00cc22dc8865d7fb5834f9f83bafc5995b6ec"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeffe622c5cec8396e61c266f65c75ec54fa4c21688a9633e8737276dc7fcc4b"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b0f8517c268d748cbfe686214bd53ac7064e85106c90e22bd7cf04940a17323e"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec65adaac6bf15f31951e25bf5fe908135db6e223cf2df0112c93afe432d5de"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb3ab05505ccb5c835a6f0401811d64f23c843e622751956ba77734f7dc20493"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13958c8628b17f9bc725bb54e910c384e211e54b057cbe069f1615aebae8735d"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eeeb8a6004a649b4a411fb25fb94a6da8e937de25b7c409c62528c937d1bb47d"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d725796e9f21eb703ae19448ceea0ab34e850c903ab01fef3de06f7217ae17f5"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-win32.whl", hash = "sha256:3f89e11f89ded410fb34e6f997d4c7f4cf7c31c3eb9537c035756a5d2a6cc4e3"}, + {file = "numpy_quaternion-2023.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:cab8b1626c6d719639360a6af920c25df3f0248ab04635b72919aa1a05cb575f"}, +] + +[package.dependencies] +numpy = ">=1.13" + +[package.extras] +docs = ["mkdocs", "mktheapidocs[plugin]", "pymdown-extensions"] +numba = ["llvmlite (<0.32.0)", "numba", "numba (<0.49.0)"] +scipy = ["scipy"] +testing = ["pytest", "pytest-cov"] + [[package]] name = "numpy-stl" version = "3.1.1" @@ -858,6 +936,26 @@ files = [ numpy = "*" python-utils = ">=3.4.5" +[[package]] +name = "ocp-tessellate" +version = "2.0.6" +description = "Tessellate OCP objects" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ocp_tessellate-2.0.6-py3-none-any.whl", hash = "sha256:a3c50c9f83b47565a5fca2c63448fe7ab9cf2a06af803eb695d165b6d960d2b3"}, + {file = "ocp_tessellate-2.0.6.tar.gz", hash = "sha256:7c3e0f09f684085e50c4af7a1f8ffd839d6821ae11aa0e693b2bad5cabe5270c"}, +] + +[package.dependencies] +cachetools = ">=5.2.0,<5.3.0" +numpy = "*" +numpy-quaternion = "*" +webcolors = ">=1.12,<2.0" + +[package.extras] +dev = ["black", "bumpversion", "pyYaml", "pylint", "twine"] + [[package]] name = "ocpsvg" version = "0.2.0" @@ -1299,6 +1397,21 @@ files = [ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +[[package]] +name = "webcolors" +version = "1.13" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.7" +files = [ + {file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"}, + {file = "webcolors-1.13.tar.gz", hash = "sha256:c225b674c83fa923be93d235330ce0300373d02885cef23238813b0d5668304a"}, +] + +[package.extras] +docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] +tests = ["pytest", "pytest-cov"] + [[package]] name = "wrapt" version = "1.16.0" @@ -1484,4 +1597,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "10b4a451dcd406d0a5f712abc11982fa3297c960c166a9845f91b044f988c3a5" +content-hash = "950e4ebfaa15d8cc389403d0f931a63b2c2685aa1afbf3624e5806152acd6c83" diff --git a/pyproject.toml b/pyproject.toml index ea9801f..b973245 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ python = "^3.9" # CAD build123d = "^0.3.0" +ocp-tessellate = "^2.0.6" # Web aiohttp = "^3.9.3" diff --git a/src/misc/scene.ts b/src/misc/scene.ts index d6f9527..5d61b0d 100644 --- a/src/misc/scene.ts +++ b/src/misc/scene.ts @@ -3,9 +3,7 @@ import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelSc import {Ref, ref} from 'vue'; import {Document} from '@gltf-transform/core'; import {ModelViewerInfo} from "./viewer/ModelViewerWrapper.vue"; -import {splitGlbs} from "../models/glb/glbs"; import {mergeFinalize, mergePartial, toBuffer} from "../models/glb/merge"; -import {settings} from "./settings"; export type SceneMgrRefData = { /** When updated, forces the viewer to load a new model replacing the current one */ @@ -44,33 +42,20 @@ export class SceneMgr { /** Loads a GLB/GLBS model from a URL and adds it to the viewer or replaces it if the names match */ static async loadModel(refData: Ref, data: SceneMgrData, name: string, url: string) { + let loadStart = performance.now(); + // Connect to the URL of the model let response = await fetch(url); if (!response.ok) throw new Error("Failed to fetch model: " + response.statusText); - // Split the stream into valid GLB chunks - let glbsSplitter = splitGlbs(response.body!); - let {value: numChunks} = await glbsSplitter.next(); - console.log("Loading", name, "which has", numChunks, "GLB chunks"); - - // Start merging each chunk into the current document, replacing or adding as needed - let lastShow = performance.now(); - while (true) { - let {value: glbData, done} = await glbsSplitter.next(); - if (done) break; - data.document = await mergePartial(glbData, name, data.document); - await new Promise(r => setTimeout(r, 0)); // Yield to update the UI at 60fps - // TODO: Report load progress - - // Show the partial model while loading every once in a while - if (performance.now() - lastShow > settings.displayLoadingEveryMs) { - await this.showCurrentDoc(refData, data); - lastShow = performance.now(); - } - } + // Start merging into the current document, replacing or adding as needed + let glb = new Uint8Array(await response.arrayBuffer()); + data.document = await mergePartial(glb, name, data.document); // Display the final fully loaded model await this.showCurrentDoc(refData, data); + + console.log("Model", name, "loaded in", performance.now() - loadStart, "ms"); } /** Serializes the current document into a GLB and updates the viewerSrc */ diff --git a/src/misc/settings.ts b/src/misc/settings.ts index 1d64fe7..72e8c14 100644 --- a/src/misc/settings.ts +++ b/src/misc/settings.ts @@ -2,9 +2,9 @@ export const settings = { preloadModels: [ // @ts-ignore - // new URL('../../assets/fox.glb', import.meta.url).href, + new URL('../../assets/fox.glb', import.meta.url).href, // @ts-ignore - new URL('../../assets/logo.glbs', import.meta.url).href, + new URL('../../assets/logo.glb', import.meta.url).href, // Websocket URLs automatically listen for new models from the python backend //"ws://localhost:8080/" ], diff --git a/src/models/glb/glbs.ts b/src/models/glb/glbs.ts deleted file mode 100644 index c8a013d..0000000 --- a/src/models/glb/glbs.ts +++ /dev/null @@ -1,69 +0,0 @@ -const textDecoder = new TextDecoder(); - -/** - * Given a stream of binary data (e.g. from a fetch response), splits a GLBS file into its component GLB files and - * returns them as a generator of Uint8Arrays (that starts with the expected length). - * It also supports simple GLB files (no splitting needed). - */ -export async function* splitGlbs(readerSrc: ReadableStream): AsyncGenerator { - let reader = readerSrc.getReader(); - let [buffer4Bytes, buffered] = await readN(reader, new Uint8Array(), 4); - console.assert(buffer4Bytes.length === 4, 'Expected 4 bytes for magic numbers') - let magic = textDecoder.decode(buffer4Bytes) - if (magic === 'glTF' /* GLB */ || magic[0] == '{' /* glTF */) { - yield 1 - let remaining = await readAll(reader, buffered); - // Add back the header to the beginning of the document - let finalBuffer = new Uint8Array(buffer4Bytes.length + remaining.length); - finalBuffer.set(buffer4Bytes); - finalBuffer.set(remaining, buffer4Bytes.length); - yield finalBuffer - } else if (magic === "GLBS") { - // First, we read the number of chunks (can be 0xFFFFFFFF if the number of chunks is unknown). - [buffer4Bytes, buffered] = await readN(reader, buffered, 4); - let numChunks = new DataView(buffer4Bytes.buffer).getUint32(0, true); - yield numChunks - // Then, we read the length of each chunk followed by the chunk itself. - for (let i = 0; i < numChunks; i++) { - // - Read length - [buffer4Bytes, buffered] = await readN(reader, buffered, 4); - if (buffer4Bytes.length === 0) { - if (numChunks != 0xFFFFFFFF) throw new Error('Unexpected end of stream while reading chunk length:'+ - ' expected ' + (numChunks - i) + ' more chunks'); - else break // We reached the end of the stream of unknown length, so we stop reading chunks. - } - let length = new DataView(buffer4Bytes.buffer).getUint32(0, true); - // - Read chunk - let chunk: Uint8Array - [chunk, buffered] = await readN(reader, buffered, length); - yield chunk - } - } else throw new Error('Invalid magic numbers for expected GLB/GLBS file: ' + magic); - reader.releaseLock() -} - -/** - * Reads up to `n` bytes from the reader and returns them as a Uint8Array. - * An over-read is possible, in which case the returned array will still have `n` bytes and the over-read bytes will be - * returned. They should be provided to the next call to `readN` to avoid losing data. - */ -async function readN(reader: ReadableStreamDefaultReader, buffered: Uint8Array, n: number | null = null): Promise<[Uint8Array, Uint8Array]> { - let buffer = buffered; - while (n === null || buffer.length < n) { - let {done, value} = await reader.read(); - if (done) break; - let newBuffer = new Uint8Array(buffer.length + value.length); - newBuffer.set(buffer); - newBuffer.set(value, buffer.length); - buffer = newBuffer; - } - if (n !== null) { - return [buffer.slice(0, n), buffer.slice(n)] - } else { - return [buffer, new Uint8Array()]; - } -} - -async function readAll(reader: ReadableStreamDefaultReader, buffered: Uint8Array): Promise { - return (await readN(reader, buffered, null))[0]; -} diff --git a/src/models/glb/merge.ts b/src/models/glb/merge.ts index 787835b..5dcacb6 100644 --- a/src/models/glb/merge.ts +++ b/src/models/glb/merge.ts @@ -69,7 +69,7 @@ function dropByName(name: string): Transform { function mergeScenes(): Transform { return (doc: Document) => { let root = doc.getRoot(); - let scene = root.getDefaultScene(); + let scene = root.getDefaultScene() ?? root.listScenes()[0]; for (let dropScene of root.listScenes()) { if (dropScene === scene) continue; for (let node of dropScene.listChildren()) { diff --git a/yacv_server/gltf.py b/yacv_server/gltf.py index d3fc4ba..b71f874 100644 --- a/yacv_server/gltf.py +++ b/yacv_server/gltf.py @@ -1,5 +1,4 @@ import numpy as np -from build123d import Vector from pygltflib import * _checkerboard_image_bytes = base64.decodebytes( @@ -15,41 +14,45 @@ class GLTFMgr: nodes=[Node(mesh=0)], meshes=[Mesh(primitives=[])], accessors=[], - bufferViews=[ - BufferView(buffer=0, byteLength=len(_checkerboard_image_bytes), byteOffset=0, target=ELEMENT_ARRAY_BUFFER)], + bufferViews=[BufferView(buffer=0, byteLength=len(_checkerboard_image_bytes), byteOffset=0)], buffers=[Buffer(byteLength=len(_checkerboard_image_bytes))], samplers=[Sampler(magFilter=NEAREST)], textures=[Texture(source=0, sampler=0)], images=[Image(bufferView=0, mimeType='image/png')], materials=[ - Material(name="face", pbrMetallicRoughness=PbrMetallicRoughness( + Material(name="face", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness( baseColorTexture=TextureInfo(index=0), baseColorFactor=[1, 1, 0.5, 1])), - Material(name="edge", pbrMetallicRoughness=PbrMetallicRoughness( - baseColorTexture=TextureInfo(index=0), baseColorFactor=[0, 0, 0.5, 1])), - Material(name="vertex", pbrMetallicRoughness=PbrMetallicRoughness( - baseColorTexture=TextureInfo(index=0), baseColorFactor=[0.5, 0.5, 0.5, 1])), - Material(name="selected", pbrMetallicRoughness=PbrMetallicRoughness( - baseColorTexture=TextureInfo(index=0), baseColorFactor=[1, 0, 0, 1])), + Material(name="edge", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness( + baseColorFactor=[0, 0, 0.5, 1])), + Material(name="vertex", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness( + baseColorFactor=[0.5, 0.5, 0.5, 1])), + # Material(name="selected", alphaCutoff=None, pbrMetallicRoughness=PbrMetallicRoughness( + # baseColorTexture=TextureInfo(index=0), baseColorFactor=[1, 0, 0, 1])), ], ) def __init__(self): self.gltf.set_binary_blob(_checkerboard_image_bytes) - def add_face(self, vertices: np.ndarray, indices: np.ndarray, tex_coord: np.ndarray): + def add_face(self, vertices_raw: List[Tuple[float, float, float]], indices_raw: List[Tuple[int, int, int]], + tex_coord_raw: List[Tuple[float, float]]): """Add a face to the GLTF as a new primitive of the unique mesh""" + vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32) + indices = np.array([[i[0], i[1], i[2]] for i in indices_raw], dtype=np.uint32) + tex_coord = np.array([[t[0], t[1]] for t in tex_coord_raw], dtype=np.float32) self._add_any(vertices, indices, tex_coord, mode=TRIANGLES, material=0) - def add_edge(self, vertices: np.ndarray): + def add_edge(self, vertices_raw: List[Tuple[float, float, float]]): """Add an edge to the GLTF as a new primitive of the unique mesh""" - indices = np.array(list(map(lambda i: [i, i + 1], range(len(vertices) - 1))), dtype=np.uint8) + vertices = np.array([[v[0], v[1], v[2]] for v in vertices_raw], dtype=np.float32) + indices = np.array(list(map(lambda i: [i, i + 1], range(len(vertices) - 1))), dtype=np.uint32) tex_coord = np.array([]) self._add_any(vertices, indices, tex_coord, mode=LINE_STRIP, material=1) - def add_vertex(self, vertex: Vector): + def add_vertex(self, vertex: Tuple[float, float, float]): """Add a vertex to the GLTF as a new primitive of the unique mesh""" - vertices = np.array([[vertex.X, vertex.Y, vertex.Z]]) - indices = np.array([[0]], dtype=np.uint8) + vertices = np.array([[vertex[0], vertex[1], vertex[2]]]) + indices = np.array([[0]], dtype=np.uint32) tex_coord = np.array([], dtype=np.float32) self._add_any(vertices, indices, tex_coord, mode=POINTS, material=2) @@ -63,9 +66,14 @@ class GLTFMgr: assert indices.ndim == 2 assert indices.shape[1] == 3 and mode == TRIANGLES or indices.shape[1] == 2 and mode == LINE_STRIP or \ indices.shape[1] == 1 and mode == POINTS - indices = indices.astype(np.uint8) + indices = indices.astype(np.uint32) indices_blob = indices.flatten().tobytes() + # Check that all vertices are referenced by the indices + assert indices.max() == len(vertices) - 1, f"{indices.max()} != {len(vertices) - 1}" + assert indices.min() == 0 + assert np.unique(indices.flatten()).size == len(vertices) + assert len(tex_coord) == 0 or tex_coord.ndim == 2 assert len(tex_coord) == 0 or tex_coord.shape[1] == 2 tex_coord = tex_coord.astype(np.float32) @@ -86,7 +94,7 @@ class GLTFMgr: self.gltf.accessors.extend([it for it in [ Accessor( bufferView=buffer_view_base, - componentType=UNSIGNED_BYTE, + componentType=UNSIGNED_INT, count=indices.size, type=SCALAR, max=[int(indices.max())], diff --git a/yacv_server/logo.py b/yacv_server/logo.py index 538d125..015d197 100644 --- a/yacv_server/logo.py +++ b/yacv_server/logo.py @@ -27,7 +27,7 @@ if __name__ == "__main__": # Start an offline "server" to merge the CAD part of the logo with the animated GLTF part of the logo os.environ['YACV_DISABLE_SERVER'] = '1' - from __init__ import show_object, server + from yacv_server import show_object, server ASSETS_DIR = os.getenv('ASSETS_DIR', os.path.join(os.path.dirname(__file__), '..', 'assets')) # Add the CAD part of the logo to the server diff --git a/yacv_server/tessellate.py b/yacv_server/tessellate.py index 69353b4..a5af95c 100644 --- a/yacv_server/tessellate.py +++ b/yacv_server/tessellate.py @@ -2,15 +2,17 @@ import hashlib import io import re -import numpy as np +from OCP.BRep import BRep_Tool from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.GCPnts import GCPnts_TangentialDeflection from OCP.TopExp import TopExp +from OCP.TopLoc import TopLoc_Location from OCP.TopTools import TopTools_IndexedMapOfShape from OCP.TopoDS import TopoDS_Face, TopoDS_Edge, TopoDS_Shape, TopoDS_Vertex from build123d import Shape, Vertex from pygltflib import GLTF2 +import mylogger from gltf import GLTFMgr @@ -29,20 +31,16 @@ def tessellate( mgr = GLTFMgr() shape = Shape(ocp_shape) - # Triangulate all faces at the same time - # shape.mesh(tolerance, angular_tolerance) - _tessellate_face(mgr, shape.wrapped) - # Perform tessellation tasks - # if faces: - # for face in shape.faces(): - # _tessellate_face(mgr, face.wrapped) - # if edges: - # for edge in shape.edges(): - # _tessellate_edge(mgr, edge.wrapped, angular_tolerance, angular_tolerance) - # if vertices: - # for vertex in shape.vertices(): - # _tessellate_vertex(mgr, vertex.wrapped) + if faces: + for face in shape.faces(): + _tessellate_face(mgr, face.wrapped, tolerance, angular_tolerance) + if edges: + for edge in shape.edges(): + _tessellate_edge(mgr, edge.wrapped, angular_tolerance, angular_tolerance) + if vertices: + for vertex in shape.vertices(): + _tessellate_vertex(mgr, vertex.wrapped) return mgr.gltf @@ -54,24 +52,22 @@ def _tessellate_face( angular_tolerance: float = 0.1 ): face = Shape(ocp_face) - # loc = TopLoc_Location() - # poly = BRep_Tool.Triangulation_s(face.wrapped, loc) - # if poly is None: - # mylogger.logger.warn("No triangulation found for face") - # return GLTF2() + face.mesh(tolerance, angular_tolerance) + poly = BRep_Tool.Triangulation_s(face.wrapped, TopLoc_Location()) + if poly is None: + mylogger.logger.warn("No triangulation found for face") + return GLTF2() tri_mesh = face.tessellate(tolerance, angular_tolerance) # Get UV of each face from the parameters - # uv = [ - # [v.X(), v.Y()] - # for v in (poly.UVNode(i) for i in range(1, poly.NbNodes() + 1)) - # ] - uv = [] + uv = [ + (v.X(), v.Y()) + for v in (poly.UVNode(i) for i in range(1, poly.NbNodes() + 1)) + ] - vertices = np.array(list(map(lambda v: [v.X, v.Y, v.Z], tri_mesh[0]))) - indices = np.array(tri_mesh[1]) - tex_coord = np.array(uv) - mgr.add_face(vertices, indices, tex_coord) + vertices = [(v.X, v.Y, v.Z) for v in tri_mesh[0]] + indices = tri_mesh[1] + mgr.add_face(vertices, indices, uv) def _tessellate_edge( @@ -84,37 +80,34 @@ def _tessellate_edge( discretizer = GCPnts_TangentialDeflection(curve, angular_deflection, curvature_deflection) assert discretizer.NbPoints() > 1, "Edge is too small??" - # TODO: get and apply transformation?? - # add vertices - vertices: list[list[float]] = [ - [v.X(), v.Y(), v.Z()] + vertices = [ + (v.X(), v.Y(), v.Z()) for v in ( discretizer.Value(i) # .Transformed(transformation) for i in range(1, discretizer.NbPoints() + 1) ) ] - mgr.add_edge(np.array(vertices)) + mgr.add_edge(vertices) def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex): c = Vertex(ocp_vertex).center() - mgr.add_vertex(c) + mgr.add_vertex((c.X, c.Y, c.Z)) def _hashcode(obj: TopoDS_Shape) -> str: """Utility to compute the hash code of a shape recursively without the need to tessellate it""" # NOTE: obj.HashCode(MAX_HASH_CODE) is not stable across different runs of the same program # This is best-effort and not guaranteed to be unique - data = io.BytesIO() map_of_shapes = TopTools_IndexedMapOfShape() TopExp.MapShapes_s(obj, map_of_shapes) + hasher = hashlib.md5(usedforsecurity=False) for i in range(1, map_of_shapes.Extent() + 1): sub_shape = map_of_shapes.FindKey(i) sub_data = io.BytesIO() TopoDS_Shape.DumpJson(sub_shape, sub_data) val = sub_data.getvalue() val = re.sub(b'"this": "[^"]*"', b'', val) # Remove memory address - data.write(val) - to_hash = data.getvalue() - return hashlib.md5(to_hash, usedforsecurity=False).hexdigest() + hasher.update(val) + return hasher.hexdigest()