├── .gitignore ├── README.md ├── example └── index.js ├── hello.html ├── package.json ├── solids.js ├── src ├── compilerPlugin.js ├── fetchPlugin.js ├── index.js └── viewerComponent.js ├── tutorial0.html ├── tutorial0.js ├── tutorial1.html ├── tutorial2.html ├── tutorial3.html ├── tutorial4.html ├── tutorial5.html └── tutorial6.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .DS_Store 4 | .nyc_output 5 | package-lock.json 6 | *.log 7 | *.swp 8 | *~ 9 | dist/ 10 | docs/ 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-components 2 | 3 | VUE Components for rendering and compiling JSCAD designs 4 | 5 | ## ALPHA ALPHA ALPHA 6 | ## ALPHA ALPHA ALPHA 7 | 8 | ## Table of Contents 9 | - [Usage](#usage) 10 | - [License](#license) 11 | 12 | ## Usage 13 | 14 | See the various tutorials on how to assemble the components. 15 | Each tutorial has comments about flows and usage, and should be self explanatory. 16 | 17 | The next steps require [NPM](https://www.npmjs.com/) and [Node.js](https://nodejs.org). 18 | This project always develops with the latest LTS releases, so install those versions. 19 | 20 | Be sure to run these commands before running the tutorials. 21 | ``` 22 | npm install 23 | npm run build 24 | ``` 25 | 26 | ### Tutorials 27 | - Hello - View some JSCAD solids 28 | - Tutorial 0 - Load and view a JSCAD design (kind of) from a file 29 | - Tutorial 1 - Choose a JSCAD design from a single file, then compile and view 30 | - Tutorial 2 - Choose a JSCAD project from a directory, then compile and view 31 | - Tutorial 3 - Drag and Drop any JSCAD design (file or directory), then compile and view 32 | - Tutorial 4 - Fetch a JSCAD design (file) from a remote site (URL), then compile and view 33 | - Tutorial 5 - Edit a JSCAD design on line, then compile and view 34 | - Tutorial 6 - Change a parameter for a JSCAD design, then compile and view 35 | 36 | ## License 37 | 38 | [The MIT License (MIT)](./LICENSE) 39 | (unless specified otherwise) 40 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const { booleans, colors, extrusions, geometries, primitives, transforms } = require('@jscad/modeling') 2 | 3 | // functions that this design uses 4 | const { circle, cuboid, cylinder, rectangle, roundedCylinder, roundedRectangle, sphere } = primitives 5 | 6 | const { intersect, subtract, union } = booleans 7 | 8 | const { rotate, translate } = transforms 9 | 10 | const { extrudeLinear } = extrusions 11 | 12 | const { geom3 } = geometries 13 | 14 | // parameter definitions 15 | const getParameterDefinitions = () => { 16 | return [ 17 | { name: 'board', type: 'group', initial: 0, caption: 'Raspberry PI:' }, 18 | { name: 'board_v', type: 'checkbox', checked: true, caption: 'View?' }, 19 | { name: 'base', type: 'group', initial: 0, caption: 'Base:' }, 20 | { name: 'case_b_v', type: 'checkbox', checked: true, caption: 'View?' }, 21 | { name: 'cover', type: 'group', initial: 0, caption: 'Cover:' }, 22 | { name: 'case_c_v', type: 'checkbox', checked: true, caption: 'View?' }, 23 | { name: 'case_c_gap', type: 'int', initial: 2.0, caption: 'Rim Vent (mm)?', step: 1, min: 0 }, 24 | { name: 'case_c_dim', type: 'checkbox', checked: true, caption: 'Expose DIM Slot?' }, 25 | { name: 'case_c_led', type: 'checkbox', checked: true, caption: 'Expose LEDs?' }, 26 | { name: 'case_c_net', type: 'checkbox', checked: true, caption: 'Expose Network Port?' }, 27 | { name: 'case_c_usb1', type: 'checkbox', checked: true, caption: 'Expose USB Bank 1?' }, 28 | { name: 'case_c_usb2', type: 'checkbox', checked: true, caption: 'Expose USB Bank 2?' }, 29 | { name: 'case_c_add_s', type: 'checkbox', checked: true, caption: 'Addition Support?' }, 30 | { name: 'others', type: 'group', initial: 0, caption: 'Others Settings:' }, 31 | { name: 'segments', type: 'int', initial: 36, caption: 'Segments?' }, 32 | ] 33 | } 34 | 35 | // 36 | // A case for the Raspberry PI 2 computer boards. 37 | // 38 | // By Jeff Gay 39 | // 40 | 41 | // 42 | // create a mockup of the Raspberry PI board 43 | // 44 | const createBoard = (p) => { 45 | // blank board 46 | let b = roundedRectangle({size: [p.pi_w_r * 2, p.pi_l_r * 2], roundRadius: 3.0, segments: p.segments}) 47 | // less mounting holes 48 | let h = circle({radius: p.pi_mh_ir, segments: p.segments}) 49 | let x = -p.pi_w_r + p.pi_m_x 50 | let y = -p.pi_l_r + p.pi_m_y 51 | b = subtract(b, translate([x, y], h)) 52 | y = y + 58.0 53 | b = subtract(b, translate([x, y], h)) 54 | x = x + 49.0 55 | b = subtract(b, translate([x, y], h)) 56 | y = y - 58.0 57 | b = subtract(b, translate([x, y], h)) 58 | b = extrudeLinear({height: p.pi_t_r * 2}, b) 59 | b = translate([0 , -p.pi_usb_e_r, p.case_b_t + p.case_b_h], b) 60 | b = colors.colorize(colors.colorNameToRgb('goldenrod'), b) 61 | return b 62 | } 63 | 64 | const makeRow = (h, p) => { 65 | let i = 0 66 | let x = 0 67 | let y = 0 68 | let c = circle({radius: p.case_b_v_h, segments: p.segments}) 69 | let holes = [c] 70 | for (i = 1; i < h; i++) { 71 | y = y + (p.case_b_v_h * 2) + p.case_b_v_g 72 | holes.push(translate([x, y], c)) 73 | } 74 | return translate([0, -(y / 2)], holes) 75 | } 76 | 77 | const createVentHoles = (p) => { 78 | let h = p.case_b_v_n 79 | let rows = [] 80 | let x = 0 81 | for (let i = 0; i < p.case_b_v_r; i = i + 2) { 82 | let row = makeRow(h, p) 83 | let rowa = translate([x, 0], row) 84 | let rowb = translate([-x, 0], row) 85 | rows.push(...rowa) 86 | rows.push(...rowb) 87 | h = h - 1 88 | if (h === 0) break // for safety 89 | x = x + p.case_b_v_h + p.case_b_v_g 90 | } 91 | return extrudeLinear({height: p.case_b_t}, rows) 92 | } 93 | 94 | // 95 | // Create the base of the case 96 | // 97 | const createBase = (p) => { 98 | // calculate dimensions base on the Rasberry PI board 99 | let f = p.case_b_f 100 | let r = p.pi_c_r + f 101 | let w = p.pi_w_r + f 102 | let l = p.pi_l_r + p.pi_usb_e_r + f 103 | let d = p.case_b_t + p.case_b_h 104 | 105 | let b = roundedRectangle({size: [w * 2, l * 2], roundRadius: r, segments: p.segments}) 106 | b = extrudeLinear({height: d}, b) 107 | // less center for electronics and cooling 108 | let r2 = p.pi_c_r - p.case_b_w_t + f 109 | let w2 = p.pi_w_r - p.case_b_w_t + f 110 | let l2 = p.pi_l_r - p.case_b_w_t + f 111 | let d2 = p.case_b_h 112 | 113 | let c = roundedRectangle({size: [w2 * 2, l2 * 2], roundRadius: r2, segments: p.segments}) 114 | c = extrudeLinear({height: d}, c) 115 | c = translate([0, -p.pi_usb_e_r, p.case_b_t], c) 116 | b = subtract(b, c) 117 | // less vents (optional) 118 | b = subtract(b, transforms.translate([0, -p.pi_usb_e_r, 0], createVentHoles(p))) 119 | // less slot for SSD card 120 | let s = rectangle({size: [p.pi_sd_w_r * 2, l * 2]}) 121 | s = translate([0, -l], s) 122 | s = extrudeLinear({height: p.pi_sd_d_r * 2}, s) 123 | s = translate([0, 0, d - (p.pi_sd_d_r * 2)], s) 124 | b = subtract(b, s) 125 | // plus mounts 126 | let m = circle({radius: p.pi_mh_or, segments: p.segments}) 127 | m = extrudeLinear({height: d}, m) 128 | s = sphere({radius: p.pi_mh_ir, segments: p.segments}) 129 | s = translate([0, 0, d], s) 130 | m = union(m, s) 131 | 132 | let x = -p.pi_w_r + p.pi_m_x 133 | let y = -p.pi_l_r + p.pi_m_y - p.pi_usb_e_r 134 | b = union(b, translate([x, y], m)) 135 | y = y + 58.0 136 | b = union(b, translate([x, y], m)) 137 | x = x + 49.0 138 | b = union(b, translate([x, y], m)) 139 | y = y - 58.0 140 | b = union(b, translate([x, y], m)) 141 | // plus block under the middle USB ports 142 | let b_r = 6 / 2 143 | m = rectangle({size: [b_r * 2, b_r * 2]}) 144 | m = extrudeLinear({height: d}, m) 145 | x = 0 146 | y = p.pi_l_r - b_r - p.pi_usb_e_r 147 | b = union(b, translate([x, y], m)) 148 | // plus block under the audio jack 149 | x = (p.case_c_w_t + p.case_c_f) / 2 150 | r = p.pi_audio_r + p.case_c_f 151 | m = rectangle({size: [x * 2, r * 2]}) 152 | let z = d + (p.pi_t_r * 2) + (p.pi_audio_r) 153 | m = extrudeLinear({height: z - 1}, m) 154 | c = cylinder({height: x * 2, radius: r, segments: p.segments}) 155 | c = translate([0, 0, z], c) 156 | c = rotate([0, Math.PI / 2, 0], c) 157 | m = subtract(m, c) 158 | x = w + x 159 | y = -p.pi_l_r + p.pi_audio_o - p.pi_usb_e_r 160 | b = union(b, translate([x, y], m)) 161 | // plus block under the hdmi port 162 | x = (p.case_c_w_t + p.case_c_f) / 2 163 | r = p.pi_hdmi_r + p.case_c_f 164 | m = rectangle({size: [x * 2, r * 2]}) 165 | z = d + (p.pi_t_r * 2) 166 | m = extrudeLinear({height: z}, m) 167 | x = w + x 168 | y = -p.pi_l_r + p.pi_hdmi_o - p.pi_usb_e_r 169 | b = union(b, translate([x, y], m)) 170 | // plus block under the power port 171 | x = (p.case_c_w_t + p.case_c_f) / 2 172 | r = p.pi_pwr_r + p.case_c_f 173 | m = rectangle({size: [x * 2, r * 2]}) 174 | z = d + (p.pi_t_r * 2) 175 | m = extrudeLinear({height: z}, m) 176 | x = w + x 177 | y = -p.pi_l_r + p.pi_pwr_o - p.pi_usb_e_r 178 | b = union(b, translate([x, y], m)) 179 | // plus clips 180 | z = p.clip_z 181 | let ci = roundedCylinder({height: z * 2, radius: p.clip_r, roundRadius: p.clip_r * 0.95, segments: p.segments}) 182 | let cor = ((p.clip_r / 2) + 1) / 2 183 | let co = rectangle({size: [cor * 2, (p.clip_r + 1) * 2]}) 184 | z = p.clip_z + p.clip_r + 1 185 | co = extrudeLinear({height: z}, co) 186 | x = -p.pi_w_r - p.case_b_f 187 | y = -p.pi_l_r + p.pi_m_y - p.pi_usb_e_r + 58 // located at the mount 188 | b = union(b, translate([x + cor, y], co)) 189 | b = subtract(b, translate([x, y], ci)) 190 | y = -p.pi_l_r + p.pi_pwr_o - p.pi_usb_e_r 191 | y = y + p.pi_pwr_r + ((p.pi_hdmi_o - p.pi_hdmi_r - p.pi_pwr_o - p.pi_pwr_r) / 2) 192 | b = union(b, translate([x + cor, y], co)) 193 | b = subtract(b, translate([x, y], ci)) 194 | x = p.pi_w_r + p.case_b_f 195 | b = union(b, translate([x - cor, y], co)) 196 | b = subtract(b, translate([x, y], ci)) 197 | y = -p.pi_l_r + p.pi_m_y - p.pi_usb_e_r + 58 // located at the mount 198 | b = union(b, translate([x - cor, y], co)) 199 | b = subtract(b, translate([x, y], ci)) 200 | 201 | b = colors.colorize(colors.colorNameToRgb('indianred'), b) 202 | 203 | return b 204 | } 205 | 206 | const createCover = (p) => { 207 | // calculate dimensions base on the Rasberry PI board 208 | let bf = p.case_b_f 209 | let r = p.pi_c_r + bf + p.case_c_w_t + p.case_c_f 210 | let w = p.pi_w_r + bf + p.case_c_w_t + p.case_c_f 211 | let l = p.pi_l_r + p.pi_usb_e_r + bf + p.case_c_w_t + p.case_c_f 212 | let d = p.case_c_t + p.case_c_h + p.case_c_gap + p.case_c_gap_s 213 | 214 | let c = roundedRectangle({size: [w * 2, l * 2], roundRadius: r, segments: p.segments}) 215 | c = extrudeLinear({height: d}, c) 216 | // less center for electronics and cooling 217 | let r2 = p.pi_c_r + bf + p.case_c_f 218 | let w2 = p.pi_w_r + bf + p.case_c_f 219 | let l2 = p.pi_l_r + p.pi_usb_e_r + bf + p.case_c_f 220 | let d2 = p.case_c_h + p.case_c_gap + p.case_c_gap_s 221 | 222 | let x, y, z, m 223 | let s = roundedRectangle({size: [w2 * 2, l2 * 2], roundRadius: r2, segments: p.segments}) 224 | s = extrudeLinear({height: d2}, s) 225 | c = subtract(c, s) 226 | // plus lip below the USB ports for strength (optional) 227 | if (p.case_c_add_s === true) { 228 | m = rectangle({size: [w * 2, p.pi_usb_e_r * 2]}) 229 | m = extrudeLinear({height: (p.pi_t_r * 2)}, m) 230 | z = p.case_b_t + p.case_b_h + p.case_c_f 231 | m = translate([0, l2 - p.pi_usb_e_r, z], m) 232 | m = intersect(m, s) 233 | c = union(c, m) 234 | } 235 | // less gaps around the rim for ventilation 236 | if (p.case_c_gap > 0) { 237 | // add support for the vent 238 | m = roundedRectangle({size: [w * 2, l * 2], roundRadius: r, segments: p.segments}) 239 | s = roundedRectangle({size: [(w2 - p.case_c_gap_s) * 2, (l2 - p.case_c_gap_s) * 2], roundRadius: r2, segments: p.segments}) 240 | m = subtract(m, s) 241 | 242 | x = p.pi_usb_r 243 | s = rectangle({size: [x * 2, l * 2]}) 244 | x = -w2 + r2 + p.pi_usb_r 245 | y = 0 246 | m = subtract(m, translate([x, y], s)) 247 | x = w2 - r2 - p.pi_usb_r 248 | m = subtract(m, translate([x, y], s)) 249 | let y2 = (l2 - r2 - ((5 * 3) / 2)) / 4 250 | s = rectangle({size: [w * 2, y2 * 2]}) 251 | x = 0 252 | y = -l2 + r2 + y2 253 | m = subtract(m, translate([x, y], s)) 254 | y = y + 5 + (y2 * 2) 255 | m = subtract(m, translate([x, y], s)) 256 | y = y + 5 + (y2 * 2) 257 | m = subtract(m, translate([x, y], s)) 258 | y = y + 5 + (y2 * 2) 259 | m = subtract(m, translate([x, y], s)) 260 | z = d - p.case_c_t - p.case_c_gap - p.case_c_gap_s 261 | s = extrudeLinear({height: p.case_c_gap + p.case_c_gap_s}, m) 262 | s = translate([0 , 0, z], s) 263 | c = union(c, s) 264 | // less the vent 265 | m = roundedRectangle({size: [w * 2.01, l * 2.01], roundRadius: r, segments: p.segments}) 266 | s = roundedRectangle({size: [w2 * 2, l2 * 2], roundRadius: r2, segments: p.segments}) 267 | m = subtract(m, s) 268 | z = d - p.case_c_t - p.case_c_gap 269 | s = extrudeLinear({height: p.case_c_gap}, m) 270 | s = translate([0, 0, z], s) 271 | c = subtract(c, s) 272 | } 273 | // less slot for sdd card (optional) 274 | if (p.case_c_dim === true) { 275 | z = p.case_b_t + p.case_b_h + p.case_c_f 276 | y = p.pi_l_r + p.pi_usb_e_r + bf 277 | s = rectangle({size: [p.pi_sd_w_r * 2, y * 2]}) 278 | s = translate([0, -y], s) 279 | s = extrudeLinear({height: z}, s) 280 | c = subtract(c, s) 281 | } 282 | // less holes for the LEDs 283 | if (p.case_c_led === true) { 284 | s = cylinder({height: p.case_c_w_t * 2, radius: p.case_c_l_r, segments: p.segments}) 285 | s = rotate([Math.PI / 2, 0, 0], s) 286 | x = -p.pi_w_r + 8.0 287 | y = -p.pi_l_r - p.pi_usb_e_r - p.case_c_w_t 288 | z = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + 0.5 289 | c = subtract(c, translate([x, y, z], s)) 290 | x = x + 3.5 291 | c = subtract(c, translate([x, y, z], s)) 292 | } 293 | // less slot for the audio jack 294 | r = p.pi_audio_r + p.case_c_f + p.case_c_f 295 | z = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + (p.pi_audio_r) 296 | m = rectangle({size: [p.case_c_w_t * 2, r * 2]}) 297 | m = extrudeLinear({height: z}, m) 298 | s = cylinder({height: p.case_c_w_t * 2, radius: r, segments: p.segments}) 299 | s = translate([0, 0, z], s) 300 | s = rotate([0, Math.PI / 2, 0], s) 301 | s = union(m, s) 302 | x = p.pi_w_r + bf + p.case_c_w_t 303 | y = -p.pi_l_r + p.pi_audio_o - p.pi_usb_e_r 304 | s = translate([x, y, 0], s) 305 | c = subtract(c, s) 306 | // less slot for the hdmi port 307 | x = p.case_c_w_t 308 | r = p.pi_hdmi_r + p.case_c_f + p.case_c_f 309 | m = rectangle({size: [x * 2, r * 2]}) 310 | z = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + p.pi_hdmi_h + p.case_c_f 311 | m = extrudeLinear({height: z}, m) 312 | x = p.pi_w_r + bf + p.case_c_w_t 313 | y = -p.pi_l_r + p.pi_hdmi_o - p.pi_usb_e_r 314 | c = subtract(c, translate([x, y, 0], m)) 315 | // less slot for the power port 316 | x = (p.case_c_w_t + p.case_c_f) / 2 317 | r = p.pi_pwr_r + p.case_c_f + p.case_c_f 318 | m = rectangle({size: [x * 2, r * 2]}) 319 | z = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + p.pi_pwr_h + p.case_c_f 320 | m = extrudeLinear({height: z}, m) 321 | x = p.pi_w_r + bf + x 322 | y = -p.pi_l_r + p.pi_pwr_o - p.pi_usb_e_r 323 | c = subtract(c, translate([x, y, 0], m)) 324 | // less hole for network port (optional) 325 | if (p.case_c_net === true) { 326 | y = (p.case_c_w_t + p.case_c_f) / 2 327 | x = p.pi_net_r - p.case_c_f // slightly smaller 328 | m = rectangle({size: [x * 2, y * 2]}) 329 | z = p.pi_net_h - (p.case_c_f * 2) // slightly smaller 330 | m = extrudeLinear({height: z}, m) 331 | x = p.pi_w_r - p.pi_net_o 332 | y = l - y 333 | z = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + p.case_c_f 334 | c = subtract(c, translate([x, y, z], m)) 335 | } 336 | // less holes for USB ports and supports (optional) 337 | s = rectangle({size: [1.0, (p.pi_usb_e_r * 3) * 2]}) 338 | x = d - p.case_b_t - p.case_b_h - (p.pi_t_r * 2) - p.case_c_f 339 | y = l2 - (p.pi_usb_e_r * 3) 340 | z = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + p.case_c_f 341 | s = extrudeLinear({height: x}, s) 342 | s = translate([0, y, z], s) 343 | y = (p.case_c_w_t + p.case_c_f) / 2 344 | x = p.pi_usb_r - p.case_c_f // slightly smaller 345 | m = rectangle({size: [x * 2, y * 2]}) 346 | z = (p.pi_usb_h_r * 2) - (p.case_c_f * 2) // slightly smaller 347 | m = extrudeLinear({height: z}, m) 348 | if (p.case_c_usb1 === true) { 349 | x = p.pi_w_r - p.pi_usb1_o 350 | y = l - ((p.case_c_w_t + p.case_c_f) / 2) 351 | z = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + p.case_c_f 352 | c = subtract(c, translate([x, y, z], m)) 353 | x = x + p.pi_usb_r + 1.0 + (1.0 / 2) 354 | c = union(c, translate([x, 0, 0], s)) 355 | } 356 | if (p.case_c_usb2 === true) { 357 | x = p.pi_w_r - p.pi_usb2_o 358 | y = l - ((p.case_c_w_t + p.case_c_f) / 2) 359 | z = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + p.case_c_f 360 | c = subtract(c, translate([x, y, z], m)) 361 | x = x + p.pi_usb_r + 1.0 + (1.0 / 2) 362 | c = union(c, translate([x, 0, 0], s)) 363 | } 364 | // plus clips and hold downs 365 | let cl = sphere({radius: p.clip_r, segments: p.segments}) 366 | z = (d - p.case_b_t - p.case_b_h - (p.pi_t_r * 2) - p.case_c_f) / 2 367 | let zhd = p.case_b_t + p.case_b_h + (p.pi_t_r * 2) + p.case_c_f + z 368 | let xhd = p.pi_mh_ir 369 | let hd = cuboid({size: [p.pi_mh_ir * 2, p.pi_mh_ir * 2, z * 2]}) 370 | x = -w2 371 | y = -p.pi_l_r + p.pi_m_y - p.pi_usb_e_r + 58 // located at the mount 372 | z = p.clip_z 373 | c = union(c, translate([x, y, z], cl)) 374 | let yhd = y 375 | c = union(c, translate([x + xhd, yhd, zhd], hd)) 376 | y = -p.pi_l_r + p.pi_pwr_o - p.pi_usb_e_r 377 | y = y + p.pi_pwr_r + ((p.pi_hdmi_o - p.pi_hdmi_r - p.pi_pwr_o - p.pi_pwr_r) / 2) 378 | c = union(c, translate([x, y, z], cl)) 379 | yhd = -p.pi_l_r - p.pi_usb_e_r + p.pi_m_y // located at the mount 380 | c = union(c, translate([x + xhd, yhd, zhd], hd)) 381 | x = w2 382 | c = union(c, translate([x, y, z], cl)) 383 | c = union(c, translate([x - xhd, yhd, zhd], hd)) 384 | y = -p.pi_l_r + p.pi_m_y - p.pi_usb_e_r + 58 // located at the mount 385 | c = union(c, translate([x, y, z], cl)) 386 | yhd = y 387 | c = union(c, translate([x - xhd, yhd, zhd], hd)) 388 | 389 | c = colors.colorize(colors.colorNameToRgb('forestgreen'), c) 390 | 391 | return c 392 | } 393 | 394 | 395 | const main = (p) => { 396 | // base dimensions 397 | p.case_b_w_t = 1.0 // base wall thickness (best 1.0) 398 | p.case_b_t = 1.5 // base bottom thickness (min 1.5) 399 | p.case_b_h = 3.0 // base height, above the base thickness (min 3.0) 400 | p.case_b_f = 0.25 // slack around the edges of the board 401 | 402 | p.case_b_v_h = 2.0 // vent holes 403 | p.case_b_v_g = 4.0 // gaps between vent holes 404 | p.case_b_v_n = 7 // number of vent holes (middle row) 405 | p.case_b_v_r = 5 // number of rows (1 + factor of 2) 406 | 407 | // cover dimensions 408 | p.case_c_w_t = 2.0 // cover wall thickness 409 | p.case_c_t = 1.5 // cover top thickness 410 | p.case_c_h = 23.0 // cover height, below the cover thickness (min 23) 411 | p.case_c_f = 0.25 // slack around the edges of the base 412 | p.case_c_l_r = 2.5/2 // hole in front of LEDs 413 | 414 | if (p.case_c_gap > 0) { 415 | p.case_c_gap_s = 1.5 // support 416 | } else { 417 | p.case_c_gap_s = 0 // no support 418 | } 419 | 420 | // clip dimensions 421 | p.clip_r = 2.5/2 // radius of clips 422 | p.clip_z = p.clip_r + 1.0 // position of clips 423 | 424 | // Raspberry PI 2 form factors 425 | p.pi_l_r = 85/2 426 | p.pi_w_r = 56/2 427 | p.pi_t_r = 1.35/2 428 | p.pi_c_r = 2.5 // corner radius, really 3.0 but board is different 429 | p.pi_mh_ir = 2.75/2 // M2.5 bolt holes 430 | p.pi_mh_or = 6.2/2 // spacers around the holes 431 | p.pi_m_x = 3.5 // inset of mount from corner 432 | p.pi_m_y = 3.5 // inset of mount from corner 433 | p.pi_sd_w_r = 14/2 // ssd card slot width 434 | p.pi_sd_d_r = 2/2 // ssd card slot depth 435 | p.pi_usb_e_r = 2.0/2 // extension of USB ports beyond the board 436 | p.pi_usb_h_r = 16.0/2 // height of USB ports above the board 437 | p.pi_usb1_o = 29.0 // USB ports (bank 1) offset 438 | p.pi_usb2_o = 47.0 // USB ports (bank 2) offset 439 | p.pi_usb_r = 15.0/2 // USB port width 440 | p.pi_audio_r = 6/2 // audio jack size 441 | p.pi_audio_o = 53.5 // audio jack offset 442 | p.pi_hdmi_r = 15/2 // hdmi port size 443 | p.pi_hdmi_o = 32.0 // hdmi port offset 444 | p.pi_hdmi_h = 6.0 // height of hdmi port above the board 445 | p.pi_pwr_r = 8/2 // power port size 446 | p.pi_pwr_o = 10.6 // power port offset 447 | p.pi_pwr_h = 3.0 // height of power port above the board 448 | p.pi_net_o = 10.25 // network port offset 449 | p.pi_net_h = 13.5 // height of network port above the board 450 | p.pi_net_r = 16.0/2 // network port width 451 | 452 | let objs = [] 453 | if (p.board_v === true) { 454 | objs.push( createBoard(p) ) 455 | } 456 | if (p.case_b_v === true) { 457 | objs.push( createBase(p) ) 458 | } 459 | if (p.case_c_v === true) { 460 | objs.push( createCover(p) ) 461 | } 462 | if (objs.length === 0) { 463 | objs.push( primitives.cube() ) // return something 464 | } 465 | 466 | return objs 467 | } 468 | 469 | module.exports = {main, getParameterDefinitions} 470 | -------------------------------------------------------------------------------- /hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSCAD Solids 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-components", 3 | "version": "0.0.0", 4 | "description": "VUE components for JSCAD", 5 | "repository": "https://github.com/jscad/OpenJSCAD.org/", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "build": "browserify src/index.js -g uglifyify -o dist/jscad-vue-components.js --standalone jscadVueComponents", 9 | "coverage": "nyc --all --reporter=html --reporter=text npm test", 10 | "test": "ava --verbose --timeout 2m 'tests/**/*.test.js'" 11 | }, 12 | "keywords": [ 13 | "jscad", 14 | "vue" 15 | ], 16 | "license": "MIT", 17 | "dependencies": { 18 | "@jscad/regl-renderer": "2.3.0", 19 | "@jscad/core": "2.3.6" 20 | }, 21 | "devDependencies": { 22 | "ava": "3.10.0", 23 | "nyc": "15.1.0", 24 | "browserify": "16.5.1", 25 | "uglifyify": "5.0.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /solids.js: -------------------------------------------------------------------------------- 1 | const solids = [{"polygons":[{"vertices":[[-11,-11,-11],[-11,-11,11],[-11,11,11],[-11,11,-11]],"plane":[-1,0,0,11]},{"vertices":[[11,-11,-11],[11,11,-11],[11,11,11],[11,-11,11]],"plane":[1,0,0,11]},{"vertices":[[-11,-11,-11],[11,-11,-11],[11,-11,11],[-11,-11,11]],"plane":[0,-1,0,11]},{"vertices":[[-11,11,-11],[-11,11,11],[11,11,11],[11,11,-11]],"plane":[0,1,0,11]},{"vertices":[[-11,-11,-11],[-11,11,-11],[11,11,-11],[11,-11,-11]],"plane":[0,0,-1,11]},{"vertices":[[-11,-11,11],[11,-11,11],[11,11,11],[-11,11,11]],"plane":[0,0,1,11]}],"isRetesselated":false,"transforms":[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],"color":[1,1,0,1]}] 2 | -------------------------------------------------------------------------------- /src/compilerPlugin.js: -------------------------------------------------------------------------------- 1 | const { web, evaluation } = require('@jscad/core') 2 | 3 | const compilerPlugin = (store) => { 4 | // called when the store is initialized 5 | store.subscribe((mutation, state) => { 6 | if (mutation.type === 'compile') { 7 | const defaults = { fileList: [], parameterValues: {} } 8 | const { fileList, parameterValues } = Object.assign({}, defaults, mutation.payload) 9 | 10 | // how to compile depends on the payload 11 | // console.log('fileList',fileList) 12 | // console.log('parameterValues',parameterValues) 13 | 14 | if (fileList.item && ('function' === typeof fileList.item)) { 15 | store.commit('setStatus',`processing (${fileList.length} files)...`) 16 | // convert FileList to array of File (from selection of files) 17 | const files = [] 18 | for (let i = 0; i < fileList.length; i++) { 19 | files.push(fileList.item ? fileList.item(i) : fileList[i]) 20 | } 21 | // compile 22 | compileFromFiles(store, files, parameterValues) 23 | } else if (Array.isArray(fileList) && fileList.length > 0 && fileList[0].filesystem) { 24 | // compile the list of FileSystem entries (from drag and drop of files) 25 | store.commit('setStatus',`processing (file system)...`) 26 | compileFromFiles(store, fileList, parameterValues) 27 | } else if (fileList.name && fileList.ext && fileList.source && fileList.fullPath) { 28 | store.commit('setStatus',`processing (fake file entry)...`) 29 | compileFromSource(store, fileList, parameterValues) 30 | } else { 31 | store.commit('setStatus',`error (Invalid source; should be FileList, Array[FileSystem], or Object)`) 32 | } 33 | } 34 | }) 35 | } 36 | 37 | const compileFromFiles = (store, files, parameterValues) => { 38 | // console.log('compileFromFiles',files) 39 | 40 | const handleParamsOrSolids = (crap, paramsOrSolids) => { 41 | if (paramsOrSolids.type === 'solids') { 42 | store.commit('setStatus','solids...') 43 | store.commit('setSolids', paramsOrSolids.solids) 44 | } 45 | } 46 | 47 | web.walkFileTree(files) 48 | .then((filesAndFolders) => { 49 | if (filesAndFolders.length === 1 && filesAndFolders[0].children && filesAndFolders[0].children.length === 1) { 50 | // single file entry 51 | const fileEntry = filesAndFolders[0].children[0] 52 | compileFromSource(store, fileEntry, parameterValues) 53 | } else { 54 | store.commit('setStatus','compiling...') 55 | const data = { filesAndFolders, serialize: false } 56 | const objects = evaluation.rebuildGeometry(data, handleParamsOrSolids) 57 | } 58 | }) 59 | .then(() => { 60 | store.commit('setStatus','done') 61 | }) 62 | .catch((error) => { 63 | store.commit('setStatus',`${error}`) 64 | }) 65 | } 66 | 67 | const compileFromSource = (store, fileEntry, parameterValues) => { 68 | store.commit('setStatus','compiling...') 69 | 70 | const handleParamsOrSolids = (crap, paramsOrSolids) => { 71 | if (paramsOrSolids.type === 'solids') { 72 | store.commit('setStatus','solids...') 73 | store.commit('setSolids', paramsOrSolids.solids) 74 | } 75 | } 76 | 77 | try { 78 | const data = { filesAndFolders: [ fileEntry ], parameterValues, serialize: false } 79 | const objects = evaluation.rebuildGeometry(data, handleParamsOrSolids) 80 | } catch (error) { 81 | store.commit('setStatus',`${error}`) 82 | } 83 | } 84 | 85 | module.exports = compilerPlugin 86 | -------------------------------------------------------------------------------- /src/fetchPlugin.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | const path = require('path') 3 | 4 | const { web, evaluation } = require('@jscad/core') 5 | 6 | const fetchPlugin = (store) => { 7 | // called when the store is initialized 8 | store.subscribe((mutation, state) => { 9 | if (mutation.type === 'fetch') { 10 | const remoteURL = mutation.payload 11 | 12 | store.commit('setStatus',`fetching (${remoteURL})...`) 13 | 14 | const onerror = (error) => { 15 | store.commit('setStatus',`error (${error})`) 16 | } 17 | const onsucess = (fileEntry) => { 18 | store.commit('setStatus',`fetched...`) 19 | store.commit('compile',{ fileList: fileEntry }) 20 | } 21 | 22 | // validate the URL 23 | try { 24 | const parts = new URL(remoteURL) 25 | fetchFile(remoteURL, onsucess, onerror) 26 | } catch (error) { 27 | store.commit('setStatus',`error (${error.toString()})`) 28 | } 29 | } 30 | }) 31 | } 32 | 33 | const getFileExtensionFromString = (input) => { 34 | if (input.indexOf('.') === -1) return undefined 35 | return (input.substring(input.lastIndexOf('.') + 1)).toLowerCase() 36 | } 37 | 38 | const fetchFile = (remoteURL, onsucess, onerror) => { 39 | const xhr = new XMLHttpRequest() 40 | 41 | xhr.onerror = () => { 42 | const error = new Error(`failed to load ${remoteURL} see console for more details`) 43 | onerror(error) 44 | } 45 | 46 | xhr.onload = (event) => { 47 | const source = event.currentTarget.responseText 48 | const status = event.target.status 49 | if (`${status}`.startsWith('4')) { 50 | const error = new Error(source) 51 | onerror(error) 52 | } else { 53 | const name = path.basename(remoteURL) 54 | const fullPath = `/${name}` // fake path for fake filesystem lookup 55 | const ext = getFileExtensionFromString(fullPath) 56 | const fileEntry = { name, ext, source, fullPath } 57 | onsucess(fileEntry) 58 | } 59 | } 60 | 61 | xhr.open('GET', remoteURL, true) 62 | //xhr.withCredentials = true 63 | xhr.send() 64 | } 65 | 66 | module.exports = fetchPlugin 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | viewerComponent: require('./viewerComponent'), 3 | compilerPlugin: require('./compilerPlugin'), 4 | fetchPlugin: require('./fetchPlugin') 5 | } 6 | -------------------------------------------------------------------------------- /src/viewerComponent.js: -------------------------------------------------------------------------------- 1 | const { prepareRender, drawCommands, cameras, controls, entitiesFromSolids } = require('@jscad/regl-renderer') 2 | 3 | const perspectiveCamera = cameras.perspective 4 | const orbitControls = controls.orbit 5 | 6 | let numberOfInstances = 0 7 | 8 | const rotateSpeed = 0.002 9 | const panSpeed = 0.75 10 | const zoomSpeed = 0.08 11 | 12 | /** 13 | * Set up the JSCAD renderer. 14 | * @param {DOM Element} containerElement - HTML element of which to add the renderer (canvas) 15 | * @param {Data} data - viewer data (value of options) 16 | * @return new JSCAD renderer (already running) 17 | */ 18 | const setupRenderer = (containerElement, data) => { 19 | const width = containerElement.clientWidth 20 | const height = containerElement.clientHeight 21 | 22 | // each viewer has state 23 | data.state = { 24 | // state to control rendering 25 | camera: {}, 26 | controls: {}, 27 | rotateDelta: [0, 0], 28 | panDelta: [0, 0], 29 | zoomDelta: 0, 30 | // state to track mouse 31 | mouse: { 32 | buttons: 0, 33 | shiftKey: false, 34 | isOrbiting: false, 35 | lastClick: 0, // ms 36 | lastZoom: 0 37 | } 38 | } 39 | 40 | // prepare the camera 41 | data.state.camera = Object.assign({}, perspectiveCamera.defaults) 42 | data.state.camera.position = [150, -180, 233] 43 | perspectiveCamera.setProjection(data.state.camera, data.state.camera, { width, height }) 44 | perspectiveCamera.update(data.state.camera, data.state.camera) 45 | 46 | // prepare the controls 47 | data.state.controls = orbitControls.defaults 48 | 49 | // prepare the renderer 50 | const setupOptions = { 51 | glOptions: { container: containerElement }, 52 | } 53 | const renderer = prepareRender(setupOptions) 54 | 55 | // assemble the options for rendering 56 | const gridOptions = { 57 | visuals: { 58 | drawCmd: 'drawGrid', 59 | show: data.gridOptions.show, 60 | color: data.gridOptions.color, 61 | subColor: data.gridOptions.subColor, 62 | fadeOut: data.gridOptions.fadeOut, 63 | transparent: data.gridOptions.transparent 64 | }, 65 | size: data.gridOptions.size, 66 | ticks: data.gridOptions.ticks 67 | } 68 | const axisOptions = { 69 | visuals: { 70 | drawCmd: 'drawAxis', 71 | show: data.axisOptions.show 72 | } 73 | } 74 | data.state.content = { 75 | // define the visual content 76 | camera: data.state.camera, 77 | drawCommands: { 78 | drawGrid: drawCommands.drawGrid, 79 | drawAxis: drawCommands.drawAxis, 80 | drawLines: drawCommands.drawLines, 81 | drawMesh: drawCommands.drawMesh 82 | }, 83 | entities: [ 84 | gridOptions, 85 | axisOptions 86 | //...solids 87 | ] 88 | } 89 | 90 | const doRotatePanZoom = (state) => { 91 | let { rotateDelta, panDelta, zoomDelta, controls, camera } = state 92 | if (rotateDelta[0] || rotateDelta[1]) { 93 | const updated = orbitControls.rotate({ controls, camera, speed: rotateSpeed }, rotateDelta) 94 | state.controls = { ...controls, ...updated.controls } 95 | state.rotateDelta[0] = 0 96 | state.rotateDelta[1] = 0 97 | } 98 | 99 | if (panDelta[0] || panDelta[1]) { 100 | const updated = orbitControls.pan({ controls, camera, speed: panSpeed }, panDelta) 101 | state.camera.position = updated.camera.position 102 | state.camera.target = updated.camera.target 103 | state.panDelta[0] = 0 104 | state.panDelta[1] = 0 105 | } 106 | 107 | if (zoomDelta) { 108 | if (Number.isFinite(zoomDelta)) { 109 | const updated = orbitControls.zoom({ controls, camera, speed: zoomSpeed }, zoomDelta) 110 | state.controls = { ...controls, ...updated.controls } 111 | } else { 112 | const entities = state.content.entities 113 | const updated = orbitControls.zoomToFit({ controls, camera, entities }) 114 | state.controls = { ...controls, ...updated.controls } 115 | } 116 | state.zoomDelta = 0 117 | } 118 | } 119 | 120 | // the heart of rendering, as themes, controls, etc change 121 | const updateAndRender = (timestamp) => { 122 | doRotatePanZoom(data.state) 123 | 124 | const updates = orbitControls.update({ controls: data.state.controls, camera: data.state.camera }) 125 | data.state.controls = { ...data.state.controls, ...updates.controls } 126 | data.state.camera.position = updates.camera.position 127 | perspectiveCamera.update(data.state.camera) 128 | 129 | renderer(data.state.content) 130 | window.requestAnimationFrame(updateAndRender) 131 | } 132 | window.requestAnimationFrame(updateAndRender) 133 | 134 | return renderer 135 | } 136 | 137 | /** 138 | * Update the state of the viewer, replacing the solids. 139 | * @param {State) state - current state of the viewer 140 | * @param {Array} solids - an array of JSCAD geometries (geom2, geom3, path2) 141 | */ 142 | const updateSolids = (state, solids) => { 143 | state.content.entities = state.content.entities.filter((entity) => !entity.type) // remove old solids 144 | let entities = entitiesFromSolids({}, solids) 145 | state.content.entities.push(...entities) 146 | 147 | const updated = orbitControls.zoomToFit({ controls: state.controls, camera: state.camera, entities }) 148 | state.controls = { ...state.controls, ...updated.controls } 149 | } 150 | 151 | const viewerComponent = { 152 | name: 'jscad-viewer', 153 | properties: { 154 | props: [ 'likes' ], 155 | data: function () { 156 | // options which can be changed 157 | return { 158 | gridOptions: { 159 | show: true, 160 | color: [0, 0, 0, 1], 161 | subColor: [0, 0, 1, 0.5], 162 | fadeOut: false, 163 | transparent: true, 164 | size: [100, 100], 165 | ticks: [100, 10] 166 | }, 167 | axisOptions: { 168 | show: true 169 | }, 170 | viewerOptions: { 171 | rotateSpeed: 0.002, 172 | zoomSpeed: 0.08, 173 | doubleClickSpeed: 300 // ms 174 | } 175 | } 176 | }, 177 | computed: { 178 | count () { 179 | const state = this._data.state 180 | if (state) { 181 | updateSolids(state, this.$store.state.solids) 182 | } 183 | // update the fake DOM entry 184 | return this.$store.state.count 185 | }, 186 | solids () { 187 | return this.$store.state.solids 188 | } 189 | }, 190 | methods: { 191 | // mouse event handling 192 | onMouseDown: function (event) { 193 | const state = this._data.state 194 | state.mouse.buttons = event.buttons 195 | state.mouse.shiftKey = event.shiftKey 196 | state.mouse.isOrbiting = true 197 | }, 198 | onMouseUp: function (event) { 199 | // handle double clicks 200 | const now = Date.now() 201 | const state = this._data.state 202 | if (state.mouse.lastClick) { 203 | const ms = now - state.mouse.lastClick 204 | if (ms < this.viewerOptions.doubleClickSpeed) { 205 | if (state.mouse.isOrbiting) { 206 | state.zoomDelta = Number.POSITIVE_INFINITY 207 | } 208 | } 209 | } 210 | state.mouse.lastClick = now 211 | // reset state 212 | state.mouse.buttons = 0 213 | state.mouse.shiftKey = false 214 | state.mouse.isOrbiting = false 215 | }, 216 | onMouseMove: function (event) { 217 | const state = this._data.state 218 | if (state.mouse.isOrbiting) { 219 | if (state.mouse.shiftKey) { 220 | state.panDelta[0] -= event.movementX 221 | state.panDelta[1] += event.movementY 222 | } else { 223 | state.rotateDelta[0] += event.movementX 224 | state.rotateDelta[1] -= event.movementY 225 | } 226 | } 227 | }, 228 | onScroll: function (event) { 229 | event.preventDefault() 230 | 231 | const state = this._data.state 232 | state.zoomDelta = event.deltaY 233 | } 234 | }, 235 | // VUE lifecycle additions 236 | created: function () { 237 | numberOfInstances++ 238 | this.id = numberOfInstances 239 | }, 240 | mounted: function () { 241 | this.$el.id = `viewer${this.id}` 242 | 243 | this.renderer = setupRenderer(this.$el, this.$data) 244 | }, 245 | // VUE HTML template for the viewer (and 3D canvas) 246 | template: '
' 247 | } 248 | } 249 | 250 | module.exports = viewerComponent 251 | -------------------------------------------------------------------------------- /tutorial0.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSCAD Design - Tutorial 0 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 |

