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>