Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
906b94fc4f | |||
ddc87b8ec7 | |||
27a4cf5626 |
32
Makefile
32
Makefile
@@ -67,41 +67,15 @@ install:
|
||||
install -m644 build/case.stl $(SHAREDIR)
|
||||
install -m644 build/case.gcode $(SHAREDIR)
|
||||
|
||||
%/pinephone_case.vtk: cq_toplevel.py src/*.py
|
||||
mkdir -p "$(@D)"
|
||||
./cq_toplevel.py --export-vtk $@
|
||||
%/pinephone_phone.vtk: cq_toplevel.py src/*.py
|
||||
mkdir -p "$(@D)"
|
||||
./cq_toplevel.py --render-phone-only --export-vtk $@
|
||||
|
||||
build/web-viewer/vtk.js: doc.in/vtk.js
|
||||
mkdir -p build/web-viewer
|
||||
cp $< $@
|
||||
build/web-viewer/index.html: doc.in/index.html build/web-viewer/vtk.js build/web-viewer/pinephone_case.vtk build/web-viewer/pinephone_phone.vtk
|
||||
mkdir -p build/web-viewer
|
||||
cp $< $@
|
||||
|
||||
%_case.png: cq_toplevel.py src/*.py
|
||||
mkdir -p "$(@D)"
|
||||
./cq_toplevel.py --export-png $@
|
||||
%_case_with_phone.png: cq_toplevel.py src/*.py
|
||||
mkdir -p "$(@D)"
|
||||
./cq_toplevel.py --render-phone --export-png $@
|
||||
|
||||
readme: readme_files/pinephone_front_case_with_phone.png readme_files/pinephone_back_case_with_phone.png readme_files/pinephone_side_case_with_phone.png
|
||||
|
||||
doc: readme build/web-viewer/index.html
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
build/case.stl: cq_toplevel.py src/*.py
|
||||
mkdir -p "$(@D)"
|
||||
build/case.stl: src/*.py
|
||||
mkdir -p $(shell dirname $@)
|
||||
./cq_toplevel.py --export-stl $@
|
||||
|
||||
%.gcode: %.stl
|
||||
mkdir -p $(shell dirname $@)
|
||||
slic3r $(SLIC3R_FLAGS) $< -o $@
|
||||
|
||||
.PHONY: all install readme doc clean
|
||||
.PHONY: all install clean
|
||||
|
||||
|
23
README.md
23
README.md
@@ -1,23 +1,8 @@
|
||||
this is a 3d-printable case designed for the PinePhone,
|
||||
but implemented with an eye towards generalizing beyond just that model
|
||||
and supporting future phones/preferences alongside this first model.
|
||||
|
||||
as an example, the default case (pictured below) includes a pouch for carrying an external battery: a quick solution to achieve all-day battery life with any of the stock OSes. the back of this case prints flat, with a mesh that stretches to fit the battery upon installation.
|
||||
|
||||
|
||||
## Images
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
for an interactive viewer, see [build/web-viewer/index.html](https://git.uninsane.org/colin/phone-case-cq/raw/branch/docs-dev/build/web-viewer/index.html).
|
||||
you'll need a browser which supports webGL (e.g. chromium).
|
||||
this is a 3d-printable case designed for the PinePhone, but implemented with an eye towards
|
||||
generalizing beyond just that model and supporting future phones/preferences alongside
|
||||
this first model.
|
||||
|
||||
status: case is usable, but the print is rough and requires a good deal of manual cleaning due to aggressive fillets/overhangs.
|
||||
|
||||
## Prerequisites
|
||||
- run `nix develop` to enter a dev environment.
|
||||
|
47
build/doc/index.html
Normal file
47
build/doc/index.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<html>
|
||||
<head>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://unpkg.com/three/build/three.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- based on three.js example here: <https://jsfiddle.net/2nyxkmco/> -->
|
||||
<script type="module">
|
||||
import * as THREE from 'three';
|
||||
|
||||
const width = window.innerWidth, height = window.innerHeight;
|
||||
|
||||
// init
|
||||
|
||||
const camera = new THREE.PerspectiveCamera( 70, width / height, 0.01, 10 );
|
||||
camera.position.z = 1;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
const geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 );
|
||||
const material = new THREE.MeshNormalMaterial();
|
||||
|
||||
const mesh = new THREE.Mesh( geometry, material );
|
||||
scene.add( mesh );
|
||||
|
||||
const renderer = new THREE.WebGLRenderer( { antialias: true } );
|
||||
renderer.setSize( width, height );
|
||||
renderer.setAnimationLoop( animation );
|
||||
document.body.appendChild( renderer.domElement );
|
||||
|
||||
// animation
|
||||
|
||||
function animation( time ) {
|
||||
mesh.rotation.x = time / 2000;
|
||||
mesh.rotation.y = time / 1000;
|
||||
|
||||
renderer.render( scene, camera );
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,240 +0,0 @@
|
||||
<!-- heavily borrows from rendered cadquery docs: <https://cadquery.readthedocs.io/en/latest/examples.html> -->
|
||||
<!-- vtk.js = Visualization ToolKit; JS version of the same library cadquery uses during runtime -->
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
.vtk-viewer {
|
||||
width: 100%;
|
||||
height: 70%;
|
||||
border: 1px solid #bbb;
|
||||
}
|
||||
</style>
|
||||
<script src="vtk.js"></script>
|
||||
<script>
|
||||
const RENDERERS = {};
|
||||
var ID = 0;
|
||||
var rootContainer = null;
|
||||
|
||||
const renderWindow = vtk.Rendering.Core.vtkRenderWindow.newInstance();
|
||||
const openglRenderWindow = vtk.Rendering.OpenGL.vtkRenderWindow.newInstance();
|
||||
renderWindow.addView(openglRenderWindow);
|
||||
|
||||
const interact_style = vtk.Interaction.Style.vtkInteractorStyleManipulator.newInstance();
|
||||
|
||||
const manips = {
|
||||
rot: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRotateManipulator.newInstance(),
|
||||
pan: vtk.Interaction.Manipulators.vtkMouseCameraTrackballPanManipulator.newInstance(),
|
||||
zoom1: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(),
|
||||
zoom2: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(),
|
||||
roll: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRollManipulator.newInstance(),
|
||||
};
|
||||
|
||||
manips.zoom1.setControl(true);
|
||||
manips.zoom2.setButton(3);
|
||||
manips.roll.setShift(true);
|
||||
manips.pan.setButton(2);
|
||||
|
||||
for (var k in manips){{
|
||||
interact_style.addMouseManipulator(manips[k]);
|
||||
}};
|
||||
|
||||
const interactor = vtk.Rendering.Core.vtkRenderWindowInteractor.newInstance();
|
||||
interactor.setView(openglRenderWindow);
|
||||
interactor.initialize();
|
||||
interactor.setInteractorStyle(interact_style);
|
||||
|
||||
function setVtkRoot(rootContainer_) {
|
||||
rootContainer = rootContainer_;
|
||||
|
||||
rootContainer.style.position = 'fixed';
|
||||
//rootContainer.style.zIndex = -1;
|
||||
rootContainer.style.left = 0;
|
||||
rootContainer.style.top = 0;
|
||||
rootContainer.style.pointerEvents = 'none';
|
||||
rootContainer.style.width = '100%';
|
||||
rootContainer.style.height = '100%';
|
||||
|
||||
openglRenderWindow.setContainer(rootContainer);
|
||||
};
|
||||
|
||||
function updateViewPort(element, renderer) {
|
||||
const { innerHeight, innerWidth } = window;
|
||||
const { x, y, width, height } = element.getBoundingClientRect();
|
||||
const viewport = [
|
||||
x / innerWidth,
|
||||
1 - (y + height) / innerHeight,
|
||||
(x + width) / innerWidth,
|
||||
1 - y / innerHeight,
|
||||
];
|
||||
if (renderer) {
|
||||
renderer.setViewport(...viewport);
|
||||
}
|
||||
}
|
||||
|
||||
function recomputeViewports() {
|
||||
const rendererElems = document.querySelectorAll('.renderer');
|
||||
for (let i = 0; i < rendererElems.length; i++) {
|
||||
const elem = rendererElems[i];
|
||||
const { id } = elem;
|
||||
const renderer = RENDERERS[id];
|
||||
updateViewPort(elem, renderer);
|
||||
}
|
||||
renderWindow.render();
|
||||
}
|
||||
|
||||
function resize() {
|
||||
rootContainer.style.width = `${window.innerWidth}px`;
|
||||
openglRenderWindow.setSize(window.innerWidth, window.innerHeight);
|
||||
recomputeViewports();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
document.addEventListener('scroll', recomputeViewports);
|
||||
|
||||
|
||||
function enterCurrentRenderer(e) {
|
||||
interactor.bindEvents(document.body);
|
||||
interact_style.setEnabled(true);
|
||||
interactor.setCurrentRenderer(RENDERERS[e.target.id]);
|
||||
}
|
||||
|
||||
function exitCurrentRenderer(e) {
|
||||
interactor.setCurrentRenderer(null);
|
||||
interact_style.setEnabled(false);
|
||||
interactor.unbindEvents();
|
||||
}
|
||||
|
||||
function applyStyle(element) {
|
||||
element.classList.add('renderer');
|
||||
element.style.width = '100%';
|
||||
element.style.height = '100%';
|
||||
element.style.display = 'inline-block';
|
||||
element.style.boxSizing = 'border';
|
||||
return element;
|
||||
}
|
||||
|
||||
window.addEventListener('load', resize);
|
||||
|
||||
function render(data, parent_element, ratio){
|
||||
|
||||
// Initial setup
|
||||
const renderer = vtk.Rendering.Core.vtkRenderer.newInstance({ background: [1, 1, 1 ] });
|
||||
|
||||
// iterate over all children children
|
||||
for (var el of data){
|
||||
var trans = el.position;
|
||||
var rot = el.orientation;
|
||||
var rgba = el.color;
|
||||
var shape = el.shape;
|
||||
|
||||
// load the inline data
|
||||
var reader = vtk.IO.XML.vtkXMLPolyDataReader.newInstance();
|
||||
const textEncoder = new TextEncoder();
|
||||
reader.parseAsArrayBuffer(textEncoder.encode(shape));
|
||||
|
||||
// setup actor,mapper and add
|
||||
const mapper = vtk.Rendering.Core.vtkMapper.newInstance();
|
||||
mapper.setInputConnection(reader.getOutputPort());
|
||||
mapper.setResolveCoincidentTopologyToPolygonOffset();
|
||||
mapper.setResolveCoincidentTopologyPolygonOffsetParameters(0.5,100);
|
||||
|
||||
const actor = vtk.Rendering.Core.vtkActor.newInstance();
|
||||
actor.setMapper(mapper);
|
||||
|
||||
// set color and position
|
||||
actor.getProperty().setColor(rgba.slice(0,3));
|
||||
actor.getProperty().setOpacity(rgba[3]);
|
||||
|
||||
actor.rotateZ(rot[2]*180/Math.PI);
|
||||
actor.rotateY(rot[1]*180/Math.PI);
|
||||
actor.rotateX(rot[0]*180/Math.PI);
|
||||
|
||||
actor.setPosition(trans);
|
||||
|
||||
renderer.addActor(actor);
|
||||
};
|
||||
|
||||
//add the container
|
||||
const container = applyStyle(document.createElement("div"));
|
||||
parent_element.appendChild(container);
|
||||
container.addEventListener('mouseenter', enterCurrentRenderer);
|
||||
container.addEventListener('mouseleave', exitCurrentRenderer);
|
||||
container.id = ID;
|
||||
|
||||
renderWindow.addRenderer(renderer);
|
||||
updateViewPort(container, renderer);
|
||||
renderer.getActiveCamera().set({ position: [1, -1, 1], viewUp: [0, 0, 1] });
|
||||
renderer.resetCamera();
|
||||
|
||||
RENDERERS[ID] = renderer;
|
||||
ID++;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- load model data. populates a global variable whose name matches the filename -->
|
||||
<script src="pinephone_case.vtk.js"></script>
|
||||
<script src="pinephone_phone.vtk.js"></script>
|
||||
|
||||
<script>
|
||||
function renderToConsole(modelName) {
|
||||
// turns out there's just one canvas for the whole renderer,
|
||||
// so this ignores the modelName and exports the whole canvas
|
||||
// var viewerCanvas = document.querySelector(`#vtk-viewer-${modelName} > canvas`);
|
||||
var viewerCanvas = document.querySelector("#vtk-viewer-root > canvas");
|
||||
console.log("logging viewer image to console to allow the Makefile to capture a static image for docs...");
|
||||
var image = viewerCanvas.toDataURL("image/png")
|
||||
console.log("vtk-viewer-canvas: pinephone_case: " + image);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="vtk-viewer-root">
|
||||
<script>
|
||||
setVtkRoot(document.currentScript.parentNode);
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="vtk-viewer" id="vtk-viewer-pinephone_case">
|
||||
<script>
|
||||
var parent_element = document.currentScript.parentNode;
|
||||
|
||||
var pinephone_case_options = [{
|
||||
"color": [ 1.0, 0.8, 0.0, 1.0 ],
|
||||
"position": [ 0.0, 0.0, 0.0 ],
|
||||
"orientation": [ 0.0, 0.0, 0.0 ],
|
||||
"shape": pinephone_case
|
||||
}];
|
||||
render(pinephone_case_options, parent_element);
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="vtk-viewer">
|
||||
<script>
|
||||
var parent_element = document.currentScript.parentNode;
|
||||
|
||||
var pinephone_case_with_phone_options = [
|
||||
{
|
||||
"color": [ 1.0, 0.8, 0.0, 1.0 ],
|
||||
"position": [ 0.0, 0.0, 0.0 ],
|
||||
"orientation": [ 0.0, 0.0, 0.0 ],
|
||||
"shape": pinephone_case
|
||||
},
|
||||
{
|
||||
"color": [ 0.2, 0.2, 0.2, 1.0 ],
|
||||
"position": [ 0.0, 0.0, 0.0 ],
|
||||
"orientation": [ 0.0, 0.0, 0.0 ],
|
||||
"shape": pinephone_phone
|
||||
}
|
||||
];
|
||||
render(pinephone_case_with_phone_options, parent_element);
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
resize();
|
||||
renderToConsole("pinephone_case");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
142
cq_toplevel.py
142
cq_toplevel.py
@@ -20,84 +20,45 @@ import case
|
||||
import pinephone
|
||||
import ldtek_battery
|
||||
|
||||
from cadquery.occ_impl.assembly import toVTK
|
||||
from cadquery.vis import _to_assy
|
||||
from vtkmodules.vtkRenderingCore import vtkRenderWindow, vtkWindowToImageFilter
|
||||
from vtkmodules.vtkIOImage import vtkPNGWriter
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def export_png_image(obj, file_: str, orientation: str):
|
||||
assy = _to_assy(obj)
|
||||
renderer = toVTK(assy)
|
||||
win = vtkRenderWindow()
|
||||
win.AddRenderer(renderer)
|
||||
win.Render()
|
||||
camera = renderer.GetActiveCamera()
|
||||
if orientation == "front":
|
||||
camera.Roll(-28)
|
||||
camera.Elevation(-50)
|
||||
elif orientation == "back":
|
||||
camera.Roll(0)
|
||||
camera.Yaw(180)
|
||||
camera.Elevation(-35)
|
||||
elif orientation == "side":
|
||||
camera.Yaw(75)
|
||||
camera.Elevation(30)
|
||||
# adjust camera so full object is visible.
|
||||
# this also resizes the window, potentially changing its aspect ratio.
|
||||
# it's important that the window has been `Render()`'d at least once by now,
|
||||
# else it'll adjust the camera based on the wrong aspect ratio.
|
||||
renderer.ResetCamera()
|
||||
renderer.SetBackground(0.8, 0.8, 0.8)
|
||||
def svg_export_options(view: str):
|
||||
proj = (1, 1, 1)
|
||||
if view == "front":
|
||||
proj = (0.00, -0.05, 0.10)
|
||||
elif view == "back":
|
||||
proj = (0.00, -0.05, -0.10)
|
||||
elif view == "right":
|
||||
proj = (-0.10, 0.00, 0.00)
|
||||
return dict(
|
||||
width = 1024,
|
||||
height = 1024,
|
||||
marginLeft = 100,
|
||||
marginTop = 10,
|
||||
showAxes = False,
|
||||
# projectionDir controls both the angle and the distance from the camera to the model
|
||||
projectionDir = proj,
|
||||
strokeWidth = 0.25,
|
||||
strokeColor = (255, 0, 0),
|
||||
hiddenColor = (0, 0, 255),
|
||||
showHidden = True,
|
||||
)
|
||||
|
||||
win.Render()
|
||||
|
||||
# documented here: <https://examples.vtk.org/site/Python/IO/ImageWriter/>
|
||||
win_to_input = vtkWindowToImageFilter()
|
||||
win_to_input.SetInput(win)
|
||||
win_to_input.SetInputBufferTypeToRGB()
|
||||
win_to_input.ReadFrontBufferOff()
|
||||
win_to_input.Update()
|
||||
|
||||
exporter = vtkPNGWriter()
|
||||
exporter.SetFileName(file_)
|
||||
exporter.SetInputConnection(win_to_input.GetOutputPort())
|
||||
exporter.Write()
|
||||
|
||||
|
||||
def _model(as_assy: bool=False):
|
||||
logger.info("computing model ...")
|
||||
def model():
|
||||
render_phone = os.environ.get("CASE_RENDER_PHONE", "") not in ("", "0")
|
||||
render_phone_only = os.environ.get("CASE_RENDER_PHONE_ONLY", "") not in ("", "0")
|
||||
|
||||
phone = pinephone.PinePhone()
|
||||
|
||||
if render_phone_only:
|
||||
return case.orient_for_printing(phone)
|
||||
|
||||
battery = ldtek_battery.LdtekBattery()
|
||||
return case.case(phone, battery=battery, render_phone=render_phone, as_assy=as_assy)
|
||||
|
||||
_computedModels = {}
|
||||
def model(as_assy: bool=False):
|
||||
""" memoized wrapper around `_model` """
|
||||
global _computedModels
|
||||
if as_assy not in _computedModels:
|
||||
_computedModels[as_assy] = _model(as_assy=as_assy)
|
||||
return _computedModels[as_assy]
|
||||
return case.case(phone, battery=battery, render_phone=render_phone)
|
||||
|
||||
def main():
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
parser = argparse.ArgumentParser(description="toplevel cadquery interface")
|
||||
parser.add_argument("--render-phone", action="store_true", help="render the case and also the phone within it; useful to confirm fit visually before printing")
|
||||
parser.add_argument("--render-phone-only", action="store_true", help="render *only* the phone, not even the case")
|
||||
parser.add_argument("--render-phone", action="store_true", help="include the phone model itself in the stl; useful to confirm fit visually before printing")
|
||||
parser.add_argument("--export-stl")
|
||||
parser.add_argument("--export-png")
|
||||
parser.add_argument("--export-vtk")
|
||||
parser.add_argument("--export-svg")
|
||||
parser.add_argument("--export-tjs")
|
||||
parser.add_argument("--editor", action="store_true", help="view in cq-editor")
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -105,40 +66,33 @@ def main():
|
||||
if args.render_phone:
|
||||
os.environ["CASE_RENDER_PHONE"] = "1"
|
||||
|
||||
if args.render_phone_only:
|
||||
os.environ["CASE_RENDER_PHONE_ONLY"] = "1"
|
||||
logger.info("computing model ...")
|
||||
model_ = model()
|
||||
|
||||
if args.export_stl:
|
||||
model_ = model()
|
||||
logger.info("exporting stl to %s", args.export_stl)
|
||||
cq.exporters.export(model_, args.export_stl)
|
||||
|
||||
if args.export_png:
|
||||
orientation = None
|
||||
if "side" in args.export_png:
|
||||
orientation = "side"
|
||||
if "back" in args.export_png:
|
||||
orientation = "back"
|
||||
if "front" in args.export_png:
|
||||
orientation = "front"
|
||||
model_ = model(as_assy=True)
|
||||
logger.info("exporting png to %s", args.export_png)
|
||||
export_png_image(model_, args.export_png, orientation)
|
||||
if args.export_svg:
|
||||
view = None
|
||||
if "front" in args.export_svg:
|
||||
view = "front"
|
||||
elif "back" in args.export_svg:
|
||||
view = "back"
|
||||
elif "right" in args.export_svg:
|
||||
view = "right"
|
||||
logger.info("exporting svg to %s (view: %s)", args.export_svg, str(view))
|
||||
cq.exporters.export(model_, args.export_svg, opt=svg_export_options(view))
|
||||
|
||||
if args.export_vtk:
|
||||
vtk_file = args.export_vtk
|
||||
js_var, _ext = os.path.splitext(os.path.basename(vtk_file))
|
||||
js_file = f'{vtk_file}.js'
|
||||
model_ = model()
|
||||
logger.info("exporting VTK (for web rendering) to %s", vtk_file)
|
||||
cq.exporters.export(model_, vtk_file, cq.exporters.ExportTypes.VTP)
|
||||
|
||||
logger.info("wrapping VTK data in a javascript variable (var %s) in %s", js_var, js_file)
|
||||
vtk_data = open(vtk_file).read()
|
||||
with open(js_file, 'w') as js:
|
||||
js.write(f"var {js_var} = `\n")
|
||||
js.write(vtk_data)
|
||||
js.write("`;\n")
|
||||
if args.export_tjs:
|
||||
logger.info("exporting three.js to %s", args.export_tjs)
|
||||
cq.exporters.export(
|
||||
model_,
|
||||
args.export_tjs,
|
||||
tolerance=0.01,
|
||||
angularTolerance=0.1,
|
||||
exportType=cq.exporters.ExportTypes.TJS,
|
||||
)
|
||||
|
||||
if args.editor:
|
||||
logger.info("launching cq-editor")
|
||||
@@ -148,6 +102,4 @@ def main():
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
else:
|
||||
# this `result` var should be picked up by cadquery, in case we were imported by it.
|
||||
# note that we don't actually get here until the user presses the `>` render button.
|
||||
result = model()
|
||||
|
@@ -1,240 +0,0 @@
|
||||
<!-- heavily borrows from rendered cadquery docs: <https://cadquery.readthedocs.io/en/latest/examples.html> -->
|
||||
<!-- vtk.js = Visualization ToolKit; JS version of the same library cadquery uses during runtime -->
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
.vtk-viewer {
|
||||
width: 100%;
|
||||
height: 70%;
|
||||
border: 1px solid #bbb;
|
||||
}
|
||||
</style>
|
||||
<script src="vtk.js"></script>
|
||||
<script>
|
||||
const RENDERERS = {};
|
||||
var ID = 0;
|
||||
var rootContainer = null;
|
||||
|
||||
const renderWindow = vtk.Rendering.Core.vtkRenderWindow.newInstance();
|
||||
const openglRenderWindow = vtk.Rendering.OpenGL.vtkRenderWindow.newInstance();
|
||||
renderWindow.addView(openglRenderWindow);
|
||||
|
||||
const interact_style = vtk.Interaction.Style.vtkInteractorStyleManipulator.newInstance();
|
||||
|
||||
const manips = {
|
||||
rot: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRotateManipulator.newInstance(),
|
||||
pan: vtk.Interaction.Manipulators.vtkMouseCameraTrackballPanManipulator.newInstance(),
|
||||
zoom1: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(),
|
||||
zoom2: vtk.Interaction.Manipulators.vtkMouseCameraTrackballZoomManipulator.newInstance(),
|
||||
roll: vtk.Interaction.Manipulators.vtkMouseCameraTrackballRollManipulator.newInstance(),
|
||||
};
|
||||
|
||||
manips.zoom1.setControl(true);
|
||||
manips.zoom2.setButton(3);
|
||||
manips.roll.setShift(true);
|
||||
manips.pan.setButton(2);
|
||||
|
||||
for (var k in manips){{
|
||||
interact_style.addMouseManipulator(manips[k]);
|
||||
}};
|
||||
|
||||
const interactor = vtk.Rendering.Core.vtkRenderWindowInteractor.newInstance();
|
||||
interactor.setView(openglRenderWindow);
|
||||
interactor.initialize();
|
||||
interactor.setInteractorStyle(interact_style);
|
||||
|
||||
function setVtkRoot(rootContainer_) {
|
||||
rootContainer = rootContainer_;
|
||||
|
||||
rootContainer.style.position = 'fixed';
|
||||
//rootContainer.style.zIndex = -1;
|
||||
rootContainer.style.left = 0;
|
||||
rootContainer.style.top = 0;
|
||||
rootContainer.style.pointerEvents = 'none';
|
||||
rootContainer.style.width = '100%';
|
||||
rootContainer.style.height = '100%';
|
||||
|
||||
openglRenderWindow.setContainer(rootContainer);
|
||||
};
|
||||
|
||||
function updateViewPort(element, renderer) {
|
||||
const { innerHeight, innerWidth } = window;
|
||||
const { x, y, width, height } = element.getBoundingClientRect();
|
||||
const viewport = [
|
||||
x / innerWidth,
|
||||
1 - (y + height) / innerHeight,
|
||||
(x + width) / innerWidth,
|
||||
1 - y / innerHeight,
|
||||
];
|
||||
if (renderer) {
|
||||
renderer.setViewport(...viewport);
|
||||
}
|
||||
}
|
||||
|
||||
function recomputeViewports() {
|
||||
const rendererElems = document.querySelectorAll('.renderer');
|
||||
for (let i = 0; i < rendererElems.length; i++) {
|
||||
const elem = rendererElems[i];
|
||||
const { id } = elem;
|
||||
const renderer = RENDERERS[id];
|
||||
updateViewPort(elem, renderer);
|
||||
}
|
||||
renderWindow.render();
|
||||
}
|
||||
|
||||
function resize() {
|
||||
rootContainer.style.width = `${window.innerWidth}px`;
|
||||
openglRenderWindow.setSize(window.innerWidth, window.innerHeight);
|
||||
recomputeViewports();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
document.addEventListener('scroll', recomputeViewports);
|
||||
|
||||
|
||||
function enterCurrentRenderer(e) {
|
||||
interactor.bindEvents(document.body);
|
||||
interact_style.setEnabled(true);
|
||||
interactor.setCurrentRenderer(RENDERERS[e.target.id]);
|
||||
}
|
||||
|
||||
function exitCurrentRenderer(e) {
|
||||
interactor.setCurrentRenderer(null);
|
||||
interact_style.setEnabled(false);
|
||||
interactor.unbindEvents();
|
||||
}
|
||||
|
||||
function applyStyle(element) {
|
||||
element.classList.add('renderer');
|
||||
element.style.width = '100%';
|
||||
element.style.height = '100%';
|
||||
element.style.display = 'inline-block';
|
||||
element.style.boxSizing = 'border';
|
||||
return element;
|
||||
}
|
||||
|
||||
window.addEventListener('load', resize);
|
||||
|
||||
function render(data, parent_element, ratio){
|
||||
|
||||
// Initial setup
|
||||
const renderer = vtk.Rendering.Core.vtkRenderer.newInstance({ background: [1, 1, 1 ] });
|
||||
|
||||
// iterate over all children children
|
||||
for (var el of data){
|
||||
var trans = el.position;
|
||||
var rot = el.orientation;
|
||||
var rgba = el.color;
|
||||
var shape = el.shape;
|
||||
|
||||
// load the inline data
|
||||
var reader = vtk.IO.XML.vtkXMLPolyDataReader.newInstance();
|
||||
const textEncoder = new TextEncoder();
|
||||
reader.parseAsArrayBuffer(textEncoder.encode(shape));
|
||||
|
||||
// setup actor,mapper and add
|
||||
const mapper = vtk.Rendering.Core.vtkMapper.newInstance();
|
||||
mapper.setInputConnection(reader.getOutputPort());
|
||||
mapper.setResolveCoincidentTopologyToPolygonOffset();
|
||||
mapper.setResolveCoincidentTopologyPolygonOffsetParameters(0.5,100);
|
||||
|
||||
const actor = vtk.Rendering.Core.vtkActor.newInstance();
|
||||
actor.setMapper(mapper);
|
||||
|
||||
// set color and position
|
||||
actor.getProperty().setColor(rgba.slice(0,3));
|
||||
actor.getProperty().setOpacity(rgba[3]);
|
||||
|
||||
actor.rotateZ(rot[2]*180/Math.PI);
|
||||
actor.rotateY(rot[1]*180/Math.PI);
|
||||
actor.rotateX(rot[0]*180/Math.PI);
|
||||
|
||||
actor.setPosition(trans);
|
||||
|
||||
renderer.addActor(actor);
|
||||
};
|
||||
|
||||
//add the container
|
||||
const container = applyStyle(document.createElement("div"));
|
||||
parent_element.appendChild(container);
|
||||
container.addEventListener('mouseenter', enterCurrentRenderer);
|
||||
container.addEventListener('mouseleave', exitCurrentRenderer);
|
||||
container.id = ID;
|
||||
|
||||
renderWindow.addRenderer(renderer);
|
||||
updateViewPort(container, renderer);
|
||||
renderer.getActiveCamera().set({ position: [1, -1, 1], viewUp: [0, 0, 1] });
|
||||
renderer.resetCamera();
|
||||
|
||||
RENDERERS[ID] = renderer;
|
||||
ID++;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- load model data. populates a global variable whose name matches the filename -->
|
||||
<script src="pinephone_case.vtk.js"></script>
|
||||
<script src="pinephone_phone.vtk.js"></script>
|
||||
|
||||
<script>
|
||||
function renderToConsole(modelName) {
|
||||
// turns out there's just one canvas for the whole renderer,
|
||||
// so this ignores the modelName and exports the whole canvas
|
||||
// var viewerCanvas = document.querySelector(`#vtk-viewer-${modelName} > canvas`);
|
||||
var viewerCanvas = document.querySelector("#vtk-viewer-root > canvas");
|
||||
console.log("logging viewer image to console to allow the Makefile to capture a static image for docs...");
|
||||
var image = viewerCanvas.toDataURL("image/png")
|
||||
console.log("vtk-viewer-canvas: pinephone_case: " + image);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="vtk-viewer-root">
|
||||
<script>
|
||||
setVtkRoot(document.currentScript.parentNode);
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="vtk-viewer" id="vtk-viewer-pinephone_case">
|
||||
<script>
|
||||
var parent_element = document.currentScript.parentNode;
|
||||
|
||||
var pinephone_case_options = [{
|
||||
"color": [ 1.0, 0.8, 0.0, 1.0 ],
|
||||
"position": [ 0.0, 0.0, 0.0 ],
|
||||
"orientation": [ 0.0, 0.0, 0.0 ],
|
||||
"shape": pinephone_case
|
||||
}];
|
||||
render(pinephone_case_options, parent_element);
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="vtk-viewer">
|
||||
<script>
|
||||
var parent_element = document.currentScript.parentNode;
|
||||
|
||||
var pinephone_case_with_phone_options = [
|
||||
{
|
||||
"color": [ 1.0, 0.8, 0.0, 1.0 ],
|
||||
"position": [ 0.0, 0.0, 0.0 ],
|
||||
"orientation": [ 0.0, 0.0, 0.0 ],
|
||||
"shape": pinephone_case
|
||||
},
|
||||
{
|
||||
"color": [ 0.2, 0.2, 0.2, 1.0 ],
|
||||
"position": [ 0.0, 0.0, 0.0 ],
|
||||
"orientation": [ 0.0, 0.0, 0.0 ],
|
||||
"shape": pinephone_phone
|
||||
}
|
||||
];
|
||||
render(pinephone_case_with_phone_options, parent_element);
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
resize();
|
||||
renderToConsole("pinephone_case");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
@@ -15,7 +15,6 @@
|
||||
cqPkgs.cadquery
|
||||
cqPkgs.cq-editor
|
||||
pkgs.slic3r
|
||||
pkgs.chromium
|
||||
# (pkgs.python37.withPackages (ps: with ps; [ cadquery ]))
|
||||
];
|
||||
};
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 133 KiB |
Binary file not shown.
Before Width: | Height: | Size: 88 KiB |
Binary file not shown.
Before Width: | Height: | Size: 195 KiB |
Binary file not shown.
Before Width: | Height: | Size: 137 KiB |
Binary file not shown.
Before Width: | Height: | Size: 84 KiB |
Binary file not shown.
Before Width: | Height: | Size: 49 KiB |
26
src/case.py
26
src/case.py
@@ -44,17 +44,6 @@ battery_harness_shell_thickness = 0.8
|
||||
battery_harness_margin_bottom = 21.0
|
||||
battery_harness_margin_top = 2.0
|
||||
|
||||
dark_gray = cq.Color(95/256, 87/256, 79/256)
|
||||
mid_gray = cq.Color(128/256, 118/256, 108/256)
|
||||
mid_light_gray = cq.Color(160/256, 150/256, 140/256)
|
||||
light_gray = cq.Color(192/256, 182/256, 172/256)
|
||||
yellow = cq.Color(255/256, 236/256, 39/256)
|
||||
orange = cq.Color(255/256, 192/256, 39/256)
|
||||
yellow_orange = cq.Color(255/256, 208/256, 49/256)
|
||||
|
||||
body_color = mid_light_gray
|
||||
case_color = yellow_orange
|
||||
|
||||
def _thicken(solid, thickness: float=1, combine=False, **kwargs):
|
||||
"""
|
||||
dilate the solid in all dimensions by `thickness` and then subtract the original.
|
||||
@@ -346,7 +335,7 @@ def orient_for_printing(case):
|
||||
"""
|
||||
return case.rotate((0, 0, 0), (1000, 0, 0), angleDegrees=180)
|
||||
|
||||
def case(phone, battery=None, render_phone: bool=False, as_assy: bool=True):
|
||||
def case(phone, battery=None, render_phone: bool=False):
|
||||
body = phone.solids(tag="body")
|
||||
power = phone.solids(tag="power")
|
||||
volume = phone.solids(tag="volume")
|
||||
@@ -379,15 +368,8 @@ def case(phone, battery=None, render_phone: bool=False, as_assy: bool=True):
|
||||
|
||||
# TODO: compress the case along the Z axis, to give a snugger fit (0.8mm is a good compression)
|
||||
|
||||
if render_phone:
|
||||
case = case.union(phone)
|
||||
|
||||
case = orient_for_printing(case)
|
||||
phone = orient_for_printing(phone)
|
||||
|
||||
if as_assy:
|
||||
case = cq.Assembly(case, color=case_color)
|
||||
if render_phone:
|
||||
case = case.add(phone, color=body_color)
|
||||
else:
|
||||
if render_phone:
|
||||
case = case.union(phone)
|
||||
|
||||
return case
|
||||
|
Reference in New Issue
Block a user