kopia lustrzana https://github.com/vilemduha/blendercam
398 wiersze
15 KiB
Python
398 wiersze
15 KiB
Python
"""Fabex 'simulation.py' © 2012 Vilem Novak
|
|
Functions to generate a mesh simulation from CAM Chain / Operation data.
|
|
"""
|
|
|
|
import math
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
import bpy
|
|
from mathutils import Vector
|
|
|
|
from .utilities.async_utils import progress_async
|
|
from .utilities.bounds_utils import get_bounds_multiple
|
|
from .utilities.image_utils import (
|
|
get_cutter_array,
|
|
numpy_save,
|
|
)
|
|
from .utilities.operation_utils import get_operation_sources
|
|
from .utilities.simple_utils import get_simulation_path
|
|
|
|
|
|
def create_simulation_object(name, operations, i):
|
|
"""Create a simulation object in Blender.
|
|
This function creates a simulation object in Blender with the specified
|
|
name and operations. If an object with the given name already exists, it
|
|
retrieves that object; otherwise, it creates a new plane object and
|
|
applies several modifiers to it. The function also sets the object's
|
|
location and scale based on the provided operations and assigns a
|
|
texture to the object.
|
|
Args:
|
|
name (str): The name of the simulation object to be created.
|
|
operations (list): A list of operation objects that contain bounding box information.
|
|
i: The image to be used as a texture for the simulation object.
|
|
"""
|
|
|
|
oname = "csim_" + name
|
|
|
|
o = operations[0]
|
|
|
|
if oname in bpy.data.objects:
|
|
ob = bpy.data.objects[oname]
|
|
else:
|
|
bpy.ops.mesh.primitive_plane_add(
|
|
align="WORLD", enter_editmode=False, location=(0, 0, 0), rotation=(0, 0, 0)
|
|
)
|
|
ob = bpy.context.active_object
|
|
ob.name = oname
|
|
|
|
bpy.ops.object.modifier_add(type="SUBSURF")
|
|
ss = ob.modifiers[-1]
|
|
ss.subdivision_type = "SIMPLE"
|
|
ss.levels = 6
|
|
ss.render_levels = 6
|
|
bpy.ops.object.modifier_add(type="SUBSURF")
|
|
ss = ob.modifiers[-1]
|
|
ss.subdivision_type = "SIMPLE"
|
|
ss.levels = 4
|
|
ss.render_levels = 3
|
|
bpy.ops.object.modifier_add(type="DISPLACE")
|
|
|
|
ob.location = ((o.max.x + o.min.x) / 2, (o.max.y + o.min.y) / 2, o.min.z)
|
|
ob.scale.x = (o.max.x - o.min.x) / 2
|
|
ob.scale.y = (o.max.y - o.min.y) / 2
|
|
print(o.max.x, o.min.x)
|
|
print(o.max.y, o.min.y)
|
|
print("Bounds")
|
|
disp = ob.modifiers[-1]
|
|
disp.direction = "Z"
|
|
disp.texture_coords = "LOCAL"
|
|
disp.mid_level = 0
|
|
|
|
if oname in bpy.data.textures:
|
|
t = bpy.data.textures[oname]
|
|
|
|
t.type = "IMAGE"
|
|
disp.texture = t
|
|
|
|
t.image = i
|
|
else:
|
|
bpy.ops.texture.new()
|
|
for t in bpy.data.textures:
|
|
if t.name == "Texture":
|
|
t.type = "IMAGE"
|
|
t.name = oname
|
|
t = t.type_recast()
|
|
t.type = "IMAGE"
|
|
t.image = i
|
|
disp.texture = t
|
|
ob.hide_render = True
|
|
bpy.ops.object.shade_smooth()
|
|
|
|
|
|
async def do_simulation(name, operations):
|
|
"""Perform simulation of operations for a 3-axis system.
|
|
This function iterates through a list of operations, retrieves the
|
|
necessary sources for each operation, and computes the bounds for the
|
|
operations. It then generates a simulation image based on the operations
|
|
and their limits, saves the image to a specified path, and finally
|
|
creates a simulation object in Blender using the generated image.
|
|
Args:
|
|
name (str): The name to be used for the simulation object.
|
|
operations (list): A list of operations to be simulated.
|
|
"""
|
|
for o in operations:
|
|
get_operation_sources(o)
|
|
limits = get_bounds_multiple(
|
|
operations
|
|
) # this is here because some background computed operations still didn't have bounds data
|
|
i = await generate_simulation_image(operations, limits)
|
|
# cp = getCachePath(operations[0])[:-len(operations[0].name)] + name
|
|
cp = get_simulation_path() + name
|
|
|
|
print("cp=", cp)
|
|
iname = cp + "_sim.exr"
|
|
|
|
numpy_save(i, iname)
|
|
|
|
i = bpy.data.images.load(iname)
|
|
|
|
create_simulation_object(name, operations, i)
|
|
|
|
|
|
async def generate_simulation_image(operations, limits):
|
|
"""Generate a simulation image based on provided operations and limits.
|
|
This function creates a 2D simulation image by processing a series of
|
|
operations that define how the simulation should be conducted. It uses
|
|
the limits provided to determine the boundaries of the simulation area.
|
|
The function calculates the necessary resolution for the simulation
|
|
image based on the specified simulation detail and border width. It
|
|
iterates through each operation, simulating the effect of each operation
|
|
on the image, and updates the shape keys of the corresponding Blender
|
|
object to reflect the simulation results. The final output is a 2D array
|
|
representing the simulated image.
|
|
|
|
Args:
|
|
operations (list): A list of operation objects that contain details
|
|
about the simulation, including feed rates and other parameters.
|
|
limits (tuple): A tuple containing the minimum and maximum coordinates
|
|
(minx, miny, minz, maxx, maxy, maxz) that define the simulation
|
|
boundaries.
|
|
|
|
Returns:
|
|
np.ndarray: A 2D array representing the simulated image.
|
|
"""
|
|
|
|
minx, miny, minz, maxx, maxy, maxz = limits
|
|
# print(minx,miny,minz,maxx,maxy,maxz)
|
|
sx = maxx - minx
|
|
sy = maxy - miny
|
|
|
|
o = operations[0] # getting sim detail and others from first op.
|
|
simulation_detail = o.optimisation.simulation_detail
|
|
borderwidth = o.borderwidth
|
|
resx = math.ceil(sx / simulation_detail) + 2 * borderwidth
|
|
resy = math.ceil(sy / simulation_detail) + 2 * borderwidth
|
|
|
|
# create array in which simulation happens, similar to an image to be painted in.
|
|
si = np.full(shape=(resx, resy), fill_value=maxz, dtype=float)
|
|
|
|
num_operations = len(operations)
|
|
|
|
start_time = time.time()
|
|
|
|
for op_count, o in enumerate(operations):
|
|
ob = bpy.data.objects["cam_path_{}".format(o.name)]
|
|
m = ob.data
|
|
verts = m.vertices
|
|
|
|
if o.do_simulation_feedrate:
|
|
kname = "feedrates"
|
|
m.attributes.new(".edge_creases", "FLOAT", "EDGE")
|
|
|
|
if m.shape_keys is None or m.shape_keys.key_blocks.find(kname) == -1:
|
|
ob.shape_key_add()
|
|
if len(m.shape_keys.key_blocks) == 1:
|
|
ob.shape_key_add()
|
|
shapek = m.shape_keys.key_blocks[-1]
|
|
shapek.name = kname
|
|
else:
|
|
shapek = m.shape_keys.key_blocks[kname]
|
|
shapek.data[0].co = (0.0, 0, 0)
|
|
|
|
totalvolume = 0.0
|
|
|
|
cutterArray = get_cutter_array(o, simulation_detail)
|
|
|
|
cutterArray = -cutterArray
|
|
lasts = verts[1].co
|
|
perc = -1
|
|
vtotal = len(verts)
|
|
dropped = 0
|
|
|
|
xs = 0
|
|
ys = 0
|
|
|
|
for i, vert in enumerate(verts):
|
|
if perc != int(100 * i / vtotal):
|
|
perc = int(100 * i / vtotal)
|
|
total_perc = (perc + op_count * 100) / num_operations
|
|
await progress_async(f"Simulation", int(total_perc))
|
|
|
|
if i > 0:
|
|
volume = 0
|
|
volume_partial = 0
|
|
s = vert.co
|
|
v = s - lasts
|
|
|
|
l = v.length
|
|
if (lasts.z < maxz or s.z < maxz) and not (
|
|
v.x == 0 and v.y == 0 and v.z > 0
|
|
): # only simulate inside material, and exclude lift-ups
|
|
if v.x == 0 and v.y == 0 and v.z < 0:
|
|
# if the cutter goes straight down, we don't have to interpolate.
|
|
pass
|
|
|
|
elif v.length > simulation_detail: # and not :
|
|
v.length = simulation_detail
|
|
lastxs = xs
|
|
lastys = ys
|
|
while v.length < l:
|
|
xs = int(
|
|
(lasts.x + v.x - minx) / simulation_detail
|
|
+ borderwidth
|
|
+ simulation_detail / 2
|
|
)
|
|
# -middle
|
|
ys = int(
|
|
(lasts.y + v.y - miny) / simulation_detail
|
|
+ borderwidth
|
|
+ simulation_detail / 2
|
|
)
|
|
# -middle
|
|
z = lasts.z + v.z
|
|
# print(z)
|
|
if lastxs != xs or lastys != ys:
|
|
volume_partial = sim_cutter_spot(
|
|
xs, ys, z, cutterArray, si, o.do_simulation_feedrate
|
|
)
|
|
if o.do_simulation_feedrate:
|
|
totalvolume += volume
|
|
volume += volume_partial
|
|
lastxs = xs
|
|
lastys = ys
|
|
else:
|
|
dropped += 1
|
|
v.length += simulation_detail
|
|
|
|
xs = int(
|
|
(s.x - minx) / simulation_detail + borderwidth + simulation_detail / 2
|
|
) # -middle
|
|
ys = int(
|
|
(s.y - miny) / simulation_detail + borderwidth + simulation_detail / 2
|
|
) # -middle
|
|
volume_partial = sim_cutter_spot(
|
|
xs, ys, s.z, cutterArray, si, o.do_simulation_feedrate
|
|
)
|
|
if o.do_simulation_feedrate: # compute volumes and write data into shapekey.
|
|
volume += volume_partial
|
|
totalvolume += volume
|
|
if l > 0:
|
|
load = volume / l
|
|
else:
|
|
load = 0
|
|
|
|
# this will show the shapekey as debugging graph and will use same data to estimate parts
|
|
# with heavy load
|
|
if l != 0:
|
|
shapek.data[i].co.y = (load) * 0.000002
|
|
else:
|
|
shapek.data[i].co.y = shapek.data[i - 1].co.y
|
|
shapek.data[i].co.x = shapek.data[i - 1].co.x + l * 0.04
|
|
shapek.data[i].co.z = 0
|
|
lasts = s
|
|
|
|
# print('dropped '+str(dropped))
|
|
if o.do_simulation_feedrate: # smoothing ,but only backward!
|
|
xcoef = shapek.data[len(shapek.data) - 1].co.x / len(shapek.data)
|
|
for a in range(0, 10):
|
|
# print(shapek.data[-1].co)
|
|
nvals = []
|
|
val1 = 0 #
|
|
val2 = 0
|
|
w1 = 0 #
|
|
w2 = 0
|
|
|
|
for i, d in enumerate(shapek.data):
|
|
val = d.co.y
|
|
|
|
if i > 1:
|
|
d1 = shapek.data[i - 1].co
|
|
val1 = d1.y
|
|
if d1.x - d.co.x != 0:
|
|
w1 = 1 / (abs(d1.x - d.co.x) / xcoef)
|
|
|
|
if i < len(shapek.data) - 1:
|
|
d2 = shapek.data[i + 1].co
|
|
val2 = d2.y
|
|
if d2.x - d.co.x != 0:
|
|
w2 = 1 / (abs(d2.x - d.co.x) / xcoef)
|
|
|
|
# print(val,val1,val2,w1,w2)
|
|
|
|
val = (val + val1 * w1 + val2 * w2) / (1.0 + w1 + w2)
|
|
nvals.append(val)
|
|
for i, d in enumerate(shapek.data):
|
|
d.co.y = nvals[i]
|
|
|
|
# apply mapping - convert the values to actual feedrates.
|
|
total_load = 0
|
|
max_load = 0
|
|
for i, d in enumerate(shapek.data):
|
|
total_load += d.co.y
|
|
max_load = max(max_load, d.co.y)
|
|
normal_load = total_load / len(shapek.data)
|
|
|
|
thres = 0.5
|
|
|
|
scale_graph = 0.05 # warning this has to be same as in export in utils!!!!
|
|
|
|
totverts = len(shapek.data)
|
|
for i, d in enumerate(shapek.data):
|
|
if d.co.y > normal_load:
|
|
d.co.z = scale_graph * max(0.3, normal_load / d.co.y)
|
|
else:
|
|
d.co.z = scale_graph * 1
|
|
if i < totverts - 1:
|
|
m.attributes[".edge_creases"].data[i].value = d.co.y / (normal_load * 4)
|
|
|
|
si = si[borderwidth:-borderwidth, borderwidth:-borderwidth]
|
|
si += -minz
|
|
|
|
await progress_async("Simulated:", time.time() - start_time, "s")
|
|
return si
|
|
|
|
|
|
def sim_cutter_spot(xs, ys, z, cutterArray, si, getvolume=False):
|
|
"""Simulates a cutter cutting into stock and optionally returns the volume
|
|
removed.
|
|
|
|
This function takes the position of a cutter and modifies a stock image
|
|
by simulating the cutting process. It updates the stock image based on
|
|
the cutter's dimensions and position, ensuring that the stock does not
|
|
go below a certain level defined by the cutter's height. If requested,
|
|
it also calculates and returns the volume of material that has been
|
|
milled away.
|
|
|
|
Args:
|
|
xs (int): The x-coordinate of the cutter's position.
|
|
ys (int): The y-coordinate of the cutter's position.
|
|
z (float): The height of the cutter.
|
|
cutterArray (numpy.ndarray): A 2D array representing the cutter's shape.
|
|
si (numpy.ndarray): A 2D array representing the stock image to be modified.
|
|
getvolume (bool?): If True, the function returns the volume removed. Defaults to False.
|
|
|
|
Returns:
|
|
float: The volume of material removed if `getvolume` is True; otherwise,
|
|
returns 0.
|
|
"""
|
|
m = int(cutterArray.shape[0] / 2)
|
|
size = cutterArray.shape[0]
|
|
# whole cutter in image there
|
|
if xs > m and xs < si.shape[0] - m and ys > m and ys < si.shape[1] - m:
|
|
if getvolume:
|
|
volarray = si[xs - m : xs - m + size, ys - m : ys - m + size].copy()
|
|
si[xs - m : xs - m + size, ys - m : ys - m + size] = np.minimum(
|
|
si[xs - m : xs - m + size, ys - m : ys - m + size], cutterArray + z
|
|
)
|
|
if getvolume:
|
|
volarray = si[xs - m : xs - m + size, ys - m : ys - m + size] - volarray
|
|
vsum = abs(volarray.sum())
|
|
# print(vsum)
|
|
return vsum
|
|
|
|
elif xs > -m and xs < si.shape[0] + m and ys > -m and ys < si.shape[1] + m:
|
|
# part of cutter in image, for extra large cutters
|
|
|
|
startx = max(0, xs - m)
|
|
starty = max(0, ys - m)
|
|
endx = min(si.shape[0], xs - m + size)
|
|
endy = min(si.shape[0], ys - m + size)
|
|
castartx = max(0, m - xs)
|
|
castarty = max(0, m - ys)
|
|
caendx = min(size, si.shape[0] - xs + m)
|
|
caendy = min(size, si.shape[1] - ys + m)
|
|
|
|
if getvolume:
|
|
volarray = si[startx:endx, starty:endy].copy()
|
|
si[startx:endx, starty:endy] = np.minimum(
|
|
si[startx:endx, starty:endy], cutterArray[castartx:caendx, castarty:caendy] + z
|
|
)
|
|
if getvolume:
|
|
volarray = si[startx:endx, starty:endy] - volarray
|
|
vsum = abs(volarray.sum())
|
|
# print(vsum)
|
|
return vsum
|
|
return 0
|