piecad_viewer 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- piecad_viewer-1.0.0/.gitignore +12 -0
- piecad_viewer-1.0.0/LICENSE +30 -0
- piecad_viewer-1.0.0/PKG-INFO +11 -0
- piecad_viewer-1.0.0/README.md +24 -0
- piecad_viewer-1.0.0/piecad_viewer/__init__.py +215 -0
- piecad_viewer-1.0.0/piecad_viewer/server.py +47 -0
- piecad_viewer-1.0.0/pyproject.toml +27 -0
- piecad_viewer-1.0.0/setup +6 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
The bulk of the code is covered by this license:
|
|
2
|
+
|
|
3
|
+
The MIT License (MIT)
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2025 Brian Sturgill
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in
|
|
15
|
+
all copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
THE SOFTWARE.
|
|
24
|
+
----
|
|
25
|
+
|
|
26
|
+
The file `piecad/_poly_point_isect.py` has it's own license (located at the top of the file).
|
|
27
|
+
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
Fonts are covered by a separate license, see the License files in piecad/fonts for details.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: piecad_viewer
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: "Viewer for Easy as Pie" CAD (Piecad)
|
|
5
|
+
Author-email: Brian Sturgill <codersnext@gmail.com>
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Requires-Dist: pyglet == 1.5.31
|
|
8
|
+
Requires-Dist: trimesh >= 1.26.4
|
|
9
|
+
Requires-Dist: glooey >= 0.3.6
|
|
10
|
+
Requires-Dist: scipy >= 1.15.2
|
|
11
|
+
Project-URL: Home, https://github.com/briansturgill/Piecad-Viewer
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Viewer for "Easy as Pie" CAD (Piecad)
|
|
2
|
+
|
|
3
|
+
[Piecad](https://github.com/briansturgill/Piecad) is a CAD API used to construct 3D models in [Python](https://www.python.org).
|
|
4
|
+
Its primary focus is the creation of models for 3D printing.
|
|
5
|
+
|
|
6
|
+
To use Piecad-Viewer, run `piecad-viewer` and then use the `view` function in Piecad, which works like a 3d `print` (also does 2D).
|
|
7
|
+
Piecad-Viewer allows you to see the output from multiple `view` calls.
|
|
8
|
+
|
|
9
|
+
Type `h` in the Piecad-Viewer window for a list of commands.
|
|
10
|
+
|
|
11
|
+
To install:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pip install piecad_viewer
|
|
15
|
+
|
|
16
|
+
# This will install the script:
|
|
17
|
+
piecad_viewer
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## CREDITS
|
|
22
|
+
|
|
23
|
+
Piecad viewer is based on [trimesh](https://github.com/mikedh/trimesh)'s viewer, which in
|
|
24
|
+
turn is based on version 1 of [pyglet](https://github.com/pyglet/pyglet).
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import pyglet
|
|
2
|
+
from . import server
|
|
3
|
+
from threading import Thread, Lock
|
|
4
|
+
import json
|
|
5
|
+
import numpy as np
|
|
6
|
+
import trimesh
|
|
7
|
+
from trimesh.viewer import windowed
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
view_lock = Lock()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def display_help():
|
|
14
|
+
from tkinter import Tk, Label
|
|
15
|
+
|
|
16
|
+
text_l = [
|
|
17
|
+
"",
|
|
18
|
+
"Key Action",
|
|
19
|
+
"---------------------------------------------",
|
|
20
|
+
"a Toggle axis.",
|
|
21
|
+
"c Toggle culling.",
|
|
22
|
+
"f Toggle fullscreen.",
|
|
23
|
+
"g Toggle grid.",
|
|
24
|
+
"h, ? or ESC View/dismiss this help.",
|
|
25
|
+
"q Quit CAD Viewer.",
|
|
26
|
+
"w Toggle wireframe.",
|
|
27
|
+
"z Reset view.",
|
|
28
|
+
"LEFT Move to the previous object.",
|
|
29
|
+
"RIGHT Move to the next object.",
|
|
30
|
+
"SHIFT-m Launch meshlab on current object.",
|
|
31
|
+
"SHIFT-LEFT Rotate image left.",
|
|
32
|
+
"SHIFT-RIGHT Rotate image right.",
|
|
33
|
+
"SHIFT-UP Rotate image up.",
|
|
34
|
+
"SHIFT-DOWN Rotate image down.",
|
|
35
|
+
"",
|
|
36
|
+
]
|
|
37
|
+
longest = 0
|
|
38
|
+
for t in text_l:
|
|
39
|
+
if longest < len(t):
|
|
40
|
+
longest = len(t)
|
|
41
|
+
root = Tk()
|
|
42
|
+
|
|
43
|
+
def keydown(e):
|
|
44
|
+
ch = e.char
|
|
45
|
+
if ch == "h" or ch == "?" or ord(ch) == 27 or ord(ch) == 13:
|
|
46
|
+
root.destroy()
|
|
47
|
+
|
|
48
|
+
root.bind("<KeyPress>", keydown)
|
|
49
|
+
|
|
50
|
+
x = (longest) * 14
|
|
51
|
+
y = (len(text_l) + 10) * 14
|
|
52
|
+
root.geometry(f"{x}x{y}")
|
|
53
|
+
root.title("CAD Viewer Help")
|
|
54
|
+
text = Label(
|
|
55
|
+
root,
|
|
56
|
+
height=len(text_l),
|
|
57
|
+
justify="left",
|
|
58
|
+
text="\n".join(text_l),
|
|
59
|
+
font=("Courier", 14, "bold"),
|
|
60
|
+
)
|
|
61
|
+
text.pack(padx=10, pady=10)
|
|
62
|
+
root.mainloop()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
display_needed = False
|
|
66
|
+
cur_idx = 0
|
|
67
|
+
meshes = []
|
|
68
|
+
titles = []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MySceneViewer(trimesh.viewer.windowed.SceneViewer):
|
|
72
|
+
def __init__(self, scene, **kwargs):
|
|
73
|
+
super().__init__(scene, **kwargs)
|
|
74
|
+
|
|
75
|
+
def on_key_press(self, symbol, modifiers):
|
|
76
|
+
global cur_idx, meshes, display_needed
|
|
77
|
+
ctrl = modifiers & pyglet.window.key.MOD_CTRL
|
|
78
|
+
shift = modifiers & pyglet.window.key.MOD_SHIFT
|
|
79
|
+
if symbol == pyglet.window.key.W:
|
|
80
|
+
self.toggle_wireframe()
|
|
81
|
+
elif symbol == pyglet.window.key.Z:
|
|
82
|
+
self.reset_view()
|
|
83
|
+
elif symbol == pyglet.window.key.C:
|
|
84
|
+
self.toggle_culling()
|
|
85
|
+
elif symbol == pyglet.window.key.A:
|
|
86
|
+
self.toggle_axis()
|
|
87
|
+
elif symbol == pyglet.window.key.G:
|
|
88
|
+
self.toggle_grid()
|
|
89
|
+
elif symbol == pyglet.window.key.Q:
|
|
90
|
+
self.on_close()
|
|
91
|
+
elif symbol == pyglet.window.key.M:
|
|
92
|
+
if shift:
|
|
93
|
+
tmpfile = (
|
|
94
|
+
"/tmp/cadviewer_tmp.obj" # LATER surely I can do better for temp?
|
|
95
|
+
)
|
|
96
|
+
saved = False
|
|
97
|
+
view_lock.acquire()
|
|
98
|
+
if len(meshes) > 0:
|
|
99
|
+
trimesh.exchange.export.export_mesh(meshes[cur_idx], tmpfile, "obj")
|
|
100
|
+
saved = True
|
|
101
|
+
view_lock.release()
|
|
102
|
+
if saved:
|
|
103
|
+
os.system("meshlab " + tmpfile)
|
|
104
|
+
return # Doesn't really work right, supress.
|
|
105
|
+
elif symbol == pyglet.window.key.F:
|
|
106
|
+
self.toggle_fullscreen()
|
|
107
|
+
elif symbol == pyglet.window.key.H or symbol == pyglet.window.key.QUESTION:
|
|
108
|
+
Thread(target=display_help, daemon=True).start()
|
|
109
|
+
|
|
110
|
+
if symbol in [
|
|
111
|
+
pyglet.window.key.LEFT,
|
|
112
|
+
pyglet.window.key.RIGHT,
|
|
113
|
+
pyglet.window.key.DOWN,
|
|
114
|
+
pyglet.window.key.UP,
|
|
115
|
+
]:
|
|
116
|
+
if ctrl or shift:
|
|
117
|
+
super().on_key_press(symbol, modifiers)
|
|
118
|
+
return
|
|
119
|
+
mlen = len(meshes)
|
|
120
|
+
if symbol == pyglet.window.key.LEFT:
|
|
121
|
+
if mlen > 1:
|
|
122
|
+
view_lock.acquire()
|
|
123
|
+
cur_idx = (cur_idx - 1) % mlen
|
|
124
|
+
display_needed = True
|
|
125
|
+
view_lock.release()
|
|
126
|
+
elif symbol == pyglet.window.key.RIGHT:
|
|
127
|
+
if mlen > 1:
|
|
128
|
+
view_lock.acquire()
|
|
129
|
+
cur_idx = (cur_idx + 1) % mlen
|
|
130
|
+
display_needed = True
|
|
131
|
+
view_lock.release()
|
|
132
|
+
elif symbol == pyglet.window.key.DOWN:
|
|
133
|
+
pass
|
|
134
|
+
elif symbol == pyglet.window.key.UP:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _queue_handler():
|
|
139
|
+
global display_needed, meshes, titles, cur_idx
|
|
140
|
+
while True:
|
|
141
|
+
data = server.viewQueue.get()
|
|
142
|
+
view_lock.acquire()
|
|
143
|
+
try:
|
|
144
|
+
obj = json.loads(data)
|
|
145
|
+
if "clear" in obj:
|
|
146
|
+
meshes = []
|
|
147
|
+
titles = []
|
|
148
|
+
display_needed = True
|
|
149
|
+
cur_idx = 0
|
|
150
|
+
else:
|
|
151
|
+
display_needed = True
|
|
152
|
+
vertices = np.array(obj["vertices"], np.float64)
|
|
153
|
+
faces = np.array(obj["faces"], np.int64)
|
|
154
|
+
mesh = trimesh.Trimesh(vertices, faces)
|
|
155
|
+
mesh.visual.vertex_colors = obj["color"]
|
|
156
|
+
meshes.append(mesh)
|
|
157
|
+
titles.append(obj["title"])
|
|
158
|
+
cur_idx = len(meshes) - 1
|
|
159
|
+
except Exception as err:
|
|
160
|
+
print("Exception:", err)
|
|
161
|
+
pass
|
|
162
|
+
view_lock.release()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
server.start_server()
|
|
166
|
+
Thread(target=_queue_handler, daemon=True).start()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
window = None
|
|
170
|
+
|
|
171
|
+
cur_mesh = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cb(scene: trimesh.Scene):
|
|
175
|
+
global display_needed, meshes, titles, cur_mesh, window
|
|
176
|
+
changed = False
|
|
177
|
+
view_lock.acquire()
|
|
178
|
+
if display_needed:
|
|
179
|
+
scene.delete_geometry(cur_mesh)
|
|
180
|
+
if len(meshes) > 0:
|
|
181
|
+
window.set_caption(
|
|
182
|
+
f"CAD Viewer - #{cur_idx+1} of {len(meshes)} - {titles[cur_idx]}"
|
|
183
|
+
)
|
|
184
|
+
mesh = meshes[cur_idx]
|
|
185
|
+
scene2 = trimesh.Scene(mesh)
|
|
186
|
+
window._initial_camera_transform = scene2.camera_transform
|
|
187
|
+
scene = window.scene = window._scene = scene2
|
|
188
|
+
cur_mesh = scene.add_geometry(mesh)
|
|
189
|
+
window.reset_view()
|
|
190
|
+
# window.view["ball"].scroll(-(mesh.bounds[1,1]-mesh.bounds[0,1])*0.2)
|
|
191
|
+
# window.scene.camera_transform = window.view["ball"].pose
|
|
192
|
+
else:
|
|
193
|
+
window.set_caption(f"CAD Viewer - #0 of 0")
|
|
194
|
+
changed = True
|
|
195
|
+
display_needed = False
|
|
196
|
+
view_lock.release()
|
|
197
|
+
return changed
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def main():
|
|
201
|
+
global cur_mesh, window
|
|
202
|
+
scene = trimesh.Scene()
|
|
203
|
+
cur_mesh = scene.add_geometry(trimesh.creation.box((100, 100, 100)))
|
|
204
|
+
window = MySceneViewer(
|
|
205
|
+
scene,
|
|
206
|
+
callback=cb,
|
|
207
|
+
callback_period=1.0,
|
|
208
|
+
start_loop=False,
|
|
209
|
+
background=(173, 216, 230, 255),
|
|
210
|
+
smooth=False,
|
|
211
|
+
)
|
|
212
|
+
scene.delete_geometry(cur_mesh)
|
|
213
|
+
window.set_caption(f"CAD Viewer - #0 of 0")
|
|
214
|
+
|
|
215
|
+
pyglet.app.run()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
|
2
|
+
from threading import Thread
|
|
3
|
+
from queue import Queue
|
|
4
|
+
|
|
5
|
+
addr = "127.0.0.1"
|
|
6
|
+
port = 8037
|
|
7
|
+
|
|
8
|
+
viewQueue = Queue()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CADViewerHandler(BaseHTTPRequestHandler):
|
|
12
|
+
def do_POST(self):
|
|
13
|
+
len = int(self.headers["Content-Length"])
|
|
14
|
+
print("Content-Length:", len)
|
|
15
|
+
data = self.rfile.read(len)
|
|
16
|
+
self.send_response(200)
|
|
17
|
+
self.end_headers()
|
|
18
|
+
try:
|
|
19
|
+
self.wfile.write(bytes("got it", "utf-8"))
|
|
20
|
+
except:
|
|
21
|
+
print("Exception writing response")
|
|
22
|
+
pass
|
|
23
|
+
viewQueue.put(data)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def start_server():
|
|
27
|
+
Thread(target=_start_server, daemon=True).start()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _start_server():
|
|
31
|
+
print("Viewer server started on (", addr, port, ")")
|
|
32
|
+
httpd = ThreadingHTTPServer((addr, port), CADViewerHandler)
|
|
33
|
+
try:
|
|
34
|
+
httpd.serve_forever()
|
|
35
|
+
except KeyboardInterrupt:
|
|
36
|
+
pass
|
|
37
|
+
httpd.server_close()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
start_server()
|
|
42
|
+
print("^C to exit.")
|
|
43
|
+
while True:
|
|
44
|
+
try:
|
|
45
|
+
input("")
|
|
46
|
+
except KeyboardInterrupt:
|
|
47
|
+
break
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "piecad_viewer"
|
|
7
|
+
description = '"Viewer for Easy as Pie" CAD (Piecad)'
|
|
8
|
+
version = "1.0.0"
|
|
9
|
+
authors = [{name = "Brian Sturgill", email = "codersnext@gmail.com"}]
|
|
10
|
+
license = {file = "LICENSE"}
|
|
11
|
+
classifiers = ["License :: OSI Approved :: MIT License"]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pyglet == 1.5.31",
|
|
14
|
+
"trimesh >= 1.26.4",
|
|
15
|
+
"glooey >= 0.3.6",
|
|
16
|
+
"scipy >= 1.15.2"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
piecad-viewer = "piecad_viewer:main"
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Home = "https://github.com/briansturgill/Piecad-Viewer"
|
|
24
|
+
|
|
25
|
+
[tool.flit.sdist]
|
|
26
|
+
include = []
|
|
27
|
+
exclude = []
|