Geometries: {{ count }}

29 |
30 |
31 | 32 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /tutorial0.js: -------------------------------------------------------------------------------- 1 | // UG! 'modeling' is cluttering GLOBAL name space 2 | const {primitives} = jscadModeling 3 | 4 | // UG! By not using the latest JSCAD library, fixes / enhancements could be missed 5 | // const {primitives} = require('@jscad/modeling') 6 | 7 | const main = () => { 8 | let geom1 = primitives.star({center: [30, 30], outerRadius: 10}) 9 | let geom2 = primitives.ellipsoid({radius: [30, 20, 10]}) 10 | 11 | return [geom1, geom2] 12 | } 13 | 14 | // UG! By removing these 'exports', this script cannot be reused within other JSCAD designs. 15 | // module.exports = {main} 16 | -------------------------------------------------------------------------------- /tutorial1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSCAD Design - Tutorial 1 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |

Geometries: {{ count }}

27 |

Status: {{ status }}

28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /tutorial2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSCAD Design - Tutorial 2 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |

Geometries: {{ count }}

27 |

Status: {{ status }}

28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /tutorial3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSCAD Design - Tutorial 3 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |

Geometries: {{ count }}

27 |

Status: {{ status }}

28 |
29 |
30 |

DRAG AND DROP ANY JSCAD DESIGN TO THIS PAGE

31 | 36 |
37 |
38 | 39 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /tutorial4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSCAD Design - Tutorial 4 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |

Geometries: {{ count }}

27 |

Status: {{ status }}

28 |
29 |
30 |

ENTER URL OF REMOTE JSCAD DESIGN

31 | 32 | 33 |

OR DRAG AND DROP ANY URL TO THIS PAGE

34 |
35 |
36 | 37 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /tutorial5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSCAD Design - Tutorial 5 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |

Geometries: {{ count }}

27 |

Status: {{ status }}

28 |
29 |
30 |

ENTER SOME TEXT FOR A JSCAD DESIGN

31 | 33 | 34 |
35 |
36 | 37 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /tutorial6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSCAD Design - Tutorial 6 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |

Geometries: {{ count }}

27 |

Status: {{ status }}

28 |
29 |
30 |

ENTER SOME TEXT FOR A JSCAD DESIGN

31 | 33 | 34 |

CHANGE THE PARAMETER

35 | 36 |
37 |
38 | 39 | 139 | 140 | 141 | 142 | --------------------------------------------------------------------------------