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.
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)
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).