@deepblend/dsh-blender-provider-local 0.2.20 → 0.3.1
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.
- package/lib/index.js +50 -35
- package/package.json +2 -2
- package/python/bootstrap.py +4 -2
- package/python/deepblend_anisotropy.py +110 -0
- package/python/deepblend_asset_bundle.py +193 -0
- package/python/deepblend_avif_headers.py +222 -0
- package/python/deepblend_bridge.py +46 -4
- package/python/deepblend_capabilities.py +7 -0
- package/python/deepblend_frames.py +11 -1
- package/python/deepblend_geometry.py +210 -0
- package/python/deepblend_gltf_images.py +183 -0
- package/python/deepblend_gltf_import.py +95 -0
- package/python/deepblend_gltf_roles.py +56 -0
- package/python/deepblend_image_headers.py +294 -0
- package/python/deepblend_image_roles.py +2 -0
- package/python/deepblend_images.py +203 -0
- package/python/deepblend_imported_image_headers.py +364 -0
- package/python/deepblend_mesh_checks.py +145 -0
- package/python/deepblend_obj_images.py +54 -0
- package/python/deepblend_obj_import.py +58 -0
- package/python/deepblend_obj_resources.py +79 -0
- package/python/deepblend_parts.py +106 -0
- package/python/deepblend_render.py +53 -0
- package/python/deepblend_scene.py +307 -49
- package/python/deepblend_vessel.py +192 -0
- package/python/deepblend_vessel_math.py +116 -0
- package/python/deepblend_vessel_parameters.py +65 -0
- package/python/deepblend_views.py +18 -19
- package/python/__pycache__/bootstrap.cpython-313.pyc +0 -0
- package/python/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_bridge.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_bridge.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_capabilities.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_frames.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_frames.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_render.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_render.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_scene.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_scene.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_util.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_util.cpython-314.pyc +0 -0
- package/python/__pycache__/deepblend_validate.cpython-313.pyc +0 -0
- package/python/__pycache__/deepblend_views.cpython-313.pyc +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Shared-boundary cup geometry. Construction is isolated from runtime actions.
|
|
2
|
+
|
|
3
|
+
Root strips use quintic endpoint jets. Numeric output checks do not certify
|
|
4
|
+
arbitrary continuous surfaces or artistic quality.
|
|
5
|
+
"""
|
|
6
|
+
import math
|
|
7
|
+
import bpy
|
|
8
|
+
import bmesh
|
|
9
|
+
from mathutils import Vector
|
|
10
|
+
from mathutils.geometry import delaunay_2d_cdt
|
|
11
|
+
from deepblend_vessel_parameters import handled_cup_parameters
|
|
12
|
+
from deepblend_mesh_checks import validate_handled_cup_mesh
|
|
13
|
+
from deepblend_util import ActionError
|
|
14
|
+
from deepblend_vessel_math import CupSurface, add, unit
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MeshBuilder:
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.vertices = []
|
|
21
|
+
self.faces = []
|
|
22
|
+
self.uv = []
|
|
23
|
+
self.regions = []
|
|
24
|
+
self.keys = {}
|
|
25
|
+
self.normals = []
|
|
26
|
+
|
|
27
|
+
def vertex(self, p, normal):
|
|
28
|
+
normal = unit(normal)
|
|
29
|
+
key = tuple((round(c, 10) for c in p))
|
|
30
|
+
if key not in self.keys:
|
|
31
|
+
self.keys[key] = len(self.vertices)
|
|
32
|
+
self.vertices.append(tuple(p))
|
|
33
|
+
self.normals.append(normal)
|
|
34
|
+
else:
|
|
35
|
+
previous = self.normals[self.keys[key]]
|
|
36
|
+
if sum((a-b)**2 for a,b in zip(previous, normal)) > 1e-10:
|
|
37
|
+
raise ActionError('SCENE_SPEC_INVALID', 'handled_cup output: incompatible shared surface normals')
|
|
38
|
+
return self.keys[key]
|
|
39
|
+
|
|
40
|
+
def face(self, ids, uv, region):
|
|
41
|
+
unique = []
|
|
42
|
+
coords = []
|
|
43
|
+
for (i, c) in zip(ids, uv):
|
|
44
|
+
if i not in unique:
|
|
45
|
+
unique.append(i)
|
|
46
|
+
coords.append(c)
|
|
47
|
+
if len(unique) < 3:
|
|
48
|
+
return
|
|
49
|
+
self.faces.append(unique)
|
|
50
|
+
self.uv.append(coords)
|
|
51
|
+
self.regions.append(region)
|
|
52
|
+
|
|
53
|
+
def finish(self, name):
|
|
54
|
+
mesh = bpy.data.meshes.new(name)
|
|
55
|
+
mesh.from_pydata(self.vertices, [], self.faces)
|
|
56
|
+
mesh.update()
|
|
57
|
+
uv = mesh.uv_layers.new(name='UVMap')
|
|
58
|
+
attr = mesh.attributes.new('deepblend_cup_region', 'INT', 'FACE')
|
|
59
|
+
for (i, (polygon, coords)) in enumerate(zip(mesh.polygons, self.uv)):
|
|
60
|
+
attr.data[i].value = self.regions[i]
|
|
61
|
+
for (loop, c) in zip(polygon.loop_indices, coords):
|
|
62
|
+
uv.data[loop].uv = c
|
|
63
|
+
bm = bmesh.new()
|
|
64
|
+
bm.from_mesh(mesh)
|
|
65
|
+
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
|
66
|
+
bm.to_mesh(mesh)
|
|
67
|
+
bm.free()
|
|
68
|
+
obj = bpy.data.objects.new(name, mesh)
|
|
69
|
+
bpy.context.scene.collection.objects.link(obj)
|
|
70
|
+
select(obj)
|
|
71
|
+
bpy.ops.object.shade_smooth_by_angle(angle=math.radians(30))
|
|
72
|
+
mesh.normals_split_custom_set([self.normals[loop.vertex_index] for loop in mesh.loops])
|
|
73
|
+
return obj
|
|
74
|
+
|
|
75
|
+
def select(obj):
|
|
76
|
+
bpy.ops.object.select_all(action='DESELECT')
|
|
77
|
+
obj.select_set(True)
|
|
78
|
+
bpy.context.view_layer.objects.active = obj
|
|
79
|
+
|
|
80
|
+
def inside(point, polygon):
|
|
81
|
+
(x, y) = point
|
|
82
|
+
hit = False
|
|
83
|
+
for (a, b) in zip(polygon, polygon[1:] + polygon[:1]):
|
|
84
|
+
if (a[1] > y) != (b[1] > y) and x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0]:
|
|
85
|
+
hit = not hit
|
|
86
|
+
return hit
|
|
87
|
+
|
|
88
|
+
class _HandledCupMesh(CupSurface):
|
|
89
|
+
"""Analytic profiles sampled into one mesh; roots share cup/handle vertices.
|
|
90
|
+
|
|
91
|
+
R/T/B are cup radius/wall/base, r is the tube radius, A/L are root
|
|
92
|
+
footprint/length, H is attachment half-gap, N/M are angular resolutions.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def add_wall(self, builder, holes):
|
|
99
|
+
(lower, upper) = (self.C, self.HEIGHT - self.T / 2)
|
|
100
|
+
p = self.profile()
|
|
101
|
+
dist = [0]
|
|
102
|
+
for (a, b) in zip(p, p[1:]):
|
|
103
|
+
dist.append(dist[-1] + math.dist(a, b))
|
|
104
|
+
(v0, v1) = (dist[9] / dist[-1], dist[10] / dist[-1])
|
|
105
|
+
hole_coords = []
|
|
106
|
+
for z in (self.P['lower'], self.P['upper']):
|
|
107
|
+
hole_coords.append([(self.R * math.asin(self.A * math.cos(self.TAU * j / self.M) / self.R), z + self.A * math.sin(self.TAU * j / self.M)) for j in range(self.M)])
|
|
108
|
+
coords = []
|
|
109
|
+
edges = []
|
|
110
|
+
for row in range(self.P['wallRows'] + 1):
|
|
111
|
+
z = lower + (upper - lower) * row / self.P['wallRows']
|
|
112
|
+
for col in range(self.N + 1):
|
|
113
|
+
u = -math.pi * self.R + self.TAU * self.R * col / self.N
|
|
114
|
+
if holes and any((inside((u, z), p) for p in hole_coords)):
|
|
115
|
+
continue
|
|
116
|
+
coords.append((u, z))
|
|
117
|
+
if holes:
|
|
118
|
+
for polygon in hole_coords:
|
|
119
|
+
start = len(coords)
|
|
120
|
+
coords.extend(polygon)
|
|
121
|
+
edges.extend(((start + j, start + (j + 1) % self.M) for j in range(self.M)))
|
|
122
|
+
output = delaunay_2d_cdt([Vector(p) for p in coords], edges, [], 0, 1e-09, True)
|
|
123
|
+
(points, _, faces, orig, _, _) = output
|
|
124
|
+
points = [coords[ids[0]] if len(ids) == 1 else tuple(p) for (p, ids) in zip(points, orig)]
|
|
125
|
+
for face in faces:
|
|
126
|
+
center = tuple((sum((points[i][axis] for i in face)) / len(face) for axis in range(2)))
|
|
127
|
+
if holes and any((inside(center, p) for p in hole_coords)):
|
|
128
|
+
continue
|
|
129
|
+
ids = [builder.vertex((self.R * math.cos(points[i][0] / self.R), self.R * math.sin(points[i][0] / self.R), points[i][1]),
|
|
130
|
+
(math.cos(points[i][0] / self.R), math.sin(points[i][0] / self.R), 0)) for i in face]
|
|
131
|
+
builder.face(ids, [(0.25 * (points[i][0] / (self.TAU * self.R) + 0.5), v0 + (v1 - v0) * (points[i][1] - lower) / (upper - lower)) for i in face], 0)
|
|
132
|
+
|
|
133
|
+
def add_cup(self, builder, holes):
|
|
134
|
+
p = self.profile()
|
|
135
|
+
rings = []
|
|
136
|
+
dist = [0]
|
|
137
|
+
for (a, b) in zip(p, p[1:]):
|
|
138
|
+
dist.append(dist[-1] + math.dist(a, b))
|
|
139
|
+
vs = [v / dist[-1] for v in dist]
|
|
140
|
+
for (radius, z), (radial, vertical) in zip(p, self.profile_normals()):
|
|
141
|
+
rings.append([builder.vertex((radius * math.cos(-math.pi + self.TAU * j / self.N), radius * math.sin(-math.pi + self.TAU * j / self.N), z),
|
|
142
|
+
(radial * math.cos(-math.pi + self.TAU * j / self.N), radial * math.sin(-math.pi + self.TAU * j / self.N), vertical)) for j in range(self.N)])
|
|
143
|
+
for row in range(len(p) - 1):
|
|
144
|
+
if row == 9:
|
|
145
|
+
continue
|
|
146
|
+
for j in range(self.N):
|
|
147
|
+
k = (j + 1) % self.N
|
|
148
|
+
builder.face([rings[row][j], rings[row][k], rings[row + 1][k], rings[row + 1][j]], [(0.25 * j / self.N, vs[row]), (0.25 * (j + 1) / self.N, vs[row]), (0.25 * (j + 1) / self.N, vs[row + 1]), (0.25 * j / self.N, vs[row + 1])], 1)
|
|
149
|
+
self.add_wall(builder, holes)
|
|
150
|
+
|
|
151
|
+
def add_roots(self, builder):
|
|
152
|
+
for upper in (False, True):
|
|
153
|
+
rings = []
|
|
154
|
+
for i in range(self.P['rootSegments'] + 1):
|
|
155
|
+
rings.append([builder.vertex(self.root_point(i / self.P['rootSegments'], self.TAU * j / self.M, upper),
|
|
156
|
+
self.root_normal(i / self.P['rootSegments'], self.TAU * j / self.M, upper)) for j in range(self.M)])
|
|
157
|
+
for i in range(self.P['rootSegments']):
|
|
158
|
+
for j in range(self.M):
|
|
159
|
+
k = (j + 1) % self.M
|
|
160
|
+
u = 0.3 if upper else 0.45
|
|
161
|
+
builder.face([rings[i][j], rings[i][k], rings[i + 1][k], rings[i + 1][j]], [(u + 0.1 * j / self.M, i / self.P['rootSegments']), (u + 0.1 * (j + 1) / self.M, i / self.P['rootSegments']), (u + 0.1 * (j + 1) / self.M, (i + 1) / self.P['rootSegments']), (u + 0.1 * j / self.M, (i + 1) / self.P['rootSegments'])], 2 if upper else 3)
|
|
162
|
+
|
|
163
|
+
def add_handle(self, builder):
|
|
164
|
+
sections = []
|
|
165
|
+
for i in range(self.P['handleSegments'] + 1):
|
|
166
|
+
a = math.pi * i / self.P['handleSegments']
|
|
167
|
+
sections.append(((self.R + self.L + self.H * math.sin(a), 0, self.MID + self.H * math.cos(a)), (math.sin(a), 0, math.cos(a))))
|
|
168
|
+
rings = []
|
|
169
|
+
for (center, normal) in sections:
|
|
170
|
+
rings.append([builder.vertex(add(center, (self.r * math.sin(self.TAU * j / self.M) * normal[0], self.r * math.cos(self.TAU * j / self.M), self.r * math.sin(self.TAU * j / self.M) * normal[2])),
|
|
171
|
+
(math.sin(self.TAU * j / self.M) * normal[0], math.cos(self.TAU * j / self.M), math.sin(self.TAU * j / self.M) * normal[2])) for j in range(self.M)])
|
|
172
|
+
for i in range(len(rings) - 1):
|
|
173
|
+
for j in range(self.M):
|
|
174
|
+
k = (j + 1) % self.M
|
|
175
|
+
builder.face([rings[i][j], rings[i][k], rings[i + 1][k], rings[i + 1][j]], [(0.6 + 0.4 * j / self.M, i / (len(rings) - 1)), (0.6 + 0.4 * (j + 1) / self.M, i / (len(rings) - 1)), (0.6 + 0.4 * (j + 1) / self.M, (i + 1) / (len(rings) - 1)), (0.6 + 0.4 * j / self.M, (i + 1) / (len(rings) - 1))], 4)
|
|
176
|
+
|
|
177
|
+
def create_handled_cup(name, spec):
|
|
178
|
+
parameters=handled_cup_parameters(spec)
|
|
179
|
+
shape=_HandledCupMesh(parameters)
|
|
180
|
+
builder=MeshBuilder()
|
|
181
|
+
shape.add_cup(builder, True)
|
|
182
|
+
shape.add_roots(builder)
|
|
183
|
+
shape.add_handle(builder)
|
|
184
|
+
obj=builder.finish(name)
|
|
185
|
+
try:
|
|
186
|
+
validate_handled_cup_mesh(obj.data, parameters)
|
|
187
|
+
except Exception:
|
|
188
|
+
mesh=obj.data
|
|
189
|
+
bpy.data.objects.remove(obj, do_unlink=True)
|
|
190
|
+
bpy.data.meshes.remove(mesh)
|
|
191
|
+
raise
|
|
192
|
+
return obj
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Analytic cup surfaces, independent of Blender and mesh tessellation."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
def unit(vector):
|
|
5
|
+
size = math.sqrt(sum(value * value for value in vector))
|
|
6
|
+
if size == 0 or not math.isfinite(size):
|
|
7
|
+
raise ValueError('invalid surface normal')
|
|
8
|
+
return tuple(value / size for value in vector)
|
|
9
|
+
|
|
10
|
+
def add(a, b):
|
|
11
|
+
return tuple((x + y for (x, y) in zip(a, b)))
|
|
12
|
+
|
|
13
|
+
def mul(a, s):
|
|
14
|
+
return tuple((x * s for x in a))
|
|
15
|
+
|
|
16
|
+
def sub(a, b):
|
|
17
|
+
return add(a, mul(b, -1))
|
|
18
|
+
|
|
19
|
+
def bezier(controls, t):
|
|
20
|
+
n = len(controls) - 1
|
|
21
|
+
return tuple((sum((math.comb(n, i) * (1 - t) ** (n - i) * t ** i * p[k] for (i, p) in enumerate(controls))) for k in range(3)))
|
|
22
|
+
|
|
23
|
+
class CupSurface:
|
|
24
|
+
|
|
25
|
+
def __init__(self, parameters):
|
|
26
|
+
self.P = parameters
|
|
27
|
+
for (key, source) in {'R': 'radius', 'T': 'wall', 'B': 'bottom', 'HEIGHT': 'height', 'C': 'footRound', 'r': 'handleRadius', 'A': 'rootRadius', 'L': 'rootLength', 'N': 'bodySegments', 'M': 'sectionSegments'}.items():
|
|
28
|
+
setattr(self, key, parameters[source])
|
|
29
|
+
self.H = (parameters['upper'] - parameters['lower']) / 2
|
|
30
|
+
self.MID = (parameters['upper'] + parameters['lower']) / 2
|
|
31
|
+
self.TAU = math.tau
|
|
32
|
+
|
|
33
|
+
def root_controls(self, theta, upper):
|
|
34
|
+
"""Quintic Hermite jets from the cylindrical wall and half-torus surface.
|
|
35
|
+
|
|
36
|
+
rho decreases at the wall. At the tube end, alpha increases along the
|
|
37
|
+
outgoing torus (opposite traversal for the lower attachment).
|
|
38
|
+
Tension scales endpoint speeds; the second jets scale quadratically,
|
|
39
|
+
keeping the same adjacent-surface tangent and curvature at each join.
|
|
40
|
+
Missing tension resolves to 1 to retain the original surface.
|
|
41
|
+
"""
|
|
42
|
+
(ct, st) = (math.cos(theta), math.sin(theta))
|
|
43
|
+
z = self.P['upper'] if upper else self.P['lower']
|
|
44
|
+
q0 = (math.sqrt(self.R * self.R - self.A * self.A * ct * ct), self.A * ct, z + self.A * st)
|
|
45
|
+
speed = (self.A - self.r) * self.P['rootTension']
|
|
46
|
+
d0 = (self.A * speed * ct * ct / q0[0], -speed * ct, -speed * st)
|
|
47
|
+
dd0 = (-speed * speed * ct * ct * self.R * self.R / q0[0] ** 3, 0, 0)
|
|
48
|
+
q1 = (self.R + self.L, self.r * ct, z + self.r * st)
|
|
49
|
+
sign = 1 if upper else -1
|
|
50
|
+
alpha_speed = self.L / self.H * self.P['rootTension']
|
|
51
|
+
d1 = (alpha_speed * (self.H + sign * self.r * st), 0, 0)
|
|
52
|
+
dd1 = (0, 0, -sign * alpha_speed ** 2 * (self.H + sign * self.r * st))
|
|
53
|
+
return [q0, add(q0, mul(d0, 0.2)), add(add(q0, mul(d0, 0.4)), mul(dd0, 0.05)), add(sub(q1, mul(d1, 0.4)), mul(dd1, 0.05)), sub(q1, mul(d1, 0.2)), q1]
|
|
54
|
+
|
|
55
|
+
def root_point(self, t, theta, upper):
|
|
56
|
+
return bezier(self.root_controls(theta, upper), t)
|
|
57
|
+
|
|
58
|
+
def root_dt(self, t, theta, upper):
|
|
59
|
+
c = self.root_controls(theta, upper)
|
|
60
|
+
return bezier([mul(sub(c[i + 1], c[i]), 5) for i in range(5)], t)
|
|
61
|
+
|
|
62
|
+
def profile(self):
|
|
63
|
+
points = [(0, 0), (self.R - self.C, 0)]
|
|
64
|
+
for k in range(1, 9):
|
|
65
|
+
a = -math.pi / 2 + k * math.pi / 16
|
|
66
|
+
points.append((self.R - self.C + self.C * math.cos(a), self.C + self.C * math.sin(a)))
|
|
67
|
+
points.append((self.R, self.HEIGHT - self.T / 2))
|
|
68
|
+
for k in range(1, 17):
|
|
69
|
+
a = k * math.pi / 16
|
|
70
|
+
points.append((self.R - self.T / 2 + self.T / 2 * math.cos(a), self.HEIGHT - self.T / 2 + self.T / 2 * math.sin(a)))
|
|
71
|
+
points.append((self.R - self.T, self.B + self.C))
|
|
72
|
+
for k in range(1, 9):
|
|
73
|
+
a = k * math.pi / 16
|
|
74
|
+
points.append((self.R - self.T - self.C + self.C * math.cos(a), self.B + self.C - self.C * math.sin(a)))
|
|
75
|
+
points.append((0, self.B))
|
|
76
|
+
return points
|
|
77
|
+
|
|
78
|
+
def root_dtheta(self, t, theta, upper):
|
|
79
|
+
"""Exact theta partial of the quintic root, including endpoint jets."""
|
|
80
|
+
ct, st = math.cos(theta), math.sin(theta)
|
|
81
|
+
x = math.sqrt(self.R * self.R - self.A * self.A * ct * ct)
|
|
82
|
+
speed = (self.A - self.r) * self.P['rootTension']
|
|
83
|
+
q0 = (self.A * self.A * ct * st / x, -self.A * st, self.A * ct)
|
|
84
|
+
d0 = (-self.A * speed * ct * st * (2 / x + self.A * self.A * ct * ct / x**3),
|
|
85
|
+
speed * st, -speed * ct)
|
|
86
|
+
dd0 = (speed * speed * self.R * self.R * ct * st *
|
|
87
|
+
(2 / x**3 + 3 * self.A * self.A * ct * ct / x**5), 0, 0)
|
|
88
|
+
q1 = (0, -self.r * st, self.r * ct)
|
|
89
|
+
sign = 1 if upper else -1
|
|
90
|
+
alpha_speed = self.L / self.H * self.P['rootTension']
|
|
91
|
+
d1 = (alpha_speed * sign * self.r * ct, 0, 0)
|
|
92
|
+
dd1 = (0, 0, -alpha_speed * alpha_speed * self.r * ct)
|
|
93
|
+
controls = [q0, add(q0, mul(d0, .2)), add(add(q0, mul(d0, .4)), mul(dd0, .05)),
|
|
94
|
+
add(sub(q1, mul(d1, .4)), mul(dd1, .05)), sub(q1, mul(d1, .2)), q1]
|
|
95
|
+
return bezier(controls, t)
|
|
96
|
+
|
|
97
|
+
def root_normal(self, t, theta, upper):
|
|
98
|
+
a, b = self.root_dtheta(t, theta, upper), self.root_dt(t, theta, upper)
|
|
99
|
+
return unit((a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]))
|
|
100
|
+
|
|
101
|
+
def profile_normals(self):
|
|
102
|
+
"""Outward radial/Z normal at each exact profile sample."""
|
|
103
|
+
result = [(0, -1), (0, -1)]
|
|
104
|
+
for k in range(1, 9):
|
|
105
|
+
angle = -math.pi / 2 + k * math.pi / 16
|
|
106
|
+
result.append((math.cos(angle), math.sin(angle)))
|
|
107
|
+
result.append((1, 0))
|
|
108
|
+
for k in range(1, 17):
|
|
109
|
+
angle = k * math.pi / 16
|
|
110
|
+
result.append((math.cos(angle), math.sin(angle)))
|
|
111
|
+
result.append((-1, 0))
|
|
112
|
+
for k in range(1, 9):
|
|
113
|
+
angle = k * math.pi / 16
|
|
114
|
+
result.append((-math.cos(angle), math.sin(angle)))
|
|
115
|
+
result.append((0, 1))
|
|
116
|
+
return result
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Handled-cup inputs, validated before creating Blender data blocks."""
|
|
2
|
+
import math
|
|
3
|
+
from deepblend_util import ActionError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def handled_cup_parameters(spec):
|
|
7
|
+
def invalid(message):
|
|
8
|
+
raise ActionError('SCENE_SPEC_INVALID', 'handled_cup: '+message)
|
|
9
|
+
fields=['radius','height','wallThickness','baseThickness','footRound','handleRadius','handleLower','handleUpper',
|
|
10
|
+
'rootRadius','rootLength','rootTension','segments','sectionSegments','handleSegments','rootSegments','wallRows']
|
|
11
|
+
for key in fields:
|
|
12
|
+
if key in spec and (isinstance(spec[key],bool) or not isinstance(spec[key],(int,float))
|
|
13
|
+
or not math.isfinite(spec[key]) or spec[key]<=0):
|
|
14
|
+
invalid(key+' must be a finite positive number')
|
|
15
|
+
radius = spec.get('radius', .04)
|
|
16
|
+
height = spec.get('height', radius*2.625)
|
|
17
|
+
wall = spec.get('wallThickness', radius*.075)
|
|
18
|
+
bottom = spec.get('baseThickness', radius*.125)
|
|
19
|
+
values = {
|
|
20
|
+
'radius': radius, 'height': height, 'wall': wall, 'bottom': bottom,
|
|
21
|
+
'footRound': spec.get('footRound', min(wall, bottom*.8)),
|
|
22
|
+
'handleRadius': spec.get('handleRadius', radius*.1375),
|
|
23
|
+
'lower': spec.get('handleLower', height*(4/15)),
|
|
24
|
+
'upper': spec.get('handleUpper', height*(26/35)),
|
|
25
|
+
'rootRadius': spec.get('rootRadius', radius*.2625),
|
|
26
|
+
'rootLength': spec.get('rootLength', radius*.2),
|
|
27
|
+
'rootTension': spec.get('rootTension', 1),
|
|
28
|
+
'bodySegments': spec.get('segments', 192),
|
|
29
|
+
'sectionSegments': spec.get('sectionSegments', 96),
|
|
30
|
+
'handleSegments': spec.get('handleSegments', 96),
|
|
31
|
+
'rootSegments': spec.get('rootSegments', 32),
|
|
32
|
+
'wallRows': spec.get('wallRows', 40),
|
|
33
|
+
}
|
|
34
|
+
if any(isinstance(v, bool) or not isinstance(v, (int,float)) or not math.isfinite(v) or v<=0 for v in values.values()):
|
|
35
|
+
invalid('dimensions and resolutions must be finite positive numbers')
|
|
36
|
+
def within(v,lo,hi): return lo-1e-12<=v<=hi+1e-12
|
|
37
|
+
if not 1 <= values['rootTension'] <= 2.5:
|
|
38
|
+
invalid('rootTension must be within 1–2.5')
|
|
39
|
+
ratios=[('radius',radius,.02,.08),('height/radius',height/radius,1.8,4),
|
|
40
|
+
('wallThickness/radius',wall/radius,.025,.15),('baseThickness/radius',bottom/radius,.05,.25),
|
|
41
|
+
('handleRadius/radius',values['handleRadius']/radius,.05,.2),
|
|
42
|
+
('rootRadius/radius',values['rootRadius']/radius,.12,.35),
|
|
43
|
+
('rootRadius/handleRadius',values['rootRadius']/values['handleRadius'],1.4,3),
|
|
44
|
+
('rootLength/rootRadius',values['rootLength']/values['rootRadius'],.4,1.2),
|
|
45
|
+
('footRound/radius',values['footRound']/radius,.01,.15)]
|
|
46
|
+
for name,value,lo,hi in ratios:
|
|
47
|
+
if not within(value,lo,hi):invalid('%s must be within %s–%s' % (name,lo,hi))
|
|
48
|
+
gap=(values['upper']-values['lower'])/2
|
|
49
|
+
if gap<=0 or values['rootLength']/gap>.8+1e-12:
|
|
50
|
+
invalid('handleUpper must exceed handleLower; rootLength/half-gap must be <=0.8')
|
|
51
|
+
if gap+radius*1e-12<max(1.1*values['rootRadius'],2*values['handleRadius']):
|
|
52
|
+
invalid('attachment half-gap must be >=1.1*rootRadius and >=2*handleRadius')
|
|
53
|
+
if values['lower']-values['rootRadius']+radius*1e-12<values['footRound']+.01*radius:
|
|
54
|
+
invalid('lower root footprint must clear the rounded foot by 0.01*radius')
|
|
55
|
+
if values['upper']+values['rootRadius']>height-wall/2-.01*radius+radius*1e-12:
|
|
56
|
+
invalid('upper root footprint must clear the rounded lip by 0.01*radius')
|
|
57
|
+
for key,lo,hi,multiple in [('bodySegments',64,256,4),('sectionSegments',32,96,4),
|
|
58
|
+
('handleSegments',24,128,4),('rootSegments',8,48,1),('wallRows',16,64,1)]:
|
|
59
|
+
value=values[key]
|
|
60
|
+
if value!=int(value) or not lo<=value<=hi or value%multiple:
|
|
61
|
+
invalid('%s must be an integer within %s–%s, divisible by %s' % (key,lo,hi,multiple))
|
|
62
|
+
values[key] = int(value)
|
|
63
|
+
if values['footRound']>min(wall,bottom*.8)+radius*1e-12:
|
|
64
|
+
invalid('footRound must not exceed wallThickness or 0.8*baseThickness')
|
|
65
|
+
return values
|
|
@@ -40,6 +40,8 @@ import bpy
|
|
|
40
40
|
import numpy as np
|
|
41
41
|
|
|
42
42
|
from deepblend_render import (
|
|
43
|
+
_camera_facts,
|
|
44
|
+
_render_config as _actual_render_config,
|
|
43
45
|
apply_render_overrides,
|
|
44
46
|
checkpoint_profile,
|
|
45
47
|
find_camera,
|
|
@@ -202,6 +204,11 @@ def _render_one(scene, entry, tracked, parts, object_index, guard, base_percent,
|
|
|
202
204
|
{"view": view_id, "output": output},
|
|
203
205
|
)
|
|
204
206
|
|
|
207
|
+
# Capture the frame and evaluated camera that produced these bytes before
|
|
208
|
+
# isolation measurements temporarily change the scene's render settings.
|
|
209
|
+
actual_camera = scene.camera
|
|
210
|
+
render_config = _render_config(scene)
|
|
211
|
+
camera_facts = _camera_facts(scene, actual_camera)
|
|
205
212
|
report_progress("measure", base_percent + 5.0, {"view": view_id})
|
|
206
213
|
width, height = png_dimensions(output) or (
|
|
207
214
|
int(scene.render.resolution_x),
|
|
@@ -215,16 +222,20 @@ def _render_one(scene, entry, tracked, parts, object_index, guard, base_percent,
|
|
|
215
222
|
"bytes": os.path.getsize(output),
|
|
216
223
|
"width": width,
|
|
217
224
|
"height": height,
|
|
218
|
-
"frame":
|
|
219
|
-
"cameraId":
|
|
220
|
-
"cameraName":
|
|
221
|
-
"lens": round(
|
|
222
|
-
"engine":
|
|
225
|
+
"frame": int(scene.frame_current),
|
|
226
|
+
"cameraId": actual_camera.get("deepblend_id") or actual_camera.name,
|
|
227
|
+
"cameraName": actual_camera.name,
|
|
228
|
+
"lens": round(camera_facts["lens"], 6),
|
|
229
|
+
"engine": render_config["engine"],
|
|
230
|
+
"renderConfig": render_config,
|
|
231
|
+
"cameraFacts": camera_facts,
|
|
223
232
|
"metrics": measure_view(scene, output, tracked, parts, object_index, guard, view_id, scratch),
|
|
224
233
|
}
|
|
225
234
|
return entry_report
|
|
226
235
|
|
|
227
236
|
|
|
237
|
+
|
|
238
|
+
|
|
228
239
|
def measure_view(scene, image_path, tracked, parts, object_index, guard, view_id, scratch):
|
|
229
240
|
"""Compute the deterministic measurements for one rendered frame.
|
|
230
241
|
|
|
@@ -716,21 +727,9 @@ def _load_rgb(path):
|
|
|
716
727
|
|
|
717
728
|
|
|
718
729
|
def _render_config(scene):
|
|
719
|
-
"""
|
|
720
|
-
samples = None
|
|
721
|
-
try:
|
|
722
|
-
if scene.render.engine == "CYCLES":
|
|
723
|
-
samples = int(scene.cycles.samples)
|
|
724
|
-
except Exception:
|
|
725
|
-
samples = None
|
|
730
|
+
"""Actual settings, retaining the plan report's checkpoint profile field."""
|
|
726
731
|
return {
|
|
727
|
-
|
|
728
|
-
"resolution": [int(scene.render.resolution_x), int(scene.render.resolution_y)],
|
|
729
|
-
"samples": samples,
|
|
730
|
-
"viewTransform": scene.view_settings.view_transform,
|
|
731
|
-
"frameStart": int(scene.frame_start),
|
|
732
|
-
"frameEnd": int(scene.frame_end),
|
|
733
|
-
"fps": int(scene.render.fps),
|
|
732
|
+
**_actual_render_config(scene),
|
|
734
733
|
"checkpointProfile": checkpoint_profile(scene),
|
|
735
734
|
}
|
|
736
735
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|