mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
Ability to explode models and minor improvements
This commit is contained in:
@@ -10,7 +10,7 @@ in a web browser.
|
||||
- All [GLTF 2.0](https://www.khronos.org/gltf/) features (textures, PBR materials, animations...).
|
||||
- All [model-viewer](https://modelviewer.dev/) features (smooth controls, augmented reality...).
|
||||
- Load multiple models at once, load external models and even images as quads.
|
||||
- Control clipping planes and transparency of each model.
|
||||
- Control clipping planes, transparency, edge/vertex sizes and explode each model.
|
||||
- View and interact with topological entities: faces, edges, vertices and locations.
|
||||
- Select any entity and measure bounding box size and distances.
|
||||
- Hot reloading while editing the CAD model (using the `yacv-server` package).
|
||||
@@ -23,9 +23,9 @@ in a web browser.
|
||||
The [example](example) is a fully working project that shows how to use the viewer.
|
||||
|
||||
You can play with the latest
|
||||
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)
|
||||
demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.glb&preload=location.glb)
|
||||
(or
|
||||
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.jpg.glb&preload=location.glb)).
|
||||
[without animation](https://yeicor-3d.github.io/yet-another-cad-viewer/?autoplay=false&preload=logo.glb&preload=logo_hl.glb&preload=logo_hl_tex.glb&preload=fox.glb&preload=img.glb&preload=location.glb)).
|
||||
|
||||

|
||||
|
||||
@@ -36,3 +36,6 @@ demo [here](https://yeicor-3d.github.io/yet-another-cad-viewer/?preload=logo.glb
|
||||
Uses the same backend and frontend behind the scenes.
|
||||
- [build123d-docker](https://github.com/derhuerst/build123d-docker/pkgs/container/build123d) provides docker images for
|
||||
Yet Another CAD Viewer and other projects, with automatic updates.
|
||||
- [OCP.wasm](https://github.com/yeicor/OCP.wasm/) ports OCP (OpenCASCADE for Python) and supporting libraries to
|
||||
WebAssembly, enabling full in-browser CAD model generation and manipulation. This powers the build123d playground
|
||||
provided by this viewer.
|
||||
@@ -109,6 +109,12 @@ async function loadModelManual() {
|
||||
if (modelUrl) await networkMgr.load(modelUrl);
|
||||
}
|
||||
|
||||
function loadDemoModels() {
|
||||
for (let name of ['fox.glb', 'img.glb', 'location.glb', 'logo.glb', 'logo_hl.glb', 'logo_hl_tex.glb']) {
|
||||
networkMgr.load(`https://yeicor-3d.github.io/yet-another-cad-viewer/${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect dropped .glb files and load them manually
|
||||
document.body.addEventListener("dragover", e => {
|
||||
e.preventDefault(); // Allow drop
|
||||
@@ -124,7 +130,6 @@ document.body.addEventListener("drop", async e => {
|
||||
await networkMgr.load(file);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -139,9 +144,8 @@ document.body.addEventListener("drop", async e => {
|
||||
<v-btn @click="() => tools?.openPlayground()" class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiScriptTextPlay" type="mdi"/> Open playground...
|
||||
</v-btn>
|
||||
<v-btn @click="networkMgr.load('https://yeicor-3d.github.io/yet-another-cad-viewer/logo.glb')"
|
||||
class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiCube" type="mdi"/> Load demo model...
|
||||
<v-btn @click="loadDemoModels" class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiCube" type="mdi"/> Load demo models...
|
||||
</v-btn>
|
||||
<v-btn @click="loadModelManual" class="mx-auto d-block my-4">
|
||||
<svg-icon :path="mdiPlus" type="mdi"/> Load model manually...
|
||||
|
||||
@@ -14,7 +14,7 @@ export const settings = (async () => {
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
|
||||
// @ts-ignore
|
||||
// new URL('../../assets/logo_build/img.jpg.glb', import.meta.url).href,
|
||||
// new URL('../../assets/logo_build/img.glb', import.meta.url).href,
|
||||
// Websocket URLs automatically listen for new models from the python backend
|
||||
'<auto>', // Get the default preload URL if not overridden
|
||||
],
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
} from "vuetify/lib/components/index.mjs";
|
||||
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
||||
import {Mesh} from "@gltf-transform/core";
|
||||
import {ref, watch} from "vue";
|
||||
import {nextTick, ref, watch} from "vue";
|
||||
import type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||
import {
|
||||
mdiArrowExpand,
|
||||
mdiCircleOpacity,
|
||||
mdiCube,
|
||||
mdiDelete,
|
||||
@@ -58,7 +59,12 @@ const clipPlaneY = ref(1);
|
||||
const clipPlaneSwappedY = ref(false);
|
||||
const clipPlaneZ = ref(1);
|
||||
const clipPlaneSwappedZ = ref(false);
|
||||
|
||||
const edgeWidth = ref(0);
|
||||
const explodeStrength = ref(0);
|
||||
const explodeSwapped = ref(false);
|
||||
|
||||
// Load the settings for the default edge width
|
||||
(async () => {
|
||||
let s = await settings;
|
||||
edgeWidth.value = s.edgeWidth;
|
||||
@@ -246,6 +252,76 @@ function onEdgeWidthChange(newEdgeWidth: number) {
|
||||
|
||||
watch(edgeWidth, onEdgeWidthChange);
|
||||
|
||||
// Explode the model
|
||||
function onExplodeChange(newExplodeStrength: number) {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
if (!scene || !sceneModel) return;
|
||||
|
||||
// Get direction and size of the explosion in a first pass
|
||||
const meBbox = new Box3();
|
||||
const othersBbox = new Box3();
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child == sceneModel) return; // Skip the scene itself
|
||||
const isMe = child.userData[extrasNameKey] === modelName;
|
||||
if ((child.type === 'Mesh' || child.type === 'SkinnedMesh' ||
|
||||
child.type === 'Line' || child.type === 'LineSegments' ||
|
||||
child.type === 'Points') && !child.userData.noHit) {
|
||||
if (isMe) {
|
||||
meBbox.expandByObject(child);
|
||||
} else if (!isMe && child.userData[extrasNameKey]) {
|
||||
othersBbox.expandByObject(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
const modelSize = new Vector3();
|
||||
meBbox.getSize(modelSize);
|
||||
const maxDimension = Math.max(modelSize.x, modelSize.y, modelSize.z);
|
||||
const pushDirection = new Vector3().subVectors(meBbox.getCenter(new Vector3()), othersBbox.getCenter(new Vector3())).normalize();
|
||||
|
||||
|
||||
// Use absolute value for strength calculation
|
||||
let strength = Math.abs(newExplodeStrength);
|
||||
if (explodeSwapped.value) strength = -strength;
|
||||
|
||||
// Apply explosion
|
||||
sceneModel.traverse((child: MObject3D) => {
|
||||
if (child.userData[extrasNameKey] === modelName) {
|
||||
if ((child.type === 'Mesh' || child.type === 'SkinnedMesh' ||
|
||||
child.type === 'Line' || child.type === 'LineSegments' ||
|
||||
child.type === 'Points')) {
|
||||
|
||||
// Handle zero vector case (if object is at origin)
|
||||
const direction = pushDirection.clone();
|
||||
if (direction.lengthSq() < 0.0001) {
|
||||
direction.set(0, 1, 0);
|
||||
console.warn("Explode direction was zero, using (0, 1, 0) instead");
|
||||
}
|
||||
|
||||
// Calculate new position based on model size
|
||||
const factor = strength * maxDimension;
|
||||
const newPosition = new Vector3().add(direction.multiplyScalar(factor));
|
||||
|
||||
// Apply new position
|
||||
child.position.copy(newPosition);
|
||||
|
||||
// Update related objects (back is automatically updated)
|
||||
if (child.userData.niceLine) {
|
||||
child.userData.niceLine.position.copy(newPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scene.queueRender();
|
||||
onClipPlanesChange();
|
||||
}
|
||||
|
||||
// Add watchers for explode variables
|
||||
watch(explodeStrength, (newVal) => onExplodeChange(newVal));
|
||||
watch(explodeSwapped, () => onExplodeChange(explodeStrength.value));
|
||||
|
||||
|
||||
function onModelLoad() {
|
||||
let scene = props.viewer?.scene;
|
||||
let sceneModel = (scene as any)?._model;
|
||||
@@ -312,15 +388,17 @@ function onModelLoad() {
|
||||
|
||||
// Furthermore...
|
||||
// Enabled features may have been reset after a reload
|
||||
onEnabledFeaturesChange(enabledFeatures.value)
|
||||
onEnabledFeaturesChange(enabledFeatures.value);
|
||||
// Opacity may have been reset after a reload
|
||||
onOpacityChange(opacity.value)
|
||||
onOpacityChange(opacity.value);
|
||||
// Wireframe may have been reset after a reload
|
||||
onWireframeChange(wireframe.value)
|
||||
onWireframeChange(wireframe.value);
|
||||
// Clip planes may have been reset after a reload
|
||||
onClipPlanesChange()
|
||||
onClipPlanesChange();
|
||||
// Edge width may have been reset after a reload
|
||||
onEdgeWidthChange(edgeWidth.value)
|
||||
onEdgeWidthChange(edgeWidth.value);
|
||||
// Explode may have been reset after a reload
|
||||
if (explodeStrength.value > 0) nextTick(() => onExplodeChange(explodeStrength.value));
|
||||
|
||||
scene.queueRender()
|
||||
}
|
||||
@@ -370,6 +448,21 @@ if (props.viewer) onViewerReady(props.viewer); else watch((() => props.viewer) a
|
||||
<v-checkbox-btn v-model="wireframe" falseIcon="mdi-triangle" trueIcon="mdi-triangle-outline"></v-checkbox-btn>
|
||||
</template>
|
||||
</v-slider>
|
||||
<v-slider v-model="explodeStrength" hide-details max="1" min="0">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip activator="parent">Explode model</v-tooltip>
|
||||
<svg-icon :path="mdiArrowExpand" type="mdi"></svg-icon>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-tooltip activator="parent">Swap explode direction (may go crazy)</v-tooltip>
|
||||
<v-checkbox-btn v-model="explodeSwapped" falseIcon="mdi-checkbox-blank-outline"
|
||||
trueIcon="mdi-checkbox-marked-outline">
|
||||
<template v-slot:label>
|
||||
<svg-icon :path="mdiSwapHorizontal" type="mdi"></svg-icon>
|
||||
</template>
|
||||
</v-checkbox-btn>
|
||||
</template>
|
||||
</v-slider>
|
||||
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details max="1" min="0">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>
|
||||
|
||||
@@ -117,7 +117,7 @@ def image_to_gltf(source: str | bytes, center: any, width: Optional[float] = Non
|
||||
# Handle arguments
|
||||
if name is None:
|
||||
if isinstance(source, str):
|
||||
name = os.path.basename(source)
|
||||
name, _ = os.path.splitext(os.path.basename(source))
|
||||
else:
|
||||
hasher = hashlib.md5()
|
||||
hasher.update(source)
|
||||
|
||||
Reference in New Issue
Block a user