#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright © 2019 Michael J. Hayford
"""
.. Created on Wed Oct 16 14:20:49 2019
.. codeauthor: Michael J. Hayford
"""
import math
import numpy as np
from copy import deepcopy
from rayoptics.gui.util import bbox_from_poly, fit_data_range
from rayoptics.gui.actions import ReplaceGlassAction
import rayoptics.optical.model_constants as mc
from rayoptics.util.rgb2mpl import rgb2mpl
from rayoptics.util.misc_math import (normalize, distance_sqr_2d,
projected_point_on_radial_line)
from rayoptics.util.line_intersection import get_intersect
from rayoptics.util import misc_math
from rayoptics.util import colors
from rayoptics.gui.util import calc_render_color_for_material
from rayoptics.parax import paraxialdesign
# --- color management
dgm_lw = {
'data': 2,
'data_hilite': 3,
'line': 0.5,
'hilite': 2,
'guide': 1,
'edge': 0.5,
'node': 6,
'node_hilite': 8,
'node_edge': 0.75,
}
[docs]def light_or_dark(is_dark=True):
accent = colors.accent_colors(is_dark)
fb = colors.foreground_background(is_dark)
rgb = {
'node': fb['foreground'],
'edge': fb['foreground'],
# 'node': accent['cyan'],
# 'edge': accent['cyan'],
'slide': accent['blue'],
'object_image': accent['magenta'],
'stop': accent['magenta'],
'conj_line': accent['orange'],
'shift': fb['foreground'],
'barrel': accent['green'],
}
return {**rgb, **fb}
[docs]class Diagram():
""" class for paraxial ray rendering/editing """
def __init__(self, opt_model, parax_model, parax_model_key, dgm_type,
seq_start=1, do_barrel_constraint=False, barrel_constraint=1.0,
label='paraxial', bend_or_gap='bend', do_node_annotation=False,
is_dark=True):
self.label = label
self.opt_model = opt_model
self.parax_model = parax_model
self.dgm_rgb = light_or_dark(is_dark=is_dark)
self.dgm_type = dgm_type
self.setup_dgm_type(dgm_type)
self.do_barrel_constraint = do_barrel_constraint
self.barrel_constraint_radius = barrel_constraint
self.do_node_annotation = do_node_annotation
self.bend_or_gap = bend_or_gap
self.active_layer = parax_model_key
[docs] def setup_dgm_type(self, dgm_type):
if dgm_type == 'ht':
self.type_sel = mc.ht
elif dgm_type == 'slp':
self.type_sel = mc.slp
pm = self.parax_model
self._apply_data = pm.update_composite_node_fct(self.type_sel)
[docs] def get_label(self):
return self.label
[docs] def sync_light_or_dark(self, is_dark):
self.dgm_rgb = light_or_dark(is_dark)
[docs] def set_active_layer(self, layer_key):
opm = self.opt_model
prx = paraxialdesign.update_diagram_for_key(opm, layer_key,
self.type_sel)
prx_key, prx_model = prx
self.active_layer = prx_key
self.parax_model = prx_model
self._apply_data = prx_model.update_composite_node_fct(self.type_sel)
return self
[docs] def update_data(self, fig, **kwargs):
parax_model = self.parax_model
self.shape = self.render_shape()
self.shape_bbox = bbox_from_poly(self.shape)
build = kwargs.get('build', fig.build)
if build == 'update':
# just a change in node positions - handled above
# self.shape = self.render_shape()
pass
else:
# number of nodes have changed, rebuild everything
num_nodes = len(parax_model.sys)
if self.dgm_type == 'slp':
num_nodes -= 1
self.node_list = []
for i in range(num_nodes):
self.node_list.append(DiagramNode(self, i))
self.edge_list = []
for i in range(num_nodes-1):
self.edge_list.append(DiagramEdge(self, i))
self.object_shift = ConjugateLine(self, 'object_image')
if self.opt_model.seq_model.stop_surface is None:
# stop shift conjugate line is only editable for floating stop
self.stop_shift = ConjugateLine(self, 'stop')
if self.do_barrel_constraint:
self.barrel_constraint = BarrelConstraint(self)
if self.do_node_annotation:
pass
self.node_bbox = fig.update_patches(self.node_list)
self.edge_bbox = fig.update_patches(self.edge_list)
self.object_shift_bbox = fig.update_patches([self.object_shift])
if self.opt_model.seq_model.stop_surface is None:
self.stop_shift_bbox = fig.update_patches([self.stop_shift])
if self.do_barrel_constraint:
self.barrel_bbox = fig.update_patches([self.barrel_constraint])
return self.shape_bbox
[docs] def apply_data(self, node, vertex):
self._apply_data(node, vertex)
self.opt_model.parax_model.paraxial_lens_to_seq_model()
[docs] def assign_object_to_node(self, node, factory, **kwargs):
parax_model = self.parax_model
inputs = parax_model.assign_object_to_node(node, factory, **kwargs)
return inputs
[docs] def register_commands(self, *args, **inputs):
fig = inputs.pop('figure')
self.command_inputs = dict(inputs)
def do_command_action(event, target, event_key):
nonlocal fig
if target is not None:
shape, handle = target.artist.shape
try:
# print(type(shape).__name__, shape.node, handle, event_key)
handle_action_obj = shape.actions[handle]
if isinstance(handle_action_obj, dict):
handle_action_obj[event_key](fig, event)
else:
handle_action_obj.actions[event_key](fig, event)
except KeyError:
pass
fig.do_action = do_command_action
[docs] def register_add_replace_element(self, *args, **inputs):
fig = inputs.pop('figure')
self.command_inputs = dict(inputs)
action_obj = AddReplaceElementAction(self, **inputs)
def do_command_action(event, target, event_key):
nonlocal fig
shape, handle = target.artist.shape
if isinstance(shape, DiagramNode) or \
isinstance(shape, DiagramEdge):
try:
action_obj.actions[event_key](fig, event, shape)
except KeyError:
pass
fig.do_action = do_command_action
[docs] def render_shape(self):
""" render the diagram into a shape list """
parax_model = self.parax_model
shape = []
for x, y in zip(parax_model.pr, parax_model.ax):
shape.append([x[self.type_sel], y[self.type_sel]])
shape = np.array(shape)
return shape
[docs] def update_diagram_from_shape(self, shape):
''' use the shape list to update the paraxial model '''
parax_model = self.parax_model
for x, y, shp in zip(parax_model.pr, parax_model.ax, shape):
x[self.type_sel] = shp[0]
y[self.type_sel] = shp[1]
[docs] def fit_axis_limits(self):
''' define diagram axis limits as the extent of the shape polygon '''
x_min, x_max = fit_data_range([x[0] for x in self.shape])
y_min, y_max = fit_data_range([x[1] for x in self.shape])
return np.array([[x_min, y_min], [x_max, y_max]])
[docs]def compute_slide_line(shape, node, imode):
""" compute a constraint line to keep the overall length of the airspaces
surrounding `node` constant
"""
def distance_sqr(pt):
return pt[0]**2 + pt[1]**2
num_nodes = len(shape)
if node > 0 and node < num_nodes-1:
pt0 = shape[node-1]
pt1 = shape[node]
pt2 = shape[node+1]
if imode == 'transmit':
# if transmitting, constrain movement of node to a line parallel
# to the line between the previous and next nodes.
origin2line = misc_math.perpendicular_from_origin(pt0, pt2)
pt2line = misc_math.perpendicular_to_line(pt1, pt0, pt2)
scale_factor = (origin2line - pt2line)/origin2line
new_pt0 = scale_factor*pt0
new_pt2 = scale_factor*pt2
return new_pt0, new_pt2
elif imode == 'reflect':
# if reflecting, constrain movement to a radial line from the
# origin through the selected node
origin = np.array([0., 0.])
dist_pt0 = distance_sqr(pt0)
dist_pt1 = distance_sqr(pt1)
dist_pt2 = distance_sqr(pt2)
# figure out how long a line to draw
dist = dist_pt0 if dist_pt0 > dist_pt1 else dist_pt1
dist = dist_pt2 if dist_pt2 > dist else dist
scale_factor = math.sqrt(dist/dist_pt1)
new_pt1 = scale_factor*pt1
return origin, new_pt1
return None
[docs]def constrain_to_line_action(pt0, pt2):
def constrain_to_line(input_pt):
""" constrain the input point to the line pt0 to pt2 """
output_pt = misc_math.projected_point_on_line(input_pt, pt0, pt2)
return output_pt
return constrain_to_line
[docs]class DiagramNode():
def __init__(self, diagram, idx):
self.diagram = diagram
self.node = idx
self.select_pt = None
self.move_direction = None
self.handles = {}
self.actions = self.handle_actions()
[docs] def update_shape(self, view):
diagram = self.diagram
# set alpha to 25% -> #40
bkgrnd_rbga = diagram.dgm_rgb['background1'] + '40'
opt_model = diagram.opt_model
shape = diagram.shape
dgm_rgb = diagram.dgm_rgb
if self.diagram.dgm_type == 'ht':
hilite_kwargs = {
'color': dgm_rgb['node'],
'markersize': dgm_lw['node_hilite'],
}
self.handles['shape'] = (shape[self.node], 'vertex',
{'linestyle': '',
'linewidth': dgm_lw['data'],
'marker': 'o',
'markersize': dgm_lw['node'],
'pickradius': 6,
'color': dgm_rgb['node'],
'hilite': dgm_rgb['hilite'],
'zorder': 3.})
# define the "constant spacing" or "slide" constraint
if view.enable_slide:
sys = self.diagram.parax_model.sys
slide_pts = compute_slide_line(shape, self.node,
sys[self.node][mc.rmd])
if slide_pts is not None:
seg = [*slide_pts]
hilite_kwargs = {
'color': dgm_rgb['slide'],
'linewidth': dgm_lw['guide'],
'linestyle': '-'
}
self.handles['slide'] = (seg, 'polyline',
{'linestyle': '--',
'linewidth': dgm_lw['guide'],
'pickradius': 6,
'color': dgm_rgb['slide'],
'hilite': hilite_kwargs,
'zorder': 2.5})
elif self.diagram.dgm_type == 'slp':
hilite_kwargs = {
'color': dgm_rgb['node'],
'markersize': dgm_lw['node_hilite'],
}
gap, z_dir = diagram.parax_model.get_gap_for_node(self.node, 'slp')
e_node = opt_model.part_tree.parent_node((gap, z_dir),
'#element#airgap')
e = e_node.id if e_node else None
marker_color = bkgrnd_rbga
if e and '#airgap' not in e_node.tag:
marker_color = calc_render_color_for_material(gap.medium)
marker_color = rgb2mpl(marker_color)
self.handles['shape'] = (shape[self.node], 'vertex',
{'linestyle': '',
'linewidth': dgm_lw['data'],
'marker': 'o',
'markersize': dgm_lw['node'],
'markeredgewidth': dgm_lw['node_edge'],
'markeredgecolor': dgm_rgb['node'],
'pickradius': 6,
'markerfacecolor': marker_color,
'hilite': dgm_rgb['hilite'],
'zorder': 3.})
return view.create_patches(self.handles)
[docs] def get_label(self):
return 'node' + str(self.node)
[docs] def handle_actions(self):
actions = {}
wedge_constraint = True if self.diagram.dgm_type == 'ht' else False
actions['shape'] = EditNodeAction(self,
constrain_to_wedge=wedge_constraint)
slide_filter = None
sys = self.diagram.parax_model.sys
slide_pts = compute_slide_line(self.diagram.shape, self.node,
sys[self.node][mc.rmd])
if slide_pts is not None:
slide_filter = constrain_to_line_action(*slide_pts)
actions['slide'] = EditNodeAction(self, filter=slide_filter,
constrain_to_wedge=wedge_constraint)
return actions
[docs]class DiagramEdge():
def __init__(self, diagram, idx):
self.diagram = diagram
self.node = idx
self.select_pt = None
self.move_direction = None
self.handles = {}
self.actions = self.handle_actions()
[docs] def update_shape(self, view):
shape = self.diagram.shape
dgm_rgb = self.diagram.dgm_rgb
edge_poly = shape[self.node:self.node+2]
self.handles['shape'] = (edge_poly, 'polyline',
{'pickradius': 6,
'linewidth': dgm_lw['data'],
'hilite_linewidth': dgm_lw['data_hilite'],
'color': dgm_rgb['edge'],
'hilite': dgm_rgb['hilite'],
'zorder': 2.})
area_poly = [[0, 0]]
area_poly.extend(edge_poly)
fill_color = self.render_color()
self.handles['area'] = (area_poly, 'polygon',
{'fill_color': fill_color,
'zorder': 1.})
return view.create_patches(self.handles)
[docs] def render_color(self):
diagram = self.diagram
opt_model = diagram.opt_model
# set alpha to 25% -> #40
bkgrnd_rbga = diagram.dgm_rgb['background1'] + '40'
if diagram.dgm_type == 'ht':
gap, z_dir = diagram.parax_model.get_gap_for_node(self.node, 'ht')
e_node = opt_model.part_tree.parent_node((gap, z_dir),
'#element#airgap')
e = e_node.id if e_node else None
if e:
if '#airgap' in e_node.tag:
return bkgrnd_rbga
else:
return calc_render_color_for_material(gap.medium)
else:
# single surface element, like mirror or thinlens, use airgap
return bkgrnd_rbga
elif diagram.dgm_type == 'slp':
return bkgrnd_rbga
[docs] def get_label(self):
return 'edge' + str(self.node)
[docs] def handle_actions(self):
actions = {}
actions['shape'] = EditLensAction(self)
actions['area'] = EditAreaAction(self)
return actions
# --- Editing actions
[docs]class BarrelConstraint():
def __init__(self, diagram):
self.diagram = diagram
self.handles = {}
self.actions = self.handle_actions()
[docs] def update_shape(self, view):
dgm_rgb = self.diagram.dgm_rgb
barrel_radius = self.diagram.barrel_constraint_radius
diamond = []
diamond.append([0., barrel_radius])
diamond.append([barrel_radius, 0.])
diamond.append([0., -barrel_radius])
diamond.append([-barrel_radius, 0.])
diamond.append([0., barrel_radius])
diamond = np.array(diamond)
self.handles['shape'] = (diamond, 'polyline',
{'color': dgm_rgb['barrel'],
'linewidth': dgm_lw['guide'],
'zorder': 1.})
square = []
square.append([ barrel_radius, barrel_radius])
square.append([-barrel_radius, barrel_radius])
square.append([-barrel_radius, -barrel_radius])
square.append([ barrel_radius, -barrel_radius])
square.append([ barrel_radius, barrel_radius])
square = np.array(square)
self.handles['square'] = (square, 'polyline',
{'color': dgm_rgb['barrel'],
'linewidth': dgm_lw['guide'],
'zorder': 1.})
return view.create_patches(self.handles)
[docs] def get_label(self):
return 'barrel constraint'
[docs] def handle_actions(self):
actions = {}
return actions
[docs]class ConjugateLine():
def __init__(self, diagram, line_type):
self.diagram = diagram
self.line_type = line_type
self.sys_orig = []
self.shape_orig = []
self.handles = {}
self.actions = self.handle_actions()
[docs] def update_shape(self, view):
dgm_rgb = self.diagram.dgm_rgb
shape_bbox = self.diagram.shape_bbox
line = []
if self.line_type == 'stop':
ht = shape_bbox[1][1] - shape_bbox[0][1]
line.append([0., -2*ht])
line.append([0., 2*ht])
color = dgm_rgb['stop']
elif self.line_type == 'object_image':
wid = shape_bbox[1][0] - shape_bbox[0][0]
line.append([-2*wid, 0.])
line.append([2*wid, 0.])
color = dgm_rgb['object_image']
self.handles['shape'] = (line, 'polyline',
{'color': dgm_rgb['foreground'],
'hilite': color,
'zorder': 1.})
if len(self.shape_orig) > 0:
conj_line = []
if self.line_type == 'stop':
lwr, upr = view.ax.get_ybound()
ht = upr - lwr
conj_line.append([-self.k*ht, -ht])
conj_line.append([self.k*ht, ht])
elif self.line_type == 'object_image':
lwr, upr = view.ax.get_xbound()
wid = upr - lwr
conj_line.append([-wid, -self.k*wid])
conj_line.append([wid, self.k*wid])
self.handles['conj_line'] = (conj_line, 'polyline',
{'color': dgm_rgb['conj_line'],
'linewidth': dgm_lw['guide'],
'zorder': 1.})
self.handles['shift'] = (self.shape_orig, 'polyline',
{'color': dgm_rgb['shift'],
'linewidth': 1.5,
'zorder': 1.})
return view.create_patches(self.handles)
[docs] def get_label(self):
if self.line_type == 'stop':
return 'stop shift line'
elif self.line_type == 'object_image':
return 'object shift line'
else:
return ''
[docs] def edit_conjugate_line_actions(self):
dgm = self.diagram
pm = dgm.parax_model
def calculate_slope(x, y):
''' x=ybar, y=y '''
if self.line_type == 'stop':
k = x/y
return k, np.array([[1, 0], [-k, 1]])
elif self.line_type == 'object_image':
k = y/x
return k, np.array([[1, -k], [0, 1]])
else:
k = 0
return 0, np.array([[1, 0], [0, 1]])
def apply_data(event_data):
self.k, mat = calculate_slope(event_data[0], event_data[1])
pm.apply_conjugate_shift(self.shape_orig, self.k, mat,
self.line_type)
def on_select(fig, event):
self.sys_orig = deepcopy(pm.sys)
self.shape_orig = deepcopy(dgm.shape)
def on_edit(fig, event):
if event.xdata is not None and event.ydata is not None:
event_data = np.array([event.xdata, event.ydata])
apply_data(event_data)
fig.build = 'update'
fig.refresh_gui(build='update', src_model=pm)
def on_release(fig, event):
event_data = np.array([event.xdata, event.ydata])
apply_data(event_data)
self.sys_orig = []
self.shape_orig = []
fig.refresh_gui(build='rebuild', src_model=pm)
actions = {}
actions['press'] = on_select
actions['drag'] = on_edit
actions['release'] = on_release
return actions
[docs] def handle_actions(self):
actions = {}
actions['shape'] = self.edit_conjugate_line_actions()
return actions
[docs]class EditNodeAction():
""" Action to move a diagram node, using an input pt """
def __init__(self, dgm_node, filter=None, constrain_to_wedge=True):
diagram = dgm_node.diagram
parax_model = diagram.parax_model
self.cur_node = None
self.pt0 = None
self.pt2 = None
self.filter = filter
self.constrain_to_wedge = constrain_to_wedge
def point_on_line(pt1, pt2, t):
d = pt2 - pt1
return pt1 + t*d
def do_constrain_to_wedge(input_pt):
""" keep the input point inside the wedge of adjacent points """
if self.pt0 is not None:
x_prod0 = input_pt[0]*self.pt0[1] - self.pt0[0]*input_pt[1]
if x_prod0 < 0:
# pin to boundary
output_pt = projected_point_on_radial_line(input_pt,
self.pt0)
return output_pt
if self.pt2 is not None:
x_prod2 = input_pt[0]*self.pt2[1] - self.pt2[0]*input_pt[1]
if x_prod2 > 0:
# pin to boundary
output_pt = projected_point_on_radial_line(input_pt,
self.pt2)
return output_pt
return input_pt
def on_select(fig, event):
nonlocal diagram
# we don't allow points to move onto their adjacent neighbors. Use
# a buffer amount when constraining to the wedge
buffer_fraction = 0.0025
self.cur_node = dgm_node.node
pt1 = diagram.shape[self.cur_node]
if self.cur_node == 0:
pt2 = diagram.shape[self.cur_node+1]
self.pt2 = point_on_line(pt1, pt2, 1-buffer_fraction)
self.pt0 = None
elif self.cur_node == len(diagram.shape)-1:
pt0 = diagram.shape[self.cur_node-1]
self.pt0 = point_on_line(pt0, pt1, buffer_fraction)
self.pt2 = None
else:
pt0 = diagram.shape[self.cur_node-1]
self.pt0 = point_on_line(pt0, pt1, buffer_fraction)
pt2 = diagram.shape[self.cur_node+1]
self.pt2 = point_on_line(pt1, pt2, 1-buffer_fraction)
def on_edit(fig, event):
if event.xdata is not None and event.ydata is not None:
event_data = np.array([event.xdata, event.ydata])
if self.filter:
event_data = self.filter(event_data)
if self.constrain_to_wedge:
event_data = do_constrain_to_wedge(event_data)
diagram.apply_data(self.cur_node, event_data)
fig.build = 'update'
fig.refresh_gui(build='update', src_model=parax_model)
def on_release(fig, event):
if event.xdata is not None and event.ydata is not None:
event_data = np.array([event.xdata, event.ydata])
if self.filter:
event_data = self.filter(event_data)
if self.constrain_to_wedge:
event_data = do_constrain_to_wedge(event_data)
diagram.apply_data(self.cur_node, event_data)
fig.build = 'rebuild'
fig.refresh_gui(build='rebuild', src_model=parax_model)
self.cur_node = None
self.actions = {}
self.actions['drag'] = on_edit
self.actions['press'] = on_select
self.actions['release'] = on_release
[docs]class EditLensAction():
""" Action for diagram edge, using an input pt
This is a simple wrapper class to choose the correct action, i.e. bending
or thickness change, depending on the UI setting.
"""
def __init__(self, dgm_edge):
diagram = dgm_edge.diagram
actions = {}
actions['gap'] = EditThicknessAction(dgm_edge)
actions['bend'] = EditBendingAction(dgm_edge)
def create_dispatch_action(event_key):
def dispatch_action(fig, event):
actions[diagram.bend_or_gap].actions[event_key](fig, event)
return dispatch_action
self.actions = {}
self.actions['press'] = create_dispatch_action('press')
self.actions['drag'] = create_dispatch_action('drag')
self.actions['release'] = create_dispatch_action('release')
[docs]class EditAreaAction():
""" Action for diagram area, placeholder for now
This is a simple wrapper class to choose the correct action, i.e. bending
or thickness change, depending on the UI setting.
"""
def __init__(self, dgm_edge):
# actions['power'] = EditPowerAction(dgm_edge)
# actions['bend'] = EditBendingAction(dgm_edge)
def create_dispatch_action(event_key):
def dispatch_action(fig, event):
pass
# actions[diagram.bend_or_gap].actions[event_key](fig, event)
return dispatch_action
self.actions = {}
self.actions['press'] = create_dispatch_action('press')
self.actions['drag'] = create_dispatch_action('drag')
self.actions['release'] = create_dispatch_action('release')
[docs]class EditThicknessAction():
""" Action to move a diagram edge, using an input pt
The movement is constrained to be parallel to the original edge. By doing
this the power and bending of the element remains constant, while the
element thickness changes. Movement of the edge is limited to keep
the thickness greater than zero and not to interfere with adjacent spaces.
"""
def __init__(self, dgm_edge):
diagram = dgm_edge.diagram
parax_model = diagram.parax_model
self.node = None
self.bundle = None
def on_select(fig, event):
nonlocal diagram
shape = diagram.shape
self.node = node = dgm_edge.node
if node > 0 and node < (len(shape)-2):
# get the virtual vertex of the combined element surfaces
vertex = np.array(get_intersect(shape[node-1], shape[node],
shape[node+1], shape[node+2]))
# get direction cosines for the edge
edge = normalize(shape[node+1] - shape[node])
# construct perpendicular to the edge. use this to define a
# range for allowed inputs
perp_edge = np.array([edge[1], -edge[0]])
self.bundle = vertex, edge, perp_edge
# measure distances along the perpendicular thru the vertex
# and project the 2 outer nodes and the first vertex of the
# edge
pp0 = np.dot(shape[node-1]-vertex, perp_edge)
pp1 = np.dot(shape[node]-vertex, perp_edge)
pp3 = np.dot(shape[node+2]-vertex, perp_edge)
# use the first edge vertex to calculate an allowed input range
if pp1 > 0:
self.pp_lim = 0, min(i for i in (pp0, pp3) if i > 0)
else:
self.pp_lim = max(i for i in (pp0, pp3) if i < 0), 0
def on_edit(fig, event):
buffer = 0.0025
nonlocal diagram
shape = diagram.shape
node = self.node
if node > 0 and node < (len(shape)-2):
if event.xdata is not None and event.ydata is not None:
inpt = np.array([event.xdata, event.ydata])
vertex, edge, perp_edge = self.bundle
# project input pt onto perp_edge
ppi = np.dot(inpt - vertex, perp_edge)
# constrain the input point within the range, if needed
if ppi < self.pp_lim[0]:
inpt = vertex + (1+buffer)*self.pp_lim[0]*perp_edge
elif ppi > self.pp_lim[1]:
inpt = vertex + (1-buffer)*self.pp_lim[1]*perp_edge
# compute new edge vertices from intersection of adjacent
# edges and line shifted parallel to the initial edge
edge_pt = inpt + edge
pt1 = np.array(get_intersect(shape[node-1], vertex,
inpt, edge_pt))
diagram.apply_data(self.node, pt1)
pt2 = np.array(get_intersect(vertex, shape[node+2],
inpt, edge_pt))
diagram.apply_data(self.node+1, pt2)
fig.build = 'update'
fig.refresh_gui(build='update', src_model=parax_model)
def on_release(fig, event):
if event.xdata is not None and event.ydata is not None:
event_data = np.array([event.xdata, event.ydata])
fig.build = 'rebuild'
fig.refresh_gui(build='rebuild', src_model=parax_model)
self.node = None
self.bundle = None
self.actions = {}
self.actions['drag'] = on_edit
self.actions['press'] = on_select
self.actions['release'] = on_release
[docs]class EditBendingAction():
""" Action to bend the lens element for diagram edge, using an input pt.
The movement is constrained to be along the object ray for the lens if the
input point is closer to the leading node of the edge. Otherwise the
movement is constrained to be along the image ray. The unconstrained point
is solved to keep the element thickness constant and maintain the
object-image properties of the lens.
"""
def __init__(self, dgm_edge):
diagram = dgm_edge.diagram
pm = diagram.parax_model
self.node = None
self.bundle = None
self.filter = None
def cross_prod(pt1, pt2):
return pt1[1]*pt2[0] - pt2[1]*pt1[0]
def calc_coef_fct(vertex, iNode, dir_inpt, oNode, dir_out):
nonlocal pm
tau_factor = pm.sys[self.node][mc.tau]*pm.opt_inv
constrain_to_line = constrain_to_line_action(vertex,
vertex+dir_inpt)
def calc_t(inpt):
pt = constrain_to_line(inpt)
if iNode < oNode:
arg1 = pt, vertex
arg2 = pt, dir_out
else:
arg1 = vertex, pt
arg2 = dir_out, pt
t = (tau_factor - cross_prod(*arg1))/(cross_prod(*arg2))
return (iNode, pt), (oNode, vertex + t*dir_out)
return calc_t
def on_select(fig, event):
nonlocal diagram
if event.xdata is None or event.ydata is None:
return
shape = diagram.shape
self.node = node = dgm_edge.node
if node > 0 and node < (len(shape)-2):
inpt = np.array([event.xdata, event.ydata])
# get the virtual vertex of the combined element surfaces
vertex = np.array(get_intersect(shape[node-1], shape[node],
shape[node+1], shape[node+2]))
edge_dir_01 = normalize(shape[node] - shape[node-1])
edge_dir_23 = normalize(shape[node+2] - shape[node+1])
# which node is closer to the input point?
pt1_dist = distance_sqr_2d(shape[node], inpt)
pt2_dist = distance_sqr_2d(shape[node+1], inpt)
if pt1_dist < pt2_dist:
self.filter = calc_coef_fct(vertex, node, edge_dir_01,
node+1, edge_dir_23)
else:
self.filter = calc_coef_fct(vertex, node+1, edge_dir_23,
node, edge_dir_01)
# get direction cosines for the edge
edge = normalize(shape[node+1] - shape[node])
# construct perpendicular to the edge. use this to define a
# range for allowed inputs
perp_edge = np.array([edge[1], -edge[0]])
self.bundle = (vertex, edge, perp_edge, edge_dir_01,
edge_dir_23)
def on_edit(fig, event):
nonlocal diagram
shape = diagram.shape
node = self.node
if node > 0 and node < (len(shape)-2):
if event.xdata is not None and event.ydata is not None:
inpt = np.array([event.xdata, event.ydata])
inp, out = self.filter(inpt)
diagram.apply_data(inp[0], inp[1])
diagram.apply_data(out[0], out[1])
fig.refresh_gui(build='update', src_model=pm)
def on_release(fig, event):
if event.xdata is not None and event.ydata is not None:
event_data = np.array([event.xdata, event.ydata])
fig.build = 'rebuild'
fig.refresh_gui(build='rebuild', src_model=pm)
self.node = None
self.bundle = None
self.filter = None
self.actions = {}
self.actions['drag'] = on_edit
self.actions['press'] = on_select
self.actions['release'] = on_release
[docs]class AddReplaceElementAction():
''' insert or replace a node with a chunk from a factory fct
The do_command_action fct registered for this operation passes the shape
being operated upon; these can be:
- DiagramEdge: insert/add the chunk returned by the factory fct
- DiagramNode: replace the selected node with the factory fct output
Inserting is done by splitting the corresponding gap in two. A new gap
and an AirGap element are tacked on to the chunk returned from the factory
fct.
Replacing is done when a DiagramNode is selected. The gaps surrounding the
node are retained, and modified as needed to accomodate the chunk.
'''
def __init__(self, diagram, **kwargs):
seq_model = diagram.opt_model.seq_model
parax_model = diagram.parax_model
self.cur_node = None
self.init_inputs = None
def on_press_add_point(fig, event, shape):
# if we don't have factory functions, skip the command
if isinstance(shape, DiagramEdge):
if 'node_init' in diagram.command_inputs and \
'factory' in diagram.command_inputs:
self.cur_node = shape.node
event_data = np.array([event.xdata, event.ydata])
interact = diagram.command_inputs['interact_mode']
parax_model.add_node(self.cur_node, event_data,
diagram.type_sel, interact)
self.cur_node += 1
# create a node for editing during the drag action
# 'node_init' will currently be a thinlens or a mirror
node_init = diagram.command_inputs['node_init']
self.init_inputs = diagram.assign_object_to_node(
self.cur_node,
node_init,
insert=True)
parax_model.paraxial_lens_to_seq_model()
fig.build = 'rebuild'
fig.refresh_gui(build='rebuild', src_model=parax_model)
elif isinstance(shape, DiagramNode):
if 'factory' in diagram.command_inputs:
# replacing a node with a chunk only requires recording
# what chunk corresponds to the current node. There is
# no drag action
self.cur_node = node = shape.node
self.init_inputs = parax_model.get_object_for_node(node)
def on_drag_add_point(fig, event, shape):
if self.cur_node is not None and isinstance(shape, DiagramEdge):
event_data = np.array([event.xdata, event.ydata])
diagram.apply_data(self.cur_node, event_data)
fig.build = 'update'
fig.refresh_gui(build='update', src_model=parax_model)
def on_release_add_point(fig, event, shape):
if self.cur_node is not None:
factory = diagram.command_inputs['factory']
# if factory and node_init fcts are the same, we're done;
# always call factory fct for a node
if factory != diagram.command_inputs['node_init'] or \
isinstance(shape, DiagramNode):
prev_ifc = seq_model.ifcs[self.cur_node]
inputs = diagram.assign_object_to_node(self.cur_node,
factory)
idx = seq_model.ifcs.index(prev_ifc)
n_after = parax_model.sys[idx-1][mc.indx]
thi = n_after*parax_model.sys[idx-1][mc.tau]
seq_model.gaps[idx-1].thi = thi
# remove the edit scaffolding or previous node from model
seq, eles, e_node = self.init_inputs[0]
diagram.opt_model.remove_node(e_node)
parax_model.paraxial_lens_to_seq_model()
fig.build = 'rebuild'
fig.refresh_gui(build='rebuild', src_model=parax_model)
self.cur_node = None
self.actions = {}
self.actions['press'] = on_press_add_point
self.actions['drag'] = on_drag_add_point
self.actions['release'] = on_release_add_point
[docs]class GlassDropAction():
[docs] def dragEnterEvent(self, view, event):
def glass_target_filter(artist):
shape, handle = artist.shape
if handle == 'area' and 'area' in shape.actions:
sm = shape.diagram.opt_model.seq_model
gap = sm.gaps[shape.node]
if gap.medium.name().lower() != 'air':
return False
return True
view.figure.artist_filter = glass_target_filter
[docs] def dragMoveEvent(self, view, event):
x, y = view.mouseEventCoords(event.pos())
view.motion_notify_event(x, y, guiEvent=event)
[docs] def dragLeaveEvent(self, view, event):
view.figure.artist_filter = None
[docs] def dropEvent(self, view, event):
dropped_it = False
fig = view.figure
if fig.hilited is not None:
target = fig.hilited
shape, handle = target.artist.shape
if handle == 'area' and 'area' in shape.actions:
sm = shape.diagram.opt_model.seq_model
gap = sm.gaps[shape.node]
action = ReplaceGlassAction(gap, update=False)
action(fig, event)
pm = shape.diagram.parax_model
pm.update_rindex(shape.node)
pm.paraxial_lens_to_seq_model()
fig.refresh_gui()
dropped_it = True
view.figure.artist_filter = None
return dropped_it