def _save(self, outdir):
        """save es and stats"""
        # save raw ES to one csv file
        samplesRawES = pd.DataFrame(self.resultsOnSamples)
        samplesRawES.index.name = 'Term|ES'
        # normalize enrichment scores by using the entire data set, as indicated
        # by Barbie et al., 2009, online methods, pg. 2
        samplesNES = samplesRawES / (samplesRawES.values.max() - samplesRawES.values.min())
        samplesNES = samplesNES.copy()
        samplesNES.index.rename('Term|NES', inplace=True)
        self.res2d = samplesNES
        self._logger.info("Congratulations. GSEApy runs successfully................\n")
        if self._outdir is None: return
        # write es
        outESfile = os.path.join(outdir, "gseapy.samples.raw.es.txt")
        with open(outESfile, 'a') as f:
            if self.scale:
                f.write('# scale the enrichment scores by number of genes in the gene sets\n')
                f.write('# this normalization has not effects on the final NES, ' + \
                        'as indicated by Barbie et al., 2009, online methods, pg. 2\n')
            else:
                f.write('# raw enrichment scores of all data\n')
                f.write('# no scale es by numbers of genes in the gene sets\n')
            samplesRawES.to_csv(f, sep='\t')

        outNESfile = os.path.join(outdir, "gseapy.samples.normalized.es.txt")
        with open(outNESfile, 'a') as f:
            f.write('# normalize enrichment scores by using the entire data set\n')
            f.write('# as indicated by Barbie et al., 2009, online methods, pg. 2\n')
            samplesNES.to_csv(f, sep='\t')
        return


def run(self):
        """main replot function"""
        assert self.min_size <= self.max_size
        assert self.fignum > 0
        import glob
        from bs4 import BeautifulSoup

        # parsing files.......
        try:
            results_path = glob.glob(self.indir+'*/edb/results.edb')[0]
            rank_path =  glob.glob(self.indir+'*/edb/*.rnk')[0]
            gene_set_path =  glob.glob(self.indir+'*/edb/gene_sets.gmt')[0]
        except IndexError as e:
            sys.stderr.write("Could not locate GSEA files in the given directory!")
            sys.exit(1)
        # extract sample names from .cls file
        cls_path = glob.glob(self.indir+'*/edb/*.cls')
        if cls_path:
            pos, neg, classes = gsea_cls_parser(cls_path[0])
        else:
            # logic for prerank results
            pos, neg = '',''
        # start reploting
        self.gene_sets=gene_set_path
        # obtain gene sets
        gene_set_dict = self.parse_gmt(gmt=gene_set_path)
        # obtain rank_metrics
        rank_metric = self._load_ranking(rank_path)
        correl_vector =  rank_metric.values
        gene_list = rank_metric.index.values
        # extract each enriment term in the results.edb files and plot.
        database = BeautifulSoup(open(results_path), features='xml')
        length = len(database.findAll('DTG'))
        fig_num = self.fignum if self.fignum <= length else length
        for idx in range(fig_num):
            # extract statistical resutls from results.edb file
            enrich_term, hit_ind, nes, pval, fdr= gsea_edb_parser(results_path, index=idx)
            gene_set = gene_set_dict.get(enrich_term)
            # calculate enrichment score
            RES = enrichment_score(gene_list=gene_list,
                                   correl_vector=correl_vector,
                                   gene_set=gene_set,
                                   weighted_score_type=self.weighted_score_type,
                                   nperm=0)[-1]
            # plotting
            term = enrich_term.replace('/','_').replace(":","_")
            outfile = '{0}/{1}.{2}.{3}'.format(self.outdir, term, self.module, self.format)
            gseaplot(rank_metric=rank_metric, term=enrich_term,
                         hits_indices=hit_ind, nes=nes, pval=pval, fdr=fdr,
                         RES=RES, pheno_pos=pos, pheno_neg=neg,
                         figsize=self.figsize, ofname=outfile)

        self._logger.info("Congratulations! Your plots have been reproduced successfully!\n")


def enrichr(gene_list, gene_sets, organism='human', description='',
            outdir='Enrichr', background='hsapiens_gene_ensembl', cutoff=0.05,
            format='pdf', figsize=(8,6), top_term=10, no_plot=False, verbose=False):
    """Enrichr API.

    :param gene_list: Flat file with list of genes, one gene id per row, or a python list object
    :param gene_sets: Enrichr Library to query. Required enrichr library name(s). Separate each name by comma.
    :param organism: Enrichr supported organism. Select from (human, mouse, yeast, fly, fish, worm).
                     see here for details: https://amp.pharm.mssm.edu/modEnrichr
    :param description: name of analysis. optional.
    :param outdir: Output file directory
    :param float cutoff: Adjusted P-value (benjamini-hochberg correction) cutoff. Default: 0.05
    :param int background: BioMart dataset name for retrieving background gene information.
                           This argument only works when gene_sets input is a gmt file or python dict.
                           You could also specify a number by yourself, e.g. total expressed genes number.
                           In this case, you will skip retrieving background infos from biomart.

    Use the code below to see valid background dataset names from BioMart.
    Here are example code:
    >>> from gseapy.parser import Biomart
    >>> bm = Biomart(verbose=False, host="asia.ensembl.org")
    >>> ## view validated marts
    >>> marts = bm.get_marts()
    >>> ## view validated dataset
    >>> datasets = bm.get_datasets(mart='ENSEMBL_MART_ENSEMBL')

    :param str format: Output figure format supported by matplotlib,('pdf','png','eps'...). Default: 'pdf'.
    :param list figsize: Matplotlib figsize, accept a tuple or list, e.g. (width,height). Default: (6.5,6).
    :param bool no_plot: If equals to True, no figure will be drawn. Default: False.
    :param bool verbose: Increase output verbosity, print out progress of your job, Default: False.

    :return: An Enrichr object, which obj.res2d stores your last query, obj.results stores your all queries.

    """
    enr = Enrichr(gene_list, gene_sets, organism, description, outdir,
                  cutoff, background, format, figsize, top_term, no_plot, verbose)
    enr.run()

    return enr

def enrichr ( gene_list , gene_sets , organism = 'human' , description = '' , outdir = 'Enrichr' , background = 'hsapiens_gene_ensembl' , cutoff = 0.05 , format = 'pdf' , figsize = ( 8 , 6 ) , top_term = 10 , no_plot = False , verbose = False ) : enr = Enrichr ( gene_list , gene_sets , organism , description , outdir , cutoff , background , format , figsize , top_term , no_plot , verbose ) enr . run ( ) return enr


def parse_genesets(self):
        """parse gene_sets input file type"""

        enrichr_library = self.get_libraries()
        if isinstance(self.gene_sets, list):
            gss = self.gene_sets
        elif isinstance(self.gene_sets, str):
            gss = [ g.strip() for g in self.gene_sets.strip().split(",") ]
        elif isinstance(self.gene_sets, dict):
            gss = [self.gene_sets]
        else:
            raise Exception("Error parsing enrichr libraries, please provided corrected one")

        # gss: a list contain .gmt, dict, enrichr_liraries.
        # now, convert .gmt to dict
        gss_exist = []
        for g in gss:
            if isinstance(g, dict):
                gss_exist.append(g)
                continue

            if isinstance(g, str):
                if g in enrichr_library:
                    gss_exist.append(g)
                    continue
                if g.lower().endswith(".gmt") and os.path.exists(g):
                    self._logger.info("User Defined gene sets is given: %s"%g)
                    with open(g) as genesets:
                        g_dict = { line.strip().split("\t")[0]: line.strip().split("\t")[2:]
                                        for line in genesets.readlines() }
                    gss_exist.append(g_dict)
        return gss_exist


def parse_genelists(self):
        """parse gene list"""
        if isinstance(self.gene_list, list):
            genes = self.gene_list
        elif isinstance(self.gene_list, pd.DataFrame):
            # input type is bed file
            if self.gene_list.shape[1] >=3:
                genes= self.gene_list.iloc[:,:3].apply(lambda x: "\t".join([str(i) for i in x]), axis=1).tolist()
            # input type with weight values
            elif self.gene_list.shape[1] == 2:
               genes= self.gene_list.apply(lambda x: ",".join([str(i) for i in x]), axis=1).tolist()
            else:
               genes = self.gene_list.squeeze().tolist()
        elif isinstance(self.gene_list, pd.Series):
            genes = self.gene_list.squeeze().tolist()
        else:
            # get gene lists or bed file, or gene list with weighted values.
            genes=[]
            with open(self.gene_list) as f:
                for gene in f:
                    genes.append(gene.strip())

        self._isezid = all(map(self._is_entrez_id, genes))
        if self._isezid:
            self._gls = set(map(int, self._gls))
        else:
            self._gls = genes

        return '\n'.join(genes)


def send_genes(self, gene_list, url):
        """ send gene list to enrichr server"""
        payload = {
          'list': (None, gene_list),
          'description': (None, self.descriptions)
           }
        # response
        response = requests.post(url, files=payload)
        if not response.ok:
            raise Exception('Error analyzing gene list')
        sleep(1)
        job_id = json.loads(response.text)

        return job_id

def
send_genes
(
self
,
gene_list
,
url
)
:
payload
=
{
'list'
:
(
,
gene_list
)
,
'description'
:
(
,
self
.
descriptions
)
}
# response
response
=
requests
.
post
(
url
,
files
=
payload
)
if
not
response
.
ok
:
raise
Exception
(
'Error analyzing gene list'
)
sleep
(
1
)
job_id
=
json
.
loads
(
response
.
text
)
return
job_id


def check_genes(self, gene_list, usr_list_id):
        '''
        Compare the genes sent and received to get successfully recognized genes
        '''
        response = requests.get('http://amp.pharm.mssm.edu/Enrichr/view?userListId=%s' % usr_list_id)
        if not response.ok:
            raise Exception('Error getting gene list back')
        returnedL = json.loads(response.text)["genes"]
        returnedN = sum([1 for gene in gene_list if gene in returnedL])
        self._logger.info('{} genes successfully recognized by Enrichr'.format(returnedN))


