mirror of
https://github.com/yeicor-3d/yet-another-cad-viewer.git
synced 2025-12-19 22:24:17 +01:00
add working initial bounding box implementation
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<!--suppress SillyAssignmentJS -->
|
<!--suppress SillyAssignmentJS -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {defineAsyncComponent, ref, Ref, shallowRef} from "vue";
|
import {defineAsyncComponent, ref, Ref, shallowRef, provide} from "vue";
|
||||||
import Sidebar from "./misc/Sidebar.vue";
|
import Sidebar from "./misc/Sidebar.vue";
|
||||||
import Loading from "./misc/Loading.vue";
|
import Loading from "./misc/Loading.vue";
|
||||||
import Tools from "./tools/Tools.vue";
|
import Tools from "./tools/Tools.vue";
|
||||||
@@ -27,6 +27,7 @@ let sceneUrl = ref("")
|
|||||||
let viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
|
let viewer: Ref<InstanceType<typeof ModelViewerWrapperT> | null> = ref(null);
|
||||||
let document = shallowRef(new Document());
|
let document = shallowRef(new Document());
|
||||||
let models: Ref<InstanceType<typeof Models> | null> = ref(null)
|
let models: Ref<InstanceType<typeof Models> | null> = ref(null)
|
||||||
|
provide('document', document);
|
||||||
|
|
||||||
async function onModelLoadRequest(model: NetworkUpdateEvent) {
|
async function onModelLoadRequest(model: NetworkUpdateEvent) {
|
||||||
await SceneMgr.loadModel(sceneUrl, document, model.name, model.url);
|
await SceneMgr.loadModel(sceneUrl, document, model.name, model.url);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {Ref, ShallowRef} from 'vue';
|
|||||||
import {Document} from '@gltf-transform/core';
|
import {Document} from '@gltf-transform/core';
|
||||||
import {extrasNameKey, mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf";
|
import {extrasNameKey, mergeFinalize, mergePartial, removeModel, toBuffer} from "./gltf";
|
||||||
import {newAxes, newGridBox} from "./helpers";
|
import {newAxes, newGridBox} from "./helpers";
|
||||||
import {Matrix4, Vector3} from 'three';
|
import {Box3, Matrix4, Vector3} from 'three';
|
||||||
|
|
||||||
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
/** This class helps manage SceneManagerData. All methods are static to support reactivity... */
|
||||||
export class SceneMgr {
|
export class SceneMgr {
|
||||||
@@ -27,6 +27,18 @@ export class SceneMgr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async reloadHelpers(sceneUrl: Ref<string>, document: ShallowRef<Document>) {
|
private static async reloadHelpers(sceneUrl: Ref<string>, document: ShallowRef<Document>) {
|
||||||
|
let bb = SceneMgr.getBoundingBox(document);
|
||||||
|
|
||||||
|
// Create the helper axes and grid box
|
||||||
|
let helpersDoc = new Document();
|
||||||
|
let transform = (new Matrix4()).makeTranslation(bb.getCenter(new Vector3()));
|
||||||
|
newAxes(helpersDoc, bb.getSize(new Vector3()).multiplyScalar(0.5), transform);
|
||||||
|
newGridBox(helpersDoc, bb.getSize(new Vector3()), transform);
|
||||||
|
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
|
||||||
|
await SceneMgr.loadModel(sceneUrl, document, "__helpers", helpersUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getBoundingBox(document: ShallowRef<Document>): Box3 {
|
||||||
// Get bounding box of the model and use it to set the size of the helpers
|
// Get bounding box of the model and use it to set the size of the helpers
|
||||||
let bbMin: number[] = [1e6, 1e6, 1e6];
|
let bbMin: number[] = [1e6, 1e6, 1e6];
|
||||||
let bbMax: number[] = [-1e6, -1e6, -1e6];
|
let bbMax: number[] = [-1e6, -1e6, -1e6];
|
||||||
@@ -46,14 +58,7 @@ export class SceneMgr {
|
|||||||
});
|
});
|
||||||
let bbSize = new Vector3().fromArray(bbMax).sub(new Vector3().fromArray(bbMin));
|
let bbSize = new Vector3().fromArray(bbMax).sub(new Vector3().fromArray(bbMin));
|
||||||
let bbCenter = new Vector3().fromArray(bbMin).add(bbSize.clone().multiplyScalar(0.5));
|
let bbCenter = new Vector3().fromArray(bbMin).add(bbSize.clone().multiplyScalar(0.5));
|
||||||
let bbTransform = new Matrix4().makeTranslation(bbCenter.x, bbCenter.y, bbCenter.z);
|
return new Box3().setFromCenterAndSize(bbCenter, bbSize);
|
||||||
|
|
||||||
// Create the helper axes and grid box
|
|
||||||
let helpersDoc = new Document();
|
|
||||||
newAxes(helpersDoc, bbSize.clone().multiplyScalar(0.5), bbTransform);
|
|
||||||
newGridBox(helpersDoc, bbSize, bbTransform);
|
|
||||||
let helpersUrl = URL.createObjectURL(new Blob([await toBuffer(helpersDoc)]));
|
|
||||||
await SceneMgr.loadModel(sceneUrl, document, "__helpers", helpersUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Removes a model from the viewer */
|
/** Removes a model from the viewer */
|
||||||
|
|||||||
@@ -118,13 +118,9 @@ function onModelLoad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// props.viewer.elem may not yet be available, so we need to wait for it
|
// props.viewer.elem may not yet be available, so we need to wait for it
|
||||||
if (props.viewer.elem) {
|
watch(() => props.viewer?.elem, (elem) => {
|
||||||
props.viewer.elem.addEventListener('load', onModelLoad);
|
elem.addEventListener('load', onModelLoad);
|
||||||
} else {
|
});
|
||||||
watch(() => props.viewer?.elem, (elem) => {
|
|
||||||
if (elem) elem.addEventListener('load', onModelLoad);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {defineModel, ref} from "vue";
|
import {defineModel, inject, ref, ShallowRef, watch} from "vue";
|
||||||
import {VBtn, VSelect, VTooltip} from "vuetify/lib/components";
|
import {VBtn, VSelect, VTooltip} from "vuetify/lib/components";
|
||||||
import SvgIcon from '@jamescoyle/vue-icon/lib/svg-icon.vue';
|
import SvgIcon from '@jamescoyle/vue-icon/lib/svg-icon.vue';
|
||||||
import type {ModelViewerElement} from '@google/model-viewer';
|
import type {ModelViewerElement} from '@google/model-viewer';
|
||||||
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
import type {ModelScene} from "@google/model-viewer/lib/three-components/ModelScene";
|
||||||
import {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js';
|
import {mdiCubeOutline, mdiCursorDefaultClick, mdiFeatureSearch, mdiRuler} from '@mdi/js';
|
||||||
import type {Intersection, Material, Object3D} from "three";
|
import type {Intersection, Material, Object3D} from "three";
|
||||||
import {Raycaster} from "three";
|
import {Raycaster, Vector3} from "three";
|
||||||
import type ModelViewerWrapperT from "./ModelViewerWrapper.vue";
|
import type ModelViewerWrapperT from "./ModelViewerWrapper.vue";
|
||||||
import {extrasNameKey} from "../misc/gltf";
|
import {extrasNameKey} from "../misc/gltf";
|
||||||
|
import {SceneMgr} from "../misc/scene";
|
||||||
|
import {Document} from "@gltf-transform/core";
|
||||||
|
|
||||||
export type MObject3D = Object3D & {
|
export type MObject3D = Object3D & {
|
||||||
userData: { noHit?: boolean },
|
userData: { noHit?: boolean },
|
||||||
@@ -22,30 +24,30 @@ let selected = defineModel<Array<Intersection<MObject3D>>>({default: []});
|
|||||||
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
|
let highlightNextSelection = ref([false, false]); // Second is whether selection was enabled before
|
||||||
let showBoundingBox = ref<Boolean>(false);
|
let showBoundingBox = ref<Boolean>(false);
|
||||||
|
|
||||||
let hasListener = false;
|
|
||||||
let mouseDownAt: [number, number] | null = null;
|
let mouseDownAt: [number, number] | null = null;
|
||||||
let selectFilter = ref('Any');
|
let selectFilter = ref('Any');
|
||||||
const raycaster = new Raycaster();
|
const raycaster = new Raycaster();
|
||||||
|
|
||||||
let selectionMoveListener = (event: MouseEvent) => {
|
let selectionMoveListener = (event: MouseEvent) => {
|
||||||
if (!selectionEnabled.value) return;
|
|
||||||
mouseDownAt = [event.clientX, event.clientY];
|
mouseDownAt = [event.clientX, event.clientY];
|
||||||
|
if (!selectionEnabled.value) return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let selectionListener = (event: MouseEvent) => {
|
let selectionListener = (event: MouseEvent) => {
|
||||||
if (!selectionEnabled.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the mouse moved while clicked (dragging), avoid selection logic
|
// If the mouse moved while clicked (dragging), avoid selection logic
|
||||||
if (mouseDownAt) {
|
if (mouseDownAt) {
|
||||||
let [x, y] = mouseDownAt;
|
let [x, y] = mouseDownAt;
|
||||||
mouseDownAt = undefined;
|
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) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If disabled, avoid selection logic
|
||||||
|
if (!selectionEnabled.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Define the 3D ray from the camera to the mouse
|
// Define the 3D ray from the camera to the mouse
|
||||||
// NOTE: Need to access internal as the API has issues with small faces surrounded by edges
|
// NOTE: Need to access internal as the API has issues with small faces surrounded by edges
|
||||||
let scene: ModelScene = props.viewer?.scene;
|
let scene: ModelScene = props.viewer?.scene;
|
||||||
@@ -60,6 +62,12 @@ let selectionListener = (event: MouseEvent) => {
|
|||||||
}
|
}
|
||||||
console.log('Ray', raycaster.ray);
|
console.log('Ray', raycaster.ray);
|
||||||
|
|
||||||
|
// TODO DEBUG: Draw the ray for debugging
|
||||||
|
let actualFrom = scene.getTarget().clone().add(scene.target.position);
|
||||||
|
let actualTo = actualFrom.clone().add(raycaster.ray.direction.clone().multiplyScalar(50));
|
||||||
|
let lineHandle = props.viewer?.addLine3D(actualFrom, actualTo, "Ray")
|
||||||
|
setTimeout(() => props.viewer?.removeLine3D(lineHandle), 30000)
|
||||||
|
|
||||||
// Find all hit objects and select the wanted one based on the filter
|
// Find all hit objects and select the wanted one based on the filter
|
||||||
const hits = raycaster.intersectObject(scene, true);
|
const hits = raycaster.intersectObject(scene, true);
|
||||||
let hit = hits.find((hit) => {
|
let hit = hits.find((hit) => {
|
||||||
@@ -135,11 +143,6 @@ function toggleSelection() {
|
|||||||
if (!viewer) return;
|
if (!viewer) return;
|
||||||
selectionEnabled.value = !selectionEnabled.value;
|
selectionEnabled.value = !selectionEnabled.value;
|
||||||
if (selectionEnabled.value) {
|
if (selectionEnabled.value) {
|
||||||
if (!hasListener) {
|
|
||||||
viewer.addEventListener('mouseup', selectionListener);
|
|
||||||
viewer.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
|
|
||||||
hasListener = true;
|
|
||||||
}
|
|
||||||
for (let material of selected.value) {
|
for (let material of selected.value) {
|
||||||
select(material);
|
select(material);
|
||||||
}
|
}
|
||||||
@@ -163,19 +166,99 @@ function toggleHighlightNextSelection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleShowBoundingBox() {
|
|
||||||
// let scene: ModelScene = props.viewer?.scene;
|
|
||||||
// if (!scene) return;
|
|
||||||
showBoundingBox.value = !showBoundingBox.value;
|
|
||||||
// scene.model?.traverse((child) => {
|
|
||||||
// if (child.userData[extrasNameKey] === modelName) {
|
|
||||||
// if (child.type === 'BoxHelper') {
|
|
||||||
// child.visible = showBoundingBox.value;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// scene.queueRender() // Force rerender of model-viewer
|
|
||||||
|
|
||||||
|
let boundingBoxLineIds: Array<number> = []
|
||||||
|
|
||||||
|
function toggleShowBoundingBox() {
|
||||||
|
showBoundingBox.value = !showBoundingBox.value;
|
||||||
|
updateBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasListeners = false;
|
||||||
|
let firstLoad = true;
|
||||||
|
watch(() => props.viewer?.elem, (elem) => {
|
||||||
|
if (hasListeners) return;
|
||||||
|
hasListeners = true;
|
||||||
|
elem.addEventListener('mouseup', selectionListener);
|
||||||
|
elem.addEventListener('mousedown', selectionMoveListener); // Avoid clicking when dragging
|
||||||
|
elem.addEventListener('load', () => {
|
||||||
|
if (firstLoad) {
|
||||||
|
toggleShowBoundingBox();
|
||||||
|
firstLoad = false;
|
||||||
|
} else {
|
||||||
|
updateBoundingBox();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let isWaiting = false;
|
||||||
|
let lastBoundingBoxUpdate = performance.now();
|
||||||
|
elem.addEventListener('camera-change', () => {
|
||||||
|
// Avoid updates while dragging (slow operation)
|
||||||
|
if (isWaiting) return;
|
||||||
|
if (performance.now() - lastBoundingBoxUpdate < 1000) return;
|
||||||
|
isWaiting = true;
|
||||||
|
let waitingHandler: () => void;
|
||||||
|
waitingHandler = () => {
|
||||||
|
if (mouseDownAt === null) {
|
||||||
|
updateBoundingBox();
|
||||||
|
isWaiting = false;
|
||||||
|
lastBoundingBoxUpdate = performance.now();
|
||||||
|
} else {
|
||||||
|
// If the mouse is still down, wait a little more
|
||||||
|
setTimeout(waitingHandler, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(waitingHandler, 100); // Wait for the camera to stop moving
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let document: ShallowRef<Document> = inject('document');
|
||||||
|
|
||||||
|
function updateBoundingBox() {
|
||||||
|
boundingBoxLineIds.forEach((id) => props.viewer?.removeLine3D(id));
|
||||||
|
boundingBoxLineIds = [];
|
||||||
|
if (!showBoundingBox.value) return;
|
||||||
|
let bb = SceneMgr.getBoundingBox(document);
|
||||||
|
// TODO: Get bounding box of selection instead if selection is enabled
|
||||||
|
// For each edge of the bounding box, draw a line
|
||||||
|
let corners = [
|
||||||
|
[bb.min.x, bb.min.y, bb.min.z],
|
||||||
|
[bb.min.x, bb.min.y, bb.max.z],
|
||||||
|
[bb.min.x, bb.max.y, bb.min.z],
|
||||||
|
[bb.min.x, bb.max.y, bb.max.z],
|
||||||
|
[bb.max.x, bb.min.y, bb.min.z],
|
||||||
|
[bb.max.x, bb.min.y, bb.max.z],
|
||||||
|
[bb.max.x, bb.max.y, bb.min.z],
|
||||||
|
[bb.max.x, bb.max.y, bb.max.z],
|
||||||
|
];
|
||||||
|
let edgesByAxis = [
|
||||||
|
[[0, 1], [2, 3], [4, 5], [6, 7]], // X
|
||||||
|
[[0, 2], [1, 3], [4, 6], [5, 7]], // Y
|
||||||
|
[[0, 4], [1, 5], [2, 6], [3, 7]], // Z
|
||||||
|
];
|
||||||
|
for (let axisEdges of edgesByAxis) {
|
||||||
|
// Only draw one edge per axis, the closest one to the camera
|
||||||
|
let edgeToDraw: Array<number> = axisEdges[0];
|
||||||
|
let edgeToDrawDist = Infinity;
|
||||||
|
let cameraPos: Vector3 = props.viewer?.scene.camera.position;
|
||||||
|
for (let edge of axisEdges) {
|
||||||
|
let from = new Vector3(...corners[edge[0]]);
|
||||||
|
let to = new Vector3(...corners[edge[1]]);
|
||||||
|
let mid = from.clone().add(to).multiplyScalar(0.5);
|
||||||
|
let newDist = cameraPos.distanceTo(mid);
|
||||||
|
if (newDist < edgeToDrawDist) {
|
||||||
|
edgeToDraw = edge;
|
||||||
|
edgeToDrawDist = newDist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let from = new Vector3(...corners[edgeToDraw[0]]);
|
||||||
|
let to = new Vector3(...corners[edgeToDraw[1]]);
|
||||||
|
let lineHandle = props.viewer?.addLine3D(from, to, to.clone().sub(from).length().toFixed(1) + "mm", {
|
||||||
|
"stroke": "blue",
|
||||||
|
"stroke-width": "2"
|
||||||
|
});
|
||||||
|
boundingBoxLineIds.push(lineHandle);
|
||||||
|
}
|
||||||
|
props.viewer?.scene?.queueRender() // Force rerender of model-viewer
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -200,7 +283,7 @@ function toggleShowBoundingBox() {
|
|||||||
<svg-icon type="mdi" :path="mdiFeatureSearch"/>
|
<svg-icon type="mdi" :path="mdiFeatureSearch"/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<!-- TODO: Show BB -->
|
<!-- TODO: Show BB -->
|
||||||
<v-btn icon disabled @click="toggleShowBoundingBox" :color="showBoundingBox ? 'surface-light' : ''">
|
<v-btn icon @click="toggleShowBoundingBox" :color="showBoundingBox ? 'surface-light' : ''">
|
||||||
<v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection bounds' : 'Show selection bounds' }}
|
<v-tooltip activator="parent">{{ showBoundingBox ? 'Hide selection bounds' : 'Show selection bounds' }}
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<svg-icon type="mdi" :path="mdiCubeOutline"/>
|
<svg-icon type="mdi" :path="mdiCubeOutline"/>
|
||||||
|
|||||||
@@ -22,27 +22,17 @@ const props = defineProps<{ src: string }>();
|
|||||||
const elem = ref<ModelViewerElement | null>(null);
|
const elem = ref<ModelViewerElement | null>(null);
|
||||||
const scene = ref<ModelScene | null>(null);
|
const scene = ref<ModelScene | null>(null);
|
||||||
const renderer = ref<Renderer | null>(null);
|
const renderer = ref<Renderer | null>(null);
|
||||||
defineExpose({elem, scene, renderer});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
elem.value.addEventListener('load', async () => {
|
elem.value.addEventListener('load', async () => {
|
||||||
if (elem.value) {
|
// Delete the initial load banner
|
||||||
// Delete the initial load banner
|
let banner = elem.value.querySelector('.initial-load-banner');
|
||||||
let banner = elem.value.querySelector('.initial-load-banner');
|
if (banner) banner.remove();
|
||||||
if (banner) banner.remove();
|
// Set the scene
|
||||||
// Set the scene
|
scene.value = elem.value[$scene] as ModelScene;
|
||||||
scene.value = elem.value[$scene] as ModelScene;
|
renderer.value = elem.value[$renderer] as Renderer;
|
||||||
renderer.value = elem.value[$renderer] as Renderer;
|
// Emit the load event
|
||||||
// Emit the load event
|
emit('load')
|
||||||
emit('load')
|
|
||||||
|
|
||||||
// Test adding a fake 3D line
|
|
||||||
let lineHandle = await addLine3D(new Vector3(0, 0, 0), new Vector3(0, 100, 0), "Hello!", {
|
|
||||||
"stroke": "green",
|
|
||||||
"stroke-width": "2",
|
|
||||||
});
|
|
||||||
setTimeout(() => removeLine3D(lineHandle), 10000);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
elem.value.addEventListener('camera-change', onCameraChange);
|
elem.value.addEventListener('camera-change', onCameraChange);
|
||||||
});
|
});
|
||||||
@@ -65,9 +55,11 @@ function positionToHotspot(position: Vector3): string {
|
|||||||
return position.x + ' ' + position.y + ' ' + position.z;
|
return position.x + ' ' + position.y + ' ' + position.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addLine3D(p1: Vector3, p2: Vector3, centerText: string = "", lineAttrs: {
|
function addLine3D(p1: Vector3, p2: Vector3, centerText: string = "",
|
||||||
[key: string]: string
|
lineAttrs: { [key: string]: string } = {
|
||||||
} = {}): Promise<number> {
|
"stroke-width": "2",
|
||||||
|
"stroke": "red",
|
||||||
|
}): number {
|
||||||
if (!scene.value) return -1;
|
if (!scene.value) return -1;
|
||||||
let id = nextLineId++;
|
let id = nextLineId++;
|
||||||
let hotspotName1 = 'line' + id + '_start';
|
let hotspotName1 = 'line' + id + '_start';
|
||||||
@@ -77,26 +69,23 @@ async function addLine3D(p1: Vector3, p2: Vector3, centerText: string = "", line
|
|||||||
lines.value[id] = {
|
lines.value[id] = {
|
||||||
startHotspot: elem.value.shadowRoot.querySelector('slot[name="' + hotspotName1 + '"]').parentElement,
|
startHotspot: elem.value.shadowRoot.querySelector('slot[name="' + hotspotName1 + '"]').parentElement,
|
||||||
endHotspot: elem.value.shadowRoot.querySelector('slot[name="' + hotspotName2 + '"]').parentElement,
|
endHotspot: elem.value.shadowRoot.querySelector('slot[name="' + hotspotName2 + '"]').parentElement,
|
||||||
start2D: [0, 0],
|
start2D: [-1000, -1000],
|
||||||
end2D: [20, 20],
|
end2D: [-1000, -1000],
|
||||||
centerText: centerText,
|
centerText: centerText,
|
||||||
centerTextSize: [0, 0],
|
centerTextSize: [0, 0],
|
||||||
lineAttrs: lineAttrs
|
lineAttrs: lineAttrs
|
||||||
};
|
};
|
||||||
|
scene.value.queueRender() // Needed to update the hotspots
|
||||||
requestIdleCallback(() => onCameraChangeLine(id));
|
requestIdleCallback(() => onCameraChangeLine(id));
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeLine3D(id: number) {
|
function removeLine3D(id: number) {
|
||||||
if (!scene.value) return;
|
if (!scene.value) return;
|
||||||
if (lines.value[id].startHotspot) {
|
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_start'}));
|
||||||
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_start'}));
|
lines.value[id].startHotspot.parentElement.remove()
|
||||||
lines.value[id].startHotspot.parentElement.remove()
|
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_end'}));
|
||||||
}
|
lines.value[id].endHotspot.parentElement.remove()
|
||||||
if (lines.value[id].endHotspot) {
|
|
||||||
scene.value.removeHotspot(new Hotspot({name: 'line' + id + '_end'}));
|
|
||||||
lines.value[id].endHotspot.parentElement.remove()
|
|
||||||
}
|
|
||||||
delete lines.value[id];
|
delete lines.value[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +99,7 @@ function onCameraChange() {
|
|||||||
let svg = ref<SVGElement | null>(null);
|
let svg = ref<SVGElement | null>(null);
|
||||||
|
|
||||||
function onCameraChangeLine(lineId: number) {
|
function onCameraChangeLine(lineId: number) {
|
||||||
|
if (!(lineId in lines.value)) return // Silently ignore (not updated yet)
|
||||||
// Update start and end 2D positions
|
// Update start and end 2D positions
|
||||||
let {x: xB, y: yB} = elem.value.getBoundingClientRect();
|
let {x: xB, y: yB} = elem.value.getBoundingClientRect();
|
||||||
let {x, y} = lines.value[lineId].startHotspot.getBoundingClientRect();
|
let {x, y} = lines.value[lineId].startHotspot.getBoundingClientRect();
|
||||||
@@ -127,6 +117,7 @@ function onCameraChangeLine(lineId: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({elem, scene, renderer, addLine3D, removeLine3D});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user