Implementing Polygon Resize Functionality

Hello TechSoft3D,

I’m currently working on implementing a polygon resize feature. I have created a polygon with a parent node and 6 child nodes (faces), as shown in the screenshot I’ve shared. The polygon is constructed using the code snippet below.

Now, I want to enable resize functionality where each face or node added a handle. When a user drags any of these handles, the polygon should resize in that specific direction only—adjusting the geometry accordingly.

Important Note:
I’m specifically not looking for a symmetrical scaling solution. I want directional resizing, where moving a single node or face affects only that part of the polygon.

Could you please let me know:

  1. Is this kind of directional resizing supported or possible to implement with your toolkit?
  2. If so, what would be the recommended approach to achieve this?
  3. Is it possible using setNodeMatrix API?

Looking forward to your guidance.

Code snippet -

function createBoxFaceMeshData(
pos: { x: number; y: number; z: number },
width: number,
height: number,
depth: number,
face: number
): MeshData {
const p0 = new Point3(pos.x, pos.y, pos.z);
const p1 = new Point3(pos.x + width, pos.y, pos.z);
const p2 = new Point3(pos.x + width, pos.y + height, pos.z);
const p3 = new Point3(pos.x, pos.y + height, pos.z);
const p4 = new Point3(pos.x, pos.y + height, pos.z + depth);
const p5 = new Point3(pos.x + width, pos.y + height, pos.z + depth);
const p6 = new Point3(pos.x + width, pos.y, pos.z + depth);
const p7 = new Point3(pos.x, pos.y, pos.z + depth);

let triangles: Point3[] = [];

// Determine which face to create based on the 'face' parameter
switch (face) {
  case 0: // Front face
    triangles = [p0, p2, p1, p0, p3, p2];
    break;
  case 1: // Top face
    triangles = [p2, p3, p4, p2, p4, p5];
    break;
  case 2: // Right face
    triangles = [p1, p2, p5, p1, p5, p6];
    break;
  case 3: // Left face
    triangles = [p0, p7, p4, p0, p4, p3];
    break;
  case 4: // Back face
    triangles = [p5, p4, p7, p5, p7, p6];
    break;
  case 5: // Bottom face
    triangles = [p0, p6, p7, p0, p1, p6];
    break;
  default:
    throw new Error("Invalid face number.");
}

const positions: number[] = [];
triangles.forEach((point) => {
  positions.push(point.x, point.y, point.z);
});

// Simplified UVs and normals (optional: you can refine these for proper mapping)
const uvs: number[] = new Array(triangles.length * 2).fill(0);
const normals: number[] = new Array(triangles.length * 3).fill(0);

const meshData = new MeshData();
meshData.addFaces(positions, normals, undefined, uvs);
meshData.setFaceWinding(FaceWinding.CounterClockwise);
return meshData;

}

async function createBoxWithParent(
position: { x: number; y: number; z: number },
width: number,
height: number,
depth: number,
faceColor: Color,
hwv: WebViewer
): Promise {
// Create an empty parent node
const parentNodeId = hwv.model.createNode(null, ‘box-parent’);

// Loop through each of the 6 faces
for (let face = 0; face < 6; face++) {
  const meshData = createBoxFaceMeshData(position, width, height, depth, face);
  const meshId = await hwv.model.createMesh(meshData);
  const meshInstanceData = new MeshInstanceData(meshId, new Matrix(), `face-${face}`);
  meshInstanceData.setOpacity(0.2);
  
  // Create the mesh instance as a child of the parent
  await hwv.model.createMeshInstance(meshInstanceData, parentNodeId);
}

// Return the parent node ID so it can be manipulated later
return parentNodeId;

}

const createBoundingBox = async (hwv: WebViewer): Promise<Box | null> => {
const selectionItems = gatherLeafNodeIds(hwv);
// hwv.lineManager.removeAllLines();
try {
// Get the bounding box for the selected nodes
const boundingBox = await hwv.model.getNodesBounding(selectionItems);
const { min, max } = boundingBox;

  const padding = 0.01; // or 0.1 depending on model scale

  // Expanded bounding box
  const expandedMin = {
    x: min.x - padding,
    y: min.y - padding,
    z: min.z - padding,
  };

  const expandedMax = {
    x: max.x + padding,
    y: max.y + padding,
    z: max.z + padding,
  };

  // const nodesInsideBoundingBox = await getNodesInsideBoundingBox(hwv, min, max);

  // Calculate position (center) and size (dimensions)
  const width = max.x - min.x;
  const height = max.y - min.y;
  const depth = max.z - min.z;

  const corner = new Point3(min.x, min.y, min.z);

  const boundingBoxNodeId = await createBoxWithParent(
    corner,
    width,
    height,
    depth,
    Color.createFromFloat(255 / 255, 0 / 255, 0 / 255),
    hwv,
  );


  var selectionManager = hwv.selectionManager;

  // Function to create a plane
  var createPlane = function (
    px: number,
    py: number,
    pz: number,
    nx: number,
    ny: number,
    nz: number,
  ) {
    var p = new Point3(px, py, pz);
    var n = new Point3(nx, ny, nz);
    return Plane.createFromPointAndNormal(p, n);
  };

  var planes = [
    createPlane(expandedMax.x, 0, 0, -1, 0, 0), // Right
    createPlane(expandedMin.x, 0, 0, +1, 0, 0), // Left
    createPlane(0, expandedMax.y, 0, 0, -1, 0), // Top
    createPlane(0, expandedMin.y, 0, 0, +1, 0), // Bottom
    createPlane(0, 0, expandedMax.z, 0, 0, -1), // Front
    createPlane(0, 0, expandedMin.z, 0, 0, +1), // Back
  ];
  

  // Create the configuration object
  var config = new IncrementalPickConfig();
  config.mustBeFullyContained = true;
  var center = new Point3(0, 0, 0);

  // Start the selection process
  async function runSelection() {
    try {
      // hwv.selectionManager.clear();
      const handle = await selectionManager.beginConvexPolyhedronSelection(
        planes,
        center,
        config,
      );

      const loop = async (stillProcessing: boolean): Promise<void> => {
        if (!stillProcessing) return;
        const next = await selectionManager.advanceIncrementalSelection(handle);
        return loop(next);
      };

      await loop(true);
      await selectionManager.endIncrementalSelection(handle);

      const partIds: number[] = [];
      selectionManager.each((selectionItem) => {
        partIds.push(selectionItem.getNodeId());
      });

      if (boundingBoxNodeId) {
        dispatch(modelActions.setBoundingBoxNodeId(boundingBoxNodeId));
        const nodeSelectionItem = Selection.SelectionItem.create(boundingBoxNodeId);
        hwv.selectionManager.set(nodeSelectionItem);
      }

      const nodes = await getNodesInsideBoundingBox(hwv, min, max, partIds);
      console.log("Nodes inside bb - ", nodes)
    } catch (error) {
      console.error('Error during selection process:', error);
    }
  }

  runSelection();

  // nodesInsideBoundingBox.forEach((nodeId) => {
  //   hwv.selectionManager.selectNode(nodeId, SelectionMode.Add)
  // })

  return boundingBox;
} catch (error) {
  console.error('Error retrieving bounding box:', error);
  return null;
}

};

Hello @cad-developer,

For the granular face-level control that you want to implement, you will need to modify the position of the vertices of the face in question based on the transformation applied through the handle operator.

Please see this post about iterating through the MeshData.faces.element object via a selection:

A more “complete” example of interacting with the mesh data is available as part of the quickstart module. To view this example, please do the following:

Thanks,
Tino