def get_background(self):
        """get background gene"""

        # input is a file
        if os.path.isfile(self.background):
            with open(self.background) as b:
                bg2 = b.readlines()
            bg = [g.strip() for g in bg2]
            return set(bg)

        # package included data
        DB_FILE = resource_filename("gseapy", "data/{}.background.genes.txt".format(self.background))
        filename = os.path.join(DEFAULT_CACHE_PATH, "{}.background.genes.txt".format(self.background))
        if os.path.exists(filename):
            df = pd.read_csv(filename,sep="\t")
        elif os.path.exists(DB_FILE):
            df = pd.read_csv(DB_FILE,sep="\t")
        else:
            # background is a biomart database name
            self._logger.warning("Downloading %s for the first time. It might take a couple of miniutes."%self.background)
            bm = Biomart()
            df = bm.query(dataset=self.background)
            df.dropna(subset=['go_id'], inplace=True)
        self._logger.info("using all annotated genes with GO_ID as background genes")
        df.dropna(subset=['entrezgene'], inplace=True)
        # input id type: entrez or gene_name
        if self._isezid:
            bg = df['entrezgene'].astype(int)
        else:
            bg = df['external_gene_name']

        return set(bg)


def run(self):
        """run enrichr for one sample gene list but multi-libraries"""

        # set organism
        self.get_organism()
        # read input file
        genes_list = self.parse_genelists()
        gss = self.parse_genesets()
        # if gmt
        self._logger.info("Connecting to Enrichr Server to get latest library names")
        if len(gss) < 1:
            sys.stderr.write("Not validated Enrichr library name provided\n")
            sys.stdout.write("Hint: use get_library_name() to view full list of supported names")
            sys.exit(1)
        self.results = pd.DataFrame()

        for g in gss:
            if isinstance(g, dict):
                ## local mode
                res = self.enrich(g)
                shortID, self._gs = str(id(g)), "CUSTOM%s"%id(g)
                if res is None:
                    self._logger.info("No hits return, for gene set: Custom%s"%shortID)
                    continue
            else:
                ## online mode
                self._gs = str(g)
                self._logger.debug("Start Enrichr using library: %s" % (self._gs))
                self._logger.info('Analysis name: %s, Enrichr Library: %s' % (self.descriptions, self._gs))
                shortID, res = self.get_results(genes_list)
                # Remember gene set library used
            res.insert(0, "Gene_set", self._gs)
            # Append to master dataframe
            self.results = self.results.append(res, ignore_index=True, sort=True)
            self.res2d = res
            if self._outdir is None: continue
            self._logger.info('Save file of enrichment results: Job Id:' + str(shortID))
            outfile = "%s/%s.%s.%s.reports.txt" % (self.outdir, self._gs, self.descriptions, self.module)
            self.res2d.to_csv(outfile, index=False, encoding='utf-8', sep="\t")
            # plotting
            if not self.__no_plot:
                msg = barplot(df=res, cutoff=self.cutoff, figsize=self.figsize,
                              top_term=self.__top_term, color='salmon',
                              title=self._gs,
                              ofname=outfile.replace("txt", self.format))
                if msg is not None : self._logger.warning(msg)
            self._logger.info('Done.\n')
        # clean up tmpdir
        if self._outdir is None: self._tmpdir.cleanup()

        return

run enrichr for one sample gene list but multi - libraries


