Browse Source

use new 3d rendering in docs

Thomas Buck 3 weeks ago
parent
commit
57aca6a04d
9 changed files with 312 additions and 79 deletions
  1. 0
    3
      .gitmodules
  2. 1
    1
      README.md
  3. 74
    28
      docs/generate_docs.sh
  4. 0
    1
      docs/modelview
  5. 231
    0
      docs/src/js/3d.js
  6. 0
    1
      docs/src/js/modelview.js
  7. 1
    32
      docs/src/main_board_pcb.md
  8. 1
    1
      docs/src/main_board_sch.md
  9. 4
    12
      hardware/generate_plot.sh

+ 0
- 3
.gitmodules View File

@@ -1,6 +1,3 @@
1 1
 [submodule "docs/svg-pan-zoom"]
2 2
 	path = docs/svg-pan-zoom
3 3
 	url = https://github.com/WebSVG/svg-pan-zoom
4
-[submodule "docs/modelview"]
5
-	path = docs/modelview
6
-	url = https://github.com/jschobben/modelview

+ 1
- 1
README.md View File

@@ -13,7 +13,7 @@ This project is licensed under the [CERN Open Hardware Licence Version 2 - Stron
13 13
 
14 14
 The docs are built using [mdbook](https://github.com/rust-lang/mdBook), licensed as `MPL-2.0`.
15 15
 The PCB SVG files in the documentation are displayed using [svg-pan-zoom](https://github.com/bumbu/svg-pan-zoom), licensed as `BSD-2-Clause`.
16
-The 3D PCB files in the documentation are displayed using [modelview](https://github.com/jschobben/modelview), licensed as `MIT`.
16
+The 3D PCB files in the documentation are displayed using [three.js](hhttps://github.com/mrdoob/three.js), licensed as `MIT`.
17 17
 
18 18
 Please also take a look at the `README.md` files in the subfolders of this project for more info.
19 19
 

+ 74
- 28
docs/generate_docs.sh View File

@@ -26,41 +26,87 @@
26 26
 # | source.                                                                      |
27 27
 #  ------------------------------------------------------------------------------
28 28
 
29
+INSCH="../hardware/dispensy.kicad_sch"
30
+INPCB="../hardware/dispensy.kicad_pcb"
31
+
29 32
 cd "$(dirname "$0")"
30 33
 
31
-#echo "Generating plots"
34
+if [ "$1" = "build" ] ; then
35
+    echo "Generating plots"
36
+    ../hardware/generate_plot.sh
37
+    echo
38
+
39
+    echo "Generating fab"
40
+    ../hardware/generate_fab.sh
41
+    echo
42
+fi
43
+
32 44
 rm -rf src/plot
33
-#../hardware/generate_plot.sh
34 45
 cp -r ../hardware/plot src
35 46
 
36
-echo "Generating plot includes"
37
-rm -rf src/inc_dispensy_sch.md
38
-echo "<script src=\"js/svg-pan-zoom.js\" charset=\"UTF-8\"></script>" >> src/inc_dispensy_sch.md
39
-for f in `ls src/plot/dispensy.kicad_sch.svg/*.svg | sort -r`; do
40
-    file=`echo $f | sed 's:src/:./:g'`
41
-    name=`echo $f | sed 's:src/plot/dispensy.kicad_sch.svg/::g' | sed 's:.svg::g'`
42
-    echo $name
43
-    echo "<h2>$name</h2>" >> src/inc_dispensy_sch.md
44
-    echo "<div style=\"background-color: white; border: 1px solid black;\">" >> src/inc_dispensy_sch.md
45
-    echo "<embed type=\"image/svg+xml\" src=\"$file\" id=\"pz_$name\" style=\"width:100%;\"/>" >> src/inc_dispensy_sch.md
46
-    echo "<script>" >> src/inc_dispensy_sch.md
47
-    echo "document.getElementById('pz_$name').addEventListener('load', function(){" >> src/inc_dispensy_sch.md
48
-    echo "svgPanZoom(document.getElementById('pz_$name'), {controlIconsEnabled: true, minZoom: 1.0});" >> src/inc_dispensy_sch.md
49
-    echo "})" >> src/inc_dispensy_sch.md
50
-    echo "</script>" >> src/inc_dispensy_sch.md
51
-    echo "</div>" >> src/inc_dispensy_sch.md
52
-    echo >> src/inc_dispensy_sch.md
53
-    echo "[Direct link to \`$name\`]($file)." >> src/inc_dispensy_sch.md
54
-    echo >> src/inc_dispensy_sch.md
47
+for path in $INSCH
48
+do
49
+    IN=`echo $path | sed "s:../hardware/::g"`
50
+    o="src/inc_$IN.md"
51
+    echo "Include for $IN at $o"
52
+
53
+    rm -rf $o
54
+    echo "<script src=\"js/svg-pan-zoom.js\" charset=\"UTF-8\"></script>" >> $o
55
+
56
+    for f in `ls src/plot/$IN.svg/*.svg | sort -r`; do
57
+        file=`echo $f | sed 's:src/:./:g'`
58
+        name=`echo $f | sed "s:src/plot/$IN.svg/::g" | sed 's:.svg::g'`
59
+        echo "Sheet at $name"
60
+
61
+        echo "<h2>$name</h2>" >> $o
62
+        echo "<div style=\"background-color: white; border: 1px solid black;\">" >> $o
63
+        echo "<embed type=\"image/svg+xml\" src=\"$file\" id=\"pz_$name\" style=\"width:100%;\"/>" >> $o
64
+        echo "<script>" >> $o
65
+        echo "document.getElementById('pz_$name').addEventListener('load', function(){" >> $o
66
+        echo "svgPanZoom(document.getElementById('pz_$name'), {controlIconsEnabled: true, minZoom: 1.0});" >> $o
67
+        echo "})" >> $o
68
+        echo "</script>" >> $o
69
+        echo "</div>" >> $o
70
+        echo >> $o
71
+        echo "[Direct link to \`$name\`]($file)." >> $o
72
+        echo >> $o
73
+    done
74
+
75
+    echo
55 76
 done
56 77
 
57
-echo "Preparing modelview dependencies"
58
-cd modelview
59
-sed -i 's/info\.inner/info_elem\.inner/g' modelview.js
60
-./fetch_deps.sh
61
-cd ..
62
-rm -rf src/js/deps
63
-cp -r modelview/deps src/js
78
+plot_3d() {
79
+    echo '<script type="importmap">' >> $1
80
+    echo '    {' >> $1
81
+    echo '        "imports": {' >> $1
82
+    echo '            "three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",' >> $1
83
+    echo '            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/examples/jsm/"' >> $1
84
+    echo '        }' >> $1
85
+    echo '    }' >> $1
86
+    echo '</script>' >> $1
87
+    echo "<p>Status: \"<span id=\"3d_info_$2\">Preparing 3D model...</span>\"</p>" >> $1
88
+    echo "<div id=\"3d_viewer_$2\" style=\"width: 100%; height: 100%; background-color: white; border: 1px solid black; position: relative;\"></div>" >> $1
89
+    echo '<script type="module">' >> $1
90
+    echo "    var info = document.getElementById(\"3d_info_$2\");" >> $1
91
+    echo "    var view = document.getElementById(\"3d_viewer_$2\");" >> $1
92
+    echo '    view.style.height = Math.floor(view.clientWidth * 0.707) + "px";' >> $1
93
+    echo '    import { init_3d } from "./js/3d.js";' >> $1
94
+    echo "    init_3d(\"$3\", view, info, view.clientWidth, view.clientHeight);" >> $1
95
+    echo '</script>' >> $1
96
+}
97
+
98
+for path in $INPCB
99
+do
100
+    IN=`echo $path | sed "s:../hardware/::g"`
101
+    o="src/inc_$IN.md"
102
+    file="plot/$IN.wrl"
103
+    name=`echo $file | sed "s:plot/::g" | sed 's:.wrl::g'`
104
+    echo "Include for $IN at $o, $file, $name"
105
+    rm -rf $o
106
+
107
+    plot_3d $o $name $file
108
+done
109
+echo
64 110
 
65 111
 echo "Generating docs"
66 112
 if [ "$1" = "serve" ] ; then

+ 0
- 1
docs/modelview

@@ -1 +0,0 @@
1
-Subproject commit d3f2ffd2cd04e263b4db3910023b6c028bd0eb64

+ 231
- 0
docs/src/js/3d.js View File

@@ -0,0 +1,231 @@
1
+/*
2
+ * 3d.js
3
+ *
4
+ * Copyright (c) 2024 Thomas Buck (thomas@xythobuz.de)
5
+ *
6
+ * For the fitCameraToObject() function also:
7
+ * Copyright (c) 2024 Michal Jirků
8
+ * https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/
9
+ *
10
+ * For everything else:
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU General Public License as published by
14
+ * the Free Software Foundation, either version 3 of the License, or
15
+ * (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
+ * GNU General Public License for more details.
21
+ *
22
+ * See <http://www.gnu.org/licenses/>.
23
+ */
24
+
25
+import * as THREE from 'three';
26
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
27
+import { STLLoader } from 'three/addons/loaders/STLLoader.js'
28
+import { VRMLLoader } from 'three/addons/loaders/VRMLLoader.js';
29
+
30
+function fitCameraToObject(camera, object, offset, orbitControls, yOffset) {
31
+    const boundingBox = new THREE.Box3();
32
+    boundingBox.setFromObject( object );
33
+
34
+    var middle = new THREE.Vector3();
35
+    boundingBox.getCenter(middle);
36
+
37
+    var size = new THREE.Vector3();
38
+    boundingBox.getSize(size);
39
+
40
+    // figure out how to fit the box in the view:
41
+    // 1. figure out horizontal FOV (on non-1.0 aspects)
42
+    // 2. figure out distance from the object in X and Y planes
43
+    // 3. select the max distance (to fit both sides in)
44
+    //
45
+    // The reason is as follows:
46
+    //
47
+    // Imagine a bounding box (BB) is centered at (0,0,0).
48
+    // Camera has vertical FOV (camera.fov) and horizontal FOV
49
+    // (camera.fov scaled by aspect, see fovh below)
50
+    //
51
+    // Therefore if you want to put the entire object into the field of view,
52
+    // you have to compute the distance as: z/2 (half of Z size of the BB
53
+    // protruding towards us) plus for both X and Y size of BB you have to
54
+    // figure out the distance created by the appropriate FOV.
55
+    //
56
+    // The FOV is always a triangle:
57
+    //
58
+    //  (size/2)
59
+    // +--------+
60
+    // |       /
61
+    // |      /
62
+    // |     /
63
+    // | F° /
64
+    // |   /
65
+    // |  /
66
+    // | /
67
+    // |/
68
+    //
69
+    // F° is half of respective FOV, so to compute the distance (the length
70
+    // of the straight line) one has to: `size/2 / Math.tan(F)`.
71
+    //
72
+    // FTR, from https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
73
+    // the camera.fov is the vertical FOV.
74
+
75
+    const fov = camera.fov * ( Math.PI / 180 );
76
+    const fovh = 2*Math.atan(Math.tan(fov/2) * camera.aspect);
77
+    let dx = size.z / 2 + Math.abs( size.x / 2 / Math.tan( fovh / 2 ) );
78
+    let dy = size.z / 2 + Math.abs( size.y / 2 / Math.tan( fov / 2 ) );
79
+    let cameraZ = Math.max(dx, dy);
80
+
81
+    // offset the camera, if desired (to avoid filling the whole canvas)
82
+    if( offset !== undefined && offset !== 0 ) cameraZ *= offset;
83
+
84
+    camera.target = middle;
85
+    camera.position.set( middle.x, middle.y + yOffset * cameraZ, middle.z + cameraZ );
86
+
87
+    // set the far plane of the camera so that it easily encompasses the whole object
88
+    const minZ = boundingBox.min.z;
89
+    const cameraToFarEdge = ( minZ < 0 ) ? -minZ + cameraZ : cameraZ - minZ;
90
+
91
+    camera.far = cameraToFarEdge * 3;
92
+    camera.updateProjectionMatrix();
93
+
94
+    if ( orbitControls !== undefined ) {
95
+        // set camera to rotate around the center
96
+        orbitControls.target = middle;
97
+
98
+        // prevent camera from zooming out far enough to create far plane cutoff
99
+        orbitControls.maxDistance = cameraToFarEdge * 2;
100
+    }
101
+}
102
+
103
+export function init_3d(path, container, status, div_width, div_height) {
104
+    const width = div_width;
105
+    const height = div_height;
106
+
107
+    const scene = new THREE.Scene();
108
+    //scene.add(new THREE.AxesHelper(1));
109
+
110
+    const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
111
+
112
+    const renderer = new THREE.WebGLRenderer({ antialias: true });
113
+    renderer.setSize(width, height);
114
+
115
+    const controls = new OrbitControls(camera, renderer.domElement);
116
+    controls.enableDamping = true;
117
+    controls.autoRotate = true;
118
+
119
+    for (const i of [1, -1]) {
120
+        for (const j of [0, 1, 2]) {
121
+            const light = new THREE.DirectionalLight(0xffffff, 0.5);
122
+            light.position.set(i * (j == 0 ? 1 : 0),
123
+                                i * (j == 1 ? 1 : 0),
124
+                                i * (j == 2 ? 1 : 0));
125
+            scene.add(light);
126
+        }
127
+    }
128
+
129
+    if (path.endsWith(".stl")) {
130
+        const light_amb = new THREE.AmbientLight(0x424242);
131
+        scene.add(light_amb);
132
+
133
+        const material = new THREE.MeshStandardMaterial();
134
+        //material.roughness = 0.75;
135
+
136
+        const loader = new STLLoader();
137
+        loader.load(
138
+            path,
139
+            function (geometry) {
140
+                const mesh = new THREE.Mesh(geometry, material);
141
+                mesh.rotation.setFromVector3(new THREE.Vector3(-Math.PI / 2, 0, 0));
142
+                scene.add(mesh);
143
+                fitCameraToObject(camera, scene, 0, controls, 0);
144
+                controls.update();
145
+                status.textContent = "Loaded STL 100%";
146
+            },
147
+            (xhr) => {
148
+                const s = Math.floor((xhr.loaded / xhr.total) * 100) + '% loaded';
149
+                console.log(s);
150
+                status.textContent = s;
151
+            },
152
+            (error) => {
153
+                console.log(error);
154
+                status.textContent = error;
155
+            }
156
+        );
157
+    } else if (path.endsWith(".wrl")) {
158
+        const light_amb = new THREE.AmbientLight(0xffffff);
159
+        scene.add(light_amb);
160
+
161
+        const loader = new VRMLLoader();
162
+        loader.load(
163
+            path,
164
+            function (object) {
165
+                scene.add(object);
166
+                fitCameraToObject(camera, scene, 0, controls, 0);
167
+                controls.update();
168
+                status.textContent = "Loaded VRML 100%";
169
+            },
170
+            (xhr) => {
171
+                const s = Math.floor((xhr.loaded / xhr.total) * 100) + '% loaded';
172
+                console.log(s);
173
+                status.textContent = s;
174
+            },
175
+            (error) => {
176
+                console.log(error);
177
+                status.textContent = error;
178
+            }
179
+        );
180
+    } else {
181
+        const s = "error: unknown filetype for " + path;
182
+        console.log(s);
183
+        status.textContent = s;
184
+    }
185
+
186
+    camera.position.z = 50;
187
+    controls.update();
188
+
189
+    function animate() {
190
+        requestAnimationFrame(animate);
191
+        controls.update();
192
+        renderer.render(scene, camera);
193
+    }
194
+
195
+    animate();
196
+    status.textContent = "3D model ready!";
197
+
198
+    container.appendChild(renderer.domElement);
199
+
200
+    const div = document.createElement("div");
201
+    div.style.position = "absolute";
202
+    div.style.left = "5px";
203
+    div.style.top = "5px";
204
+    div.style.background = "white";
205
+    div.style.color = "black";
206
+    container.appendChild(div);
207
+
208
+    const chk_ar = document.createElement("input");
209
+    chk_ar.type = "checkbox";
210
+    chk_ar.checked = true;
211
+    chk_ar.addEventListener('change', function() {
212
+        controls.autoRotate = this.checked;
213
+    });
214
+
215
+    const div_ar = document.createElement("div");
216
+    div_ar.appendChild(chk_ar);
217
+    div_ar.appendChild(document.createTextNode("Auto-Rotate"));
218
+    div.appendChild(div_ar);
219
+
220
+    const btn_rst = document.createElement("input");
221
+    btn_rst.type = "button";
222
+    btn_rst.value = "Reset Camera";
223
+    btn_rst.addEventListener('click', function() {
224
+        fitCameraToObject(camera, scene, 0, controls, 0);
225
+        controls.update();
226
+    });
227
+
228
+    const div_rst = document.createElement("div");
229
+    div_rst.appendChild(btn_rst);
230
+    div.appendChild(div_rst);
231
+}

+ 0
- 1
docs/src/js/modelview.js View File

@@ -1 +0,0 @@
1
-../../modelview/modelview.js

+ 1
- 32
docs/src/main_board_pcb.md View File

@@ -50,35 +50,4 @@ You can also view the [Main-Board PCB layout as PDF](./plot/dispensy.kicad_pcb.p
50 50
 
51 51
 ## 3D PCB Model
52 52
 
53
-<p>Status: "<span id="3d_info">Preparing 3D model...</span>"</p>
54
-<div id="3d_viewer" style="width: 100%; height: 100%; background-color: white; border: 1px solid black;"></div>
55
-<script type="module">
56
-    var view = document.getElementById('3d_viewer');
57
-    view.style.height = (view.clientWidth * 0.707) + "px";
58
-    var info = document.getElementById('3d_info');
59
-    import * as View from './js/modelview.js';
60
-    View.init(view, info);
61
-    const file = './plot/dispensy.3mf';
62
-    var xhttp = new XMLHttpRequest();
63
-    xhttp.responseType = 'arraybuffer';
64
-    xhttp.onload = function() {
65
-        if (this.status != 200) {
66
-            info.textContent = "Download of " + file + " failed: " + this.status + " " + this.statusText;
67
-            return;
68
-        }
69
-        info.textContent = "Downloaded: " + file;
70
-        var file_parts = file.split(".");
71
-        var ext = file_parts.pop().toLowerCase();
72
-        if (ext == "zip") {
73
-            ext = file_parts.pop().toLowerCase();
74
-        }
75
-        info.textContent = "Loaded file with extension: " + ext;
76
-        var model_data = this.response;
77
-        View.view(ext, model_data);
78
-    };
79
-    info.textContent = "Fetching: " + file;
80
-    xhttp.open("GET", file);
81
-    xhttp.send();
82
-</script>
83
-
84
-[Direct link to this file](./plot/dispensy.3mf).
53
+{{#include inc_dispensy.kicad_pcb.md}}

+ 1
- 1
docs/src/main_board_sch.md View File

@@ -4,4 +4,4 @@ This page shows the current version of the schematics as SVG graphics.
4 4
 
5 5
 You can also view the [Main-Board schematics as PDF](./plot/dispensy.kicad_sch.pdf).
6 6
 
7
-{{#include inc_dispensy_sch.md}}
7
+{{#include inc_dispensy.kicad_sch.md}}

+ 4
- 12
hardware/generate_plot.sh View File

@@ -72,7 +72,7 @@ do
72 72
         echo "Exporting board $TYPE"
73 73
         kicad-cli pcb export $TYPE \
74 74
             -t "KiCad Classic"  \
75
-            -l $LAYER_F,$LAYER_B \
75
+            -l $LAYER_B,$LAYER_F \
76 76
             -o $OUTDIR/$IN.$TYPE/0_both.$TYPE \
77 77
             $IN
78 78
         echo
@@ -103,16 +103,8 @@ do
103 103
     # | 3D Export |
104 104
     #  -----------
105 105
 
106
-    echo "Exporting board step file"
107
-    rm -rf $OUTDIR/$IN.step
108
-    kicad-cli pcb export step \
109
-        -o $OUTDIR/$IN.step \
106
+    echo "Exporting board 3D"
107
+    kicad-cli pcb export vrml \
108
+        -o $OUTDIR/$IN.wrl \
110 109
         $IN
111
-
112
-    echo "Converting step to 3mf"
113
-    rm -rf $OUTDIR/$IN.3mf
114
-    prusa-slicer --export-3mf $OUTDIR/$IN.step
115
-
116
-    echo "Deleting step file"
117
-    rm -rf $OUTDIR/$IN.step
118 110
 done

Loading…
Cancel
Save