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 [GLTF 2.0](https://www.khronos.org/gltf/) features (textures, PBR materials, animations...).
|
||||||
- All [model-viewer](https://modelviewer.dev/) features (smooth controls, augmented reality...).
|
- All [model-viewer](https://modelviewer.dev/) features (smooth controls, augmented reality...).
|
||||||
- Load multiple models at once, load external models and even images as quads.
|
- 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.
|
- View and interact with topological entities: faces, edges, vertices and locations.
|
||||||
- Select any entity and measure bounding box size and distances.
|
- Select any entity and measure bounding box size and distances.
|
||||||
- Hot reloading while editing the CAD model (using the `yacv-server` package).
|
- 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.
|
The [example](example) is a fully working project that shows how to use the viewer.
|
||||||
|
|
||||||
You can play with the latest
|
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
|
(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.
|
Uses the same backend and frontend behind the scenes.
|
||||||
- [build123d-docker](https://github.com/derhuerst/build123d-docker/pkgs/container/build123d) provides docker images for
|
- [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.
|
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);
|
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
|
// Detect dropped .glb files and load them manually
|
||||||
document.body.addEventListener("dragover", e => {
|
document.body.addEventListener("dragover", e => {
|
||||||
e.preventDefault(); // Allow drop
|
e.preventDefault(); // Allow drop
|
||||||
@@ -124,7 +130,6 @@ document.body.addEventListener("drop", async e => {
|
|||||||
await networkMgr.load(file);
|
await networkMgr.load(file);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -139,9 +144,8 @@ document.body.addEventListener("drop", async e => {
|
|||||||
<v-btn @click="() => tools?.openPlayground()" class="mx-auto d-block my-4">
|
<v-btn @click="() => tools?.openPlayground()" class="mx-auto d-block my-4">
|
||||||
<svg-icon :path="mdiScriptTextPlay" type="mdi"/> Open playground...
|
<svg-icon :path="mdiScriptTextPlay" type="mdi"/> Open playground...
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn @click="networkMgr.load('https://yeicor-3d.github.io/yet-another-cad-viewer/logo.glb')"
|
<v-btn @click="loadDemoModels" class="mx-auto d-block my-4">
|
||||||
class="mx-auto d-block my-4">
|
<svg-icon :path="mdiCube" type="mdi"/> Load demo models...
|
||||||
<svg-icon :path="mdiCube" type="mdi"/> Load demo model...
|
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn @click="loadModelManual" class="mx-auto d-block my-4">
|
<v-btn @click="loadModelManual" class="mx-auto d-block my-4">
|
||||||
<svg-icon :path="mdiPlus" type="mdi"/> Load model manually...
|
<svg-icon :path="mdiPlus" type="mdi"/> Load model manually...
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const settings = (async () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
|
// new URL('../../assets/logo_build/location.glb', import.meta.url).href,
|
||||||
// @ts-ignore
|
// @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
|
// Websocket URLs automatically listen for new models from the python backend
|
||||||
'<auto>', // Get the default preload URL if not overridden
|
'<auto>', // Get the default preload URL if not overridden
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import {
|
|||||||
} from "vuetify/lib/components/index.mjs";
|
} from "vuetify/lib/components/index.mjs";
|
||||||
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
import {extrasNameKey, extrasNameValueHelpers} from "../misc/gltf";
|
||||||
import {Mesh} from "@gltf-transform/core";
|
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 type ModelViewerWrapper from "../viewer/ModelViewerWrapper.vue";
|
||||||
import {
|
import {
|
||||||
|
mdiArrowExpand,
|
||||||
mdiCircleOpacity,
|
mdiCircleOpacity,
|
||||||
mdiCube,
|
mdiCube,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
@@ -58,7 +59,12 @@ const clipPlaneY = ref(1);
|
|||||||
const clipPlaneSwappedY = ref(false);
|
const clipPlaneSwappedY = ref(false);
|
||||||
const clipPlaneZ = ref(1);
|
const clipPlaneZ = ref(1);
|
||||||
const clipPlaneSwappedZ = ref(false);
|
const clipPlaneSwappedZ = ref(false);
|
||||||
|
|
||||||
const edgeWidth = ref(0);
|
const edgeWidth = ref(0);
|
||||||
|
const explodeStrength = ref(0);
|
||||||
|
const explodeSwapped = ref(false);
|
||||||
|
|
||||||
|
// Load the settings for the default edge width
|
||||||
(async () => {
|
(async () => {
|
||||||
let s = await settings;
|
let s = await settings;
|
||||||
edgeWidth.value = s.edgeWidth;
|
edgeWidth.value = s.edgeWidth;
|
||||||
@@ -246,6 +252,76 @@ function onEdgeWidthChange(newEdgeWidth: number) {
|
|||||||
|
|
||||||
watch(edgeWidth, onEdgeWidthChange);
|
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() {
|
function onModelLoad() {
|
||||||
let scene = props.viewer?.scene;
|
let scene = props.viewer?.scene;
|
||||||
let sceneModel = (scene as any)?._model;
|
let sceneModel = (scene as any)?._model;
|
||||||
@@ -312,15 +388,17 @@ function onModelLoad() {
|
|||||||
|
|
||||||
// Furthermore...
|
// Furthermore...
|
||||||
// Enabled features may have been reset after a reload
|
// Enabled features may have been reset after a reload
|
||||||
onEnabledFeaturesChange(enabledFeatures.value)
|
onEnabledFeaturesChange(enabledFeatures.value);
|
||||||
// Opacity may have been reset after a reload
|
// Opacity may have been reset after a reload
|
||||||
onOpacityChange(opacity.value)
|
onOpacityChange(opacity.value);
|
||||||
// Wireframe may have been reset after a reload
|
// Wireframe may have been reset after a reload
|
||||||
onWireframeChange(wireframe.value)
|
onWireframeChange(wireframe.value);
|
||||||
// Clip planes may have been reset after a reload
|
// Clip planes may have been reset after a reload
|
||||||
onClipPlanesChange()
|
onClipPlanesChange();
|
||||||
// Edge width may have been reset after a reload
|
// 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()
|
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>
|
<v-checkbox-btn v-model="wireframe" falseIcon="mdi-triangle" trueIcon="mdi-triangle-outline"></v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-slider>
|
</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">
|
<v-slider v-if="edgeCount > 0 || vertexCount > 0" v-model="edgeWidth" hide-details max="1" min="0">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-tooltip activator="parent">Edge and vertex sizes</v-tooltip>
|
<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
|
# Handle arguments
|
||||||
if name is None:
|
if name is None:
|
||||||
if isinstance(source, str):
|
if isinstance(source, str):
|
||||||
name = os.path.basename(source)
|
name, _ = os.path.splitext(os.path.basename(source))
|
||||||
else:
|
else:
|
||||||
hasher = hashlib.md5()
|
hasher = hashlib.md5()
|
||||||
hasher.update(source)
|
hasher.update(source)
|
||||||
|
|||||||
Reference in New Issue
Block a user