def cube(script, size=1.0, center=False, color=None):
    """Create a cube primitive

    Note that this is made of 6 quads, not triangles
    """

    """# Convert size to list if it isn't already
    if not isinstance(size, list):
        size = list(size)
    # If a single value was supplied use it for all 3 axes
    if len(size) == 1:
        size = [size[0], size[0], size[0]]"""
    size = util.make_list(size, 3)
    if script.ml_version == '1.3.4BETA':
        filter_name = 'Box'
    else:
        filter_name = 'Box/Cube'
    filter_xml = ''.join([
        '  <filter name="{}">\n'.format(filter_name),
        '    <Param name="size" ',
        'value="1.0" ',
        'description="Scale factor" ',
        'type="RichFloat" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.add_layer('Cube', change_layer=True)
    transform.scale(script, value=size)
    # Box is centered on origin at creation
    if not center:
        transform.translate(script, value=[size[0]/2, size[1]/2, size[2]/2])
    if color is not None:
        vert_color.function(script, color=color)
    return None


def icosphere(script, radius=1.0, diameter=None, subdivisions=3, color=None):
    """create an icosphere mesh

    radius Radius of the sphere
    # subdivisions = Subdivision level; Number of the recursive subdivision of the
    # surface. Default is 3 (a sphere approximation composed by 1280 faces).
    # Admitted values are in the range 0 (an icosahedron) to 8 (a 1.3 MegaTris
    # approximation of a sphere). Formula for number of faces: F=20*4^subdiv
    # color = specify a color name to apply vertex colors to the newly
    # created mesh"""
    if diameter is not None:
        radius = diameter / 2
    filter_xml = ''.join([
        '  <filter name="Sphere">\n',
        '    <Param name="radius" ',
        'value="%s" ' % radius,
        'description="Radius" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="subdiv" ',
        'value="%d" ' % subdivisions,
        'description="Subdiv. Level" ',
        'type="RichInt" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.add_layer('Sphere', change_layer=True)
    if color is not None:
        vert_color.function(script, color=color)
    return None

def
icosphere
(
script
,
radius
=
1.0
,
diameter
=
,
subdivisions
=
3
,
color
=
)
:
if
diameter
is
not
:
radius
=
diameter
/
2
filter_xml
=
''
.
join
(
[
'  <filter name="Sphere">\n'
,
'    <Param name="radius" '
,
'value="%s" '
%
radius
,
'description="Radius" '
,
'type="RichFloat" '
,
'/>\n'
,
'    <Param name="subdiv" '
,
'value="%d" '
%
subdivisions
,
'description="Subdiv. Level" '
,
'type="RichInt" '
,
'/>\n'
,
'  </filter>\n'
]
)
util
.
write_filter
(
script
,
filter_xml
)
if
isinstance
(
script
,
FilterScript
)
:
script
.
add_layer
(
'Sphere'
,
change_layer
=
True
)
if
color
is
not
:
vert_color
.
function
(
script
,
color
=
color
)
return


def torus(script, major_radius=3.0, minor_radius=1.0, inner_diameter=None,
          outer_diameter=None, major_segments=48, minor_segments=12,
          color=None):
    """Create a torus mesh

    Args:
        major_radius (float, (optional)): radius from the origin to the
            center of the cross sections
        minor_radius (float, (optional)): radius of the torus cross
            section
        inner_diameter (float, (optional)): inner diameter of torus. If
            both inner_diameter and outer_diameter are provided then
            these will override major_radius and minor_radius.,
        outer_diameter (float, (optional)): outer diameter of torus. If
            both inner_diameter and outer_diameter are provided then
            these will override major_radius and minor_radius.
        major_segments (int (optional)): number of segments for the main
            ring of the torus
        minor_segments (int (optional)): number of segments for the minor
            ring of the torus
        color (str (optional)): color name to apply vertex colors to the
            newly created mesh

    Returns:
        None

    """
    if inner_diameter is not None and outer_diameter is not None:
        major_radius = (inner_diameter + outer_diameter) / 4
        minor_radius = major_radius - inner_diameter / 2
        # Ref: inner_diameter = 2 * (major_radius - minor_radius)
        # Ref: outer_diameter = 2 * (major_radius + minor_radius)
    filter_xml = ''.join([
        '  <filter name="Torus">\n',
        '    <Param name="hRadius" ',
        'value="%s" ' % major_radius,
        'description="Horizontal Radius" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="vRadius" ',
        'value="%s" ' % minor_radius,
        'description="Vertical Radius" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="hSubdiv" ',
        'value="%d" ' % major_segments,
        'description="Horizontal Subdivision" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="vSubdiv" ',
        'value="%d" ' % minor_segments,
        'description="Vertical Subdivision" ',
        'type="RichInt" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.add_layer('Torus', change_layer=True)
    if color is not None:
        vert_color.function(script, color=color)
    return None


def plane_hires_edges(script, size=1.0, x_segments=1, y_segments=1,
                      center=False, color=None):
    """ Creates a plane with a specified number of vertices
    on it sides, but no vertices on the interior.

    Currently used to create a simpler bottom for cube_hires.

    """
    size = util.make_list(size, 2)

    grid(script, size=[x_segments + y_segments - 1, 1],
         x_segments=(x_segments + y_segments - 1), y_segments=1)
    if ml_script1.ml_version == '1.3.4BETA':
        and_val = 'and'
    else:
        and_val = '&&'

    if script.ml_version == '1.3.4BETA': # muparser version: 1.3.2
        # Deform left side
        transform.vert_function(
            script,
            x_func='if((y>0) and (x<%s),0,x)' % (y_segments),
            y_func='if((y>0) and (x<%s),(x+1)*%s,y)' % (
                y_segments, size[1] / y_segments))
        # Deform top
        transform.vert_function(
            script,
            x_func='if((y>0) and (x>=%s),(x-%s+1)*%s,x)' % (
                y_segments, y_segments, size[0] / x_segments),
            y_func='if((y>0) and (x>=%s),%s,y)' % (y_segments, size[1]))
        # Deform right side
        transform.vert_function(
            script,
            x_func='if((y<.00001) and (x>%s),%s,x)' % (
                x_segments, size[0]),
            y_func='if((y<.00001) and (x>%s),(x-%s)*%s,y)' % (
                x_segments, x_segments, size[1] / y_segments))
        # Deform bottom
        transform.vert_function(
            script,
            x_func='if((y<.00001) and (x<=%s) and (x>0),(x)*%s,x)' % (
                x_segments, size[0] / x_segments),
            y_func='if((y<.00001) and (x<=%s) and (x>0),0,y)' % (x_segments))
    else: # muparser version: 2.2.5
        # Deform left side
        transform.vert_function(
            script,
            x_func='((y>0) && (x<{yseg}) ? 0 : x)'.format(yseg=y_segments),
            y_func='((y>0) && (x<%s) ? (x+1)*%s : y)' % (
                y_segments, size[1] / y_segments))
        # Deform top
        transform.vert_function(
            script,
            x_func='((y>0) && (x>=%s) ? (x-%s+1)*%s : x)' % (
                y_segments, y_segments, size[0] / x_segments),
            y_func='((y>0) && (x>=%s) ? %s : y)' % (y_segments, size[1]))
        # Deform right side
        transform.vert_function(
            script,
            x_func='((y<.00001) && (x>%s) ? %s : x)' % (
                x_segments, size[0]),
            y_func='((y<.00001) && (x>%s) ? (x-%s)*%s : y)' % (
                x_segments, x_segments, size[1] / y_segments))
        # Deform bottom
        transform.vert_function(
            script,
            x_func='((y<.00001) && (x<=%s) && (x>0) ? (x)*%s : x)' % (
                x_segments, size[0] / x_segments),
            y_func='((y<.00001) && (x<=%s) && (x>0) ? 0 : y)' % (x_segments))
    if center:
        transform.translate(script, [-size[0] / 2, -size[1] / 2])
    if color is not None:
        vert_color.function(script, color=color)
    return None

Creates a plane with a specified number of vertices on it sides but no vertices on the interior .


def cube_hires(script, size=1.0, x_segments=1, y_segments=1, z_segments=1,
               simple_bottom=True, center=False, color=None):
    """Create a box with user defined number of segments in each direction.

    Grid spacing is the same as its dimensions (spacing = 1) and its
    thickness is one. Intended to be used for e.g. deforming using functions
    or a height map (lithopanes) and can be resized after creation.

    Warnings: function uses layers.join

    top_option
        0 open
        1 full
        2 simple
    bottom_option
        0 open
        1 full
        2 simple
    """
    """# Convert size to list if it isn't already
    if not isinstance(size, list):
        size = list(size)
    # If a single value was supplied use it for all 3 axes
    if len(size) == 1:
        size = [size[0], size[0], size[0]]"""
    size = util.make_list(size, 3)

    # Top
    grid(script,
         size,
         x_segments,
         y_segments)
    transform.translate(script, [0, 0, size[2]])

    # Bottom
    if simple_bottom:
        plane_hires_edges(
            script, size, x_segments, y_segments)
    else:
        layers.duplicate(script)
        transform.translate(script, [0, 0, -size[2]])
    # Rotate to correct normals
    transform.rotate(script, 'x', 180)
    transform.translate(script, [0, size[1], 0])

    # Sides
    cube_open_hires(
        script=script, size=size, x_segments=x_segments,
        y_segments=y_segments, z_segments=z_segments)

    # Join everything together
    layers.join(script)
    # Need some tolerance on merge_vert due to rounding errors
    clean.merge_vert(script, threshold=0.00002)
    if center:
        transform.translate(script, [-size[0] / 2, -size[1] / 2, -size[2] / 2])
    if color is not None:
        vert_color.function(script, color=color)
    return None

Create a box with user defined number of segments in each direction .


def color_values(color):
    """Read color_names.txt and find the red, green, and blue values
        for a named color.
    """
    # Get the directory where this script file is located:
    this_dir = os.path.dirname(
        os.path.realpath(
            inspect.getsourcefile(
                lambda: 0)))
    color_name_file = os.path.join(this_dir, 'color_names.txt')
    found = False
    for line in open(color_name_file, 'r'):
        line = line.rstrip()
        if color.lower() == line.split()[0]:
            #hex_color = line.split()[1]
            red = line.split()[2]
            green = line.split()[3]
            blue = line.split()[4]
            found = True
            break
    if not found:
        print('Color name "%s" not found, using default (white)' % color)
        red = 255
        green = 255
        blue = 255
    return red, green, blue

Read color_names . txt and find the red green and blue values for a named color .


def check_list(var, num_terms):
    """ Check if a variable is a list and is the correct length.

    If variable is not a list it will make it a list of the correct length with
    all terms identical.
    """
    if not isinstance(var, list):
        if isinstance(var, tuple):
            var = list(var)
        else:
            var = [var]
        for _ in range(1, num_terms):
            var.append(var[0])
    if len(var) != num_terms:
        print(
            '"%s" has the wrong number of terms; it needs %s. Exiting ...' %
            (var, num_terms))
        sys.exit(1)
    return var

Check if a variable is a list and is the correct length .


def make_list(var, num_terms=1):
    """ Make a variable a list if it is not already

    If variable is not a list it will make it a list of the correct length with
    all terms identical.
    """
    if not isinstance(var, list):
        if isinstance(var, tuple):
            var = list(var)
        else:
            var = [var]
    #if len(var) == 1:
            for _ in range(1, num_terms):
                var.append(var[0])
    return var

def
make_list
(
var
,
num_terms
=
1
)
:
if
not
isinstance
(
var
,
list
)
:
if
isinstance
(
var
,
tuple
)
:
var
=
list
(
var
)
else
:
var
=
[
var
]
#if len(var) == 1:
for
_
in
range
(
1
,
num_terms
)
:
var
.
append
(
var
[
0
]
)
return
var


def write_filter(script, filter_xml):
    """ Write filter to FilterScript object or filename

    Args:
        script (FilterScript object or filename str): the FilterScript object
            or script filename to write the filter to.
        filter_xml (str): the xml filter string

    """
    if isinstance(script, mlx.FilterScript):
        script.filters.append(filter_xml)
    elif isinstance(script, str):
        script_file = open(script, 'a')
        script_file.write(filter_xml)
        script_file.close()
    else:
        print(filter_xml)
    return None

def write_filter ( script , filter_xml ) : if isinstance ( script , mlx . FilterScript ) : script . filters . append ( filter_xml ) elif isinstance ( script , str ) : script_file = open ( script , 'a' ) script_file . write ( filter_xml ) script_file . close ( ) else : print ( filter_xml ) return None


def ls3loop(script, iterations=1, loop_weight=0, edge_threshold=0,
            selected=False):
    """ Apply LS3 Subdivision Surface algorithm using Loop's weights.

    This refinement method take normals into account.
    See: Boye', S. Guennebaud, G. & Schlick, C.
    "Least squares subdivision surfaces"
    Computer Graphics Forum, 2010.

    Alternatives weighting schemes are based on the paper:
    Barthe, L. & Kobbelt, L.
    "Subdivision scheme tuning around extraordinary vertices"
    Computer Aided Geometric Design, 2004, 21, 561-583.

    The current implementation of these schemes don't handle vertices of
    valence > 12

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        iterations (int): Number of times the model is subdivided.
        loop_weight (int): Change the weights used. Allow to optimize some
            behaviours in spite of others. Valid values are:
            0 - Loop (default)
            1 - Enhance regularity
            2 - Enhance continuity
        edge_threshold (float): All the edges longer than this threshold will
            be refined. Setting this value to zero will force a uniform
            refinement.
        selected (bool): If selected the filter is performed only on the
            selected faces.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Subdivision Surfaces: LS3 Loop">\n',
        '    <Param name="LoopWeight" ',
        'value="{:d}" '.format(loop_weight),
        'description="Weighting scheme" ',
        'enum_val0="Loop" ',
        'enum_val1="Enhance regularity" ',
        'enum_val2="Enhance continuity" ',
        'enum_cardinality="3" ',
        'type="RichEnum" ',
        '/>\n',
        '    <Param name="Iterations" ',
        'value="{:d}" '.format(iterations),
        'description="Iterations" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="Threshold" ',
        'value="{}" '.format(edge_threshold),
        'description="Edge Threshold" ',
        'min="0" ',
        'max="100" ',
        'type="RichAbsPerc" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Affect only selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Apply LS3 Subdivision Surface algorithm using Loop s weights .


def merge_vert(script, threshold=0.0):
    """ Merge together all the vertices that are nearer than the specified
    threshold. Like a unify duplicate vertices but with some tolerance.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        threshold (float): Merging distance. All the vertices that are closer
            than this threshold are merged together. Use very small values,
            default is zero.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Merge Close Vertices">\n',
        '    <Param name="Threshold" ',
        'value="{}" '.format(threshold),
        'description="Merging distance" ',
        'min="0" ',
        'max="1" ',
        'type="RichAbsPerc" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Merge together all the vertices that are nearer than the specified threshold . Like a unify duplicate vertices but with some tolerance .


def close_holes(script, hole_max_edge=30, selected=False,
                sel_new_face=True, self_intersection=True):
    """ Close holes smaller than a given threshold

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        hole_max_edge (int): The size is expressed as number of edges composing
            the hole boundary.
        selected (bool): Only the holes with at least one of the boundary faces
            selected are closed.
        sel_new_face (bool): After closing a hole the faces that have been
            created are left selected. Any previous selection is lost. Useful
            for example for smoothing or subdividing the newly created holes.
        self_intersection (bool): When closing an holes it tries to prevent the
            creation of faces that intersect faces adjacent to the boundary of
            the hole. It is an heuristic, non intersecting hole filling can be
            NP-complete.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Close Holes">\n',
        '    <Param name="maxholesize" ',
        'value="{:d}" '.format(hole_max_edge),
        'description="Max size to be closed" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="Selected" ',
        'value="{}" '.format(str(selected).lower()),
        'description="Close holes with selected faces" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="NewFaceSelected" ',
        'value="{}" '.format(str(sel_new_face).lower()),
        'description="Select the newly created faces" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SelfIntersection" ',
        'value="{}" '.format(str(self_intersection).lower()),
        'description="Prevent creation of selfIntersecting faces" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


def split_vert_on_nonmanifold_face(script, vert_displacement_ratio=0.0):
    """ Split non-manifold vertices until it becomes two-manifold.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        vert_displacement_ratio (float): When a vertex is split it is moved
            along the average vector going from its position to the centroid
            of the FF connected faces sharing it.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Split Vertexes Incident on Non Manifold Faces">\n',
        '    <Param name="VertDispRatio" ',
        'value="{}" '.format(vert_displacement_ratio),
        'description="Vertex Displacement Ratio" ',
        'type="RichFloat" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Split non - manifold vertices until it becomes two - manifold .


def snap_mismatched_borders(script, edge_dist_ratio=0.01, unify_vert=True):
    """ Try to snap together adjacent borders that are slightly mismatched.

    This situation can happen on badly triangulated adjacent patches defined by
    high order surfaces. For each border vertex the filter snaps it onto the
    closest boundary edge only if it is closest of edge_legth*threshold. When
    vertex is snapped the corresponding face it split and a new vertex is
    created.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        edge_dist_ratio (float): Collapse edge when the edge / distance ratio
            is greater than this value. E.g. for default value 1000 two
            straight border edges are collapsed if the central vertex dist from
            the straight line composed by the two edges less than a 1/1000 of
            the sum of the edges length. Larger values enforce that only
            vertexes very close to the line are removed.
        unify_vert (bool): If true the snap vertices are welded together.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Snap Mismatched Borders">\n',
        '    <Param name="EdgeDistRatio" ',
        'value="{}" '.format(edge_dist_ratio),
        'description="Edge Distance Ratio" ',
        'type="RichFloat" ',
        '/>\n',
        '    <Param name="UnifyVertices" ',
        'value="{}" '.format(str(unify_vert).lower()),
        'description="UnifyVertices" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Try to snap together adjacent borders that are slightly mismatched .


def translate(script, value=(0.0, 0.0, 0.0)):
    """An alternative translate implementation that uses a geometric function.
    This is more accurate than the built-in version."""
    # Convert value to list if it isn't already
    if not isinstance(value, list):
        value = list(value)
    vert_function(script,
             x_func='x+(%s)' % value[0],
             y_func='y+(%s)' % value[1],
             z_func='z+(%s)' % value[2])
    return None

An alternative translate implementation that uses a geometric function . This is more accurate than the built - in version .


def rotate(script, axis='z', angle=0.0):
    """An alternative rotate implementation that uses a geometric function.
    This is more accurate than the built-in version."""
    angle = math.radians(angle)
    if axis.lower() == 'x':
        vert_function(script,
                 x_func='x',
                 y_func='y*cos({angle})-z*sin({angle})'.format(angle=angle),
                 z_func='y*sin({angle})+z*cos({angle})'.format(angle=angle))
    elif axis.lower() == 'y':
        vert_function(script,
                 x_func='z*sin({angle})+x*cos({angle})'.format(angle=angle),
                 y_func='y',
                 z_func='z*cos({angle})-x*sin({angle})'.format(angle=angle))
    elif axis.lower() == 'z':
        vert_function(script,
                 x_func='x*cos({angle})-y*sin({angle})'.format(angle=angle),
                 y_func='x*sin({angle})+y*cos({angle})'.format(angle=angle),
                 z_func='z')
    else:
        print('Axis name is not valid; exiting ...')
        sys.exit(1)
    return None

def
rotate
(
script
,
axis
=
'z'
,
angle
=
0.0
)
:
angle
=
math
.
radians
(
angle
)
if
axis
.
lower
(
)
==
'x'
:
vert_function
(
script
,
x_func
=
'x'
,
y_func
=
'y*cos({angle})-z*sin({angle})'
.
format
(
angle
=
angle
)
,
z_func
=
'y*sin({angle})+z*cos({angle})'
.
format
(
angle
=
angle
)
)
elif
axis
.
lower
(
)
==
'y'
:
vert_function
(
script
,
x_func
=
'z*sin({angle})+x*cos({angle})'
.
format
(
angle
=
angle
)
,
y_func
=
'y'
,
z_func
=
'z*cos({angle})-x*sin({angle})'
.
format
(
angle
=
angle
)
)
elif
axis
.
lower
(
)
==
'z'
:
vert_function
(
script
,
x_func
=
'x*cos({angle})-y*sin({angle})'
.
format
(
angle
=
angle
)
,
y_func
=
'x*sin({angle})+y*cos({angle})'
.
format
(
angle
=
angle
)
,
z_func
=
'z'
)
else
:
print
(
'Axis name is not valid; exiting ...'
)
sys
.
exit
(
1
)
return

An alternative rotate implementation that uses a geometric function . This is more accurate than the built - in version .


def scale(script, value=1.0):
    """An alternative scale implementation that uses a geometric function.
    This is more accurate than the built-in version."""
    """# Convert value to list if it isn't already
    if not isinstance(value, list):
        value = list(value)
    # If a single value was supplied use it for all 3 axes
    if len(value) == 1:
        value = [value[0], value[0], value[0]]"""
    value = util.make_list(value, 3)
    vert_function(script,
             x_func='x*(%s)' % value[0],
             y_func='y*(%s)' % value[1],
             z_func='z*(%s)' % value[2])
    return None

An alternative scale implementation that uses a geometric function . This is more accurate than the built - in version .


def function_cyl_co(script, r_func='r', theta_func='theta', z_func='z'):
    """Geometric function using cylindrical coordinates.

    Define functions in Z up cylindrical coordinates, with radius 'r',
    angle 'theta', and height 'z'

    See "function" docs for additional usage info and accepted parameters.

    Args:
        r_func (str): function to generate new coordinates for radius
        theta_func (str): function to generate new coordinates for angle.
            0 degrees is on the +X axis.
        z_func (str): function to generate new coordinates for height

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """

    r = 'sqrt(x^2+y^2)'
    # In newer MeshLab atan2 is builtin to muparser
    if isinstance(script, FilterScript) and script.ml_version >= '2016.12':
        theta = 'atan2(y, x)'
    else:
        theta = mp_func.mp_atan2('y', 'x')

    # Use re matching to match whole word; this prevents matching
    # 'sqrt' and 'rint' when replacing 'r'
    r_func = re.sub(r"\br\b", r, r_func).replace('theta', theta)
    theta_func = re.sub(r"\br\b", r, theta_func).replace('theta', theta)
    z_func = re.sub(r"\br\b", r, z_func).replace('theta', theta)

    x_func = '(r)*cos(theta)'.replace('r', r_func).replace('theta', theta_func)
    y_func = '(r)*sin(theta)'.replace('r', r_func).replace('theta', theta_func)

    vert_function(script, x_func, y_func, z_func)
    return None

Geometric function using cylindrical coordinates .


def wrap2cylinder(script, radius=1, pitch=0, taper=0, pitch_func=None,
                  taper_func=None):
    """Deform mesh around cylinder of radius and axis z

    y = 0 will be on the surface of radius "radius"
    pitch != 0 will create a helix, with distance "pitch" traveled in z for each rotation
    taper = change in r over z. E.g. a value of 0.5 will shrink r by 0.5 for every z length of 1

    """
    """vert_function(s=s, x='(%s+y-taper)*sin(x/(%s+y))' % (radius, radius),
                     y='(%s+y)*cos(x/(%s+y))' % (radius, radius),
                     z='z-%s*x/(2*%s*(%s+y))' % (pitch, pi, radius))"""
    if pitch_func is None:
        pitch_func = '-(pitch)*x/(2*pi*(radius))'
    pitch_func = pitch_func.replace(
        'pitch', str(pitch)).replace(
            'pi', str(math.pi)).replace(
                'radius', str(radius))
    if taper_func is None:
        taper_func = '-(taper)*(pitch_func)'
    taper_func = taper_func.replace(
        'taper', str(taper)).replace(
            'pitch_func', str(pitch_func)).replace(
                'pi', str(math.pi))

    x_func = '(y+(radius)+(taper_func))*sin(x/(radius))'.replace(
        'radius', str(radius)).replace('taper_func', str(taper_func))
    y_func = '(y+(radius)+(taper_func))*cos(x/(radius))'.replace(
        'radius', str(radius)).replace('taper_func', str(taper_func))
    z_func = 'z+(pitch_func)'.replace('pitch_func', str(pitch_func))

    vert_function(script, x_func, y_func, z_func)
    return None


def bend(script, radius=1, pitch=0, taper=0, angle=0, straght_start=True,
         straght_end=False, radius_limit=None, outside_limit_end=True):
    """Bends mesh around cylinder of radius radius and axis z to a certain angle

    straight_ends: Only apply twist (pitch) over the area that is bent

    outside_limit_end (bool): should values outside of the bend radius_limit be considered part
        of the end (True) or the start (False)?
    """
    if radius_limit is None:
        radius_limit = 2 * radius
    # TODO: add limit so bend only applies over y<2*radius; add option to set
    # larger limit
    angle = math.radians(angle)
    segment = radius * angle
    """vert_function(s=s, x='if(x<%s and x>-%s, (%s+y)*sin(x/%s), (%s+y)*sin(%s/%s)+(x-%s)*cos(%s/%s))'
                        % (segment, segment,  radius, radius,  radius, segment, radius, segment, segment, radius),
                     y='if(x<%s*%s/2 and x>-%s*%s/2, (%s+y)*cos(x/%s), (%s+y)*cos(%s)-(x-%s*%s)*sin(%s))'
                        % (radius, angle, radius, angle, radius, radius, radius, angle/2, radius, angle/2, angle/2),"""
    pitch_func = '-(pitch)*x/(2*pi*(radius))'.replace(
        'pitch', str(pitch)).replace(
            'pi', str(math.pi)).replace(
                'radius', str(radius))
    taper_func = '(taper)*(pitch_func)'.replace(
        'taper', str(taper)).replace(
            'pitch_func', str(pitch_func)).replace(
                'pi', str(math.pi))
    # y<radius_limit

    if outside_limit_end:
        x_func = 'if(x<(segment) and y<(radius_limit), if(x>0, (y+(radius)+(taper_func))*sin(x/(radius)), x), (y+(radius)+(taper_func))*sin(angle)+(x-(segment))*cos(angle))'
    else:
        x_func = 'if(x<(segment), if(x>0 and y<(radius_limit), (y+(radius)+(taper_func))*sin(x/(radius)), x), if(y<(radius_limit), (y+(radius)+(taper_func))*sin(angle)+(x-(segment))*cos(angle), x))'

    x_func = x_func.replace(
        # x_func = 'if(x<segment, if(x>0, (y+radius)*sin(x/radius), x),
        # (y+radius)*sin(angle)-segment)'.replace(
        'segment', str(segment)).replace(
            'radius_limit', str(radius_limit)).replace(
                'radius', str(radius)).replace(
                    'taper_func', str(taper_func)).replace(
                        'angle', str(angle))

    if outside_limit_end:
        y_func = 'if(x<(segment) and y<(radius_limit), if(x>0, (y+(radius)+(taper_func))*cos(x/(radius))-(radius), y), (y+(radius)+(taper_func))*cos(angle)-(x-(segment))*sin(angle)-(radius))'
    else:
        y_func = 'if(x<(segment), if(x>0 and y<(radius_limit), (y+(radius)+(taper_func))*cos(x/(radius))-(radius), y), if(y<(radius_limit), (y+(radius)+(taper_func))*cos(angle)-(x-(segment))*sin(angle)-(radius), y))'

    y_func = y_func.replace(
        'segment', str(segment)).replace(
            'radius_limit', str(radius_limit)).replace(
                'radius', str(radius)).replace(
                    'taper_func', str(taper_func)).replace(
                        'angle', str(angle))

    if straght_start:
        start = 'z'
    else:
        start = 'z+(pitch_func)'
    if straght_end:
        end = 'z-(pitch)*(angle)/(2*pi)'
    else:
        end = 'z+(pitch_func)'

    if outside_limit_end:
        z_func = 'if(x<(segment) and y<(radius_limit), if(x>0, z+(pitch_func), (start)), (end))'
    else:
        z_func = 'if(x<(segment), if(x>0 and y<(radius_limit), z+(pitch_func), (start)), if(y<(radius_limit), (end), z))'
    z_func = z_func.replace(
        'start', str(start)).replace(
            'end', str(end)).replace(
                'segment', str(segment)).replace(
                    'radius_limit', str(radius_limit)).replace(
                        'radius', str(radius)).replace(
                            'angle', str(angle)).replace(
                                'pitch_func', str(pitch_func)).replace(
                                    'pitch', str(pitch)).replace(
                                        'pi', str(math.pi))

    """
    if straight_ends:
        z_func = 'if(x<segment, if(x>0, z+(pitch_func), z), z-pitch*angle/(2*pi))'.replace(
            'segment', str(segment)).replace(
            'radius', str(radius)).replace(
            'angle', str(angle)).replace(
            'pitch_func', str(pitch_func)).replace(
            'pitch', str(pitch)).replace(
            'pi', str(math.pi))
    else:
        #z_func = 'if(x<segment, z+(pitch_func), z-(taper)*(pitch)*(x)/(2*pi*(radius)))'.replace(
        #z_func = 'if(x<segment, z+(pitch_func), z+(pitch_func))'.replace(
        #bestz_func = 'if(x<segment, z+(pitch_func), z+(pitch_func)+(-(taper)*(pitch)*(x-segment)/(2*pi*(radius))))'.replace(
        #z_func = 'if(x<segment, z+(pitch_func), z+(pitch_func)+(-(taper)*(pitch)*x/(2*pi*(radius))))'.replace(
        #z_func = 'if(x<segment, z+(pitch_func), z+(pitch_func)+((taper)*pitch*angle/(2*pi)))'.replace(
        z_func = 'z+(pitch_func)'.replace(
            'radius', str(radius)).replace(
            'segment', str(segment)).replace(
            'angle', str(angle)).replace(
            'taper', str(taper)).replace(
            'pitch_func', str(pitch_func)).replace(
            'pitch', str(pitch)).replace(
            'pi', str(math.pi))
    """
    """
    x_func = 'if(x<%s, if(x>-%s, (%s+y)*sin(x/%s), (%s+y)*sin(-%s)+(x+%s)*cos(-%s)), (%s+y)*sin(%s)+(x-%s)*cos(%s))' % (
        segment, segment, radius, radius, radius, angle, segment, angle, radius, angle, segment, angle)
    y_func = 'if(x<%s, if(x>-%s, (%s+y)*cos(x/%s), (%s+y)*cos(-%s)-(x+%s)*sin(-%s)), (%s+y)*cos(%s)-(x-%s)*sin(%s))' % (
        segment, segment, radius, radius, radius, angle, segment, angle, radius, angle, segment, angle)
    if straight_ends:
        z_func = 'if(x<%s, if(x>-%s, z-%s*x/(2*%s*%s), z+%s*%s/(2*%s)), z-%s*%s/(2*%s))' % (
            segment, segment, pitch, math.pi, radius, pitch, angle, math.pi, pitch, angle, math.pi)
    else:
        z_func = 'z-%s*x/(2*%s*%s)' % (pitch, math.pi, radius)
    """
    vert_function(script, x_func=x_func, y_func=y_func, z_func=z_func)
    return None


def deform2curve(script, curve=mp_func.torus_knot('t'), step=0.001):
    """ Deform a mesh along a parametric curve function

    Provide a parametric curve function with z as the parameter. This will
    deform the xy cross section of the mesh along the curve as z increases.

    Source: http://blackpawn.com/texts/pqtorus/

    Methodology:
    T  = P' - P
    N1 = P' + P
    B  = T x N1
    N  = B x T

    newPoint = point.x*N + point.y*B

    """
    curve_step = []
    for idx, val in enumerate(curve):
        curve[idx] = val.replace('t', 'z')
        curve_step.append(val.replace('t', 'z+{}'.format(step)))

    tangent = mp_func.v_subtract(curve_step, curve)
    normal1 = mp_func.v_add(curve_step, curve)
    bee = mp_func.v_cross(tangent, normal1)
    normal = mp_func.v_cross(bee, tangent)
    bee = mp_func.v_normalize(bee)
    normal = mp_func.v_normalize(normal)

    new_point = mp_func.v_add(mp_func.v_multiply('x', normal), mp_func.v_multiply('y', bee))
    function = mp_func.v_add(curve, new_point)

    vert_function(script, x_func=function[0], y_func=function[1], z_func=function[2])
    return function

def deform2curve ( script , curve = mp_func . torus_knot ( 't' ) , step = 0.001 ) : curve_step = [ ] for idx , val in enumerate ( curve ) : curve [ idx ] = val . replace ( 't' , 'z' ) curve_step . append ( val . replace ( 't' , 'z+{}' . format ( step ) ) ) tangent = mp_func . v_subtract ( curve_step , curve ) normal1 = mp_func . v_add ( curve_step , curve ) bee = mp_func . v_cross ( tangent , normal1 ) normal = mp_func . v_cross ( bee , tangent ) bee = mp_func . v_normalize ( bee ) normal = mp_func . v_normalize ( normal ) new_point = mp_func . v_add ( mp_func . v_multiply ( 'x' , normal ) , mp_func . v_multiply ( 'y' , bee ) ) function = mp_func . v_add ( curve , new_point ) vert_function ( script , x_func = function [ 0 ] , y_func = function [ 1 ] , z_func = function [ 2 ] ) return function


def vc2tex(script, tex_name='TEMP3D_texture.png', tex_width=1024,
           tex_height=1024, overwrite_tex=False, assign_tex=False,
           fill_tex=True):
    """Transfer vertex colors to texture colors

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        tex_name (str): The texture file to be created
        tex_width (int): The texture width
        tex_height (int): The texture height
        overwrite_tex (bool): If current mesh has a texture will be overwritten (with provided texture dimension)
        assign_tex (bool): Assign the newly created texture
        fill_tex (bool): If enabled the unmapped texture space is colored using a pull push filling algorithm, if false is set to black
    """
    filter_xml = ''.join([
        '  <filter name="Vertex Color to Texture">\n',
        '    <Param name="textName" ',
        'value="%s" ' % tex_name,
        'description="Texture file" ',
        'type="RichString" ',
        '/>\n',
        '    <Param name="textW" ',
        'value="%d" ' % tex_width,
        'description="Texture width (px)" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="textH" ',
        'value="%d" ' % tex_height,
        'description="Texture height (px)" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="overwrite" ',
        'value="%s" ' % str(overwrite_tex).lower(),
        'description="Overwrite texture" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="assign" ',
        'value="%s" ' % str(assign_tex).lower(),
        'description="Assign Texture" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="pullpush" ',
        'value="%s" ' % str(fill_tex).lower(),
        'description="Fill texture" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


def mesh2fc(script, all_visible_layers=False):
    """Transfer mesh colors to face colors

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        all_visible_layers (bool): If true the color mapping is applied to all the meshes
    """
    filter_xml = ''.join([
        '  <filter name="Transfer Color: Mesh to Face">\n',
        '    <Param name="allVisibleMesh" ',
        'value="%s" ' % str(all_visible_layers).lower(),
        'description="Apply to all Meshes" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None


def uniform_resampling(script, voxel=1.0, offset=0.0, merge_vert=True,
                       discretize=False, multisample=False, thicken=False):
    """ Create a new mesh that is a resampled version of the current one.

    The resampling is done by building a uniform volumetric representation
    where each voxel contains the signed distance from the original surface.
    The resampled surface is reconstructed using the marching cube algorithm
    over this volume.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        voxel (float): voxel (cell) size for resampling. Smaller cells give
            better precision at a higher computational cost. Remember that
            halving the cell size means that you build a volume 8 times larger.
        offset (float): offset amount of the created surface (i.e. distance of
            the created surface from the original one). If offset is zero, the
            created surface passes on the original mesh itself. Values greater
            than zero mean an external surface (offset), and lower than zero
            mean an internal surface (inset). In practice this value is the
            threshold passed to the Marching Cube algorithm to extract the
            isosurface from the distance field representation.
        merge_vert (bool): if True the mesh generated by MC will be cleaned by
            unifying vertices that are almost coincident.
        discretize (bool): if True the position of the intersected edge of the
            marching cube grid is not computed by linear interpolation, but it
            is placed in fixed middle position. As a consequence the resampled
            object will look severely aliased by a stairstep appearance. Useful
            only for simulating the output of 3D printing devices.
        multisample (bool): if True the distance field is more accurately
            compute by multisampling the volume (7 sample for each voxel). Much
            slower but less artifacts.
        thicken (bool): if True, you have to choose a non zero Offset and a
            double surface is built around the original surface, inside and
            outside. Is useful to convert thin floating surfaces into solid,
            thick meshes.

    Layer stack:
        Creates 1 new layer 'Offset mesh'
        Current layer is changed to new layer

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Uniform Mesh Resampling">\n',
        '    <Param name="CellSize" ',
        'value="{}" '.format(voxel),
        'description="Precision" ',
        'min="0" ',
        'max="100" ',
        'type="RichAbsPerc" ',
        '/>\n',
        '    <Param name="Offset" ',
        'value="{}" '.format(offset),
        'description="Offset" ',
        'min="-100" ',
        'max="100" ',
        'type="RichAbsPerc" ',
        '/>\n',
        '    <Param name="mergeCloseVert" ',
        'value="{}" '.format(str(merge_vert).lower()),
        'description="Clean Vertices" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="discretize" ',
        'value="{}" '.format(str(discretize).lower()),
        'description="Discretize" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="multisample" ',
        'value="{}" '.format(str(multisample).lower()),
        'description="Multisample" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="absDist" ',
        'value="{}" '.format(str(thicken).lower()),
        'description="Absolute Distance" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.add_layer('Offset mesh')
    return None

Create a new mesh that is a resampled version of the current one .


def surface_poisson_screened(script, visible_layer=False, depth=8,
                             full_depth=5, cg_depth=0, scale=1.1,
                             samples_per_node=1.5, point_weight=4.0,
                             iterations=8, confidence=False, pre_clean=False):
    """ This surface reconstruction algorithm creates watertight
        surfaces from oriented point sets.

    The filter uses the original code of Michael Kazhdan and Matthew Bolitho
    implementing the algorithm in the following paper:

    Michael Kazhdan, Hugues Hoppe,
    "Screened Poisson surface reconstruction"
    ACM Trans. Graphics, 32(3), 2013

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        visible_layer (bool): If True all the visible layers will be used for
            providing the points
        depth (int): This integer is the maximum depth of the tree that will
            be used for surface reconstruction. Running at depth d corresponds
            to solving on a voxel grid whose resolution is no larger than
            2^d x 2^d x 2^d. Note that since the reconstructor adapts the
            octree to the sampling density, the specified reconstruction depth
            is only an upper bound. The default value for this parameter is 8.
        full_depth (int): This integer specifies the depth beyond depth the
            octree will be adapted. At coarser depths, the octree will be
            complete, containing all 2^d x 2^d x 2^d nodes. The default value
            for this parameter is 5.
        cg_depth (int): This integer is the depth up to which a
            conjugate-gradients solver will be used to solve the linear system.
            Beyond this depth Gauss-Seidel relaxation will be used. The default
            value for this parameter is 0.
        scale (float): This floating point value specifies the ratio between
            the diameter of the cube used for reconstruction and the diameter
            of the samples' bounding cube.  The default value is 1.1.
        samples_per_node (float): This floating point value specifies the
            minimum number of sample points that should fall within an octree
            node as the octree construction is adapted to sampling density. For
            noise-free samples, small values in the range [1.0 - 5.0] can be
            used. For more noisy samples, larger values in the range
            [15.0 - 20.0] may be needed to provide a smoother, noise-reduced,
            reconstruction. The default value is 1.5.
        point_weight (float): This floating point value specifies the
            importance that interpolation of the point samples is given in the
            formulation of the screened Poisson equation. The results of the
            original (unscreened) Poisson Reconstruction can be obtained by
            setting this value to 0. The default value for this parameter is 4.
        iterations (int): This integer value specifies the number of
            Gauss-Seidel relaxations to be performed at each level of the
            hierarchy. The default value for this parameter is 8.
        confidence (bool): If True this tells the reconstructor to use the
            quality as confidence information; this is done by scaling the unit
            normals with the quality values. When the flag is not enabled, all
            normals are normalized to have unit-length prior to reconstruction.
        pre_clean (bool): If True will force a cleaning pre-pass on the data
            removing all unreferenced vertices or vertices with null normals.

    Layer stack:
        Creates 1 new layer 'Poisson mesh'
        Current layer is not changed

    MeshLab versions:
        2016.12
    """
    filter_xml = ''.join([
        '  <xmlfilter name="Screened Poisson Surface Reconstruction">\n',
        '    <xmlparam name="cgDepth" value="{:d}"/>\n'.format(cg_depth),
        '    <xmlparam name="confidence" value="{}"/>\n'.format(str(confidence).lower()),
        '    <xmlparam name="depth" value="{:d}"/>\n'.format(depth),
        '    <xmlparam name="fullDepth" value="{:d}"/>\n'.format(full_depth),
        '    <xmlparam name="iters" value="{:d}"/>\n'.format(iterations),
        '    <xmlparam name="pointWeight" value="{}"/>\n'.format(point_weight),
        '    <xmlparam name="preClean" value="{}"/>\n'.format(str(pre_clean).lower()),
        '    <xmlparam name="samplesPerNode" value="{}"/>\n'.format(samples_per_node),
        '    <xmlparam name="scale" value="{}"/>\n'.format(scale),
        '    <xmlparam name="visibleLayer" value="{}"/>\n'.format(str(visible_layer).lower()),
        '  </xmlfilter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.add_layer('Poisson mesh', change_layer=False)
    return None

This surface reconstruction algorithm creates watertight surfaces from oriented point sets .


def voronoi(script, hole_num=50, target_layer=None, sample_layer=None, thickness=0.5, backward=True):
    """ Turn a model into a surface with Voronoi style holes in it

    References:
    http://meshlabstuff.blogspot.com/2009/03/creating-voronoi-sphere.html
    http://meshlabstuff.blogspot.com/2009/04/creating-voronoi-sphere-2.html

    Requires FilterScript object

    Args:
        script: the FilterScript object to write the filter to. Does not
            work with a script filename.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """

    if target_layer is None:
        target_layer = script.current_layer()
    if sample_layer is None:
        # Current layer is currently not changed after poisson_disk is run
        sampling.poisson_disk(script, sample_num=hole_num)
        sample_layer = script.last_layer()

    vert_color.voronoi(script, target_layer=target_layer, source_layer=sample_layer, backward=backward)
    select.vert_quality(script, min_quality=0.0, max_quality=thickness)
    if backward:
        select.invert(script)
    delete.selected(script)
    smooth.laplacian(script, iterations=3)

    return None


def all(script, face=True, vert=True):
    """ Select all the faces of the current mesh

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        faces (bool): If True the filter will select all the faces.
        verts (bool): If True the filter will select all the vertices.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Select All">\n',
        '    <Param name="allFaces" ',
        'value="{}" '.format(str(face).lower()),
        'description="DSelect all Faces" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="allVerts" ',
        'value="{}" '.format(str(vert).lower()),
        'description="Select all Vertices" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

def
all
(
script
,
face
=
True
,
vert
=
True
)
:
filter_xml
=
''
.
join
(
[
'  <filter name="Select All">\n'
,
'    <Param name="allFaces" '
,
'value="{}" '
.
format
(
str
(
face
)
.
lower
(
)
)
,
'description="DSelect all Faces" '
,
'type="RichBool" '
,
'/>\n'
,
'    <Param name="allVerts" '
,
'value="{}" '
.
format
(
str
(
vert
)
.
lower
(
)
)
,
'description="Select all Vertices" '
,
'type="RichBool" '
,
'/>\n'
,
'  </filter>\n'
]
)
util
.
write_filter
(
script
,
filter_xml
)
return


def vert_quality(script, min_quality=0.0, max_quality=0.05, inclusive=True):
    """ Select all the faces and vertexes within the specified vertex quality
        range.

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        min_quality (float): Minimum acceptable quality value.
        max_quality (float): Maximum acceptable quality value.
        inclusive (bool): If True only the faces with ALL the vertices within
            the specified range are selected. Otherwise any face with at least
            one vertex within the range is selected.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Select by Vertex Quality">\n',
        '    <Param name="minQ" ',
        'value="{}" '.format(min_quality),
        'description="Min Quality" ',
        'min="0" ',
        'max="{}" '.format(2 * max_quality),
        'type="RichDynamicFloat" ',
        '/>\n',
        '    <Param name="maxQ" ',
        'value="{}" '.format(max_quality),
        'description="Max Quality" ',
        'min="0" ',
        'max="{}" '.format(2 * max_quality),
        'type="RichDynamicFloat" ',
        '/>\n',
        '    <Param name="Inclusive" ',
        'value="{}" '.format(str(inclusive).lower()),
        'description="Inclusive Sel." ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Select all the faces and vertexes within the specified vertex quality range .


def face_function(script, function='(fi == 0)'):
    """Boolean function using muparser lib to perform face selection over
        current mesh.

    See help(mlx.muparser_ref) for muparser reference documentation.

    It's possible to use parenthesis, per-vertex variables and boolean operator:
        (, ), and, or, <, >, =
    It's possible to use per-face variables like attributes associated to the three
    vertices of every face.

    Variables (per face):
        x0, y0, z0 for first vertex; x1,y1,z1 for second vertex; x2,y2,z2 for third vertex
        nx0, ny0, nz0, nx1, ny1, nz1, etc. for vertex normals
        r0, g0, b0, a0, etc. for vertex color
        q0, q1, q2 for quality
        wtu0, wtv0, wtu1, wtv1, wtu2, wtv2 (per wedge texture coordinates)
        ti for face texture index (>= ML2016.12)
        vsel0, vsel1, vsel2 for vertex selection (1 yes, 0 no) (>= ML2016.12)
        fr, fg, fb, fa for face color (>= ML2016.12)
        fq for face quality (>= ML2016.12)
        fnx, fny, fnz for face normal (>= ML2016.12)
        fsel face selection (1 yes, 0 no) (>= ML2016.12)

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        function (str): a boolean function that will be evaluated in order
            to select a subset of faces.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Conditional Face Selection">\n',
        '    <Param name="condSelect" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="boolean function" ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Boolean function using muparser lib to perform face selection over current mesh .


def vert_function(script, function='(q < 0)', strict_face_select=True):
    """Boolean function using muparser lib to perform vertex selection over current mesh.

    See help(mlx.muparser_ref) for muparser reference documentation.

    It's possible to use parenthesis, per-vertex variables and boolean operator:
        (, ), and, or, <, >, =
    It's possible to use the following per-vertex variables in the expression:

    Variables:
        x, y, z (coordinates)
        nx, ny, nz (normal)
        r, g, b, a (color)
        q (quality)
        rad
        vi (vertex index)
        vtu, vtv (texture coordinates)
        ti (texture index)
        vsel (is the vertex selected? 1 yes, 0 no)
        and all custom vertex attributes already defined by user.

    Args:
        script: the FilterScript object or script filename to write
            the filter] to.
        function (str): a boolean function that will be evaluated in order
            to select a subset of vertices. Example: (y > 0) and (ny > 0)
        strict_face_select (bool): if True a face is selected if ALL its
            vertices are selected. If False a face is selected if at least
            one of its vertices is selected. ML v1.3.4BETA only; this is
            ignored in 2016.12. In 2016.12 only vertices are selected.

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    if script.ml_version == '1.3.4BETA':
        strict_select = ''.join([
            '    <Param name="strictSelect" ',
            'value="{}" '.format(str(strict_face_select).lower()),
            'description="Strict face selection" ',
            'type="RichBool" ',
            '/>\n',
            ])
    else:
        strict_select = ''

    filter_xml = ''.join([
        '  <filter name="Conditional Vertex Selection">\n',
        '    <Param name="condSelect" ',
        'value="{}" '.format(str(function).replace('&', '&').replace('<', '<')),
        'description="boolean function" ',
        'type="RichString" ',
        '/>\n',
        strict_select,
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    return None

Boolean function using muparser lib to perform vertex selection over current mesh .


def cylindrical_vert(script, radius=1.0, inside=True):
    """Select all vertices within a cylindrical radius

    Args:
        radius (float): radius of the sphere
        center_pt (3 coordinate tuple or list): center point of the sphere

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    if inside:
        function = 'sqrt(x^2+y^2)<={}'.format(radius)
    else:
        function = 'sqrt(x^2+y^2)>={}'.format(radius)
    vert_function(script, function=function)
    return None

def cylindrical_vert ( script , radius = 1.0 , inside = True ) : if inside : function = 'sqrt(x^2+y^2)<={}' . format ( radius ) else : function = 'sqrt(x^2+y^2)>={}' . format ( radius ) vert_function ( script , function = function ) return None


def spherical_vert(script, radius=1.0, center_pt=(0.0, 0.0, 0.0)):
    """Select all vertices within a spherical radius

    Args:
        radius (float): radius of the sphere
        center_pt (3 coordinate tuple or list): center point of the sphere

    Layer stack:
        No impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    function = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)<={}'.format(
        center_pt[0], center_pt[1], center_pt[2], radius)
    vert_function(script, function=function)
    return None

def spherical_vert ( script , radius = 1.0 , center_pt = ( 0.0 , 0.0 , 0.0 ) ) : function = 'sqrt((x-{})^2+(y-{})^2+(z-{})^2)<={}' . format ( center_pt [ 0 ] , center_pt [ 1 ] , center_pt [ 2 ] , radius ) vert_function ( script , function = function ) return None


def join(script, merge_visible=True, merge_vert=False, delete_layer=True,
         keep_unreferenced_vert=False):
    """ Flatten all or only the visible layers into a single new mesh.

    Transformations are preserved. Existing layers can be optionally
    deleted.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        merge_visible (bool): merge only visible layers
        merge_vert (bool): merge the vertices that are duplicated among
            different layers. Very useful when the layers are spliced portions
            of a single big mesh.
        delete_layer (bool): delete all the merged layers. If all layers are
            visible only a single layer will remain after the invocation of
            this filter.
        keep_unreferenced_vert (bool): Do not discard unreferenced vertices
            from source layers. Necessary for point-only layers.

    Layer stack:
        Creates a new layer "Merged Mesh"
        Changes current layer to the new layer
        Optionally deletes all other layers

    MeshLab versions:
        2016.12
        1.3.4BETA

    Bugs:
        UV textures: not currently preserved, however will be in a future
            release. https://github.com/cnr-isti-vclab/meshlab/issues/128
        merge_visible: it is not currently possible to change the layer
            visibility from meshlabserver, however this will be possible
            in the future https://github.com/cnr-isti-vclab/meshlab/issues/123
    """
    filter_xml = ''.join([
        '  <filter name="Flatten Visible Layers">\n',
        '    <Param name="MergeVisible" ',
        'value="{}" '.format(str(merge_visible).lower()),
        'description="Merge Only Visible Layers" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="MergeVertices" ',
        'value="{}" '.format(str(merge_vert).lower()),
        'description="Merge duplicate vertices" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="DeleteLayer" ',
        'value="{}" '.format(str(delete_layer).lower()),
        'description="Delete Layers" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="AlsoUnreferenced" ',
        'value="{}" '.format(str(keep_unreferenced_vert).lower()),
        'description="Keep unreferenced vertices" ',
        'type="RichBool" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, mlx.FilterScript):
        script.add_layer('Merged Mesh')
        if delete_layer:
            # As it is not yet possible to change the layer visibility, all
            # layers will be deleted. This will be updated once layer
            # visibility is tracked.
            for i in range(script.last_layer()):
                script.del_layer(0)
    return None

Flatten all or only the visible layers into a single new mesh .


def rename(script, label='blank', layer_num=None):
    """ Rename layer label

    Can be useful for outputting mlp files, as the output file names use
    the labels.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        label (str): new label for the mesh layer
        layer_num (int): layer number to rename. Default is the
            current layer. Not supported on the file base API.

    Layer stack:
        Renames a layer

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = ''.join([
        '  <filter name="Rename Current Mesh">\n',
        '    <Param name="newName" ',
        'value="{}" '.format(label),
        'description="New Label" ',
        'type="RichString" ',
        '/>\n',
        '  </filter>\n'])
    if isinstance(script, mlx.FilterScript):
        if (layer_num is None) or (layer_num == script.current_layer()):
            util.write_filter(script, filter_xml)
            script.layer_stack[script.current_layer()] = label
        else:
            cur_layer = script.current_layer()
            change(script, layer_num)
            util.write_filter(script, filter_xml)
            change(script, cur_layer)
            script.layer_stack[layer_num] = label
    else:
        util.write_filter(script, filter_xml)
    return None


def change(script, layer_num=None):
    """ Change the current layer by specifying the new layer number.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        layer_num (int): the number of the layer to change to. Default is the
            last layer if script is a mlx.FilterScript object; if script is a
            filename the default is the first layer.

    Layer stack:
        Modifies current layer

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    if layer_num is None:
        if isinstance(script, mlx.FilterScript):
            layer_num = script.last_layer()
        else:
            layer_num = 0
    filter_xml = ''.join([
        '  <filter name="Change the current layer">\n',
        '    <Param name="mesh" ',
        'value="{:d}" '.format(layer_num),
        'description="Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, mlx.FilterScript):
        script.set_current_layer(layer_num)
        #script.layer_stack[len(self.layer_stack) - 1] = layer_num
    return None

Change the current layer by specifying the new layer number .


def duplicate(script, layer_num=None):
    """ Duplicate a layer.

    New layer label is '*_copy'.

    Args:
        script: the mlx.FilterScript object or script filename to write
            the filter to.
        layer_num (int): layer number to duplicate. Default is the
            current layer. Not supported on the file base API.

    Layer stack:
        Creates a new layer
        Changes current layer to the new layer

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    filter_xml = '  <filter name="Duplicate Current layer"/>\n'
    if isinstance(script, mlx.FilterScript):
        if (layer_num is None) or (layer_num == script.current_layer()):
            util.write_filter(script, filter_xml)
            script.add_layer('{}_copy'.format(script.layer_stack[script.current_layer()]), True)
        else:
            change(script, layer_num)
            util.write_filter(script, filter_xml)
            script.add_layer('{}_copy'.format(script.layer_stack[layer_num]), True)
    else:
        util.write_filter(script, filter_xml)
    return None


def delete_lower(script, layer_num=None):
    """ Delete all layers below the specified one.

    Useful for MeshLab ver 2016.12, whcih will only output layer 0.
    """
    if layer_num is None:
        layer_num = script.current_layer()
    if layer_num != 0:
        change(script, 0)
    for i in range(layer_num):
        delete(script, 0)
    return None

def delete_lower ( script , layer_num = None ) : if layer_num is None : layer_num = script . current_layer ( ) if layer_num != 0 : change ( script , 0 ) for i in range ( layer_num ) : delete ( script , 0 ) return None

Delete all layers below the specified one .


def handle_error(program_name, cmd, log=None):
    """Subprocess program error handling

    Args:
        program_name (str): name of the subprocess program

    Returns:
        break_now (bool): indicate whether calling program should break out of loop

    """
    print('\nHouston, we have a problem.',
          '\n%s did not finish successfully. Review the log' % program_name,
          'file and the input file(s) to see what went wrong.')
    print('%s command: "%s"' % (program_name, cmd))
    if log is not None:
        print('log: "%s"' % log)
    print('Where do we go from here?')
    print(' r  - retry running %s (probably after' % program_name,
          'you\'ve fixed any problems with the input files)')
    print(' c  - continue on with the script (probably after',
          'you\'ve manually re-run and generated the desired',
          'output file(s)')
    print(' x  - exit, keeping the TEMP3D files and log')
    print(' xd - exit, deleting the TEMP3D files and log')
    while True:
        choice = input('Select r, c, x (default), or xd: ')
        if choice not in ('r', 'c', 'x', 'xd'):
            #print('Please enter a valid option.')
            choice = 'x'
        #else:
        break
    if choice == 'x':
        print('Exiting ...')
        sys.exit(1)
    elif choice == 'xd':
        print('Deleting TEMP3D* and log files and exiting ...')
        util.delete_all('TEMP3D*')
        if log is not None:
            os.remove(log)
        sys.exit(1)
    elif choice == 'c':
        print('Continuing on ...')
        break_now = True
    elif choice == 'r':
        print('Retrying %s cmd ...' % program_name)
        break_now = False
    return break_now

' r  - retry running %s (probably after'


def begin(script='TEMP3D_default.mlx', file_in=None, mlp_in=None):
    """Create new mlx script and write opening tags.

    Performs special processing on stl files.

    If no input files are provided this will create a dummy
    file and delete it as the first filter. This works around
    the meshlab limitation that it must be provided an input
    file, even if you will be creating a mesh as the first
    filter.

    """
    script_file = open(script, 'w')
    script_file.write(''.join(['<!DOCTYPE FilterScript>\n',
                               '<FilterScript>\n']))
    script_file.close()

    current_layer = -1
    last_layer = -1
    stl = False

    # Process project files first
    if mlp_in is not None:
        # make a list if it isn't already
        if not isinstance(mlp_in, list):
            mlp_in = [mlp_in]
        for val in mlp_in:
            tree = ET.parse(val)
            #root = tree.getroot()
            for elem in tree.iter(tag='MLMesh'):
                filename = (elem.attrib['filename'])
                current_layer += 1
                last_layer += 1
                # If the mesh file extension is stl, change to that layer and
                # run clean.merge_vert
                if os.path.splitext(filename)[1][1:].strip().lower() == 'stl':
                    layers.change(script, current_layer)
                    clean.merge_vert(script)
                    stl = True

    # Process separate input files next
    if file_in is not None:
        # make a list if it isn't already
        if not isinstance(file_in, list):
            file_in = [file_in]
        for val in file_in:
            current_layer += 1
            last_layer += 1
            # If the mesh file extension is stl, change to that layer and
            # run clean.merge_vert
            if os.path.splitext(val)[1][1:].strip().lower() == 'stl':
                layers.change(script, current_layer)
                clean.merge_vert(script)
                stl = True

    # If some input files were stl, we need to change back to the last layer
    if stl:
        layers.change(script, last_layer)  # Change back to the last layer
    elif last_layer == -1:
        # If no input files are provided, create a dummy file
        # with a single vertex and delete it first in the script.
        # This works around the fact that meshlabserver will
        # not run without an input file.
        file_in = ['TEMP3D.xyz']
        file_in_descriptor = open(file_in[0], 'w')
        file_in_descriptor.write('0 0 0')
        file_in_descriptor.close()
        layers.delete(script)
    return current_layer, last_layer

Create new mlx script and write opening tags .


def add_layer(self, label, change_layer=True):
        """ Add new mesh layer to the end of the stack

        Args:
            label (str): new label for the mesh layer
            change_layer (bool): change to the newly created layer
        """
        self.layer_stack.insert(self.last_layer() + 1, label)
        if change_layer:
            self.set_current_layer(self.last_layer())
        return None

def add_layer ( self , label , change_layer = True ) : self . layer_stack . insert ( self . last_layer ( ) + 1 , label ) if change_layer : self . set_current_layer ( self . last_layer ( ) ) return None


def del_layer(self, layer_num):
        """ Delete mesh layer """
        del self.layer_stack[layer_num]
        # Adjust current layer if needed
        if layer_num < self.current_layer():
            self.set_current_layer(self.current_layer() - 1)
        return None

def
del_layer
(
self
,
layer_num
)
:
del
self
.
layer_stack
[
layer_num
]
# Adjust current layer if needed
if
layer_num
<
self
.
current_layer
(
)
:
self
.
set_current_layer
(
self
.
current_layer
(
)
1
)
return


def save_to_file(self, script_file):
        """ Save filter script to an mlx file """
        # TODO: rasie exception here instead?
        if not self.filters:
            print('WARNING: no filters to save to file!')
        script_file_descriptor = open(script_file, 'w')
        script_file_descriptor.write(''.join(self.opening + self.filters + self.closing))
        script_file_descriptor.close()

def
save_to_file
(
self
,
script_file
)
:
# TODO: rasie exception here instead?
if
not
self
.
filters
:
print
(
'WARNING: no filters to save to file!'
)
script_file_descriptor
=
open
(
script_file
,
'w'
)
script_file_descriptor
.
write
(
''
.
join
(
self
.
opening
+
self
.
filters
+
self
.
closing
)
)
script_file_descriptor
.
close
(
)


def run_script(self, log=None, ml_log=None, mlp_out=None, overwrite=False,
                   file_out=None, output_mask=None, script_file=None, print_meshlabserver_output=True):
        """ Run the script
        """

        temp_script = False
        temp_ml_log = False

        if self.__no_file_in:
            # If no input files are provided, create a dummy file
            # with a single vertex and delete it first in the script.
            # This works around the fact that meshlabserver will
            # not run without an input file.
            temp_file_in_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xyz', dir=os.getcwd())
            temp_file_in_file.write(b'0 0 0')
            temp_file_in_file.close()
            self.file_in = [temp_file_in_file.name]

        if not self.filters:
            script_file = None
        elif script_file is None:
            # Create temporary script file
            temp_script = True
            temp_script_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mlx')
            temp_script_file.close()
            self.save_to_file(temp_script_file.name)
            script_file = temp_script_file.name

        if (self.parse_geometry or self.parse_topology or self.parse_hausdorff) and (ml_log is None):
            # create temp ml_log
            temp_ml_log = True
            ml_log_file = tempfile.NamedTemporaryFile(delete=False, suffix='.txt')
            ml_log_file.close()
            ml_log = ml_log_file.name
        if file_out is None:
            file_out = self.file_out

        run(script=script_file, log=log, ml_log=ml_log,
            mlp_in=self.mlp_in, mlp_out=mlp_out, overwrite=overwrite,
            file_in=self.file_in, file_out=file_out, output_mask=output_mask, ml_version=self.ml_version,
            print_meshlabserver_output=print_meshlabserver_output)

        # Parse output
        # TODO: record which layer this is associated with?
        if self.parse_geometry:
            self.geometry = compute.parse_geometry(ml_log, log, print_output=print_meshlabserver_output)
        if self.parse_topology:
            self.topology = compute.parse_topology(ml_log, log, print_output=print_meshlabserver_output)
        if self.parse_hausdorff:
            self.hausdorff_distance = compute.parse_hausdorff(ml_log, log, print_output=print_meshlabserver_output)

        # Delete temp files
        if self.__no_file_in:
            os.remove(temp_file_in_file.name)
        if temp_script:
            os.remove(temp_script_file.name)
        if temp_ml_log:
            os.remove(ml_log_file.name)


def main():
    """Run main script"""
    # segments = number of segments to use for circles
    segments = 50
    # star_points = number of points (or sides) of the star
    star_points = 5
    # star_radius = radius of circle circumscribing the star
    star_radius = 2
    # ring_thickness = thickness of the colored rings
    ring_thickness = 1
    # sphere_radius = radius of sphere the shield will be deformed to
    sphere_radius = 2 * (star_radius + 3 * ring_thickness)

    # Star calculations:
    # Visually approximate a star by using multiple diamonds (i.e. scaled
    # squares) which overlap in the center. For the star calculations,
    # consider a central polygon with triangles attached to the edges, all
    # circumscribed by a circle.
    # polygon_radius = distance from center of circle to polygon edge midpoint
    polygon_radius = star_radius / \
        (1 + math.tan(math.radians(180 / star_points)) /
         math.tan(math.radians(90 / star_points)))
    # width = 1/2 width of polygon edge/outer triangle bottom
    width = polygon_radius * math.tan(math.radians(180 / star_points))
    # height = height of outer triangle
    height = width / math.tan(math.radians(90 / star_points))

    shield = mlx.FilterScript(file_out="shield.ply")

    # Create the colored front of the shield using several concentric
    # annuluses; combine them together and subdivide so we have more vertices
    # to give a smoother deformation later.
    mlx.create.annulus(shield, radius=star_radius, cir_segments=segments, color='blue')
    mlx.create.annulus(shield,
        radius1=star_radius + ring_thickness,
        radius2=star_radius,
        cir_segments=segments,
        color='red')
    mlx.create.annulus(shield,
        radius1=star_radius + 2 * ring_thickness,
        radius2=star_radius + ring_thickness,
        cir_segments=segments,
        color='white')
    mlx.create.annulus(shield,
        radius1=star_radius + 3 * ring_thickness,
        radius2=star_radius + 2 * ring_thickness,
        cir_segments=segments,
        color='red')
    mlx.layers.join(shield)
    mlx.subdivide.midpoint(shield, iterations=2)

    # Create the inside surface of the shield & translate down slightly so it
    # doesn't overlap the front.
    mlx.create.annulus(shield,
        radius1=star_radius + 3 * ring_thickness,
        cir_segments=segments,
        color='silver')
    mlx.transform.rotate(shield, axis='y', angle=180)
    mlx.transform.translate(shield, value=[0, 0, -0.005])
    mlx.subdivide.midpoint(shield, iterations=4)

    # Create a diamond for the center star. First create a plane, specifying
    # extra vertices to support the final deformation. The length from the
    # center of the plane to the corners should be 1 for ease of scaling, so
    # we use a side length of sqrt(2) (thanks Pythagoras!). Rotate the plane
    # by 45 degrees and scale it to stretch it out per the calculations above,
    # then translate it into place (including moving it up in z slightly so
    # that it doesn't overlap the shield front).
    mlx.create.grid(shield,
        size=math.sqrt(2),
        x_segments=10,
        y_segments=10,
        center=True,
        color='white')
    mlx.transform.rotate(shield, axis='z', angle=45)
    mlx.transform.scale(shield, value=[width, height, 1])
    mlx.transform.translate(shield, value=[0, polygon_radius, 0.001])

    # Duplicate the diamond and rotate the duplicates around, generating the
    # star.
    for _ in range(1, star_points):
        mlx.layers.duplicate(shield)
        mlx.transform.rotate(shield, axis='z', angle=360 / star_points)

    # Combine everything together and deform using a spherical function.
    mlx.layers.join(shield)
    mlx.transform.vert_function(shield,
        z_func='sqrt(%s-x^2-y^2)-%s+z' %
        (sphere_radius**2, sphere_radius))

    # Run the script using meshlabserver and generate the model
    shield.run_script()
    return None


def hausdorff_distance(script, sampled_layer=1, target_layer=0,
                       save_sample=False, sample_vert=True, sample_edge=True,
                       sample_faux_edge=False, sample_face=True,
                       sample_num=1000, maxdist=10):
    """ Compute the Hausdorff Distance between two meshes, sampling one of the
        two and finding for each sample the closest point over the other mesh.

    Args:
        script: the FilterScript object or script filename to write
            the filter to.
        sampled_layer (int): The mesh layer whose surface is sampled. For each
            sample we search the closest point on the target mesh layer.
        target_layer (int): The mesh that is sampled for the comparison.
        save_sample (bool): Save the position and distance of all the used
            samples on both the two surfaces, creating two new layers with two
            point clouds representing the used samples.
        sample_vert (bool): For the search of maxima it is useful to sample
            vertices and edges of the mesh with a greater care. It is quite
            probable that the farthest points falls along edges or on mesh
            vertexes, and with uniform montecarlo sampling approaches the
            probability of taking a sample over a vertex or an edge is
            theoretically null. On the other hand this kind of sampling could
            make the overall sampling distribution slightly biased and slightly
            affects the cumulative results.
        sample_edge (bool): see sample_vert
        sample_faux_edge (bool): see sample_vert
        sample_face (bool): see sample_vert
        sample_num (int): The desired number of samples. It can be smaller or
            larger than the mesh size, and according to the chosen sampling
            strategy it will try to adapt.
        maxdist (int): Sample points for which we do not find anything within
            this distance are rejected and not considered neither for averaging
            nor for max.

    Layer stack:
        If save_sample is True, two new layers are created: 'Hausdorff Closest
            Points' and 'Hausdorff Sample Point'; and the current layer is
            changed to the last newly created layer.
        If save_sample is False, no impacts

    MeshLab versions:
        2016.12
        1.3.4BETA
    """
    # MeshLab defaults:
    #  sample_num = number of vertices
    #  maxdist = 0.05 * AABB['diag'] #5% of AABB[diag]
    #  maxdist_max = AABB['diag']
    maxdist_max = 2*maxdist
    # TODO: parse output (min, max, mean, etc.)
    filter_xml = ''.join([
        '  <filter name="Hausdorff Distance">\n',
        '    <Param name="SampledMesh" ',
        'value="{:d}" '.format(sampled_layer),
        'description="Sampled Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="TargetMesh" ',
        'value="{:d}" '.format(target_layer),
        'description="Target Mesh" ',
        'type="RichMesh" ',
        '/>\n',
        '    <Param name="SaveSample" ',
        'value="{}" '.format(str(save_sample).lower()),
        'description="Save Samples" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleVert" ',
        'value="{}" '.format(str(sample_vert).lower()),
        'description="Sample Vertexes" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleEdge" ',
        'value="{}" '.format(str(sample_edge).lower()),
        'description="Sample Edges" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleFauxEdge" ',
        'value="{}" '.format(str(sample_faux_edge).lower()),
        'description="Sample FauxEdge" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleFace" ',
        'value="{}" '.format(str(sample_face).lower()),
        'value="%s" ' % str(sample_face).lower() +
        'description="Sample Faces" ',
        'type="RichBool" ',
        '/>\n',
        '    <Param name="SampleNum" ',
        'value="{:d}" '.format(sample_num),
        'description="Number of samples" ',
        'type="RichInt" ',
        '/>\n',
        '    <Param name="MaxDist" ',
        'value="{}" '.format(maxdist),
        'value="%s" ' % maxdist +
        'description="Max Distance" ',
        'min="0" ',
        'max="{}" '.format(maxdist_max),
        'type="RichAbsPerc" ',
        '/>\n',
        '  </filter>\n'])
    util.write_filter(script, filter_xml)
    if isinstance(script, FilterScript):
        script.parse_hausdorff = True
    if isinstance(script, FilterScript) and save_sample:
        script.add_layer('Hausdorff Closest Points')
        script.add_layer('Hausdorff Sample Point')
    return None

'  <filter name="Hausdorff Distance">\n'

Compute the Hausdorff Distance between two meshes sampling one of the two and finding for each sample the closest point over the other mesh .