mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-20 06:27:04 +01:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
345636e478 | ||
|
|
9a0fb03526 | ||
|
|
2037621afc | ||
|
|
2ba0e18479 | ||
|
|
eca2bbfa7c | ||
|
|
86180c424e | ||
|
|
e42d6be074 | ||
|
|
e2d6a3cb00 | ||
|
|
9e453b7890 | ||
|
|
0b8faa9e8b | ||
|
|
00bc2a15e0 | ||
|
|
432abcf85c | ||
|
|
4b6d3f6266 | ||
|
|
255ae72ed2 | ||
|
|
77dd9fb43e | ||
|
|
5dc2ae2f8d | ||
|
|
58440723bd | ||
|
|
bfdd656316 | ||
|
|
7408823c02 | ||
|
|
856ffbc4c5 | ||
|
|
d0f8463bbf | ||
|
|
162d3e22a2 | ||
|
|
4b06559ab8 | ||
|
|
9afa2e5786 | ||
|
|
7196fb2f32 | ||
|
|
8ec60faa04 | ||
|
|
13bbdd5956 | ||
|
|
3675d2f447 | ||
|
|
efc7a1d3b6 | ||
|
|
7166f9fe3d |
2
.github/workflows/deploy2.yml
vendored
2
.github/workflows/deploy2.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
mv "$dir/"* public/
|
||||
rmdir "$dir"
|
||||
done
|
||||
- uses: "actions/configure-pages@v4"
|
||||
- uses: "actions/configure-pages@v5"
|
||||
- uses: "actions/upload-pages-artifact@v3"
|
||||
with:
|
||||
path: 'public'
|
||||
|
||||
@@ -2225,13 +2225,13 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- three@0.162.0
|
||||
- three@0.160.1
|
||||
|
||||
This package contains the following license and notice below:
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright © 2010-2024 three.js authors
|
||||
Copyright © 2010-2023 three.js authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -2439,7 +2439,7 @@ THE SOFTWARE.
|
||||
|
||||
The following npm package may be included in this product:
|
||||
|
||||
- vuetify@3.5.11
|
||||
- vuetify@3.5.13
|
||||
|
||||
This package contains the following license and notice below:
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from yacv_server import show, export_all # Check out other exported methods for
|
||||
# Create a simple object
|
||||
with BuildPart() as example:
|
||||
Box(10, 10, 5)
|
||||
Cylinder(4, 5, mode=Mode.SUBTRACT)
|
||||
Cylinder(3, 5, mode=Mode.SUBTRACT)
|
||||
|
||||
# Show it in the frontend with hot-reloading
|
||||
show(example)
|
||||
|
||||
@@ -7,7 +7,7 @@ import Tools from "./tools/Tools.vue";
|
||||
import Models from "./models/Models.vue";
|
||||
import {VBtn, VLayout, VMain, VToolbarTitle} from "vuetify/lib/components/index.mjs";
|
||||
import {settings} from "./misc/settings";
|
||||
import {NetworkManager, NetworkUpdateEvent} from "./misc/network";
|
||||
import {NetworkManager, NetworkUpdateEvent, NetworkUpdateEventModel} from "./misc/network";
|
||||
import {SceneMgr} from "./misc/scene";
|
||||
import {Document} from "@gltf-transform/core";
|
||||
import type ModelViewerWrapperT from "./viewer/ModelViewerWrapper.vue";
|
||||
@@ -28,11 +28,16 @@ const viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
|
||||
const sceneDocument = shallowRef(new Document());
|
||||
provide('sceneDocument', {sceneDocument});
|
||||
const models: Ref<InstanceType<typeof Models> | null> = ref(null)
|
||||
const tools: Ref<InstanceType<typeof Tools> | null> = ref(null)
|
||||
const disableTap = ref(false);
|
||||
const setDisableTap = (val: boolean) => disableTap.value = val;
|
||||
provide('disableTap', {disableTap, setDisableTap});
|
||||
|
||||
async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
||||
// Trigger progress bar as soon as possible (also triggered earlier for each raw notification)
|
||||
if (viewer.value && event.models.length > 0) {
|
||||
viewer.value.onProgress(0.10);
|
||||
}
|
||||
// Load/unload a new batch of models to optimize rendering time
|
||||
console.log("Received model update request", event.models);
|
||||
let shutdownRequestIndex = event.models.findIndex((model) => model.isRemove == null);
|
||||
@@ -45,6 +50,7 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
||||
for (let modelIndex in event.models) {
|
||||
let isLast = parseInt(modelIndex) === event.models.length - 1;
|
||||
let model = event.models[modelIndex];
|
||||
tools.value?.removeObjectSelections(model.name);
|
||||
try {
|
||||
if (!model.isRemove) {
|
||||
doc = await SceneMgr.loadModel(sceneUrl, doc, model.name, model.url, isLast && settings.loadHelpers, isLast);
|
||||
@@ -64,12 +70,14 @@ async function onModelUpdateRequest(event: NetworkUpdateEvent) {
|
||||
}
|
||||
|
||||
async function onModelRemoveRequest(name: string) {
|
||||
sceneDocument.value = await SceneMgr.removeModel(sceneUrl, sceneDocument.value, name);
|
||||
triggerRef(sceneDocument); // Why not triggered automatically?
|
||||
await onModelUpdateRequest(new NetworkUpdateEvent([new NetworkUpdateEventModel(name, "", null, true)], () => {
|
||||
}));
|
||||
}
|
||||
|
||||
// Set up the load model event listener
|
||||
let networkMgr = new NetworkManager();
|
||||
networkMgr.addEventListener('update-early',
|
||||
(e) => viewer.value?.onProgress((e as CustomEvent<Array<any>>).detail.length * 0.01));
|
||||
networkMgr.addEventListener('update', (e) => onModelUpdateRequest(e as NetworkUpdateEvent));
|
||||
// Start loading all configured models ASAP
|
||||
for (let model of settings.preload) {
|
||||
@@ -108,7 +116,7 @@ async function loadModelManual() {
|
||||
<template #toolbar>
|
||||
<v-toolbar-title>Tools</v-toolbar-title>
|
||||
</template>
|
||||
<tools :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
||||
<tools ref="tools" :viewer="viewer" @findModel="(name) => models?.findModel(name)"/>
|
||||
</sidebar>
|
||||
|
||||
</v-layout>
|
||||
|
||||
@@ -8,7 +8,6 @@ function getCenterAndVertexList(selInfo: SelectionInfo, scene: ModelScene): {
|
||||
center: Vector3,
|
||||
vertices: Array<Vector3>
|
||||
} {
|
||||
selInfo.object.updateMatrixWorld();
|
||||
let pos: BufferAttribute | InterleavedBufferAttribute = selInfo.object.geometry.getAttribute('position');
|
||||
let ind: BufferAttribute | null = selInfo.object.geometry.index;
|
||||
if (ind === null) {
|
||||
|
||||
@@ -21,7 +21,32 @@ export async function mergePartial(url: string, name: string, document: Document
|
||||
networkFinished();
|
||||
|
||||
// Load the new document
|
||||
let newDoc = await io.readBinary(new Uint8Array(buffer));
|
||||
let newDoc = null;
|
||||
let alreadyTried: { [name: string]: boolean } = {}
|
||||
while (newDoc == null) { // Retry adding extensions as required until the document is loaded
|
||||
try { // Try to load fast if no extensions are used
|
||||
newDoc = await io.readBinary(new Uint8Array(buffer));
|
||||
} catch (e) { // Fallback to wait for download and register big extensions
|
||||
if (e instanceof Error && e.message.toLowerCase().includes("khr_draco_mesh_compression")) {
|
||||
if (alreadyTried["draco"]) throw e; else alreadyTried["draco"] = true;
|
||||
// WARNING: Draco decompression on web is really slow for non-trivial models! (it should work?)
|
||||
let {KHRDracoMeshCompression} = await import("@gltf-transform/extensions")
|
||||
let dracoDecoderWeb = await import("three/examples/jsm/libs/draco/draco_decoder.js");
|
||||
let dracoEncoderWeb = await import("three/examples/jsm/libs/draco/draco_encoder.js");
|
||||
io.registerExtensions([KHRDracoMeshCompression])
|
||||
.registerDependencies({
|
||||
'draco3d.decoder': await dracoDecoderWeb.default({}),
|
||||
'draco3d.encoder': await dracoEncoderWeb.default({})
|
||||
});
|
||||
} else if (e instanceof Error && e.message.toLowerCase().includes("ext_texture_webp")) {
|
||||
if (alreadyTried["webp"]) throw e; else alreadyTried["webp"] = true;
|
||||
let {EXTTextureWebP} = await import("@gltf-transform/extensions")
|
||||
io.registerExtensions([EXTTextureWebP]);
|
||||
} else { // TODO: Add more extensions as required
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any previous model with the same name
|
||||
await document.transform(dropByName(name));
|
||||
|
||||
@@ -2,7 +2,7 @@ import {settings} from "./settings";
|
||||
|
||||
const batchTimeout = 250; // ms
|
||||
|
||||
class NetworkUpdateEventModel {
|
||||
export class NetworkUpdateEventModel {
|
||||
name: string;
|
||||
url: string;
|
||||
// TODO: Detect and manage instances of the same object (same hash, different name)
|
||||
@@ -80,6 +80,9 @@ export class NetworkManager extends EventTarget {
|
||||
controller.abort(); // Notify the server that we are done
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Server is down, wait a little longer before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 10 * settings.monitorEveryMs));
|
||||
}
|
||||
controller.abort();
|
||||
} catch (e) { // Ignore errors (retry very soon)
|
||||
@@ -90,29 +93,48 @@ export class NetworkManager extends EventTarget {
|
||||
|
||||
private foundModel(name: string, hash: string | null, url: string, isRemove: boolean | null, disconnect: () => void = () => {
|
||||
}) {
|
||||
let prevHash = this.knownObjectHashes[name];
|
||||
// console.debug("Found model", name, "with hash", hash, "and previous hash", prevHash);
|
||||
if (!hash || hash !== prevHash || isRemove) {
|
||||
// Update known hashes
|
||||
if (isRemove == false) {
|
||||
this.knownObjectHashes[name] = hash;
|
||||
} else if (isRemove == true) {
|
||||
if (!(name in this.knownObjectHashes)) return; // Nothing to remove...
|
||||
delete this.knownObjectHashes[name];
|
||||
// Also update buffered updates if the model is removed
|
||||
// console.debug("Found model", name, "with hash", hash, "at", url, "isRemove", isRemove);
|
||||
|
||||
// We only care about the latest update per model name
|
||||
this.bufferedUpdates = this.bufferedUpdates.filter(m => m.name !== name);
|
||||
}
|
||||
let newModel = new NetworkUpdateEventModel(name, url, hash, isRemove);
|
||||
this.bufferedUpdates.push(newModel);
|
||||
|
||||
// Add the new model to the list of updates and dispatch the early update
|
||||
let upd = new NetworkUpdateEventModel(name, url, hash, isRemove);
|
||||
this.bufferedUpdates.push(upd);
|
||||
this.dispatchEvent(new CustomEvent("update-early", {detail: this.bufferedUpdates}));
|
||||
|
||||
// Optimization: try to batch updates automatically for faster rendering
|
||||
if (this.batchTimeout !== null) clearTimeout(this.batchTimeout);
|
||||
this.batchTimeout = setTimeout(() => {
|
||||
// Update known hashes for minimal updates
|
||||
for (let model of this.bufferedUpdates) {
|
||||
if (model.isRemove == false && model.hash && model.hash === this.knownObjectHashes[model.name]) {
|
||||
// Delete this useless update
|
||||
let foundFirst = false;
|
||||
this.bufferedUpdates = this.bufferedUpdates.filter(m => {
|
||||
if (m === model) {
|
||||
if (!foundFirst) { // Remove only first full match
|
||||
foundFirst = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
} else {
|
||||
// Keep this update and update the last known hash
|
||||
if (model.isRemove == true) {
|
||||
if (model.name in this.knownObjectHashes) delete this.knownObjectHashes[model.name];
|
||||
} else if (model.isRemove == false) {
|
||||
this.knownObjectHashes[model.name] = model.hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the event to actually update the models
|
||||
this.dispatchEvent(new NetworkUpdateEvent(this.bufferedUpdates, disconnect));
|
||||
this.bufferedUpdates = [];
|
||||
}, batchTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function* readLinesStreamings(reader: ReadableStreamDefaultReader<Uint8Array>) {
|
||||
|
||||
@@ -13,12 +13,13 @@ export class SceneMgr {
|
||||
let loadStart = performance.now();
|
||||
let loadNetworkEnd: number;
|
||||
|
||||
try {
|
||||
// Start merging into the current document, replacing or adding as needed
|
||||
document = await mergePartial(url, name, document, () => loadNetworkEnd = performance.now());
|
||||
|
||||
console.log("Model", name, "loaded in", performance.now() - loadNetworkEnd!, "ms after",
|
||||
loadNetworkEnd! - loadStart, "ms of transferring data (maybe building the object on the server)");
|
||||
|
||||
} finally {
|
||||
if (updateHelpers) {
|
||||
// Reload the helpers to fit the new model
|
||||
await this.reloadHelpers(sceneUrl, document, reloadScene);
|
||||
@@ -31,6 +32,7 @@ export class SceneMgr {
|
||||
document = await this.showCurrentDoc(sceneUrl, document);
|
||||
console.log("Scene displayed in", performance.now() - displayStart, "ms");
|
||||
}
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
@@ -97,7 +99,7 @@ export class SceneMgr {
|
||||
// Serialize the document into a GLB and update the viewerSrc
|
||||
let buffer = await toBuffer(document);
|
||||
let blob = new Blob([buffer], {type: 'model/gltf-binary'});
|
||||
console.debug("Showing current doc", document, "as", Array.from(buffer));
|
||||
console.debug("Showing current doc", document, "with", buffer.length, "total bytes");
|
||||
sceneUrl.value = URL.createObjectURL(blob);
|
||||
|
||||
return document;
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
VTooltip,
|
||||
} from "vuetify/lib/components/index.mjs";
|
||||
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
||||
import {Document, Mesh} from "@gltf-transform/core";
|
||||
import {inject, ref, type ShallowRef, watch} from "vue";
|
||||
import {Mesh} from "@gltf-transform/core";
|
||||
import {ref, watch} from "vue";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {
|
||||
mdiCircleOpacity,
|
||||
@@ -43,10 +43,11 @@ const emit = defineEmits<{ remove: [] }>()
|
||||
|
||||
let modelName = props.meshes[0].getExtras()[extrasNameKey] // + " blah blah blah blah blag blah blah blah"
|
||||
|
||||
// Reactive properties
|
||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||
const opacity = defineModel<number>("opacity", {default: 1});
|
||||
const wireframe = ref(false);
|
||||
// Count the number of faces, edges and vertices
|
||||
let faceCount = ref(-1);
|
||||
let edgeCount = ref(-1);
|
||||
let vertexCount = ref(-1);
|
||||
|
||||
// Clipping planes are handled in y-up space (swapped on interface, Z inverted later)
|
||||
const clipPlaneX = ref(1);
|
||||
const clipPlaneSwappedX = ref(false);
|
||||
@@ -56,24 +57,10 @@ const clipPlaneZ = ref(1);
|
||||
const clipPlaneSwappedZ = ref(false);
|
||||
const edgeWidth = ref(settings.edgeWidth);
|
||||
|
||||
// Count the number of faces, edges and vertices
|
||||
let faceCount = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES))
|
||||
.map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
let edgeCount = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]))
|
||||
.map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
let vertexCount = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS))
|
||||
.map(p => (p.getAttribute("POSITION")?.getCount() ?? 0))
|
||||
.reduce((a, b) => a + b, 0)
|
||||
|
||||
// Set initial defaults for the enabled features
|
||||
if (faceCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
||||
if (edgeCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
|
||||
if (vertexCount === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
|
||||
// Misc properties
|
||||
const enabledFeatures = defineModel<Array<number>>("enabledFeatures", {default: [0, 1, 2]});
|
||||
const opacity = defineModel<number>("opacity", {default: 1});
|
||||
const wireframe = ref(false);
|
||||
|
||||
// Listeners for changes in the properties (or viewer reloads)
|
||||
function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||
@@ -81,9 +68,6 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
// Iterate all primitives of the mesh and set their visibility based on the enabled features
|
||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
||||
// of not actually removing the primitives from the scene graph
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
let childIsFace = child.type == 'Mesh' || child.type == 'SkinnedMesh'
|
||||
@@ -101,16 +85,12 @@ function onEnabledFeaturesChange(newEnabledFeatures: Array<number>) {
|
||||
scene.queueRender()
|
||||
}
|
||||
|
||||
watch(enabledFeatures, onEnabledFeaturesChange);
|
||||
watch(enabledFeatures, onEnabledFeaturesChange, {deep: true});
|
||||
|
||||
function onOpacityChange(newOpacity: number) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
// Iterate all primitives of the mesh and set their opacity based on the enabled features
|
||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
||||
// of not actually removing the primitives from the scene graph
|
||||
// console.log('Opacity may have changed', newOpacity)
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.material && child.material.opacity !== newOpacity) {
|
||||
@@ -129,10 +109,6 @@ function onWireframeChange(newWireframe: boolean) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
// Iterate all primitives of the mesh and set their wireframe based on the enabled features
|
||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
||||
// of not actually removing the primitives from the scene graph
|
||||
// console.log('Wireframe may have changed', newWireframe)
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.material && child.material.wireframe !== newWireframe) {
|
||||
@@ -146,8 +122,6 @@ function onWireframeChange(newWireframe: boolean) {
|
||||
|
||||
watch(wireframe, onWireframeChange);
|
||||
|
||||
let {sceneDocument} = inject<{ sceneDocument: ShallowRef<Document> }>('sceneDocument')!!;
|
||||
|
||||
function onClipPlanesChange() {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
@@ -268,11 +242,36 @@ function onModelLoad() {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
// Iterate all primitives of the mesh and set their visibility based on the enabled features
|
||||
// Use the scene graph instead of the document to avoid reloading the same model, at the cost
|
||||
// of not actually removing the primitives from the scene graph
|
||||
|
||||
// Count the number of faces, edges and vertices
|
||||
const isFirstLoad = faceCount.value === -1;
|
||||
faceCount.value = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.TRIANGLES))
|
||||
.map(p => (p.getExtras()?.face_triangles_end as any)?.length ?? 1)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
edgeCount.value = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() in [WebGL2RenderingContext.LINE_STRIP, WebGL2RenderingContext.LINES]))
|
||||
.map(p => (p.getExtras()?.edge_points_end as any)?.length ?? 0)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
vertexCount.value = props.meshes
|
||||
.flatMap((m) => m.listPrimitives().filter(p => p.getMode() === WebGL2RenderingContext.POINTS))
|
||||
.map(p => (p.getAttribute("POSITION")?.getCount() ?? 0))
|
||||
.reduce((a, b) => a + b, 0)
|
||||
|
||||
// First time: set the enabled features to all provided features
|
||||
if (isFirstLoad) {
|
||||
if (faceCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 0)
|
||||
else if (!enabledFeatures.value.includes(0)) enabledFeatures.value.push(0)
|
||||
if (edgeCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 1)
|
||||
else if (!enabledFeatures.value.includes(1)) enabledFeatures.value.push(1)
|
||||
if (vertexCount.value === 0) enabledFeatures.value = enabledFeatures.value.filter((f) => f !== 2)
|
||||
else if (!enabledFeatures.value.includes(2)) enabledFeatures.value.push(2)
|
||||
}
|
||||
|
||||
// Add darkened back faces for all face objects to improve cutting planes
|
||||
let childrenToAdd: Array<MObject3D> = [];
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
child.updateMatrixWorld(); // Objects are mostly static, so ensure updated matrices
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if (child.type == 'Mesh' || child.type == 'SkinnedMesh') {
|
||||
// Compute a BVH for faster raycasting (MUCH faster selection)
|
||||
|
||||
@@ -36,9 +36,7 @@ function onRemove(mesh: Mesh) {
|
||||
}
|
||||
|
||||
function findModel(name: string) {
|
||||
console.log('Find model', name);
|
||||
if (!expandedNames.value.includes(name)) expandedNames.value.push(name);
|
||||
console.log('Expanded', expandedNames.value);
|
||||
}
|
||||
|
||||
defineExpose({findModel})
|
||||
|
||||
2
frontend/shims.d.ts
vendored
2
frontend/shims.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
// Avoids typescript error when importing some files
|
||||
declare module '@jamescoyle/vue-icon'
|
||||
declare module 'three-orientation-gizmo/src/OrientationGizmo'
|
||||
declare module 'three/examples/jsm/libs/draco/draco_decoder.js'
|
||||
declare module 'three/examples/jsm/libs/draco/draco_encoder.js'
|
||||
@@ -33,21 +33,23 @@ let showBoundingBox = ref<Boolean>(false); // Enabled automatically on start
|
||||
let showDistances = ref<Boolean>(true);
|
||||
|
||||
let mouseDownAt: [number, number] | null = null;
|
||||
let mouseDownTime = 0;
|
||||
let selectFilter = ref('Any (S)');
|
||||
const raycaster = new Raycaster();
|
||||
|
||||
|
||||
let selectionMoveListener = (event: MouseEvent) => {
|
||||
let mouseDownListener = (event: MouseEvent) => {
|
||||
mouseDownAt = [event.clientX, event.clientY];
|
||||
mouseDownTime = performance.now();
|
||||
if (!selectionEnabled.value) return;
|
||||
};
|
||||
|
||||
let selectionListener = (event: MouseEvent) => {
|
||||
let mouseUpListener = (event: MouseEvent) => {
|
||||
// If the mouse moved while clicked (dragging), avoid selection logic
|
||||
if (mouseDownAt) {
|
||||
let [x, y] = mouseDownAt;
|
||||
mouseDownAt = null;
|
||||
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5) {
|
||||
if (Math.abs(event.clientX - x) > 5 || Math.abs(event.clientY - y) > 5 || performance.now() - mouseDownTime > 500) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -254,14 +256,29 @@ let onViewerReady = (viewer: typeof ModelViewerWrapperT) => {
|
||||
viewer.onElemReady((elem: ModelViewerElement) => {
|
||||
if (hasListeners) return;
|
||||
hasListeners = true;
|
||||
elem.addEventListener('mouseup', selectionListener);
|
||||
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
|
||||
elem.addEventListener('mousedown', mouseDownListener); // Avoid clicking when dragging
|
||||
elem.addEventListener('mouseup', mouseUpListener);
|
||||
elem.addEventListener('load', () => {
|
||||
// After a reload of the scene, we need to recover object references and highlight them again
|
||||
for (let sel of selected.value) {
|
||||
let scene = props.viewer?.scene;
|
||||
if (!scene) continue;
|
||||
let foundObject = null;
|
||||
scene.traverse((obj: MObject3D) => {
|
||||
if (sel.matches(obj)) {
|
||||
foundObject = obj as MObject3D;
|
||||
}
|
||||
});
|
||||
if (foundObject) {
|
||||
sel.object = foundObject;
|
||||
highlight(sel);
|
||||
} else {
|
||||
selected.value = selected.value.filter((m) => m.getKey() !== sel.getKey());
|
||||
}
|
||||
}
|
||||
if (firstLoad) {
|
||||
toggleShowBoundingBox();
|
||||
firstLoad = false;
|
||||
} else {
|
||||
updateBoundingBox();
|
||||
}
|
||||
});
|
||||
elem.addEventListener('camera-change', onCameraChange);
|
||||
@@ -406,6 +423,8 @@ function updateDistances() {
|
||||
return;
|
||||
}
|
||||
|
||||
defineExpose({deselect, updateBoundingBox, updateDistances});
|
||||
|
||||
// Add keyboard shortcuts
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 's') {
|
||||
@@ -443,7 +462,6 @@ window.addEventListener('keydown', (event) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="select-parent">
|
||||
<v-btn icon @click="toggleSelection" :color="selectionEnabled ? 'surface-light' : ''">
|
||||
<v-tooltip activator="parent">{{ selectionEnabled ? 'Disable (s)election mode' : 'Enable (s)election mode' }}
|
||||
</v-tooltip>
|
||||
@@ -456,7 +474,6 @@ window.addEventListener('keydown', (event) => {
|
||||
v-model="selectFilter"/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<v-btn icon @click="toggleHighlightNextSelection" :color="highlightNextSelection[0] ? 'surface-light' : ''">
|
||||
<v-tooltip activator="parent">(H)ighlight the next clicked element in the models list</v-tooltip>
|
||||
<svg-icon type="mdi" :path="mdiFeatureSearch"/>
|
||||
@@ -475,20 +492,11 @@ window.addEventListener('keydown', (event) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Very hacky styling... */
|
||||
.select-parent {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.select-parent .v-btn {
|
||||
position: relative;
|
||||
top: -20px;
|
||||
}
|
||||
|
||||
.select-only {
|
||||
display: inline-block;
|
||||
width: calc(100% - 48px);
|
||||
float: right;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
top: -12px;
|
||||
width: calc(100% - 48px);
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,7 @@ import type {ModelViewerElement} from '@google/model-viewer';
|
||||
import type {MObject3D} from "./Selection.vue";
|
||||
import Loading from "../misc/Loading.vue";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {defineAsyncComponent, type Ref, ref} from "vue";
|
||||
import {defineAsyncComponent, ref, type Ref} from "vue";
|
||||
import type {SelectionInfo} from "./selection";
|
||||
|
||||
const SelectionComponent = defineAsyncComponent({
|
||||
@@ -107,6 +107,15 @@ async function openGithub() {
|
||||
window.open('https://github.com/yeicor-3d/yet-another-cad-viewer', '_blank')
|
||||
}
|
||||
|
||||
function removeObjectSelections(objName: string) {
|
||||
for (let selInfo of selection.value.filter((s) => s.getObjectName() === objName)) {
|
||||
selectionComp.value?.deselect(selInfo);
|
||||
}
|
||||
selectionComp.value?.updateBoundingBox();
|
||||
selectionComp.value?.updateDistances();
|
||||
}
|
||||
|
||||
defineExpose({removeObjectSelections});
|
||||
|
||||
// Add keyboard shortcuts
|
||||
window.addEventListener('keydown', (event) => {
|
||||
@@ -133,7 +142,7 @@ window.addEventListener('keydown', (event) => {
|
||||
</v-btn>
|
||||
<v-divider/>
|
||||
<h5>Selection ({{ selectionFaceCount() }}F {{ selectionEdgeCount() }}E {{ selectionVertexCount() }}V)</h5>
|
||||
<selection-component :ref="selectionComp as any" :viewer="props.viewer as any" v-model="selection"
|
||||
<selection-component ref="selectionComp" :viewer="props.viewer as any" v-model="selection"
|
||||
@findModel="(name) => emit('findModel', name)"/>
|
||||
<v-divider/>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -187,4 +196,8 @@ window.addEventListener('keydown', (event) => {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import type {MObject3D} from "./Selection.vue";
|
||||
import type {Intersection} from "three";
|
||||
import {Box3} from "three";
|
||||
import {extrasNameKey} from "../misc/gltf";
|
||||
|
||||
/** Information about a single item in the selection */
|
||||
export class SelectionInfo {
|
||||
@@ -19,6 +20,17 @@ export class SelectionInfo {
|
||||
this.indices = indices;
|
||||
}
|
||||
|
||||
public getObjectName() {
|
||||
return this.object.userData[extrasNameKey];
|
||||
}
|
||||
|
||||
public matches(object: MObject3D) {
|
||||
return this.getObjectName() === object.userData[extrasNameKey] &&
|
||||
(this.kind === 'face' && (object.type === 'Mesh' || object.type === 'SkinnedMesh') ||
|
||||
this.kind === 'edge' && (object.type === 'Line' || object.type === 'LineSegments') ||
|
||||
this.kind === 'vertex' && object.type === 'Points')
|
||||
}
|
||||
|
||||
public getKey() {
|
||||
return this.object.uuid + this.kind + this.indices[0].toFixed() + this.indices[1].toFixed();
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelSc
|
||||
import {Hotspot} from "@google/model-viewer/lib/three-components/Hotspot";
|
||||
import type {Renderer} from "@google/model-viewer/lib/three-components/Renderer";
|
||||
import type {Vector3} from "three";
|
||||
import { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } from 'three-mesh-bvh';
|
||||
import {BufferGeometry, Mesh} from "three";
|
||||
import {acceleratedRaycast, computeBoundsTree, disposeBoundsTree} from 'three-mesh-bvh';
|
||||
|
||||
ModelViewerElement.modelCacheSize = 0; // Also needed to avoid tree shaking
|
||||
//@ts-ignore
|
||||
@@ -41,8 +41,31 @@ onMounted(() => {
|
||||
emit('load')
|
||||
});
|
||||
elem.value.addEventListener('camera-change', onCameraChange);
|
||||
elem.value.addEventListener('progress', (ev) => onProgress((ev as any).detail.totalProgress));
|
||||
});
|
||||
|
||||
// Handles loading the events for <model-viewer>'s slotted progress bar
|
||||
const progressBar = ref<HTMLElement | null>(null);
|
||||
const updateBar = ref<HTMLElement | null>(null);
|
||||
let onProgressHideTimeout: number | null = null;
|
||||
const onProgress = (totalProgress: number) => {
|
||||
if (!progressBar.value || !updateBar.value) return;
|
||||
// Update the progress bar and ensure it's visible
|
||||
progressBar.value.style.display = 'block';
|
||||
progressBar.value.style.opacity = '1'; // Fade in
|
||||
updateBar.value.style.width = `${totalProgress * 100}%`;
|
||||
// Auto-hide smoothly when no progress is made for a while
|
||||
if (onProgressHideTimeout) clearTimeout(onProgressHideTimeout);
|
||||
onProgressHideTimeout = setTimeout(() => {
|
||||
if (!progressBar.value) return;
|
||||
progressBar.value.style.opacity = '0'; // Fade out
|
||||
setTimeout(() => {
|
||||
if (!progressBar.value) return;
|
||||
progressBar.value.style.display = 'none'; // Actually hide
|
||||
}, 300); // 0.3s fade out
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
class Line3DData {
|
||||
startHotspot: HTMLElement = document.body
|
||||
endHotspot: HTMLElement = document.body
|
||||
@@ -137,7 +160,7 @@ function entries(lines: { [id: number]: Line3DData }): [string, Line3DData][] {
|
||||
return Object.entries(lines);
|
||||
}
|
||||
|
||||
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D});
|
||||
defineExpose({elem, onElemReady, scene, renderer, addLine3D, removeLine3D, onProgress});
|
||||
|
||||
let {disableTap} = inject<{ disableTap: Ref<boolean> }>('disableTap')!!;
|
||||
watch(disableTap, (value) => {
|
||||
@@ -155,7 +178,8 @@ watch(disableTap, (value) => {
|
||||
:shadow-intensity="settings.shadowIntensity" interaction-prompt="none" :autoplay="settings.autoplay"
|
||||
:ar="settings.arModes.length > 0" :ar-modes="settings.arModes" :skybox-image="settings.background"
|
||||
:environment-image="settings.background">
|
||||
<slot></slot> <!-- Controls, annotations, etc. -->
|
||||
<slot></slot>
|
||||
<!-- Display some information during initial load -->
|
||||
<div class="annotation initial-load-banner">
|
||||
Trying to load models from...
|
||||
<v-list v-for="src in settings.preload" :key="src">
|
||||
@@ -163,6 +187,11 @@ watch(disableTap, (value) => {
|
||||
</v-list>
|
||||
<!-- Too much idle CPU usage: <loading></loading> -->
|
||||
</div>
|
||||
|
||||
<!-- Customize the progress bar -->
|
||||
<div class="progress-bar" slot="progress-bar" ref="progressBar">
|
||||
<div class="update-bar" ref="updateBar"/>
|
||||
</div>
|
||||
</model-viewer>
|
||||
|
||||
<!-- The SVG overlay for fake 3D lines attached to the model -->
|
||||
@@ -223,4 +252,30 @@ watch(disableTap, (value) => {
|
||||
.initial-load-banner .v-list-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 10%;
|
||||
max-height: 2%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translate3d(-50%, 0%, 0);
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 3px 10px 3px rgba(0, 0, 0, 0.5), 0 0 5px 1px rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.update-bar {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: 25px;
|
||||
float: left;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
</style>
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yet-another-cad-viewer",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.7",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -15,7 +15,8 @@
|
||||
"update-licenses": "generate-license-file --input package.json --output assets/licenses.txt --overwrite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gltf-transform/core": "^3.10.0",
|
||||
"@gltf-transform/core": "^3.10.1",
|
||||
"@gltf-transform/extensions": "^3.10.1",
|
||||
"@gltf-transform/functions": "^3.10.1",
|
||||
"@google/model-viewer": "^3.4.0",
|
||||
"@jamescoyle/vue-icon": "^0.1.2",
|
||||
@@ -25,11 +26,11 @@
|
||||
"three-mesh-bvh": "^0.7.3",
|
||||
"three-orientation-gizmo": "https://github.com/jrj2211/three-orientation-gizmo",
|
||||
"vue": "^3.4.21",
|
||||
"vuetify": "^3.5.11"
|
||||
"vuetify": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node20": "^20.1.3",
|
||||
"@types/node": "^20.11.30",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/node": "^20.12.2",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
@@ -38,9 +39,9 @@
|
||||
"commander": "^12.0.0",
|
||||
"generate-license-file": "^3.0.1",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"terser": "^5.29.2",
|
||||
"terser": "^5.30.0",
|
||||
"typescript": "~5.4.3",
|
||||
"vite": "^5.2.6",
|
||||
"vite": "^5.2.7",
|
||||
"vue-tsc": "^2.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "yacv-server"
|
||||
version = "0.8.0"
|
||||
version = "0.8.7"
|
||||
description = "Yet Another CAD Viewer (server)"
|
||||
authors = ["Yeicor <4929005+Yeicor@users.noreply.github.com>"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -26,9 +26,10 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
assetsDir: '.',
|
||||
assetsDir: '.', // Support deploying to a subdirectory using relative URLs
|
||||
cssCodeSplit: false, // Small enough to inline
|
||||
chunkSizeWarningLimit: 550, // Three.js is huge
|
||||
chunkSizeWarningLimit: 550, // Three.js is big. Draco is even bigger but not likely to be used.
|
||||
sourcemap: true, // For debugging production
|
||||
},
|
||||
define: {
|
||||
__APP_NAME__: JSON.stringify(name),
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
Utilities to work with CAD objects
|
||||
"""
|
||||
import hashlib
|
||||
from typing import Optional, Union, List, Tuple
|
||||
import io
|
||||
import re
|
||||
from typing import Optional, Union, Tuple
|
||||
|
||||
from OCP.TopExp import TopExp
|
||||
from OCP.TopLoc import TopLoc_Location
|
||||
from OCP.TopTools import TopTools_IndexedMapOfShape
|
||||
from OCP.TopoDS import TopoDS_Shape
|
||||
from build123d import Compound, Shape
|
||||
|
||||
from yacv_server.gltf import GLTFMgr
|
||||
|
||||
@@ -40,22 +45,42 @@ def get_shape(obj: CADLike, error: bool = True) -> Optional[CADCoreLike]:
|
||||
if isinstance(obj, TopoDS_Shape):
|
||||
return obj
|
||||
|
||||
# Handle iterables like Build123d ShapeList by extracting all sub-shapes and making a compound
|
||||
if isinstance(obj, list) or isinstance(obj, tuple) or isinstance(obj, set) or isinstance(obj, dict):
|
||||
try:
|
||||
if isinstance(obj, dict):
|
||||
obj_iter = iter(obj.values())
|
||||
else:
|
||||
obj_iter = iter(obj)
|
||||
# print(obj, ' -> ', obj_iter)
|
||||
shapes_raw = [get_shape(sub_obj, error=False) for sub_obj in obj_iter]
|
||||
# Silently drop non-shapes
|
||||
shapes_raw_filtered = [shape for shape in shapes_raw if shape is not None]
|
||||
if len(shapes_raw_filtered) > 0: # Continue if we found at least one shape
|
||||
# Sorting is required to improve hashcode consistency
|
||||
shapes_raw_filtered_sorted = sorted(shapes_raw_filtered, key=lambda x: _hashcode(x))
|
||||
# Build a single compound shape
|
||||
shapes_bd = [Shape(shape) for shape in shapes_raw_filtered_sorted if shape is not None]
|
||||
return get_shape(Compound(shapes_bd), error)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if error:
|
||||
raise ValueError(f'Cannot show object of type {type(obj)} (submit issue?)')
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def grab_all_cad() -> List[Tuple[str, CADCoreLike]]:
|
||||
def grab_all_cad() -> set[Tuple[str, CADCoreLike]]:
|
||||
""" Grab all shapes by inspecting the stack """
|
||||
import inspect
|
||||
stack = inspect.stack()
|
||||
shapes = []
|
||||
shapes = set()
|
||||
for frame in stack:
|
||||
for key, value in frame.frame.f_locals.items():
|
||||
shape = get_shape(value, error=False)
|
||||
if shape:
|
||||
shapes.append((key, shape))
|
||||
if shape and shape not in shapes:
|
||||
shapes.add((key, shape))
|
||||
return shapes
|
||||
|
||||
|
||||
@@ -91,13 +116,6 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
if not isinstance(center_loc, TopLoc_Location):
|
||||
raise ValueError('Center location not valid')
|
||||
plane = Plane(Location(center_loc))
|
||||
# Convert coordinates system
|
||||
plane.origin = Vector(plane.origin.X, plane.origin.Z, -plane.origin.Y)
|
||||
plane.z_dir = -plane.y_dir
|
||||
plane.y_dir = plane.z_dir
|
||||
|
||||
def vert(v: Vector) -> Tuple[float, float, float]:
|
||||
return v.X, v.Y, v.Z
|
||||
|
||||
# Load the image to a byte buffer
|
||||
img = Image.open(source)
|
||||
@@ -121,13 +139,17 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
img.save(img_buf, format=format)
|
||||
img_buf = img_buf.getvalue()
|
||||
|
||||
# Convert coordinates system as a last step (gltf is Y-up instead of Z-up)
|
||||
def vert(v: Vector) -> Vector:
|
||||
return Vector(v.X, v.Z, -v.Y)
|
||||
|
||||
# Build the gltf
|
||||
mgr = GLTFMgr(image=(img_buf, save_mime))
|
||||
mgr.add_face([
|
||||
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||
vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2),
|
||||
vert(plane.origin - plane.x_dir * width / 2 + plane.y_dir * height / 2),
|
||||
vert(plane.origin + plane.x_dir * width / 2 + plane.y_dir * height / 2),
|
||||
vert(plane.origin + plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||
vert(plane.origin - plane.x_dir * width / 2 - plane.y_dir * height / 2),
|
||||
], [
|
||||
(0, 2, 1),
|
||||
(0, 3, 2),
|
||||
@@ -140,3 +162,32 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
|
||||
# Return the GLTF binary blob and the suggested name of the image
|
||||
return b''.join(mgr.build().save_to_bytes()), name
|
||||
|
||||
|
||||
def _hashcode(obj: Union[bytes, CADCoreLike], **extras) -> str:
|
||||
"""Utility to compute the STABLE hash code of a shape"""
|
||||
# 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
|
||||
hasher = hashlib.md5(usedforsecurity=False)
|
||||
for k, v in extras.items():
|
||||
hasher.update(str(k).encode())
|
||||
hasher.update(str(v).encode())
|
||||
if isinstance(obj, bytes):
|
||||
hasher.update(obj)
|
||||
elif isinstance(obj, TopLoc_Location):
|
||||
sub_data = io.BytesIO()
|
||||
obj.DumpJson(sub_data)
|
||||
hasher.update(sub_data.getvalue())
|
||||
elif isinstance(obj, TopoDS_Shape):
|
||||
map_of_shapes = TopTools_IndexedMapOfShape()
|
||||
TopExp.MapShapes_s(obj, map_of_shapes)
|
||||
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
|
||||
hasher.update(val)
|
||||
else:
|
||||
raise ValueError(f'Cannot hash object of type {type(obj)}')
|
||||
return hasher.hexdigest()
|
||||
|
||||
@@ -19,9 +19,8 @@ def build_logo(text: bool = True) -> Dict[str, Union[Part, Location, str]]:
|
||||
Text('Yet Another\nCAD Viewer', 7, font_path='/usr/share/fonts/TTF/OpenSans-Regular.ttf')
|
||||
extrude(amount=1)
|
||||
|
||||
logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location # Avoid overlapping:
|
||||
logo_img_location.position = Vector(logo_img_location.position.X - 4e-2, logo_img_location.position.Y,
|
||||
logo_img_location.position.Z)
|
||||
logo_img_location = logo_obj.faces().group_by(Axis.X)[0].face().center_location
|
||||
logo_img_location *= Location((0, 0, 4e-2), (0, 0, 90)) # Avoid overlapping and adjust placement
|
||||
|
||||
logo_img_path = os.path.join(ASSETS_DIR, 'img.jpg')
|
||||
img_glb_bytes, img_name = image_to_gltf(logo_img_path, logo_img_location, height=18)
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import hashlib
|
||||
import io
|
||||
import re
|
||||
from typing import List, Dict, Tuple, Union
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
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, Face, Location
|
||||
from pygltflib import GLTF2
|
||||
@@ -130,30 +125,3 @@ def _tessellate_vertex(mgr: GLTFMgr, ocp_vertex: TopoDS_Vertex, faces: List[Topo
|
||||
mgr.add_vertex(_push_point((c.X, c.Y, c.Z), faces))
|
||||
|
||||
|
||||
def _hashcode(obj: Union[bytes, TopoDS_Shape], **extras) -> 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
|
||||
hasher = hashlib.md5(usedforsecurity=False)
|
||||
for k, v in extras.items():
|
||||
hasher.update(str(k).encode())
|
||||
hasher.update(str(v).encode())
|
||||
if isinstance(obj, bytes):
|
||||
hasher.update(obj)
|
||||
elif isinstance(obj, TopLoc_Location):
|
||||
sub_data = io.BytesIO()
|
||||
obj.DumpJson(sub_data)
|
||||
hasher.update(sub_data.getvalue())
|
||||
elif isinstance(obj, TopoDS_Shape):
|
||||
map_of_shapes = TopTools_IndexedMapOfShape()
|
||||
TopExp.MapShapes_s(obj, map_of_shapes)
|
||||
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
|
||||
hasher.update(val)
|
||||
else:
|
||||
raise ValueError(f'Cannot hash object of type {type(obj)}')
|
||||
return hasher.hexdigest()
|
||||
|
||||
@@ -23,7 +23,8 @@ from yacv_server.myhttp import HTTPHandler
|
||||
from yacv_server.mylogger import logger
|
||||
from yacv_server.pubsub import BufferedPubSub
|
||||
from yacv_server.rwlock import RWLock
|
||||
from yacv_server.tessellate import _hashcode, tessellate
|
||||
from yacv_server.tessellate import tessellate
|
||||
from yacv_server.cad import _hashcode
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@@ -194,7 +195,7 @@ class YACV:
|
||||
|
||||
def show_cad_all(self, **kwargs):
|
||||
"""Publishes all CAD objects in the current scope to the server"""
|
||||
all_cad = grab_all_cad()
|
||||
all_cad = list(grab_all_cad()) # List for reproducible iteration order
|
||||
self.show(*[cad for _, cad in all_cad], names=[name for name, _ in all_cad], **kwargs)
|
||||
|
||||
def remove(self, name: str):
|
||||
@@ -325,7 +326,7 @@ _find_var_name_count = 0
|
||||
def _find_var_name(obj: any, avoid_levels: int = 2) -> str:
|
||||
"""A hacky way to get a stable name for an object that may change over time"""
|
||||
global _find_var_name_count
|
||||
obj_shape = get_shape(obj)
|
||||
obj_shape = get_shape(obj, error=False) or obj
|
||||
for frame in inspect.stack()[avoid_levels:]:
|
||||
for key, value in frame.frame.f_locals.items():
|
||||
if get_shape(value, error=False) is obj_shape:
|
||||
|
||||
46
yarn.lock
46
yarn.lock
@@ -390,7 +390,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
||||
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
||||
|
||||
"@gltf-transform/core@^3.10.0", "@gltf-transform/core@^3.10.1":
|
||||
"@gltf-transform/core@^3.10.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@gltf-transform/core/-/core-3.10.1.tgz#d99c060b499482ed2c3304466405bf4c10939831"
|
||||
integrity sha512-50OYemknGNxjBmiOM6iJp04JAu0bl9jvXJfN/gFt9QdJO02cPDcoXlTfSPJG6TVWDcfl0xPlsx1vybcbPVGFcQ==
|
||||
@@ -777,10 +777,10 @@
|
||||
"@sigstore/core" "^1.0.0"
|
||||
"@sigstore/protobuf-specs" "^0.3.0"
|
||||
|
||||
"@tsconfig/node20@^20.1.3":
|
||||
version "20.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.3.tgz#b3b4cf785e1b390a6ab48a68aa594a25960d2fe8"
|
||||
integrity sha512-XeWn6Gms5MaQWdj+C4fuxuo/Icy8ckh+BwAIijhX2LKRHHt1OuctLLLlB0F4EPi55m2IUJNTnv8FH9kSBI7Ogw==
|
||||
"@tsconfig/node20@^20.1.4":
|
||||
version "20.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928"
|
||||
integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==
|
||||
|
||||
"@tufjs/canonical-json@2.0.0":
|
||||
version "2.0.0"
|
||||
@@ -805,10 +805,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/ndarray/-/ndarray-1.0.14.tgz#96b28c09a3587a76de380243f87bb7a2d63b4b23"
|
||||
integrity sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==
|
||||
|
||||
"@types/node@^20.11.30":
|
||||
version "20.11.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f"
|
||||
integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==
|
||||
"@types/node@^20.12.2":
|
||||
version "20.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
|
||||
integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@@ -2410,7 +2410,7 @@ postcss-selector-parser@^6.0.10:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss@^8.4.35, postcss@^8.4.36:
|
||||
postcss@^8.4.35, postcss@^8.4.38:
|
||||
version "8.4.38"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||
@@ -2883,10 +2883,10 @@ tar@^6.1.11, tar@^6.1.2:
|
||||
mkdirp "^1.0.3"
|
||||
yallist "^4.0.0"
|
||||
|
||||
terser@^5.29.2:
|
||||
version "5.29.2"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.2.tgz#c17d573ce1da1b30f21a877bffd5655dd86fdb35"
|
||||
integrity sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==
|
||||
terser@^5.30.0:
|
||||
version "5.30.0"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.0.tgz#64cb2af71e16ea3d32153f84d990f9be0cdc22bf"
|
||||
integrity sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.3"
|
||||
acorn "^8.8.2"
|
||||
@@ -3002,13 +3002,13 @@ validate-npm-package-name@^5.0.0:
|
||||
dependencies:
|
||||
builtins "^5.0.0"
|
||||
|
||||
vite@^5.2.6:
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.6.tgz#fc2ce309e0b4871e938cb0aca3b96c422c01f222"
|
||||
integrity sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==
|
||||
vite@^5.2.7:
|
||||
version "5.2.7"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.7.tgz#e1b8a985eb54fcb9467d7f7f009d87485016df6e"
|
||||
integrity sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==
|
||||
dependencies:
|
||||
esbuild "^0.20.1"
|
||||
postcss "^8.4.36"
|
||||
postcss "^8.4.38"
|
||||
rollup "^4.13.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
@@ -3041,10 +3041,10 @@ vue@^3.4.21:
|
||||
"@vue/server-renderer" "3.4.21"
|
||||
"@vue/shared" "3.4.21"
|
||||
|
||||
vuetify@^3.5.11:
|
||||
version "3.5.11"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.11.tgz#9e5b628544e736de0b7f236b704539d544588152"
|
||||
integrity sha512-us5I0jyFwIQYG4v41PFmVMkoc/oJddVT4C2RFjJTI99ttigbQ92gsTeG5SB8BPfmfnUS4paR5BedZwk6W3KlJw==
|
||||
vuetify@^3.5.13:
|
||||
version "3.5.13"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.13.tgz#24a45d19ce5dcf71b2653f0bcf3ea91edf1f406c"
|
||||
integrity sha512-3ZyIoHgB2GR87ojIpqNwkkRXlUNTEKh83fjUuQ1hOKdTXzEuZXBgtfUt9kp4WOVnYILGdZKWTJ6gv8nXOa/tZA==
|
||||
|
||||
walk-up-path@^3.0.1:
|
||||
version "3.0.1"
|
||||
|
||||
Reference in New Issue
Block a user