javascriptnext.jsp5.jsrhino3dsketching

JS libraries for product sketching


I need help finding a good JS library that can create a product sketch like this: Partition sketch. I am using Next.js, and are having toruble with most libraries since Next.js is using SSR and most graphical libraries run client side. I dont need to show the sketch (neccessarrily) to the user, it should just be a downloadble SVG file that are based on the users input from the webpage.

Idealy i want it to be a PDF, that can be send directly to a customer, with all the requireing information and the sketch. Is this possible?

I have tried messing around with Rhino3dm.js and P5.js, but both seem hard to implement with Next.js.


Solution

  • There is an p5.svg renderer you could try to essentially draw the same measurements/lines in p5, but render to SVG directly instead of canvas (completely bypassing the .3dm format). The library seems to lack updates recently, but you can start with an older version/example like this one and copy it/modify to prototype what you need.

    If .3dm is even better, you shouldn't need p5.js at all: the rhino3dm js library should handle both drawing lines/curves/text (if you have the input from the user) and save to .3dm.

    You can check out the SampleSketch2d live example. (source)

    rhino3dm example drawing 2d curves on a 2D viewport (with x axis in red, y axis in green and the typical rhino light gray background and dark gray square grid)

    You can run the same example bellow as well)

    const downloadButton = document.getElementById("downloadButton")
    downloadButton.onclick = download
    
    // global variables
    let _model = {
      // saved nurbs curves
      curves: [],
      // new nurbs curve
      points: null,
      // viewport for canvas
      viewport: null,
    }
    
    
    // wait for the rhino3dm web assembly to load asynchronously
    let rhino
    rhino3dm().then(async m => {
      console.log('Loaded rhino3dm.')
      rhino = m // global
      run()
    })
    /**/
    
    // initialize canvas and model
    function run() {
      let canvas = getCanvas()
      canvas.addEventListener('mousedown', onMouseDown)
      canvas.addEventListener('mousemove', onMouseMove)
      window.addEventListener('keyup', onKeyUp)
      _model.points = new rhino.Point3dList()
      _model.viewport = new rhino.ViewportInfo()
      _model.viewport.screenPort = [0, 0, canvas.clientWidth, canvas.clientHeight]
      _model.viewport.setFrustum(-30,30,-30,30,1,1000)
      draw()
    }
    
    function download() {
      if(_model.curves.length<1){
        console.log('no curves')
        return
      }
    
      let doc = new rhino.File3dm()
      for(let i=0; i<_model.curves.length;i++) {
        doc.objects().add(_model.curves[i], null)
      }
    
      let options = new rhino.File3dmWriteOptions()
      options.version = 7
      let buffer = doc.toByteArray(options)
      saveByteArray("sketch2d"+ options.version +".3dm", buffer)
      doc.delete()
    }
    
    function saveByteArray(fileName, byte) {
      let blob = new Blob([byte], {type: "application/octect-stream"})
      let link = document.createElement('a')
      link.href = window.URL.createObjectURL(blob)
      link.download = fileName
      link.click()
    }
    
    /* * * * * * * * * * * * * * * *  interaction   * * * * * * * * * * * * * * * */
    
    // handles mouse down events
    // adds a new control point at the location of the mouse
    function onMouseDown(event) {
      // get the location of the mouse on the canvas
      let [x,y] = getXY(event)
    
      // if this is a brand new curve, add the first control point
      if (_model.points.count === 0) {
        _model.points.add(x, y, 0)
      }
    
      // add a new control point that will be saved on the next mouse click
      // (the location of the previous control point is now frozen)
      _model.points.add(x, y, 0)
      draw()
    }
    
    // handles mouse move events
    // the last control point in the list follows the mouse
    function onMouseMove(event) {
      let index = _model.points.count - 1
      if (index >= 0) {
        let [x,y] = getXY(event)
        _model.points.set(index, [x, y, 0])
        draw()
      }
    }
    
    // handles key up events
    function onKeyUp( event ) {
      switch ( event.key ) {
        // when the enter key is pressed, save the new nurbs curve
        case "Enter":
          if (_model.points.count < 4) { // 3 pts (min.) + next pt
            console.error('Not enough points!')
          } else {
            // remove the last point in the list (a.k.a. next)
            let index = _model.points.count - 1
            _model.points.removeAt(index)
    
            // construct a curve from the points list
            let degree = _model.points.count - 1
            if (degree > 3)
              degree = 3
    
            // construct a nurbs curve
            // (first arg == true to create a closed periodic uniform curve)
            _model.curves.push(rhino.NurbsCurve.create(true, degree, _model.points))
          }
    
          // clear points list
          _model.points.clear()
          
          // enable download button
          downloadButton.disabled = false
          break
      }
      draw()
    }
    
    /* * * * * * * * * * * * * * * * *  helpers   * * * * * * * * * * * * * * * * */
    
    // gets the canvas
    function getCanvas() {
      return document.getElementById('canvas')
    }
    
    // gets the [x, y] location of the mouse in world coordinates
    function getXY(evt) {
      let canvas = getCanvas()
      let rect = canvas.getBoundingClientRect()
      let x = evt.clientX - rect.left
      let y = evt.clientY - rect.top
      let s2w = _model.viewport.getXform(rhino.CoordinateSystem.Screen, rhino.CoordinateSystem.World)
      let world_point = rhino.Point3d.transform([x,y,0], s2w)
      s2w.delete()
      return [world_point[0],world_point[1]]
    }
    
    /* * * * * * * * * * * * * * * * *  drawing   * * * * * * * * * * * * * * * * */
    
    // clears the canvas and draws the model
    // for some reason removing semicolons causes an error in this method
    function draw() {
      // get canvas' 2d context
      let canvas = getCanvas();
      let ctx = canvas.getContext('2d');
      let w2s = _model.viewport.getXform(rhino.CoordinateSystem.World, rhino.CoordinateSystem.Screen);
    
      // clear and draw a grid
      ctx.beginPath();
      ctx.lineWidth = 0.5;
      ctx.strokeStyle = 'rgb(130,130,130)';
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      for(let i=0; i<50; i+=1){
        let [x,y] = rhino.Point3d.transform([i,-50,0], w2s);
        let [x1,y1] = rhino.Point3d.transform([i,50,0], w2s);
        ctx.moveTo(x,y);
        ctx.lineTo(x1,y1);
        [x,y] = rhino.Point3d.transform([-i,-50,0], w2s);
        [x1,y1] = rhino.Point3d.transform([-i,50,0], w2s);
        ctx.moveTo(x,y);
        ctx.lineTo(x1,y1);
    
        [x,y] = rhino.Point3d.transform([-50, i, 0], w2s);
        [x1,y1] = rhino.Point3d.transform([50, i, 0], w2s);
        ctx.moveTo(x,y);
        ctx.lineTo(x1,y1);
        [x,y] = rhino.Point3d.transform([-50, -i, 0], w2s);
        [x1,y1] = rhino.Point3d.transform([50, -i, 0], w2s);
        ctx.moveTo(x,y);
        ctx.lineTo(x1,y1);
      }
      ctx.stroke();
    
      ctx.lineWidth = 2;
      ctx.strokeStyle = 'rgb(150,75,75)';
      let [x,y] = rhino.Point3d.transform([0,0,0], w2s);
      let [x1,y1] = rhino.Point3d.transform([50,0,0], w2s);
      ctx.beginPath();
      ctx.moveTo(x,y);
      ctx.lineTo(x1,y1);
      ctx.stroke();
      ctx.beginPath();
      ctx.strokeStyle = 'rgb(75,150,75)';
      [x1,y1] = rhino.Point3d.transform([0,50,0], w2s);
      ctx.moveTo(x,y);
      ctx.lineTo(x1,y1);
      ctx.stroke();
    
      // draw saved nurbs curves
      for (let i=0; i<_model.curves.length; i++)
        drawNurbsCurve(ctx, _model.curves[i], w2s);
    
      // create a temporary curve from the points and draw it
      if (_model.points !== null && _model.points.count > 0) {
        let degree = _model.points.count - 1;
        if (degree > 3)
          degree = 3;
        let curve = rhino.NurbsCurve.create(true, degree, _model.points);
        drawNurbsCurve(ctx, curve, w2s);
    
        // draw control polygon from the temp curve's control points
        //drawControlPolygon(ctx, curve.points());
        drawControlPolygon(ctx, _model.points);
    
        // delete the temp curve when we're done using it
        // (webassembly memory management isn't great)
        curve.delete();
      }
    
      w2s.delete();
    }
    
    // draws a nurbs curve
    function drawNurbsCurve(ctx, curve, w2s) {
      ctx.lineWidth = 1
      ctx.strokeStyle = 'black'
    
      const divisions = 200 // TODO: dynamic
      ctx.beginPath()
    
      let [t0,t1] = curve.domain
      let world_point = curve.pointAt(t0)
      let screen_point = rhino.Point3d.transform(world_point, w2s)
      ctx.moveTo(screen_point[0],screen_point[1])
      for (let j=1; j<=divisions; j++) {
        let t = t0 + j / divisions * (t1-t0)
        world_point = curve.pointAt(t)
        let screen_point = rhino.Point3d.transform(world_point, w2s)
        ctx.lineTo(screen_point[0],screen_point[1])
      }
      ctx.stroke()
    }
    
    // draws a control polygon
    function drawControlPolygon(ctx, points) {
      // draw dashed lines between control points
      ctx.strokestyle = 'darkgray'
      ctx.setLineDash([4,4])
      ctx.beginPath()
    
      let w2s = _model.viewport.getXform(rhino.CoordinateSystem.World, rhino.CoordinateSystem.Screen)
      for (let i=0; i<points.count; i++) {
        let world_point = points.get(i)
        let screen_point = rhino.Point3d.transform(world_point, w2s)
        if (0 === i)
          ctx.moveTo(screen_point[0], screen_point[1])
        else
          ctx.lineTo(screen_point[0], screen_point[1])
      }
      if( points.count > 2 ){
        let world_point = points.get(0)
        let screen_point = rhino.Point3d.transform(world_point, w2s)
        ctx.lineTo(screen_point[0], screen_point[1])
      }
    
      ctx.stroke()
    
      // draw control points
      ctx.setLineDash([])
      ctx.fillStyle = 'white'
      ctx.strokeStyle = 'black'
      for (let i=0; i<points.count; i++) {
        let world_point = points.get(i)
        let screen_point = rhino.Point3d.transform(world_point, w2s)
        let [x,y,z] = screen_point
        ctx.fillRect(x-1,y-1, 3, 3)
        ctx.strokeRect(x-2, y-2, 5, 5)
      }
      w2s.delete()
    }
    <div id="description">
      Click on the canvas to create a NURBS curve and hit <kbd>Enter</kbd> to save and start a new one!
    </div>
    <button id="downloadButton" disabled>Download</button> 
    <div>
      <canvas class="rhino3dm" id="canvas" width="500" height="500"></canvas>
    </div>
    <script src="https://unpkg.com/rhino3dm@7.15.0/rhino3dm.js" type="application/javascript"></script>

    In the above example the download button will be sanboxed, but running on a local webserver should be fine.

    (One final note, if for some reason you need to use p5 and rhino3dm (though performance may vary), you can access p5's drawingContext to get the the sketch's underlying Canvas render (when not using the SVG renderer) to draw the same as the above in a sketch's canvas).