mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
several fixes to tessellation and extremely fast (in comparison) initial load of CAD objects
This commit is contained in:
115
poetry.lock
generated
115
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -11,6 +11,7 @@ python = "^3.9"
|
||||
|
||||
# CAD
|
||||
build123d = "^0.3.0"
|
||||
ocp-tessellate = "^2.0.6"
|
||||
|
||||
# Web
|
||||
aiohttp = "^3.9.3"
|
||||
|
||||
@@ -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<SceneMgrRefData>, 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 */
|
||||
|
||||
@@ -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/"
|
||||
],
|
||||
|
||||
@@ -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<Uint8Array>): AsyncGenerator<number | Uint8Array> {
|
||||
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<Uint8Array>, 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<Uint8Array>, buffered: Uint8Array): Promise<Uint8Array> {
|
||||
return (await readN(reader, buffered, null))[0];
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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())],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user