I have been developing a POC application to load a PDF into the Autodesk GUIViewer3D, and using a custom extension, draw PolygonPaths and PolylinePaths over the PDF. My extension is simple, it adds a toolbar and manages the active tools, so that only one Edit2d tool is active at a time. The toolbar buttons I have added essentially toggle or switch between them.
I have the measurement extension loaded and can set the calibration, this carries over to the Edit2d Extension and allows me to have the correct values when getting one of the drawn shapes, and doing getArea() with the measureTransform etc.
I want to add a new toolbar button to my extension that enables the following functionality:
When the viewer selection event is fired, giving me the dbid of a my selected line in the PDF, I retrieve the raw coords / vector data of that line and draw a Polyline shape programmatically using those coords.
Note that this is a rough idea of the setup. In reality I have the viewer bound in a class called ApsViewerManager and have an instance of that assigned to the window object. The ApsViewerManager supports loading from a PDF that is downloaded from another api, or loading from the APS OSS API. I will try strip things down to the bare minimum of what is needed for the "local" PDF loading.
My setup for loading a pdf into the viewer:
const url = "some-url" // in reality this is pointing to a get endpoint on an external api that returns the pdf content
const page = 1 // you can only load 1 pdf page at a time
const localManager = this; // this is an instance of the APSViewerManager
const options = {
accessToken: "",
useCredentials: false
env: 'Local'
}
await Autodesk.Viewing.Initializer(options, async function () {
const htmlDiv = document.querySelector(localManager.containerSelector);
localManager.viewer = new Autodesk.Viewing.GuiViewer3D(htmlDiv);
attachViewerEvents.call(localManager); // I have my events handling setup in a separate module called as if part of the APSViewerManager
const startedCode = localManager.viewer.start(null, options);
if (startedCode > 0) {
localManager.report_error('Failed to create Viewer', {startCode: startedCode});
return;
}
// setup the edit2d extension from 'Autodesk.Edit2d'
localManager.edit2d = await localManager.viewer.loadExtension('Autodesk.Edit2D', {enableArcs: true});
// setup the inhouse extension to add edit2d controls as toolbar buttons + inhouse functionality consuming edit2d
localManager.rlb2dTakeOff = await localManager.viewer.loadExtension(
'RLB2DTakeOffExtension',
{
manager: localManager,
filename: localManager.shapesFilename,
}
);
// setup for loading the pdf extension from 'Autodesk.PDF'
await localManager.viewer.loadExtension('Autodesk.PDF')
// URL parameter page will override value passed to loadModel
localManager.viewer.loadModel(url, {page: page});
});
The challenge is, While I have the dbId from the selection event, I am unable to query the property database using this Id.
const dbid = 2900 //the dbid for a wall I selected on the pdf - obtained from viewer selection event
const db = apsViewerManager.viewer.model.getPropertyDb();
db.getProperties2(
dbid,
function(x){
console.log("Promise resolved")
console.dir(x)
},
function(x){
console.log("Promise rejected")
console.dir(x)
}
)
The promise rejects, and I am left with the following object:
{
"err": undefined,
"instanceTree": null,
"maxTreeDepth": 0
}
I figured maybe my db was fragmented - I am not entirely sure what this means, but I do see properties in the db object full of “fragments”.
const frags = apsViewerManager.viewer.model.getFragmentList();
console.dir(frags);
I wanted to try the following script to retrieve the props from there:
I am not sure the exact source of this script combination but I see it was derived from this APS blog: https://aps.autodesk.com/blog/working-2d-and-3d-scenes-and-geometry-forge-viewer
viewer = apsViewerManager.viewer;
viewer.addEventListener(
Autodesk.Viewing.SELECTION_CHANGED_EVENT,
function () {
const tree = viewer.model.getInstanceTree();
const frags = viewer.model.getFragmentList();
function listFragmentProperties(fragId) {
console.log('Fragment ID:', fragId);
// Get IDs of all objects linked to this fragment
const objectIds = frags.getDbIds(fragId);
console.log('Linked object IDs:', objectIds);
// Get the fragment's world matrix
let matrix = new THREE.Matrix4();
frags.getWorldMatrix(fragId, matrix);
console.log('World matrix:', JSON.stringify(matrix));
// Get the fragment's world bounds
let bbox = new THREE.Box3();
frags.getWorldBounds(fragId, bbox);
console.log('World bounds:', JSON.stringify(bbox));
}
if (tree) { // Could be null if the tree hasn't been loaded yet
const selectedIds = viewer.getSelection();
for (const dbId of selectedIds) {
const fragIds = [];
tree.enumNodeFragments(
dbId,
listFragmentProperties,
false
);
}
}
else {
console.log("No tree!");
}
}
);
Unfortunately, the tree is always null, so my browser spits out "No tree!"
What approaches / hidden documentation secrets can I look at to progress further?
I had seen a thread somewhere stating that PDFs are always converted to Vectors, hence my ambition of getting some coords of the selected wall (line) in the PDF.
I finally have things working. When loading a PDF, the conversion to SVF happens in the browser, and it doesn't build an instance tree. The PDF Page is converted to vector meshes, which get processed in batches. Each batch has an Id, the fragment id.
The viewer.model has a function reverseMapDbIdFor2D which takes in the dbId you might have gotten from a selection, and can give you a new dbId. In my case the Id was always the same. Then with const svf = model.getData() result (I think it's an SVF object) you can access the svf.fragments.dbId2fragId property and pass in your dbId to get out the fragment Id that points to the geometry that is holding the mesh for that dbId.
const viewer;
const model = viewer.model;
const svf = model.getData();
const fragmentsList = this.model.getFragmentList();
const dbId = 1234;
const modelDbId = model.reverseMapDbIdFor2D[dbId];
const fragId = svf.fragments.dbId2fragId[modelDbId];
const vizmesh = fragmentsList.getVizmesh(fragId);
const geometry = vizmesh.geometry;
Once you have the geometry from the vizmesh, you can use the Autodesk.Viewing.Private.VertexBufferReader to enumerate over the geometry of an object. Objects are the grouped individual meshes like the segments making up an arc, or the segments of a polyline. These are grouped by the dbId but also have their own dbIds.. This part had me very lost and confused and I still do not understand it. I am hoping an Autodesk rep can come in and provide some clarification.
The VextexBufferReader.enumGeomsForObject() takes in the modelDbId you got earlier, aswell as an object with callbacks for the types of meshes it finds.
const use2dInstancing = !!(this.viewer?.impl?.use2dInstancing); // not sure if this is needed, false for pdfs that I have tested
const vertexBufferReader = new Autodesk.Viewing.Private.VertexBufferReader(geometry, use2dInstancing);
// holds primatives making up the geom for the selected element on the page
const collector = {
lines: [],
circularArcs: [],
ellipticalArcs: [],
texQuads: [],
triangles: []
};
const callbacks = {
onLineSegment: (x0, y0, x1, y1, vpId, width) => collector.lines.push({x0, y0, x1, y1, vpId, width}),
onCircularArc: (cx, cy, start, end, radius, vpId) => collector.circularArcs.push({ cx, cy, start, end, radius, vpId }),
onEllipticalArc: (cx, cy, start, end, major, minor, tilt, vpId) => collector.ellipticalArcs.push({ cx, cy, start, end, major, minor, tilt, vpId }),
onTexQuad: (centerX, centerY, width, height, rotation, vpId) => collector.texQuads.push({ centerX, centerY, width, height, rotation, vpId }),
onOneTriangle: (x1, y1, x2, y2, x3, y3, vpId) => collector.triangles.push({ x1, y1, x2, y2, x3, y3, vpId })
};
// goes through the vizmesh geometry for the modelDbId and populates the collector with coords making up the various types of primatives.
vetexBufferReader.enumGeomsForObject(modelDbId, callbacks);
From there you can filter any duplicated line segments, figure out which segments have verticies that match the tail of another segment to build up a chain of segments (polyline), and manually construct the various types of shapes.
For curves/arcs, you have things like the radius or the major/minor/tilt that you can crunch some math and figure out the params for shape.setBezierArc or shape.setEllipsiesArc.
Refer to Autodesk Viewer Developer Guide: Drawing Edit2d Shapes Manually for help on manually creating Edit2D shapes.
And a hint for using AI/LLMs to accomplish anything with geometry in the viewer:
Don't bother. Do the grunt work, dig into the devtools sources folder. These llm based tools cannot see into the sdk's internals, and they do not have enough training data to help. They will confidantly spit out nonsense and waste your time.