#
##
## SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.5 (Thu Feb 8 19:11:13 CET 2024)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: https://pyformex.org
## Project page: https://savannah.nongnu.org/projects/pyformex/
## Development: https://gitlab.com/bverheg/pyformex
## Distributed under the GNU General Public License version 3 or later.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see http://www.gnu.org/licenses/.
##
"""This modules defines the CanvasSettings class
"""
import sys
from functools import partial
from dataclasses import dataclass
import pyformex as pf
from pyformex import utils
from pyformex import arraytools as at
from pyformex.mydict import Dict
from pyformex.opengl.sanitize import saneColor, saneColorSet
############### OpenGL Lighting #################################
class Material():
def __init__(self, name, ambient=0.2, diffuse=0.2, specular=0.9,
emission=0.1, shininess=2.0):
self.name = str(name)
self.ambient = float(ambient)
self.diffuse = float(diffuse)
self.specular = float(specular)
self.emission = float(emission)
self.shininess = float(shininess)
def setValues(self, **kargs):
for k in kargs:
if hasattr(self, k):
setattr(self, k, float(kargs[k]))
def dict(self):
"""Return the material light parameters as a dict"""
return dict([(k, getattr(self, k)) for k in
('ambient', 'diffuse', 'specular', 'emission', 'shininess')])
def __str__(self):
return f"""MATERIAL: {self.name}
ambient: {self.ambient}
diffuse: {self.diffuse}
specular: {self.specular}
emission: {self.emission}
shininess: {self.shininess}
"""
def getMaterials():
mats = pf.refcfg['material']
mats.update(pf.prefcfg['material'])
mats.update(pf.cfg['material'])
return mats
def createMaterials():
mats = getMaterials()
matdb = {}
for m in mats:
matdb[m] = Material(m, **mats[m])
return matdb
[docs]class Light():
"""A class representing an OpenGL light.
The light can emit 3 types of light: ambient, diffuse and specular,
which can have different color and are all off by default.
"""
def __init__(self, ambient=0.0, diffuse=0.0, specular=0.0,
position=[0., 0., 1.], enabled=True):
self.setValues(ambient, diffuse, specular, position)
self.enable(enabled)
def setValues(self, ambient=None, diffuse=None, specular=None, position=None):
if ambient is not None:
self.ambient = pf.Color(ambient)
if diffuse is not None:
self.diffuse = pf.Color(diffuse)
if specular is not None:
self.specular = pf.Color(specular)
if position is not None:
self.position = at.checkArray(position, (3,), 'f')
def enable(self, onoff=True):
self.enabled = bool(onoff)
def disable(self):
self.enable(False)
def __str__(self, name=''):
return f"""LIGHT{name} (enabled: {self.enabled}):
ambient color: {self.ambient}
diffuse color: {self.diffuse}
specular color: {self.specular}
position: {self.position}
"""
[docs]class LightProfile():
"""A lightprofile contains all the lighting parameters.
Currently this consists off:
- `ambient`: the global ambient lighting (currently a float)
- `lights`: a list of 1 to 4 Lights
"""
def __init__(self, ambient, lights):
self.ambient = ambient
self.lights = lights
def __str__(self):
s = f"""LIGHTPROFILE:
global ambient: {self.amnient}
"""
for i, l in enumerate(self.lights):
s += ' ' + l.__str__(i)
return s
#####################################
## Rendermode ##
WIREMODE = {'wire': 1, 'bord': 2, 'feat': 3}
if sys.hexversion >= 0x030A0000:
my_dataclass = partial(dataclass, slots=True)
else:
my_dataclass = dataclass
[docs]@my_dataclass
class Rendermode:
"""Store the rendermode parameters in a single object
Currently this holds the following parameters:
- fill: boolean
- smooth: boolean (smooth/flat shading)
- lighting: boolean (lights on/off)
- wiremode: int 0..3 | None
- avgnormals: boolean | None
The parameters can be set by a string value::
fill smooth wiremode avgnormals
lighting
wireframe F F None None
flat T F 0 None
flatwire T F 1 None
flatbord T F 2 None
flatfeat T F 3 None
smooth T T 0 F
smoothwire T T 1 F
smoothbord T T 2 F
smoothfeat T T 3 F
smoothnorm T T 0 T
"""
if sys.hexversion < 0x030A0000:
__slots__ = ('fill', 'smooth', 'lighting', 'wiremode', 'avgnormals')
fill: bool
smooth: bool
lighting: bool
wiremode: int
avgnormals: bool
def __post_init__(self):
self.fill = bool(self.fill)
self.smooth = bool(self.smooth)
self.lighting = bool(self.lighting)
if self.wiremode is not None:
self.wiremode = int(self.wiremode)
if self.avgnormals is not None:
self.avgnormals = bool(self.avgnormals)
@classmethod
def from_str(cls, s):
fill = not s.startswith('wire')
smooth = lighting = s.startswith('smooth')
if fill:
wiremode = WIREMODE.get(s[-4:], 0)
# if not smooth:
# wiremode = -wiremode
else:
wiremode = 0 # should become None
if smooth:
avgnormals = s[-4:] == 'norm'
else:
avgnormals = None
return Rendermode(fill, smooth, lighting, wiremode, avgnormals)
def to_str(self):
if not self.fill:
return 'wireframe'
s = 'smooth' if self.smooth else 'flat'
s += ['', 'wire', 'bord', 'feat'][max(self.wiremode, 0)]
if s == 'smooth' and self.avgnormals:
s += 'norm'
return s
# __str__ = to_str
RenderProfiles = dict((k, Rendermode.from_str(k)) for k in (
'wireframe',
'flat', 'flatwire', 'flatbord', 'flatfeat',
'smooth', 'smoothwire', 'smoothbord', 'smoothfeat',
'smoothnorm'))
def list_renderprofiles():
for k, v in RenderProfiles.items():
print(f"{k} = {v!r} = {v!s}")
##################################################################
#
# The Canvas Settings
#
[docs]class CanvasSettings(Dict):
"""A collection of settings for an OpenGL Canvas.
The canvas settings are a collection of settings and default values
affecting the rendering in an individual viewport. There are two type of
settings:
- mode settings are set during the initialization of the canvas and
can/should not be changed during the drawing of actors and decorations;
- default settings can be used as default values but may be changed during
the drawing of actors/decorations: they are reset before each individual
draw instruction.
Currently the following mode settings are defined:
- bgmode: the viewport background color mode
- bgcolor: the viewport background color: a single color or a list of
colors (max. 4 are used).
- bgimage: background image filename
- alphablend: boolean (transparency on/off)
The list of default settings includes:
- fgcolor: the default drawing color
- bkcolor: the default backface color
- slcolor: the highlight color
- colormap: the default color map to be used if color is an index
- bklormap: the default color map to be used if bkcolor is an index
- textcolor: the default color for text drawing
- culling: boolean
- transparency: float (0.0..1.0)
- pointsize: the default size for drawing points
- marksize: the default size for drawing markers
- linewidth: the default width for drawing lines
Any of these values can be set in the constructor using a keyword argument.
All items that are not set, will get their value from the configuration
file(s).
"""
bgcolor_modes = ['solid', 'vertical', 'horizontal', 'full']
edge_modes = ['none', 'feature', 'all']
def __init__(self, **kargs):
"""Create a new set of CanvasSettings."""
Dict.__init__(self)
self.reset(kargs)
[docs] def reset(self, d={}):
"""Reset the CanvasSettings to its defaults.
The default values are taken from the configuration files.
An optional dictionary may be specified to override (some of) these defaults.
"""
self.update(pf.refcfg['canvas'])
self.update(pf.prefcfg['canvas'])
self.update(pf.cfg['canvas'])
if d:
self.update(d)
[docs] def update(self, d, strict=True):
"""Update current values with the specified settings
Returns the sanitized update values.
"""
ok = self.checkDict(d, strict)
Dict.update(self, ok)
[docs] @classmethod
def checkDict(clas, dict, strict=True):
"""Transform a dict to acceptable settings."""
ok = {}
for k, v in dict.items():
try:
if k in ('bgcolor', 'fgcolor', 'bkcolor', 'slcolor',
'textcolor', 'colormap', 'bkcolormap'):
if v is not None:
v = saneColor(v)
elif k in ('bgimage',):
v = str(v)
elif k in ('culling', 'alphablend'):
v = bool(v)
elif k in ['linewidth', 'pointsize', 'marksize']:
v = float(v)
elif k in ['wiremode']:
v = int(v)
elif k == 'linestipple':
v = [int(vi) for vi in v]
elif k == 'transparency':
v = max(min(float(v), 1.0), 0.0)
elif k == 'bgmode':
v = str(v).lower()
if v not in clas.bgcolor_modes:
raise ValueError(f"Invalid background color mode {v}")
elif k == 'marktype':
pass
elif k == 'shading':
if v in ['wire', 'flat', 'smooth']:
pass
else:
raise ValueError('Invalide shading value')
else:
if strict:
raise ValueError("Invalid key")
else:
continue
ok[k] = v
except Exception as e:
if strict:
print(f"Invalid key/value for CanvasSettings: {k} = {v}")
raise e
return ok
[docs] def activate(self):
"""Activate the default canvas settings in the GL machine."""
from pyformex.opengl import gl
for k in self:
if k in ['smooth', 'fill', 'linewidth', 'pointsize']:
func = getattr(gl, 'gl_'+k)
try:
func(self[k])
except Exception:
print(f"Error in setting {k} with func {func}")
raise
__str__ = pf.software.formatDict
### End