{"id":27372202,"url":"https://github.com/apprentice3d/modelbuilderdraftblog","last_synced_at":"2026-02-22T20:03:24.265Z","repository":{"id":81715260,"uuid":"256043287","full_name":"apprentice3d/ModelBuilderDraftBlog","owner":"apprentice3d","description":null,"archived":false,"fork":false,"pushed_at":"2020-04-17T16:54:53.000Z","size":5047,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-13T10:02:22.845Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/apprentice3d.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-04-15T21:43:01.000Z","updated_at":"2020-04-17T16:54:55.000Z","dependencies_parsed_at":null,"dependency_job_id":"56e7e252-aa2c-4538-85a2-5c06f0c0b205","html_url":"https://github.com/apprentice3d/ModelBuilderDraftBlog","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/apprentice3d/ModelBuilderDraftBlog","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apprentice3d%2FModelBuilderDraftBlog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apprentice3d%2FModelBuilderDraftBlog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apprentice3d%2FModelBuilderDraftBlog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apprentice3d%2FModelBuilderDraftBlog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/apprentice3d","download_url":"https://codeload.github.com/apprentice3d/ModelBuilderDraftBlog/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apprentice3d%2FModelBuilderDraftBlog/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281079587,"owners_count":26440321,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-26T02:00:06.575Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-04-13T09:53:29.764Z","updated_at":"2025-10-26T08:46:54.400Z","avatar_url":"https://github.com/apprentice3d.png","language":null,"readme":"## The Hitchhiker's Guide to ... ModelBuilder in Viewer\n\nOne of our mighty engineers pointed well that each time when somebody builds a model viewer, there will be always attempts to transform it into an editor. \nForge Viewer is not an exception and we always get questions in this context, where the most frequent one is about adding custom geometries.\n\nAdding custom geometry to the Viewer was addressed previously in some of our blog posts and there is even a [recommended](https://forge.autodesk.com/en/docs/viewer/v7/developers_guide/advanced_options/custom-geometry/) (read \"official way\") of doing it.\n\nIt is not a secret that Forge Viewer is based on three.js and so far, all approaches of adding custom geometry to the Viewer, exploit three.js layer (a layer below Forge Viewer). This explains why Forge Viewer is not aware of existence of such geometry and there is no interaction with it.\nTo be able to select a custom geometry added this way, you have to work at three.js level and implement selection by yourself and we illustrated in some of the blogposts how to solve this using [ray casting](https://forge.autodesk.com/blog/handling-custom-meshes-selection-along-model-components-forge-viewer).\n\nNevertheless, with all this work, any custom geometry still feels like a foster child and tools like Measuring or Sectioning will ignore their pretty face.\n\n\n### Intro\n\nFor this very reason, the engineering team developed ModelBuilder extension, which facilitates integration of custom geometry into Forge Viewer and make it play well with tools like Measuring and Sectioning.\n\nThe most simplest way of adding a custom geometry is the following:\n\n```javascript\n\nviewer.loadExtension(\"Autodesk.Viewing.SceneBuilder\").then(() =\u003e {\n    sceneBuilder = viewer.getExtension(\"Autodesk.Viewing.SceneBuilder\");\n\n    sceneBuilder.addNewModel({})\n        .then((modelBuilder) =\u003e {\n            let geom = new THREE.BufferGeometry().fromGeometry(\n            \t\t\t\t\t\t\tnew THREE.BoxGeometry(210,160,10));\n            let phongMaterial = new THREE.MeshPhongMaterial({\n                color: new THREE.Color(1, 0, 0)\n            });\n            mesh = new THREE.Mesh(geom, phongMaterial);\n            modelBuilder.addMesh(mesh);\n\n        });\n```\n\nHowever, there is more than just this and I invite you to explore it's features by going through a sample project of customizing a model of a corner shelve by adding custom geometry to it:  \n\n![](./img/plan.png)\nTODO: update screen with model using Mahogany appearence\n\n### Part I: Adding a side board as custom geometry\n\nHaving our simple model, if we add the above code into a our [SimpleCustomGeometry extension](https://sample-collection.s3.amazonaws.com/assets/js/extensions/SimpleCustomGeomExt.js) we will get the following: \n![](./img/01b.png)\n\nWith just this, we already can start working with the new geometry, it is selectable using Measuring tool we check the offset of new custom geometry to original component, for further position adjustments:\n![](./img/02b.png)\n\nFrom the measuring tool we see that we should move the newly added mesh to the right by 100 units (here I am not referring to Z-axis and Y-axis on purpose).\n\nAt this point, there are three approaches of adjusting the position of newly added custom geometry:\n\n1. #### Set position before adding mesh to the model builder:\n\n\t```javascript\n\t\n\t...\n\tmesh = new THREE.Mesh(geom, phongMaterial);\n\tmesh.matrix = new THREE.Matrix4().compose(\n\t\t\t        new THREE.Vector3(0, 0, -100),\n\t\t\t        new THREE.Quaternion(0, 0, 0, 1),\n\t\t\t        new THREE.Vector3(1, 1, 1)\n\t    );\n\tmodelBuilder.addMesh(mesh);\n\t\n\t...\n\t```\n\t\n\tIf we apply this approach in our extension, we will get the following result:\n\t![](./img/03.png)\n\tand we still see that we should have moved up our mesh by 85 units, but let us do it using another approach.\n\n\n2. #### Update the mesh position and then update the mesh:\n\n\tThis approach is ok in small doses, but if you will try to build an animation using this approach, this is wrong way to do it.\n\tThe idea is to to use ModelBuilder in-build method to replace the mesh and it works both, for changing mesh geometry, as well as changing mesh transforms:\n\t\n\t```javascript\n\t\n\t\t...\n\t\tmesh = new THREE.Mesh(geom, phongMaterial);\n\t\tmodelBuilder.addMesh(mesh);\n\t\t\n\t\tmesh.matrix.setPosition(new THREE.Vector3(0,85,-100));\n\t\tmodelBuilder.updateMesh(mesh);\n\t\t\n\t\t...\n\t\t\n\t```\n\t\t\n\tusing this new position adjustments, everything starts to look better:\n\t\t\n\t![](./img/03b.png)\n\t\n\tAs mentioned above, this method is very useful for changing the geometry, all that is need is to be taken care of is the transforms for the new geometry:\n\t\n\t```javascript\n\t\n\t...\n\tmesh = new THREE.Mesh(geom, phongMaterial);\n\tmodelBuilder.addMesh(mesh);\n\t\n\tmesh.geometry = new THREE.BufferGeometry().fromGeometry(\n\t\t\t\t\t\t\t\t\tnew THREE.CylinderGeometry( 105, 105, 10, 128 ));\n \tmesh.geometry.computeBoundingBox();\n \tmesh.matrix.makeRotationFromEuler(new THREE.Euler(Math.PI/2,0,0));\n \tmesh.matrix.setPosition(new THREE.Vector3(0,0,-100));               \n\tmodelBuilder.updateMesh(mesh);\n\t\n\t...\n\t```\n\t\n and here you have a new geometry for the side panel:\n\t\t\n ![](./img/03c.png)\n\n3. #### Set position after adding the mesh by transforming the related fragments:\n\t\n\tThis approach will look more complicated, but it is very powerful and its complexity can be easily abstracted.\n\tTo explain better this, let us understand first what data the ModelBuilder instance holds when we create it and add a mesh to it:\n\t\n\t![](./img/04.png)\n\t\n\tThere is no mention about any mesh here. The mesh object can be found \"deep\" inside, but Forge Viewer works differently (for performance reasons). When adding a mesh, the information is extracted and needed parts filled with it, out of which `fragments` are the most important to us now. If we want to transform our custom component, we will have to work with fragments. \n\tLater we will see that fragments are so important, that we can even add geometries directly through fragments (instead of adding geometries through meshes creation, as we did above). \n\t\n\t\n\tBefore that, if we check `geomList.geoms` from ModelBuilder instance, we can notice that this is the place were we can find all geometries added either through mesh creation or through fragments: \n\t\n\t![](./img/04b.png)\n\t\nThis important at this step, as one of the ways of getting the fragment ids associated with our component is through having the geometry.\nSince in our case we have just one geometry, this is simple:\n\n![](./img/04c.png)\n\nNow, knowing the fragmentID, transforming our component cannot be easier:\n\n```javascript\n\nlet new_matrix = new THREE.Matrix4().compose(\n                    new THREE.Vector3(0, 85, -100),\n                    new THREE.Quaternion(0, 0, 0, 1),\n                    new THREE.Vector3(1, 1, 1)\n                    );\n\nmodelBuilder.changeFragmentTransform(1, new_matrix);\n\n```\n\nwhere in my case `1` is the fragmentID.\n\nCompared with second approach, this should be the approach when experimenting with custom component animation, but ... I never told you that.\n\nOne final touch before we go further is to quickly change the material to one already available in the scene. In our case, it would be good to replace this basic red materials with the one that the initial component is using - a nice Mahogany material.\n\nFor that we will have to first find where it is stored, and many of you might already be familiar with `matman`:\n\n![](./img/05.png)\n\nWe can notice the materials for both our models, but we are interested in materials of the original model, in our case `model:1|mat:0`, which we can then assign to our custom component, again through use of mighty `fragments`:\n\n\n![](./img/05b.png)\n\ngiving as a nicer look:\n\n![](./img/05c.png)\n\nNicer, but not nice enough, because the geometry from our new components lacks proper UV data, but we will take care if it a bit later.\n\nA live illustration of the project at this stage can be found [here](https://sample-collection.s3.amazonaws.com/simple.html).\n\t\n\n### Part II: Add middle shelve as custom geometry from geometry of original shelve\n\nBring custom geometry is useful, but not always enough. In many cases, the needed geometry is already available in the scene as another \"native\" component and the challenge is to extract that geometry and bring it again as a custom component for further \"manipulations\".\n\nIn our case, it would be nice to \"clone\" the original shelve component and put it above our side panel. This way, this is the first step for a customizable shelve, where a potential customer can set the needed number of corner shelves.\n\nTo achieve this, we would need to master the `renderProxy`:\n\n![](./img/06.png)\n\nFrom above, we see that the workflow of getting the render proxy of a fragment is to:\n\n - get the id of needed component, \n - get the associated fragment ids (could be more than one), \n - get the render proxy for that very fragment. \n\nThe `RenderProxy` by itself is actually a Mesh, and for now, we are interested only in geometry it stores:\n\n![](./img/06b.png)\n\na bit unusual geometry structure, and now our mission (should you choose to accept it) is to translate the data from this geometry format into THREE.BufferGeometry format, which we can then use ModelBuilder extension to add custom component.\n\nWe have everything we need in `vb`, which stores position, normal, uv and index, in attributes we can see the itemOffset and itemSize and vbstride shows us the \"repeating chunk size\".\n\nAll what we need to do at this step is to extract this data (or at least vertex positions and the indices for face formation) and organize into way THREE.BufferGeometry structures it:\n\n![](./img/06c.png)\n\nThis is a quite daunting task, mainly because the original data is interleaved into a single array and you have to know how to carve from it the needed info.\n\nFortunately, there is a hidden shortcut created by the engineering team, kept in total secret, yet widely available to anyone, with code name `VertexEnumerator` and it can be found in the namespace with a name that should never draw nobody's attention: `Autodesk.Viewing.Private`\n\nSo, to illustrate the power of this tool, we will use it to extract the minimal needed data from renderProxy geometry and push it into THREE.Geometry:\n\n```javascript\n\nlet geom = new THREE.Geometry();\nlet renderProxy = this.viewer.impl.getRenderProxy(this.viewer.model, fragmentId);\n\nlet VE = Autodesk.Viewing.Private.VertexEnumerator;\n\nVE.enumMeshVertices(renderProxy.geometry, (v, i) =\u003e {\n            geom.vertices.push(new THREE.Vector3(v.x, v.y, v.z));\n        });\n\nVE.enumMeshIndices(renderProxy.geometry, (a, b, c) =\u003e {\n\t\t\t\t\tgeom.faces.push(new THREE.Face3(a, b, c))\n\t\t\t});\n\ngeom.computeFaceNormals();\n```\n\n\n Having a THREE.Geometry we can easily create a BufferGeometry out of it, put into a mesh along with default material and add this \"clone\" to the scene:\n \n```javascript\n\nlet mesh = new THREE.Mesh(\n        new THREE.BufferGeometry().fromGeometry(geom),\n        new THREE.MeshPhongMaterial({\n            color: new THREE.Color(1, 0, 0)\n        }));\n        \nmodelBuilder.addMesh(mesh);\n\n```\n\nand we succeeded ... sort of ...:\n\n![](./img/06d.png)\n\nThe \"cloned\" mesh is there (I've \"ghosted\" the original component), but the scale doesn't look right, which means that the final look is the result of taking the geometry and scaling it up, all this being done at renderProxy level, easy to confirm and identify the scale ratio by looking at it's world matrix:\n\n![](./img/06e.png)\n\nthus, the simplest way of fixing the scale, potential position and rotation, is to assign it to our newly created mesh and adjust position if needed:\n\n```javascript\n\nmesh.matrix = renderProxy.matrixWorld.clone();\nmesh.matrix.setPosition(new THREE.Vector3(0,140,0));\n\n```\n\nchange the material as we did before and here we go:\n\n![](./img/07.png)\n\n\n### Part III: Adding remaining geometry and fixing material UVs\n\nAfter previous two parts, adding remaining geometry should be a breeze:\n\nTODO: add code\n\nTODO: add screenshot\n\nHowever, a problem left unsolved is the proper mapping of side components.\nIf for the \"cloned\" shelve components, we managed to borrow the UV mapping, for a completely new components, this has to be done manually.\n\nA closer look at attributes of geometry associated with our component will disclose that \n\n ![](./img/08.png)\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapprentice3d%2Fmodelbuilderdraftblog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fapprentice3d%2Fmodelbuilderdraftblog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapprentice3d%2Fmodelbuilderdraftblog/lists"}