Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
ee6d874b3c | |||
faba843dce | |||
549a48580b | |||
a82781e6b1 | |||
5e8550f5f5 | |||
1ef190d932 | |||
fd9e768fc1 | |||
425bd07d38 | |||
c19431ba1d | |||
3afdf11e8f | |||
67deb480f0 | |||
aac4584e30 | |||
b5ae83ddcd | |||
5ed54c4aad | |||
e839bb489c | |||
ccc04dbd5a | |||
9a35d265ea | |||
2b414b51ae | |||
afb41b4483 | |||
2a8b5382ef | |||
e9f5e4aec3 | |||
5da7a1a8d8 | |||
3027847301 | |||
c89764b73a |
32
Makefile
32
Makefile
@@ -67,15 +67,41 @@ 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: src/*.py
|
||||
mkdir -p $(shell dirname $@)
|
||||
build/case.stl: cq_toplevel.py src/*.py
|
||||
mkdir -p "$(@D)"
|
||||
./cq_toplevel.py --export-stl $@
|
||||
|
||||
%.gcode: %.stl
|
||||
mkdir -p $(shell dirname $@)
|
||||
slic3r $(SLIC3R_FLAGS) $< -o $@
|
||||
|
||||
.PHONY: all install clean
|
||||
.PHONY: all install readme doc clean
|
||||
|
||||
|
23
README.md
23
README.md
@@ -1,8 +1,23 @@
|
||||
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.
|
||||
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).
|
||||
|
||||
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.
|
||||
|
240
build/web-viewer/index.html
Normal file
240
build/web-viewer/index.html
Normal file
@@ -0,0 +1,240 @@
|
||||
<!-- 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>
|
38
build/web-viewer/pinephone_case.vtk.js
Normal file
38
build/web-viewer/pinephone_case.vtk.js
Normal file
File diff suppressed because one or more lines are too long
38
build/web-viewer/pinephone_phone.vtk.js
Normal file
38
build/web-viewer/pinephone_phone.vtk.js
Normal file
File diff suppressed because one or more lines are too long
3
build/web-viewer/vtk.js
Normal file
3
build/web-viewer/vtk.js
Normal file
File diff suppressed because one or more lines are too long
135
cq_toplevel.py
135
cq_toplevel.py
@@ -20,44 +20,84 @@ 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 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,
|
||||
)
|
||||
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 model():
|
||||
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 ...")
|
||||
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)
|
||||
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]
|
||||
|
||||
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="include the phone model itself in the stl; useful to confirm fit visually before printing")
|
||||
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("--export-stl")
|
||||
parser.add_argument("--export-svg")
|
||||
parser.add_argument("--export-png")
|
||||
parser.add_argument("--export-vtk")
|
||||
parser.add_argument("--editor", action="store_true", help="view in cq-editor")
|
||||
|
||||
args = parser.parse_args()
|
||||
@@ -65,22 +105,41 @@ def main():
|
||||
if args.render_phone:
|
||||
os.environ["CASE_RENDER_PHONE"] = "1"
|
||||
|
||||
logger.info("computing model ...")
|
||||
model_ = model()
|
||||
if args.render_phone_only:
|
||||
os.environ["CASE_RENDER_PHONE_ONLY"] = "1"
|
||||
|
||||
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_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_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_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.editor:
|
||||
logger.info("launching cq-editor")
|
||||
subprocess.check_call(["cq-editor", __file__])
|
||||
@@ -89,4 +148,6 @@ 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()
|
||||
|
240
doc.in/index.html
Normal file
240
doc.in/index.html
Normal file
@@ -0,0 +1,240 @@
|
||||
<!-- 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>
|
3
doc.in/vtk.js
Normal file
3
doc.in/vtk.js
Normal file
File diff suppressed because one or more lines are too long
@@ -15,6 +15,7 @@
|
||||
cqPkgs.cadquery
|
||||
cqPkgs.cq-editor
|
||||
pkgs.slic3r
|
||||
pkgs.chromium
|
||||
# (pkgs.python37.withPackages (ps: with ps; [ cadquery ]))
|
||||
];
|
||||
};
|
||||
|
BIN
readme_files/pinephone_back_case_with_phone.png
Normal file
BIN
readme_files/pinephone_back_case_with_phone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 133 KiB |
BIN
readme_files/pinephone_front_case_with_phone.png
Normal file
BIN
readme_files/pinephone_front_case_with_phone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
BIN
readme_files/pinephone_irl_back.jpg
Normal file
BIN
readme_files/pinephone_irl_back.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 195 KiB |
BIN
readme_files/pinephone_irl_front.jpg
Normal file
BIN
readme_files/pinephone_irl_front.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 137 KiB |
BIN
readme_files/pinephone_irl_side.jpg
Normal file
BIN
readme_files/pinephone_irl_side.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 84 KiB |
BIN
readme_files/pinephone_side_case_with_phone.png
Normal file
BIN
readme_files/pinephone_side_case_with_phone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
26
src/case.py
26
src/case.py
@@ -44,6 +44,17 @@ 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.
|
||||
@@ -335,7 +346,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):
|
||||
def case(phone, battery=None, render_phone: bool=False, as_assy: bool=True):
|
||||
body = phone.solids(tag="body")
|
||||
power = phone.solids(tag="power")
|
||||
volume = phone.solids(tag="volume")
|
||||
@@ -368,8 +379,15 @@ def case(phone, battery=None, render_phone: bool=False):
|
||||
|
||||
# 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