I was trying to create a layer tree component, using resources from this page:
[Layers and filters β HOOPS Communicator 2024.2.0 Documentation]
I have created this component in React.js:
import React, { useState, useEffect } from 'react';
const LayerTree = ({ hwv }) => {
const [layers, setLayers] = useState([]);
useEffect(() => {
const fetchLayers = () => {
if (hwv && hwv.model) {
const layersMap = hwv.model.getLayers();
if (layersMap.size > 0) {
const layersArray = Array.from(layersMap, ([id, name]) => ({ id, name }));
setLayers(layersArray);
console.log("Layers fetched:", layersArray);
} else {
console.log("No layers found or layers are not yet loaded.");
}
}
};
// Listen for a specific event that tells you the model is fully loaded; for now, we use a timeout for demonstration
setTimeout(fetchLayers, 1000);
return () => {
// Cleanup logic if necessary
};
}, [hwv]);
const toggleLayerVisibility = (layerId) => {
const nodeIds = hwv.model.getNodesFromLayer(layerId);
nodeIds.forEach(nodeId => {
const isVisible = hwv.model.getNodeVisibility(nodeId);
hwv.model.setNodeVisibility(nodeId, !isVisible);
});
hwv.view.update();
};
return (
<div className="layer-tree">
<ul>
{layers.map(layer => (
<li key={layer.id} onClick={() => toggleLayerVisibility(layer.id)}>
{layer.name}
</li>
))}
</ul>
</div>
);
};
export default LayerTree;
I am using LayerTree component in HoopsViewer component:
import React, { useState, useEffect } from "react";
import ModelTree from "./ModelTree";
import LayerTree from "./LayerTree";
import HomeIcon from "~/icons/HomeIcon";
import ExplodeCubeIcon from "~/icons/ExplodeCubeIcon";
import Select from 'react-select';
import Tooltip from "../basics/Tooltip";
const operatorOptions = [
{ value: 'Orbit', label: 'Orbit' },
{ value: 'Turntable', label: 'Turntable' }
];
const cameraPositions = {
Front: { position: { x: 10, y: 0, z: 0 }, target: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 0, z: 1 } },
Back: { position: { x: -10, y: 0, z: 0 }, target: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 0, z: 1 } },
Right: { position: { x: 0, y: 10, z: 0 }, target: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 0, z: 1 } },
Left: { position: { x: 0, y: -10, z: 0 }, target: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 0, z: 1 } },
Top: { position: { x: 0, y: 0, z: 10 }, target: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 1, z: 1 } },
Bottom: { position: { x: 0, y: 0, z: -10 }, target: { x: 0, y: 0, z: 0 }, up: { x: 0, y: -1, z: 1 } }
};
const cameraOptions = [
{ value: 'Front', label: 'Front View' },
{ value: 'Back', label: 'Back View' },
{ value: 'Right', label: 'Right View' },
{ value: 'Left', label: 'Left View' },
{ value: 'Top', label: 'Top View' },
{ value: 'Bottom', label: 'Bottom View' }
];
const cuttingPlaneOptions = [
{ value: 'X', label: 'X-axis' },
{ value: 'Y', label: 'Y-axis' },
{ value: 'Z', label: 'Z-axis' },
{ value: 'FaceNormal', label: 'Face Normal' }
];
const drawModeOptions = [
{
value: 'Shaded', label: "Shaded"
},
{
value: 'HiddenLine', label: "Hidden Line"
},
{
value: 'Wireframe', label: "Wireframe"
},
{
value: 'XRay', label: "X Ray"
}
];
const buttonStyle = {
backgroundColor: 'rgba(255, 255, 255, 0.50)',
border: '1px solid rgba(156, 163, 175, 0.5)',
borderRadius: '0.375rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '40px',
height: '40px'
};
const HoopsViewer = () => {
const [hwv, setHwv] = useState(null);
const [explodeMagnitude, setExplodeMagnitude] = useState(0);
const [showExplode, setShowExplode] = useState(false);
const [drawMode, setDrawMode] = useState({ value: 'Shaded', label: "Shaded" });
const [cameraView, setCameraView] = useState('Back');
const [cuttingPlanes, setCuttingPlanes] = useState([]);
useEffect(() => {
const script = document.createElement("script");
script.src = "/js/hoops/hoops_web_viewer.js";
script.onload = () => initViewer();
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
hwv?.shutdown();
};
}, []);
const initViewer = () => {
const hwvInstance = new window.Communicator.WebViewer({
containerId: "viewer",
endpointUri: "/data/microengine.scs",
});
try {
hwvInstance.start();
setHwv(hwvInstance);
initNavCube(hwvInstance);
} catch (error) {
console.error("Failed to start HWV", error);
}
};
const initNavCube = (hwvInstance) => {
const navCube = hwvInstance.view.getNavCube();
navCube.setAnchor(window.Communicator.OverlayAnchor.TopRight);
navCube.enable();
};
const addCuttingPlane = async (type) => {
if (!hwv || cuttingPlanes.length >= 6) return;
try {
const bounding = await hwv.model.getModelBounding(true, true);
let axis, normal, position;
switch (type) {
case 'X':
axis = Communicator.Axis.X;
normal = new Communicator.Point3(1, 0, 0);
position = bounding.min;
break;
case 'Y':
axis = Communicator.Axis.Y;
normal = new Communicator.Point3(0, 1, 0);
position = bounding.min;
break;
case 'Z':
axis = Communicator.Axis.Z;
normal = new Communicator.Point3(0, 0, 1);
position = bounding.min;
break;
case 'FaceNormal':
normal = new Communicator.Point3(0, 0, 1);
position = bounding.center();
break;
default:
console.error("Invalid plane type selected");
return;
}
const referenceGeometry = hwv.cuttingManager.createReferenceGeometryFromAxis(Communicator.Axis.X, bounding);
const plane = Communicator.Plane.createFromPointAndNormal(position, normal);
const cuttingSection = hwv.cuttingManager.getCuttingSection(0);
await cuttingSection.addPlane(plane, referenceGeometry);
await cuttingSection.activate();
setCuttingPlanes([...cuttingPlanes, { type, plane }]);
} catch (error) {
console.error("Error adding cutting plane:", error);
}
};
const handleCuttingPlaneChange = (selectedOption) => {
addCuttingPlane(selectedOption.value);
};
const resetCamera = () => {
hwv.view.resetCamera(750);
};
const handleChangeDrawMode = (selectedOption) => {
setDrawMode(selectedOption);
hwv.view.setDrawMode(Communicator.DrawMode[selectedOption.value]);
};
const handleChangeCameraView = (selectedOption) => {
setCameraView(selectedOption.value);
const config = cameraPositions[selectedOption.value];
const newCamera = new Communicator.Camera();
newCamera.setPosition(new Communicator.Point3(config.position.x, config.position.y, config.position.z));
newCamera.setTarget(new Communicator.Point3(config.target.x, config.target.y, config.target.z));
newCamera.setUp(new Communicator.Point3(config.up.x, config.up.y, config.up.z));
const aspectRatio = hwv.view.getCanvasSize().x / hwv.view.getCanvasSize().y;
const baseSize = 200;
newCamera.setWidth(baseSize * aspectRatio);
newCamera.setHeight(baseSize);
hwv.view.setCamera(newCamera, 750);
};
const handleChangeExplodeMagnitude = (event) => {
const newMagnitude = Number(event.target.value);
setExplodeMagnitude(newMagnitude);
if (hwv) {
hwv.getExplodeManager().setMagnitude(newMagnitude / 100);
}
};
const createSnapshotConfig = () => {
const config = new Communicator.SnapshotConfig();
config.width = 1920;
config.height = 1080;
config.layers = Communicator.SnapshotLayer.All;
return config;
};
const takeSnapshot = () => {
if (!hwv) {
console.error("HWV instance not initialized");
return;
}
const snapshotConfig = createSnapshotConfig();
hwv.takeSnapshot(snapshotConfig).then((result) => {
if (result instanceof Blob) {
const url = URL.createObjectURL(result);
const link = document.createElement('a');
link.href = url;
link.download = "snapshot.png";
link.click();
URL.revokeObjectURL(url);
} else if (result instanceof HTMLImageElement) {
const src = result.src;
const link = document.createElement('a');
link.href = src;
link.download = "snapshot.png";
link.click();
} else {
console.error("Snapshot did not return a valid result:", result);
}
}).catch((error) => {
console.error("Error taking snapshot:", error);
});
};
const handleOperationChange = (selectedOption) => {
if (!hwv) {
console.error("HWV instance not initialized");
return;
}
const operatorManager = hwv.operatorManager;
operatorManager.pop();
let operatorId;
switch (selectedOption.value) {
case 'Orbit':
operatorId = Communicator.OperatorId.Orbit;
break;
case 'Turntable':
operatorId = Communicator.OperatorId.Turntable;
break;
default:
console.error("Unsupported operation:", selectedOption.value);
return;
}
if (operatorId !== undefined) {
operatorManager.push(operatorId);
} else {
console.error("Invalid operator ID for", selectedOption.value);
}
};
return (
<div className="flex h-full w-full relative h-screen">
<div id="viewer" className="absolute top-0 left-0 w-full h-full" />
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 flex items-center">
<Tooltip text={"Reset camera"} position="left">
<div style={{ ...buttonStyle, marginRight: '10px' }} onClick={resetCamera}>
<HomeIcon className="w-4 h-4 text-black" />
</div>
</Tooltip>
<Tooltip text="Take Snapshot" position="left">
<div style={{ ...buttonStyle }} onClick={takeSnapshot}>
<span>πΈ</span>
</div>
</Tooltip>
<Tooltip text="Camera Operation" position="left">
<Select
placeholder="Ca.."
onChange={handleOperationChange}
options={operatorOptions}
menuPlacement="top"
styles={{
control: (base) => ({
...base,
minWidth: 80,
marginLeft: "10px",
cursor: "pointer",
marginRight: "10px",
backgroundColor: 'rgba(255, 255, 255, 0.50)',
borderColor: 'rgba(156, 163, 175, 0.5)',
boxShadow: 'none',
'&:hover': {
borderColor: 'rgba(156, 163, 175, 0.5)'
}
})
}}
/>
</Tooltip>
<Tooltip text="Camera menu" position="left">
<Select
value={cameraOptions.find(option => option.value === cameraView)}
onChange={handleChangeCameraView}
options={cameraOptions}
menuPlacement="top"
styles={{
control: (base) => ({
...base,
marginRight: "10px",
cursor: "pointer",
marginLeft: "10px",
minWidth: 80,
backgroundColor: 'rgba(255, 255, 255, 0.50)',
borderColor: 'rgba(156, 163, 175, 0.5)',
boxShadow: 'none',
'&:hover': {
borderColor: 'rgba(156, 163, 175, 0.5)'
}
})
}}
/>
</Tooltip>
<Tooltip text="Draw mode" position="left">
<Select
value={drawMode}
onChange={handleChangeDrawMode}
options={drawModeOptions}
menuPlacement="top"
styles={{
control: (base) => ({
...base,
minWidth: 80,
cursor: "pointer",
marginRight: "10px",
backgroundColor: 'rgba(255, 255, 255, 0.50)',
borderColor: 'rgba(156, 163, 175, 0.5)',
boxShadow: 'none',
'&:hover': {
borderColor: 'rgba(156, 163, 175, 0.5)'
}
})
}}
/>
</Tooltip>
<Tooltip text="Explode Menu" position="left">
<div style={{ ...buttonStyle, position: 'relative', marginRight: "10px" }} onClick={() => setShowExplode(!showExplode)}>
<ExplodeCubeIcon className="w-4 h-4" />
{showExplode && (
<input
type="range"
min="0"
max="100"
value={explodeMagnitude}
onChange={handleChangeExplodeMagnitude}
className="slider-vertical"
/>
)}
</div>
</Tooltip>
<Tooltip text="Cutting Plane Menu" position="left">
<Select
placeholder="Cu.."
onChange={handleCuttingPlaneChange}
options={cuttingPlaneOptions}
menuPlacement="top"
styles={{
control: (base) => ({
...base,
minWidth: 80,
cursor: "pointer",
backgroundColor: 'rgba(255, 255, 255, 0.50)',
borderColor: 'rgba(156, 163, 175, 0.5)',
boxShadow: 'none',
'&:hover': {
borderColor: 'rgba(156, 163, 175, 0.5)'
}
})
}}
/>
</Tooltip>
</div>
{hwv && <ModelTree hwv={hwv} />}
{hwv && <LayerTree hwv={hwv} />}
</div>
);
};
export default HoopsViewer;
My problem here is that I canβt visualize a layer tree. My layers come out as an empty array. Does having layers depend on the file we provide? If yes is there a file in hoops web viewer installer that I can use as a mock to test the layer tree ? Thanks in advance !