Skip to content
Commits on Source (28)
......@@ -21,3 +21,13 @@ old-todo.txt
todo.txt
tools/upload-www
trash/
Patro/GraphicEngine/TeX/__MERGE_MUSICA__
examples/output-2017/
examples/patterns/veravenus-little-bias-dress.pattern-a0.pdf
examples/patterns/veravenus-little-bias-dress.pattern-a0.svg
notes.txt
open-doc.sh
outdated.txt
ressources
src/
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# 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/>.
#
####################################################################################################
# from Matplotlib, Valentina, Qt
####################################################################################################
BASE_COLORS = {
'black': (0, 0, 0),
'blue': (0, 0, 1),
'green': (0, 1, 0),
'cyan': (0, 1, 1),
'red': (1, 0, 0),
'magenta': (1, 0, 1),
'yellow': (1, 1, 0),
'white': (1, 1, 1),
}
####################################################################################################
# These colors are from Tableau
TABLEAU_COLORS = {
'blue': '#1f77b4',
'brown': '#8c564b',
'cyan': '#17becf',
'gray': '#7f7f7f',
'green': '#2ca02c',
'olive': '#bcbd22',
'orange': '#ff7f0e',
'pink': '#e377c2',
'purple': '#9467bd',
'red': '#d62728',
}
####################################################################################################
# This mapping of color names -> hex values is taken from
# a survey run by Randel Monroe see:
# http://blog.xkcd.com/2010/05/03/color-survey-results/
# for more details. The results are hosted at
# https://xkcd.com/color/rgb.txt
#
# License: http://creativecommons.org/publicdomain/zero/1.0/
XKCD_COLORS = {
"robin's egg blue": '#98eff9',
"robin's egg": '#6dedfd',
'acid green': '#8ffe09',
'adobe': '#bd6c48',
'algae green': '#21c36f',
'algae': '#54ac68',
'almost black': '#070d0d',
'amber': '#feb308',
'amethyst': '#9b5fc0',
'apple green': '#76cd26',
'apple': '#6ecb3c',
'apricot': '#ffb16d',
'aqua blue': '#02d8e9',
'aqua green': '#12e193',
'aqua marine': '#2ee8bb',
'aqua': '#13eac9',
'aquamarine': '#04d8b2',
'army green': '#4b5d16',
'asparagus': '#77ab56',
'aubergine': '#3d0734',
'auburn': '#9a3001',
'avocado green': '#87a922',
'avocado': '#90b134',
'azul': '#1d5dec',
'azure': '#069af3',
'baby blue': '#a2cffe',
'baby green': '#8cff9e',
'baby pink': '#ffb7ce',
'baby poo': '#ab9004',
'baby poop green': '#8f9805',
'baby poop': '#937c00',
'baby puke green': '#b6c406',
'baby purple': '#ca9bf7',
'baby shit brown': '#ad900d',
'baby shit green': '#889717',
'banana yellow': '#fafe4b',
'banana': '#ffff7e',
'barbie pink': '#fe46a5',
'barf green': '#94ac02',
'barney purple': '#a00498',
'barney': '#ac1db8',
'battleship grey': '#6b7c85',
'beige': '#e6daa6',
'berry': '#990f4b',
'bile': '#b5c306',
'black': '#000000',
'bland': '#afa88b',
'blood orange': '#fe4b03',
'blood red': '#980002',
'blood': '#770001',
'blue blue': '#2242c7',
'blue green': '#137e6d',
'blue grey': '#607c8e',
'blue purple': '#5729ce',
'blue violet': '#5d06e9',
'blue with a hint of purple': '#533cc6',
'blue': '#0343df',
'blue/green': '#0f9b8e',
'blue/grey': '#758da3',
'blue/purple': '#5a06ef',
'blueberry': '#464196',
'bluegreen': '#017a79',
'bluegrey': '#85a3b2',
'bluey green': '#2bb179',
'bluey grey': '#89a0b0',
'bluey purple': '#6241c7',
'bluish green': '#10a674',
'bluish grey': '#748b97',
'bluish purple': '#703be7',
'bluish': '#2976bb',
'blurple': '#5539cc',
'blush pink': '#fe828c',
'blush': '#f29e8e',
'booger green': '#96b403',
'booger': '#9bb53c',
'bordeaux': '#7b002c',
'boring green': '#63b365',
'bottle green': '#044a05',
'brick orange': '#c14a09',
'brick red': '#8f1402',
'brick': '#a03623',
'bright aqua': '#0bf9ea',
'bright blue': '#0165fc',
'bright cyan': '#41fdfe',
'bright green': '#01ff07',
'bright lavender': '#c760ff',
'bright light blue': '#26f7fd',
'bright light green': '#2dfe54',
'bright lilac': '#c95efb',
'bright lime green': '#65fe08',
'bright lime': '#87fd05',
'bright magenta': '#ff08e8',
'bright olive': '#9cbb04',
'bright orange': '#ff5b00',
'bright pink': '#fe01b1',
'bright purple': '#be03fd',
'bright red': '#ff000d',
'bright sea green': '#05ffa6',
'bright sky blue': '#02ccfe',
'bright teal': '#01f9c6',
'bright turquoise': '#0ffef9',
'bright violet': '#ad0afd',
'bright yellow green': '#9dff00',
'bright yellow': '#fffd01',
'british racing green': '#05480d',
'bronze': '#a87900',
'brown green': '#706c11',
'brown grey': '#8d8468',
'brown orange': '#b96902',
'brown red': '#922b05',
'brown yellow': '#b29705',
'brown': '#653700',
'brownish green': '#6a6e09',
'brownish grey': '#86775f',
'brownish orange': '#cb7723',
'brownish pink': '#c27e79',
'brownish purple': '#76424e',
'brownish red': '#9e3623',
'brownish yellow': '#c9b003',
'brownish': '#9c6d57',
'browny green': '#6f6c0a',
'browny orange': '#ca6b02',
'bruise': '#7e4071',
'bubble gum pink': '#ff69af',
'bubblegum pink': '#fe83cc',
'bubblegum': '#ff6cb5',
'buff': '#fef69e',
'burgundy': '#610023',
'burnt orange': '#c04e01',
'burnt red': '#9f2305',
'burnt siena': '#b75203',
'burnt sienna': '#b04e0f',
'burnt umber': '#a0450e',
'burnt yellow': '#d5ab09',
'burple': '#6832e3',
'butter yellow': '#fffd74',
'butter': '#ffff81',
'butterscotch': '#fdb147',
'cadet blue': '#4e7496',
'camel': '#c69f59',
'camo green': '#526525',
'camo': '#7f8f4e',
'camouflage green': '#4b6113',
'canary yellow': '#fffe40',
'canary': '#fdff63',
'candy pink': '#ff63e9',
'caramel': '#af6f09',
'carmine': '#9d0216',
'carnation pink': '#ff7fa7',
'carnation': '#fd798f',
'carolina blue': '#8ab8fe',
'celadon': '#befdb7',
'celery': '#c1fd95',
'cement': '#a5a391',
'cerise': '#de0c62',
'cerulean blue': '#056eee',
'cerulean': '#0485d1',
'charcoal grey': '#3c4142',
'charcoal': '#343837',
'chartreuse': '#c1f80a',
'cherry red': '#f7022a',
'cherry': '#cf0234',
'chestnut': '#742802',
'chocolate brown': '#411900',
'chocolate': '#3d1c02',
'cinnamon': '#ac4f06',
'claret': '#680018',
'clay brown': '#b2713d',
'clay': '#b66a50',
'clear blue': '#247afd',
'cloudy blue': '#acc2d9',
'cobalt blue': '#030aa7',
'cobalt': '#1e488f',
'cocoa': '#875f42',
'coffee': '#a6814c',
'cool blue': '#4984b8',
'cool green': '#33b864',
'cool grey': '#95a3a6',
'copper': '#b66325',
'coral pink': '#ff6163',
'coral': '#fc5a50',
'cornflower blue': '#5170d7',
'cornflower': '#6a79f7',
'cranberry': '#9e003a',
'cream': '#ffffc2',
'creme': '#ffffb6',
'crimson': '#8c000f',
'custard': '#fffd78',
'cyan': '#00ffff',
'dandelion': '#fedf08',
'dark aqua': '#05696b',
'dark aquamarine': '#017371',
'dark beige': '#ac9362',
'dark blue green': '#005249',
'dark blue grey': '#1f3b4d',
'dark blue': '#00035b',
'dark brown': '#341c02',
'dark coral': '#cf524e',
'dark cream': '#fff39a',
'dark cyan': '#0a888a',
'dark forest green': '#002d04',
'dark fuchsia': '#9d0759',
'dark gold': '#b59410',
'dark grass green': '#388004',
'dark green blue': '#1f6357',
'dark green': '#033500',
'dark grey blue': '#29465b',
'dark grey': '#363737',
'dark hot pink': '#d90166',
'dark indigo': '#1f0954',
'dark khaki': '#9b8f55',
'dark lavender': '#856798',
'dark lilac': '#9c6da5',
'dark lime green': '#7ebd01',
'dark lime': '#84b701',
'dark magenta': '#960056',
'dark maroon': '#3c0008',
'dark mauve': '#874c62',
'dark mint green': '#20c073',
'dark mint': '#48c072',
'dark mustard': '#a88905',
'dark navy blue': '#00022e',
'dark navy': '#000435',
'dark olive green': '#3c4d03',
'dark olive': '#373e02',
'dark orange': '#c65102',
'dark pastel green': '#56ae57',
'dark peach': '#de7e5d',
'dark periwinkle': '#665fd1',
'dark pink': '#cb416b',
'dark plum': '#3f012c',
'dark purple': '#35063e',
'dark red': '#840000',
'dark rose': '#b5485d',
'dark royal blue': '#02066f',
'dark sage': '#598556',
'dark salmon': '#c85a53',
'dark sand': '#a88f59',
'dark sea green': '#11875d',
'dark seafoam green': '#3eaf76',
'dark seafoam': '#1fb57a',
'dark sky blue': '#448ee4',
'dark slate blue': '#214761',
'dark tan': '#af884a',
'dark taupe': '#7f684e',
'dark teal': '#014d4e',
'dark turquoise': '#045c5a',
'dark violet': '#34013f',
'dark yellow green': '#728f02',
'dark yellow': '#d5b60a',
'dark': '#1b2431',
'darkblue': '#030764',
'darkgreen': '#054907',
'darkish blue': '#014182',
'darkish green': '#287c37',
'darkish pink': '#da467d',
'darkish purple': '#751973',
'darkish red': '#a90308',
'deep aqua': '#08787f',
'deep blue': '#040273',
'deep brown': '#410200',
'deep green': '#02590f',
'deep lavender': '#8d5eb7',
'deep lilac': '#966ebd',
'deep magenta': '#a0025c',
'deep orange': '#dc4d01',
'deep pink': '#cb0162',
'deep purple': '#36013f',
'deep red': '#9a0200',
'deep rose': '#c74767',
'deep sea blue': '#015482',
'deep sky blue': '#0d75f8',
'deep teal': '#00555a',
'deep turquoise': '#017374',
'deep violet': '#490648',
'denim blue': '#3b5b92',
'denim': '#3b638c',
'desert': '#ccad60',
'diarrhea': '#9f8303',
'dirt brown': '#836539',
'dirt': '#8a6e45',
'dirty blue': '#3f829d',
'dirty green': '#667e2c',
'dirty orange': '#c87606',
'dirty pink': '#ca7b80',
'dirty purple': '#734a65',
'dirty yellow': '#cdc50a',
'dodger blue': '#3e82fc',
'drab green': '#749551',
'drab': '#828344',
'dried blood': '#4b0101',
'duck egg blue': '#c3fbf4',
'dull blue': '#49759c',
'dull brown': '#876e4b',
'dull green': '#74a662',
'dull orange': '#d8863b',
'dull pink': '#d5869d',
'dull purple': '#84597e',
'dull red': '#bb3f3f',
'dull teal': '#5f9e8f',
'dull yellow': '#eedc5b',
'dusk blue': '#26538d',
'dusk': '#4e5481',
'dusky blue': '#475f94',
'dusky pink': '#cc7a8b',
'dusky purple': '#895b7b',
'dusky rose': '#ba6873',
'dust': '#b2996e',
'dusty blue': '#5a86ad',
'dusty green': '#76a973',
'dusty lavender': '#ac86a8',
'dusty orange': '#f0833a',
'dusty pink': '#d58a94',
'dusty purple': '#825f87',
'dusty red': '#b9484e',
'dusty rose': '#c0737a',
'dusty teal': '#4c9085',
'earth': '#a2653e',
'easter green': '#8cfd7e',
'easter purple': '#c071fe',
'ecru': '#feffca',
'egg shell': '#fffcc4',
'eggplant purple': '#430541',
'eggplant': '#380835',
'eggshell blue': '#c4fff7',
'eggshell': '#ffffd4',
'electric blue': '#0652ff',
'electric green': '#21fc0d',
'electric lime': '#a8ff04',
'electric pink': '#ff0490',
'electric purple': '#aa23ff',
'emerald green': '#028f1e',
'emerald': '#01a049',
'evergreen': '#05472a',
'faded blue': '#658cbb',
'faded green': '#7bb274',
'faded orange': '#f0944d',
'faded pink': '#de9dac',
'faded purple': '#916e99',
'faded red': '#d3494e',
'faded yellow': '#feff7f',
'fawn': '#cfaf7b',
'fern green': '#548d44',
'fern': '#63a950',
'fire engine red': '#fe0002',
'flat blue': '#3c73a8',
'flat green': '#699d4c',
'fluorescent green': '#08ff08',
'fluro green': '#0aff02',
'foam green': '#90fda9',
'forest green': '#06470c',
'forest': '#0b5509',
'forrest green': '#154406',
'french blue': '#436bad',
'fresh green': '#69d84f',
'frog green': '#58bc08',
'fuchsia': '#ed0dd9',
'gold': '#dbb40c',
'golden brown': '#b27a01',
'golden rod': '#f9bc08',
'golden yellow': '#fec615',
'golden': '#f5bf03',
'goldenrod': '#fac205',
'grape purple': '#5d1451',
'grape': '#6c3461',
'grapefruit': '#fd5956',
'grass green': '#3f9b0b',
'grass': '#5cac2d',
'grassy green': '#419c03',
'green apple': '#5edc1f',
'green blue': '#06b48b',
'green brown': '#544e03',
'green grey': '#77926f',
'green teal': '#0cb577',
'green yellow': '#c9ff27',
'green': '#15b01a',
'green/blue': '#01c08d',
'green/yellow': '#b5ce08',
'greenblue': '#23c48b',
'greenish beige': '#c9d179',
'greenish blue': '#0b8b87',
'greenish brown': '#696112',
'greenish cyan': '#2afeb7',
'greenish grey': '#96ae8d',
'greenish tan': '#bccb7a',
'greenish teal': '#32bf84',
'greenish turquoise': '#00fbb0',
'greenish yellow': '#cdfd02',
'greenish': '#40a368',
'greeny blue': '#42b395',
'greeny brown': '#696006',
'greeny grey': '#7ea07a',
'greeny yellow': '#c6f808',
'grey blue': '#6b8ba4',
'grey brown': '#7f7053',
'grey green': '#789b73',
'grey pink': '#c3909b',
'grey purple': '#826d8c',
'grey teal': '#5e9b8a',
'grey': '#929591',
'grey/blue': '#647d8e',
'grey/green': '#86a17d',
'greyblue': '#77a1b5',
'greyish blue': '#5e819d',
'greyish brown': '#7a6a4f',
'greyish green': '#82a67d',
'greyish pink': '#c88d94',
'greyish purple': '#887191',
'greyish teal': '#719f91',
'greyish': '#a8a495',
'gross green': '#a0bf16',
'gunmetal': '#536267',
'hazel': '#8e7618',
'heather': '#a484ac',
'heliotrope': '#d94ff5',
'highlighter green': '#1bfc06',
'hospital green': '#9be5aa',
'hot green': '#25ff29',
'hot magenta': '#f504c9',
'hot pink': '#ff028d',
'hot purple': '#cb00f5',
'hunter green': '#0b4008',
'ice blue': '#d7fffe',
'ice': '#d6fffa',
'icky green': '#8fae22',
'indian red': '#850e04',
'indigo blue': '#3a18b1',
'indigo': '#380282',
'iris': '#6258c4',
'irish green': '#019529',
'ivory': '#ffffcb',
'jade green': '#2baf6a',
'jade': '#1fa774',
'jungle green': '#048243',
'kelley green': '#009337',
'kelly green': '#02ab2e',
'kermit green': '#5cb200',
'key lime': '#aeff6e',
'khaki green': '#728639',
'khaki': '#aaa662',
'kiwi green': '#8ee53f',
'kiwi': '#9cef43',
'lavender blue': '#8b88f8',
'lavender pink': '#dd85d7',
'lavender': '#c79fef',
'lawn green': '#4da409',
'leaf green': '#5ca904',
'leaf': '#71aa34',
'leafy green': '#51b73b',
'leather': '#ac7434',
'lemon green': '#adf802',
'lemon lime': '#bffe28',
'lemon yellow': '#fdff38',
'lemon': '#fdff52',
'lichen': '#8fb67b',
'light aqua': '#8cffdb',
'light aquamarine': '#7bfdc7',
'light beige': '#fffeb6',
'light blue green': '#7efbb3',
'light blue grey': '#b7c9e2',
'light blue': '#95d0fc',
'light bluish green': '#76fda8',
'light bright green': '#53fe5c',
'light brown': '#ad8150',
'light burgundy': '#a8415b',
'light cyan': '#acfffc',
'light eggplant': '#894585',
'light forest green': '#4f9153',
'light gold': '#fddc5c',
'light grass green': '#9af764',
'light green blue': '#56fca2',
'light green': '#96f97b',
'light greenish blue': '#63f7b4',
'light grey blue': '#9dbcd4',
'light grey green': '#b7e1a1',
'light grey': '#d8dcd6',
'light indigo': '#6d5acf',
'light khaki': '#e6f2a2',
'light lavendar': '#efc0fe',
'light lavender': '#dfc5fe',
'light light blue': '#cafffb',
'light light green': '#c8ffb0',
'light lilac': '#edc8ff',
'light lime green': '#b9ff66',
'light lime': '#aefd6c',
'light magenta': '#fa5ff7',
'light maroon': '#a24857',
'light mauve': '#c292a1',
'light mint green': '#a6fbb2',
'light mint': '#b6ffbb',
'light moss green': '#a6c875',
'light mustard': '#f7d560',
'light navy blue': '#2e5a88',
'light navy': '#155084',
'light neon green': '#4efd54',
'light olive green': '#a4be5c',
'light olive': '#acbf69',
'light orange': '#fdaa48',
'light pastel green': '#b2fba5',
'light pea green': '#c4fe82',
'light peach': '#ffd8b1',
'light periwinkle': '#c1c6fc',
'light pink': '#ffd1df',
'light plum': '#9d5783',
'light purple': '#bf77f6',
'light red': '#ff474c',
'light rose': '#ffc5cb',
'light royal blue': '#3a2efe',
'light sage': '#bcecac',
'light salmon': '#fea993',
'light sea green': '#98f6b0',
'light seafoam green': '#a7ffb5',
'light seafoam': '#a0febf',
'light sky blue': '#c6fcff',
'light tan': '#fbeeac',
'light teal': '#90e4c1',
'light turquoise': '#7ef4cc',
'light urple': '#b36ff6',
'light violet': '#d6b4fc',
'light yellow green': '#ccfd7f',
'light yellow': '#fffe7a',
'light yellowish green': '#c2ff89',
'lightblue': '#7bc8f6',
'lighter green': '#75fd63',
'lighter purple': '#a55af4',
'lightgreen': '#76ff7b',
'lightish blue': '#3d7afd',
'lightish green': '#61e160',
'lightish purple': '#a552e6',
'lightish red': '#fe2f4a',
'lilac': '#cea2fd',
'liliac': '#c48efd',
'lime green': '#89fe05',
'lime yellow': '#d0fe1d',
'lime': '#aaff32',
'lipstick red': '#c0022f',
'lipstick': '#d5174e',
'macaroni and cheese': '#efb435',
'magenta': '#c20078',
'mahogany': '#4a0100',
'maize': '#f4d054',
'mango': '#ffa62b',
'manilla': '#fffa86',
'marigold': '#fcc006',
'marine blue': '#01386a',
'marine': '#042e60',
'maroon': '#650021',
'mauve': '#ae7181',
'medium blue': '#2c6fbb',
'medium brown': '#7f5112',
'medium green': '#39ad48',
'medium grey': '#7d7f7c',
'medium pink': '#f36196',
'medium purple': '#9e43a2',
'melon': '#ff7855',
'merlot': '#730039',
'metallic blue': '#4f738e',
'mid blue': '#276ab3',
'mid green': '#50a747',
'midnight blue': '#020035',
'midnight purple': '#280137',
'midnight': '#03012d',
'military green': '#667c3e',
'milk chocolate': '#7f4e1e',
'mint green': '#8fff9f',
'mint': '#9ffeb0',
'minty green': '#0bf77d',
'mocha': '#9d7651',
'moss green': '#658b38',
'moss': '#769958',
'mossy green': '#638b27',
'mud brown': '#60460f',
'mud green': '#606602',
'mud': '#735c12',
'muddy brown': '#886806',
'muddy green': '#657432',
'muddy yellow': '#bfac05',
'mulberry': '#920a4e',
'murky green': '#6c7a0e',
'mushroom': '#ba9e88',
'mustard brown': '#ac7e04',
'mustard green': '#a8b504',
'mustard yellow': '#d2bd0a',
'mustard': '#ceb301',
'muted blue': '#3b719f',
'muted green': '#5fa052',
'muted pink': '#d1768f',
'muted purple': '#805b87',
'nasty green': '#70b23f',
'navy blue': '#001146',
'navy green': '#35530a',
'navy': '#01153e',
'neon blue': '#04d9ff',
'neon green': '#0cff0c',
'neon pink': '#fe019a',
'neon purple': '#bc13fe',
'neon red': '#ff073a',
'neon yellow': '#cfff04',
'nice blue': '#107ab0',
'night blue': '#040348',
'ocean blue': '#03719c',
'ocean green': '#3d9973',
'ocean': '#017b92',
'ocher': '#bf9b0c',
'ochre': '#bf9005',
'ocre': '#c69c04',
'off blue': '#5684ae',
'off green': '#6ba353',
'off white': '#ffffe4',
'off yellow': '#f1f33f',
'old pink': '#c77986',
'old rose': '#c87f89',
'olive brown': '#645403',
'olive drab': '#6f7632',
'olive green': '#677a04',
'olive yellow': '#c2b709',
'olive': '#6e750e',
'orange brown': '#be6400',
'orange pink': '#ff6f52',
'orange red': '#fd411e',
'orange yellow': '#ffad01',
'orange': '#f97306',
'orangeish': '#fd8d49',
'orangered': '#fe420f',
'orangey brown': '#b16002',
'orangey red': '#fa4224',
'orangey yellow': '#fdb915',
'orangish brown': '#b25f03',
'orangish red': '#f43605',
'orangish': '#fc824a',
'orchid': '#c875c4',
'pale aqua': '#b8ffeb',
'pale blue': '#d0fefe',
'pale brown': '#b1916e',
'pale cyan': '#b7fffa',
'pale gold': '#fdde6c',
'pale green': '#c7fdb5',
'pale grey': '#fdfdfe',
'pale lavender': '#eecffe',
'pale light green': '#b1fc99',
'pale lilac': '#e4cbff',
'pale lime green': '#b1ff65',
'pale lime': '#befd73',
'pale magenta': '#d767ad',
'pale mauve': '#fed0fc',
'pale olive green': '#b1d27b',
'pale olive': '#b9cc81',
'pale orange': '#ffa756',
'pale peach': '#ffe5ad',
'pale pink': '#ffcfdc',
'pale purple': '#b790d4',
'pale red': '#d9544d',
'pale rose': '#fdc1c5',
'pale salmon': '#ffb19a',
'pale sky blue': '#bdf6fe',
'pale teal': '#82cbb2',
'pale turquoise': '#a5fbd5',
'pale violet': '#ceaefa',
'pale yellow': '#ffff84',
'pale': '#fff9d0',
'parchment': '#fefcaf',
'pastel blue': '#a2bffe',
'pastel green': '#b0ff9d',
'pastel orange': '#ff964f',
'pastel pink': '#ffbacd',
'pastel purple': '#caa0ff',
'pastel red': '#db5856',
'pastel yellow': '#fffe71',
'pea green': '#8eab12',
'pea soup green': '#94a617',
'pea soup': '#929901',
'pea': '#a4bf20',
'peach': '#ffb07c',
'peachy pink': '#ff9a8a',
'peacock blue': '#016795',
'pear': '#cbf85f',
'periwinkle blue': '#8f99fb',
'periwinkle': '#8e82fe',
'perrywinkle': '#8f8ce7',
'petrol': '#005f6a',
'pig pink': '#e78ea5',
'pine green': '#0a481e',
'pine': '#2b5d34',
'pink purple': '#db4bda',
'pink red': '#f5054f',
'pink': '#ff81c0',
'pink/purple': '#ef1de7',
'pinkish brown': '#b17261',
'pinkish grey': '#c8aca9',
'pinkish orange': '#ff724c',
'pinkish purple': '#d648d7',
'pinkish red': '#f10c45',
'pinkish tan': '#d99b82',
'pinkish': '#d46a7e',
'pinky purple': '#c94cbe',
'pinky red': '#fc2647',
'pinky': '#fc86aa',
'piss yellow': '#ddd618',
'pistachio': '#c0fa8b',
'plum purple': '#4e0550',
'plum': '#580f41',
'poison green': '#40fd14',
'poo brown': '#885f01',
'poo': '#8f7303',
'poop brown': '#7a5901',
'poop green': '#6f7c00',
'poop': '#7f5e00',
'powder blue': '#b1d1fc',
'powder pink': '#ffb2d0',
'primary blue': '#0804f9',
'prussian blue': '#004577',
'puce': '#a57e52',
'puke brown': '#947706',
'puke green': '#9aae07',
'puke yellow': '#c2be0e',
'puke': '#a5a502',
'pumpkin orange': '#fb7d07',
'pumpkin': '#e17701',
'pure blue': '#0203e2',
'purple blue': '#632de9',
'purple brown': '#673a3f',
'purple grey': '#866f85',
'purple pink': '#e03fd8',
'purple red': '#990147',
'purple': '#7e1e9c,
'purple/blue': '#5d21d0',
'purple/pink': '#d725de',
'purpleish blue': '#6140ef',
'purpleish pink': '#df4ec8',
'purpleish': '#98568d',
'purpley blue': '#5f34e7',
'purpley grey': '#947e94',
'purpley pink': '#c83cb9',
'purpley': '#8756e4',
'purplish blue': '#601ef9',
'purplish brown': '#6b4247',
'purplish grey': '#7a687f',
'purplish pink': '#ce5dae',
'purplish red': '#b0054b',
'purplish': '#94568c',
'purply blue': '#661aee',
'purply pink': '#f075e6',
'purply': '#983fb2',
'putty': '#beae8a',
'racing green': '#014600',
'radioactive green': '#2cfa1f',
'raspberry': '#b00149',
'raw sienna': '#9a6200',
'raw umber': '#a75e09',
'really light blue': '#d4ffff',
'red brown': '#8b2e16',
'red orange': '#fd3c06',
'red pink': '#fa2a55',
'red purple': '#820747',
'red violet': '#9e0168',
'red wine': '#8c0034',
'red': '#e50000',
'reddish brown': '#7f2b0a',
'reddish grey': '#997570',
'reddish orange': '#f8481c',
'reddish pink': '#fe2c54',
'reddish purple': '#910951',
'reddish': '#c44240',
'reddy brown': '#6e1005',
'rich blue': '#021bf9',
'rich purple': '#720058',
'robin egg blue': '#8af1fe',
'rosa': '#fe86a4',
'rose pink': '#f7879a',
'rose red': '#be013c',
'rose': '#cf6275',
'rosy pink': '#f6688e',
'rouge': '#ab1239',
'royal blue': '#0504aa',
'royal purple': '#4b006e',
'royal': '#0c1793',
'ruby': '#ca0147',
'russet': '#a13905',
'rust brown': '#8b3103',
'rust orange': '#c45508',
'rust red': '#aa2704',
'rust': '#a83c09',
'rusty orange': '#cd5909',
'rusty red': '#af2f0d',
'saffron': '#feb209',
'sage green': '#88b378',
'sage': '#87ae73',
'salmon pink': '#fe7b7c',
'salmon': '#ff796c',
'sand brown': '#cba560',
'sand yellow': '#fce166',
'sand': '#e2ca76',
'sandstone': '#c9ae74',
'sandy brown': '#c4a661',
'sandy yellow': '#fdee73',
'sandy': '#f1da7a',
'sap green': '#5c8b15',
'sapphire': '#2138ab',
'scarlet': '#be0119',
'sea blue': '#047495',
'sea green': '#53fca1',
'sea': '#3c9992',
'seafoam blue': '#78d1b6',
'seafoam green': '#7af9ab',
'seafoam': '#80f9ad',
'seaweed green': '#35ad6b',
'seaweed': '#18d17b',
'sepia': '#985e2b',
'shamrock green': '#02c14d',
'shamrock': '#01b44c',
'shit brown': '#7b5804',
'shit green': '#758000',
'shit': '#7f5f00',
'shocking pink': '#fe02a2',
'sick green': '#9db92c',
'sickly green': '#94b21c',
'sickly yellow': '#d0e429',
'sienna': '#a9561e',
'silver': '#c5c9c7',
'sky blue': '#75bbfd',
'sky': '#82cafc',
'slate blue': '#5b7c99',
'slate green': '#658d6d',
'slate grey': '#59656d',
'slate': '#516572',
'slime green': '#99cc04',
'snot green': '#9dc100',
'snot': '#acbb0d',
'soft blue': '#6488ea',
'soft green': '#6fc276',
'soft pink': '#fdb0c0',
'soft purple': '#a66fb5',
'spearmint': '#1ef876',
'spring green': '#a9f971',
'spruce': '#0a5f38',
'squash': '#f2ab15',
'steel blue': '#5a7d9a',
'steel grey': '#6f828a',
'steel': '#738595',
'stone': '#ada587',
'stormy blue': '#507b9c',
'straw': '#fcf679',
'strawberry': '#fb2943',
'strong blue': '#0c06f7',
'strong pink': '#ff0789',
'sun yellow': '#ffdf22',
'sunflower yellow': '#ffda03',
'sunflower': '#ffc512',
'sunny yellow': '#fff917',
'sunshine yellow': '#fffd37',
'swamp green': '#748500',
'swamp': '#698339',
'tan brown': '#ab7e4c',
'tan green': '#a9be70',
'tan': '#d1b26f',
'tangerine': '#ff9408',
'taupe': '#b9a281',
'tea green': '#bdf8a3',
'tea': '#65ab7c',
'teal blue': '#01889f',
'teal green': '#25a36f',
'teal': '#029386',
'tealish green': '#0cdc73',
'tealish': '#24bca8',
'terra cotta': '#c9643b',
'terracota': '#cb6843',
'terracotta': '#ca6641',
'tiffany blue': '#7bf2da',
'tomato red': '#ec2d01',
'tomato': '#ef4026',
'topaz': '#13bbaf',
'toupe': '#c7ac7d',
'toxic green': '#61de2a',
'tree green': '#2a7e19',
'true blue': '#010fcc',
'true green': '#089404',
'turquoise blue': '#06b1c4',
'turquoise green': '#04f489',
'turquoise': '#06c2ac',
'turtle green': '#75b84f',
'twilight blue': '#0a437a',
'twilight': '#4e518b',
'ugly blue': '#31668a',
'ugly brown': '#7d7103',
'ugly green': '#7a9703',
'ugly pink': '#cd7584',
'ugly purple': '#a442a0',
'ugly yellow': '#d0c101',
'ultramarine blue': '#1805db',
'ultramarine': '#2000b1',
'umber': '#b26400',
'velvet': '#750851',
'vermillion': '#f4320c',
'very dark blue': '#000133',
'very dark brown': '#1d0200',
'very dark green': '#062e03',
'very dark purple': '#2a0134',
'very light blue': '#d5ffff',
'very light brown': '#d3b683',
'very light green': '#d1ffbd',
'very light pink': '#fff4f2',
'very light purple': '#f6cefc',
'very pale blue': '#d6fffe',
'very pale green': '#cffdbc',
'vibrant blue': '#0339f8',
'vibrant green': '#0add08',
'vibrant purple': '#ad03de',
'violet blue': '#510ac9',
'violet pink': '#fb5ffc',
'violet red': '#a50055',
'violet': '#9a0eea',
'viridian': '#1e9167',
'vivid blue': '#152eff',
'vivid green': '#2fef10',
'vivid purple': '#9900fa',
'vomit green': '#89a203',
'vomit yellow': '#c7c10c',
'vomit': '#a2a415',
'warm blue': '#4b57db',
'warm brown': '#964e02',
'warm grey': '#978a84',
'warm pink': '#fb5581',
'warm purple': '#952e8f',
'washed out green': '#bcf5a6',
'water blue': '#0e87cc',
'watermelon': '#fd4659',
'weird green': '#3ae57f',
'wheat': '#fbdd7e',
'white': '#ffffff',
'windows blue': '#3778bf',
'wine red': '#7b0323',
'wine': '#80013f',
'wintergreen': '#20f986',
'wisteria': '#a87dc2',
'yellow brown': '#b79400',
'yellow green': '#c0fb2d',
'yellow ochre': '#cb9d06',
'yellow orange': '#fcb001',
'yellow tan': '#ffe36e',
'yellow': '#ffff14',
'yellow/green': '#c8fd3d',
'yellowgreen': '#bbf90f',
'yellowish brown': '#9b7a01',
'yellowish green': '#b0dd16',
'yellowish orange': '#ffab0f',
'yellowish tan': '#fcfc81',
'yellowish': '#faee66',
'yellowy brown': '#ae8b0c',
'yellowy green': '#bff128',
}
####################################################################################################
# https://drafts.csswg.org/css-color-4/#named-colors
CSS4_COLORS = {
'aliceblue': '#F0F8FF',
'antiquewhite': '#FAEBD7',
'aqua': '#00FFFF',
'aquamarine': '#7FFFD4',
'azure': '#F0FFFF',
'beige': '#F5F5DC',
'bisque': '#FFE4C4',
'black': '#000000',
'blanchedalmond': '#FFEBCD',
'blue': '#0000FF',
'blueviolet': '#8A2BE2',
'brown': '#A52A2A',
'burlywood': '#DEB887',
'cadetblue': '#5F9EA0',
'chartreuse': '#7FFF00',
'chocolate': '#D2691E',
'coral': '#FF7F50',
'cornflowerblue': '#6495ED',
'cornsilk': '#FFF8DC',
'crimson': '#DC143C',
'cyan': '#00FFFF',
'darkblue': '#00008B',
'darkcyan': '#008B8B',
'darkgoldenrod': '#B8860B',
'darkgray': '#A9A9A9',
'darkgreen': '#006400',
'darkgrey': '#A9A9A9',
'darkkhaki': '#BDB76B',
'darkmagenta': '#8B008B',
'darkolivegreen': '#556B2F',
'darkorange': '#FF8C00',
'darkorchid': '#9932CC',
'darkred': '#8B0000',
'darksalmon': '#E9967A',
'darkseagreen': '#8FBC8F',
'darkslateblue': '#483D8B',
'darkslategray': '#2F4F4F',
'darkslategrey': '#2F4F4F',
'darkturquoise': '#00CED1',
'darkviolet': '#9400D3',
'deeppink': '#FF1493',
'deepskyblue': '#00BFFF',
'dimgray': '#696969',
'dimgrey': '#696969',
'dodgerblue': '#1E90FF',
'firebrick': '#B22222',
'floralwhite': '#FFFAF0',
'forestgreen': '#228B22',
'fuchsia': '#FF00FF',
'gainsboro': '#DCDCDC',
'ghostwhite': '#F8F8FF',
'gold': '#FFD700',
'goldenrod': '#DAA520',
'gray': '#808080',
'green': '#008000',
'greenyellow': '#ADFF2F',
'grey': '#808080',
'honeydew': '#F0FFF0',
'hotpink': '#FF69B4',
'indianred': '#CD5C5C',
'indigo': '#4B0082',
'ivory': '#FFFFF0',
'khaki': '#F0E68C',
'lavender': '#E6E6FA',
'lavenderblush': '#FFF0F5',
'lawngreen': '#7CFC00',
'lemonchiffon': '#FFFACD',
'lightblue': '#ADD8E6',
'lightcoral': '#F08080',
'lightcyan': '#E0FFFF',
'lightgoldenrodyellow': '#FAFAD2',
'lightgray': '#D3D3D3',
'lightgreen': '#90EE90',
'lightgrey': '#D3D3D3',
'lightpink': '#FFB6C1',
'lightsalmon': '#FFA07A',
'lightseagreen': '#20B2AA',
'lightskyblue': '#87CEFA',
'lightslategray': '#778899',
'lightslategrey': '#778899',
'lightsteelblue': '#B0C4DE',
'lightyellow': '#FFFFE0',
'lime': '#00FF00',
'limegreen': '#32CD32',
'linen': '#FAF0E6',
'magenta': '#FF00FF',
'maroon': '#800000',
'mediumaquamarine': '#66CDAA',
'mediumblue': '#0000CD',
'mediumorchid': '#BA55D3',
'mediumpurple': '#9370DB',
'mediumseagreen': '#3CB371',
'mediumslateblue': '#7B68EE',
'mediumspringgreen': '#00FA9A',
'mediumturquoise': '#48D1CC',
'mediumvioletred': '#C71585',
'midnightblue': '#191970',
'mintcream': '#F5FFFA',
'mistyrose': '#FFE4E1',
'moccasin': '#FFE4B5',
'navajowhite': '#FFDEAD',
'navy': '#000080',
'oldlace': '#FDF5E6',
'olive': '#808000',
'olivedrab': '#6B8E23',
'orange': '#FFA500',
'orangered': '#FF4500',
'orchid': '#DA70D6',
'palegoldenrod': '#EEE8AA',
'palegreen': '#98FB98',
'paleturquoise': '#AFEEEE',
'palevioletred': '#DB7093',
'papayawhip': '#FFEFD5',
'peachpuff': '#FFDAB9',
'peru': '#CD853F',
'pink': '#FFC0CB',
'plum': '#DDA0DD',
'powderblue': '#B0E0E6',
'purple': '#800080',
'rebeccapurple': '#663399',
'red': '#FF0000',
'rosybrown': '#BC8F8F',
'royalblue': '#4169E1',
'saddlebrown': '#8B4513',
'salmon': '#FA8072',
'sandybrown': '#F4A460',
'seagreen': '#2E8B57',
'seashell': '#FFF5EE',
'sienna': '#A0522D',
'silver': '#C0C0C0',
'skyblue': '#87CEEB',
'slateblue': '#6A5ACD',
'slategray': '#708090',
'slategrey': '#708090',
'snow': '#FFFAFA',
'springgreen': '#00FF7F',
'steelblue': '#4682B4',
'tan': '#D2B48C',
'teal': '#008080',
'thistle': '#D8BFD8',
'tomato': '#FF6347',
'turquoise': '#40E0D0',
'violet': '#EE82EE',
'wheat': '#F5DEB3',
'white': '#FFFFFF',
'whitesmoke': '#F5F5F5',
'yellow': '#FFFF00',
'yellowgreen': '#9ACD32',
}
####################################################################################################
# Theses colors are from Qt5 QML http://doc.qt.io/qt-5/qml-color.html
QML_COLORS = {
'aliceblue': '#f0f8ff',
'antiquewhite': '#faebd7',
'aqua': '#00ffff',
'aquamarine': '#7fffd4',
'azure': '#f0ffff',
'beige': '#f5f5dc',
'bisque': '#ffe4c4',
'black': '#000000',
'blanchedalmond': '#ffebcd',
'blue': '#0000ff',
'blueviolet': '#8a2be2',
'brown': '#a52a2a',
'burlywood': '#deb887',
'cadetblue': '#5f9ea0',
'chartreuse': '#7fff00',
'chocolate': '#d2691e',
'coral': '#ff7f50',
'cornflowerblue': '#6495ed',
'cornsilk': '#fff8dc',
'crimson': '#dc143c',
'cyan': '#00ffff',
'darkblue': '#00008b',
'darkcyan': '#008b8b',
'darkgoldenrod': '#b8860b',
'darkgray': '#a9a9a9',
'darkgreen': '#006400',
'darkgrey': '#a9a9a9',
'darkkhaki': '#bdb76b',
'darkmagenta': '#8b008b',
'darkolivegreen': '#556b2f',
'darkorange': '#ff8c00',
'darkorchid': '#9932cc',
'darkred': '#8b0000',
'darksalmon': '#e9967a',
'darkseagreen': '#8fbc8f',
'darkslateblue': '#483d8b',
'darkslategray': '#2f4f4f',
'darkslategrey': '#2f4f4f',
'darkturquoise': '#00ced1',
'darkviolet': '#9400d3',
'deeppink': '#ff1493',
'deepskyblue': '#00bfff',
'dimgray': '#696969',
'dimgrey': '#696969',
'dodgerblue': '#1e90ff',
'firebrick': '#b22222',
'floralwhite': '#fffaf0',
'forestgreen': '#228b22',
'fuchsia': '#ff00ff',
'gainsboro': '#dcdcdc',
'ghostwhite': '#f8f8ff',
'gold': '#ffd700',
'goldenrod': '#daa520',
'gray': '#808080',
'green': '#008000',
'greenyellow': '#adff2f',
'grey': '#808080',
'honeydew': '#f0fff0',
'hotpink': '#ff69b4',
'indianred': '#cd5c5c',
'indigo': '#4b0082',
'ivory': '#fffff0',
'khaki': '#f0e68c',
'lavender': '#e6e6fa',
'lavenderblush': '#fff0f5',
'lawngreen': '#7cfc00',
'lemonchiffon': '#fffacd',
'lightblue': '#add8e6',
'lightcoral': '#f08080',
'lightcyan': '#e0ffff',
'lightgoldenrodyellow': '#fafad2',
'lightgray': '#d3d3d3',
'lightgreen': '#90ee90',
'lightgrey': '#d3d3d3',
'lightpink': '#ffb6c1',
'lightsalmon': '#ffa07a',
'lightseagreen': '#20b2aa',
'lightskyblue': '#87cefa',
'lightslategray': '#778899',
'lightslategrey': '#778899',
'lightsteelblue': '#b0c4de',
'lightyellow': '#ffffe0',
'lime': '#00ff00',
'limegreen': '#32cd32',
'linen': '#faf0e6',
'magenta': '#ff00ff',
'maroon': '#800000',
'mediumaquamarine': '#66cdaa',
'mediumblue': '#0000cd',
'mediumorchid': '#ba55d3',
'mediumpurple': '#9370db',
'mediumseagreen': '#3cb371',
'mediumslateblue': '#7b68ee',
'mediumspringgreen': '#00fa9a',
'mediumturquoise': '#48d1cc',
'mediumvioletred': '#c71585',
'midnightblue': '#191970',
'mintcream': '#f5fffa',
'mistyrose': '#ffe4e1',
'moccasin': '#ffe4b5',
'navajowhite': '#ffdead',
'navy': '#000080',
'oldlace': '#fdf5e6',
'olive': '#808000',
'olivedrab': '#6b8e23',
'orange': '#ffa500',
'orangered': '#ff4500',
'orchid': '#da70d6',
'palegoldenrod': '#eee8aa',
'palegreen': '#98fb98',
'paleturquoise': '#afeeee',
'palevioletred': '#db7093',
'papayawhip': '#ffefd5',
'peachpuff': '#ffdab9',
'peru': '#cd853f',
'pink': '#ffc0cb',
'plum': '#dda0dd',
'powderblue': '#b0e0e6',
'purple': '#800080',
'red': '#ff0000',
'rosybrown': '#bc8f8f',
'royalblue': '#4169e1',
'saddlebrown': '#8b4513',
'salmon': '#fa8072',
'sandybrown': '#f4a460',
'seagreen': '#2e8b57',
'seashell': '#fff5ee',
'sienna': '#a0522d',
'silver': '#c0c0c0',
'skyblue': '#87ceeb',
'slateblue': '#6a5acd',
'slategray': '#708090',
'slategrey': '#708090',
'snow': '#fffafa',
'springgreen': '#00ff7f',
'steelblue': '#4682b4',
'tan': '#d2b48c',
'teal': '#008080',
'thistle': '#d8bfd8',
'tomato': '#ff6347',
'turquoise': '#40e0d0',
'violet': '#ee82ee',
'wheat': '#f5deb3',
'white': '#ffffff',
'whitesmoke': '#f5f5f5',
'yellow': '#ffff00',
'yellowgreen': '#9acd32',
}
####################################################################################################
VALENTINA_COLORS = (
'black',
'blue',
'cornflowerblue',
'darkBlue',
'darkGreen',
'darkRed',
'darkviolet',
'deeppink',
'deepskyblue',
'goldenrod',
'green',
'lightsalmon',
'lime',
'mediumseagreen',
'orange',
'violet',
'yellow',
)
VALENTINA_COLORS = {name:QML_COLORS[name.lower()] for name in VALENTINA_COLORS}
......@@ -42,13 +42,20 @@ def middle(a, b):
def cmp(a, b):
return (a > b) - (a < b)
# Fixme: sign_of ?
####################################################################################################
def sign(x):
return cmp(x, 0)
# Fixme: sign_of ?
# return cmp(x, 0)
return math.copysign(1.0, x)
####################################################################################################
def epsilon_float(a, b, epsilon = 1e-3):
return abs(a-b) <= epsilon
####################################################################################################
def trignometric_clamp(x):
"""Clamp *x* in the range [-1.,1]."""
if x > 1.:
......
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# 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 module implements root finding for second and third degree equation.
"""
####################################################################################################
__all__ = [
'quadratic_root',
'cubic_root',
]
####################################################################################################
from math import acos, cos, pi, sqrt
try:
import sympy
except ImportError:
sympy = None
from .Functions import sign
####################################################################################################
def quadratic_root(a, b, c):
# https://en.wikipedia.org/wiki/Quadratic_equation
if a == 0 and b == 0:
return None
if a == 0:
return - c / b
D = b**2 - 4*a*c
if D < 0:
return None # not real
b = -b
s = 1 / (2*a)
if D > 0:
# Fixme: sign of b ???
r1 = (b - sqrt(D)) * s
r2 = (b + sqrt(D)) * s
return r1, r2
else:
return b * s
####################################################################################################
def cubic_root(a, b, c, d):
if a == 0:
return quadratic_root(b, c, d)
else:
return cubic_root_sympy(a, b, c, d)
####################################################################################################
def cubic_root_sympy(a, b, c, d):
x = sympy.Symbol('x')
E = a*x**3 + b*x**2 + c*x + d
return [i.n() for i in sympy.real_roots(E, x)]
####################################################################################################
def cubic_root_normalised(a, b, c):
# Reference: ???
# converted from haskell http://hackage.haskell.org/package/cubicbezier-0.6.0.5
q = (a**2 - 3*b) / 9
q3 = q**3
m2sqrtQ = -2 * sqrt(q)
r = (2*a**3 - 9*a*b + 27*c) / 54
r2 = r**2
d = - sign(r)*((abs(r) + sqrt(r2-q3))**1/3) # Fixme: sqrt ??
if d == 0:
e = 0
else:
e = q/d
if r2 < q3:
t = acos(r/sqrt(q3))
return [
m2sqrtQ * cos(t/3) - a/3,
m2sqrtQ * cos((t + 2*pi)/3) - a/3,
m2sqrtQ * cos((t - 2*pi)/3) - a/3,
]
else:
return [d + e - a/3]
####################################################################################################
def _cubic_root(a, b, c, d):
# https://en.wikipedia.org/wiki/Cubic_function
# x, a, b, c, d = symbols('x a b c d')
# solveset(x**3+b*x**2+c*x+d, x)
# D0 = b**2 - 3*c
# D1 = 2*b**3 - 9*b*c + 27*d
# DD = D1**2 - 4*D0**3
# C = ((D1 + sqrt(DD) /2)**(1/3)
# - (b + C + D0/C ) /3
# - (b + (-1/2 - sqrt(3)*I/2)*C + D0/((-1/2 - sqrt(3)*I/2)*C) ) /3
# - (b + (-1/2 + sqrt(3)*I/2)*C + D0/((-1/2 + sqrt(3)*I/2)*C) ) /3
# Fixme: divide by a ???
D = 18*a*b*c*d - 4*b**3*d + b**2*c**2 - 4*a*c**3 - 27*a**2*d**2
D0 = b**2 - 3*a*c
if D == 0:
if D0 == 0:
return - b / (3*a) # triple root
else:
r1 = (9*a*d - b*c) / (2*D0) # double root
r2 = (4*a*b*c - 9*a**2*d - b**3) / (a*D0) # simple root
return r1, r2
else:
D1 = 2*b**3 - 9*a*b*c + 27*a**2*d
# DD = - D / (27*a**2)
DD = D1**2 - 4*D0**3
# Fixme: need more info ...
# can have 3 real roots, e.g. 3*x**3 - 25*x**2 + 27*x + 9
# C1 = pow((D1 +- sqrt(DD))/2, 1/3)
# r = - (b + C + D0/C) / (3*a)
raise NotImplementedError
......@@ -87,12 +87,20 @@ class Attribute:
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
"""Convert a value from XML to Python"""
raise NotImplementedError
##############################################
@classmethod
def to_xml(cls, value):
"""Convert a value from Python to XML"""
return str(value)
##############################################
def set_property(self, cls):
"""Define a property for this attribute in :class:`XmlObjectAdaptator`"""
......@@ -122,7 +130,8 @@ class BoolAttribute(Attribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
if value == "true" or value == "1":
return True
elif value == "false" or value == "0":
......@@ -130,13 +139,20 @@ class BoolAttribute(Attribute):
else:
raise ValueError("Incorrect boolean value {}".format(value))
##############################################
@classmethod
def to_xml(cls, value):
return 'true' if value else 'false'
####################################################################################################
class IntAttribute(Attribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
return int(value)
####################################################################################################
......@@ -145,7 +161,8 @@ class FloatAttribute(Attribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
return float(value)
####################################################################################################
......@@ -154,10 +171,13 @@ class FloatListAttribute(Attribute):
##############################################
@classmethod
def from_xml(self, value):
if value == 'none':
if value == 'none' or value is None:
return None
elif isinstance(value, (tuple, list)): # Python value
return value
else:
if ' ' in value:
separator = ' '
......@@ -167,13 +187,20 @@ class FloatListAttribute(Attribute):
return [float(value)]
return [float(x) for x in value.split(separator)]
##############################################
@classmethod
def to_xml(cls, value):
return ' '.join([str(x) for x in value])
####################################################################################################
class StringAttribute(Attribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
return str(value)
####################################################################################################
......@@ -218,7 +245,7 @@ class XmlObjectAdaptatorMetaClass(type):
####################################################################################################
class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass):
class XmlObjectAdaptator(metaclass=XmlObjectAdaptatorMetaClass):
"""Class to implement an object oriented adaptor for XML elements."""
......@@ -288,9 +315,18 @@ class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass):
##############################################
def to_xml(self, **kwargs):
"""Return an etree element"""
attributes = {attribute.xml_attribute:str(attribute.get_attribute(self)) for attribute in self.__attributes__}
# attributes = {attribute.xml_attribute:str(attribute.get_attribute(self)) for attribute in self.__attributes__}
attributes = {}
for attribute in self.__attributes__:
value = attribute.get_attribute(self)
if value is not None:
attributes[attribute.xml_attribute] = attribute.to_xml(value)
attributes.update(kwargs)
return etree.Element(self.__tag__, **attributes)
##############################################
......@@ -303,3 +339,32 @@ class XmlObjectAdaptator(metaclass = XmlObjectAdaptatorMetaClass):
# def __getattribute__(self, name):
# object.__getattribute__(self, '_' + name)
####################################################################################################
class TextXmlObjectAdaptator(XmlObjectAdaptator):
"""Class to implement an object oriented adaptor for text XML elements."""
##############################################
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.text = kwargs.get('text', '')
##############################################
def _init_from_xml(self, xml_element):
super()._init_from_xml(xml_element)
self.text = str(xml_element.text)
##############################################
def to_xml(self, **kwargs):
element = super().to_xml(**kwargs)
element.text = self.text
return element
......@@ -32,7 +32,6 @@ Import Algorithm:
####################################################################################################
import logging
from pathlib import Path
from lxml import etree
......@@ -45,6 +44,24 @@ _module_logger = logging.getLogger(__name__)
####################################################################################################
class RenderState:
##############################################
def __init__(self):
self._transformations = []
##############################################
def push_transformation(self, transformation):
self._transformations.append(transformation)
def pop_transformation(self):
self._transformations.pop()
####################################################################################################
class SvgDispatcher:
"""Class to dispatch XML to Python class."""
......@@ -94,6 +111,8 @@ class SvgDispatcher:
def __init__(self, root):
self._state = RenderState()
self.on_root(root)
##############################################
......@@ -149,6 +168,11 @@ class SvgFile(XmlFileMixin):
_logger = _module_logger.getChild('SvgFile')
SVG_DOCTYPE = '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
SVG_xmlns = 'http://www.w3.org/2000/svg'
SVG_xmlns_xlink = 'http://www.w3.org/1999/xlink'
SVG_version = '1.1'
##############################################
def __init__(self, path=None):
......@@ -156,7 +180,9 @@ class SvgFile(XmlFileMixin):
# Fixme: path
if path is None:
path = ''
XmlFileMixin.__init__(self, path)
# Fixme:
# if path is not None:
if path != '':
......@@ -180,16 +206,52 @@ class SvgFile(XmlFileMixin):
##############################################
def write(self, path=None):
root = etree.Element('pattern')
@classmethod
def new_root(cls, paper):
nsmap = {
None: cls.SVG_xmlns,
'xlink': cls.SVG_xmlns_xlink,
}
root = etree.Element('svg', nsmap=nsmap)
attrib = root.attrib
attrib['version'] = cls.SVG_version
# Set document dimension and user space unit to mm
# see https://mpetroff.net/2013/08/analysis-of-svg-units
attrib['width'] = '{:.3f}mm'.format(paper.width)
attrib['height'] = '{:.3f}mm'.format(paper.height)
attrib['viewBox'] = '0 0 {:.3f} {:.3f}'.format(paper.width, paper.height)
# Fixme: from conf
root.append(etree.Comment('Pattern created with Patro (https://github.com/FabriceSalvaire/Patro)'))
# Fixme: ...
return root
##############################################
def write(self, paper, root_tree, transformation=None, path=None):
root = self.new_root(paper)
# Fixme: implement tree, look at lxml
if transformation:
# transform text as well !!!
group = SvgFormat.Group(transform=transformation).to_xml()
root.append(group)
else:
group = root
for element in root_tree:
group.append(element.to_xml())
if path is None:
path = self.path
with open(str(path), 'wb') as f:
# ElementTree.write() ?
f.write(etree.tostring(root, pretty_print=True))
tree = etree.ElementTree(root)
tree.write(str(path),
pretty_print=True,
xml_declaration=True,
encoding='utf-8',
standalone=False,
doctype=self.SVG_DOCTYPE,
)
......@@ -72,10 +72,12 @@ from Patro.Common.Xml.Objectivity import (
IntAttribute, FloatAttribute,
FloatListAttribute,
StringAttribute,
XmlObjectAdaptator
XmlObjectAdaptator,
TextXmlObjectAdaptator,
)
# from Patro.GeometryEngine.Vector import Vector2D
from Patro.GeometryEngine.Transformation import AffineTransformation2D
####################################################################################################
......@@ -190,10 +192,101 @@ class IdMixin:
#
####################################################################################################
####################################################################################################
class InheritAttribute(StringAttribute):
##############################################
@classmethod
def from_xml(cls, value):
if value == 'inherit':
return value
else:
return cls._from_xml(value)
##############################################
@classmethod
def _from_xml(cls, value):
raise NotImplementedError
####################################################################################################
class NumberAttribute(InheritAttribute):
@classmethod
def _from_xml(cls, value):
return float(value)
####################################################################################################
class PercentValue:
##############################################
def __init__(self, value):
self._value = float(value) / 100
##############################################
def __float__(self):
return self._value
####################################################################################################
class UnitValue:
##############################################
def __init__(self, value, unit):
self._value = float(value)
self._unit = unit
##############################################
def __float__(self):
return self._value
##############################################
def __str__(self):
return self._unit
####################################################################################################
class PercentLengthAttribute(InheritAttribute):
##############################################
@classmethod
def _from_xml(cls, value):
# length ::= number ("em" | "ex" | "px" | "in" | "cm" | "mm" | "pt" | "pc" | "%")?
if value.endswith('%'):
return PercentValue(value[:-1])
elif value[-1].isalpha():
return UnitValue(value[:-2], value[-2])
else:
return float(value)
##############################################
@classmethod
def _to_xml(cls, value):
# Fixme: ok ???
if isinstance(value, PercentValue):
return '{}%'.format(float(value))
elif isinstance(value, UnitValue):
return '{}{}'.format(float(value), value.unit)
else:
return str(value)
####################################################################################################
class ColorMixin:
__attributes__ = (
StringAttribute('fill'), # none red #FFFFFF
StringAttribute('fill'), # none inherit red #ffbb00
StringAttribute('stroke'),
)
......@@ -204,8 +297,8 @@ class StrokeMixin:
__attributes__ = (
StringAttribute('stroke_line_cap', 'stroke-linecap'),
StringAttribute('stroke_line_join', 'stroke-linejoin'),
FloatAttribute('stroke_miter_limit', 'stroke-miterlimit'),
FloatAttribute('stroke_width', 'stroke-width'),
NumberAttribute('stroke_miter_limit', 'stroke-miterlimit'),
PercentLengthAttribute('stroke_width', 'stroke-width'),
FloatListAttribute('stroke_dasharray', 'stroke-dasharray') # stroke-dasharray="20,10,5,5,5,10"
)
......@@ -329,19 +422,30 @@ class TransformAttribute(StringAttribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
if isinstance(value, AffineTransformation2D):
# Python value
return value
else:
transforms = []
for transform in split_space_list(value):
pos0 = value.find('(')
pos1 = value.find(')')
if pos0 == -1 or pos1 != len(value) -1:
raise ValueError
transform_type = value[:pos0]
values = [float(x) for x in value[pos0+1:-1].split(',')]
transforms.append((transform_type, values))
# Fixme:
return transforms
transforms = []
for transform in split_space_list(value):
pos0 = value.find('(')
pos1 = value.find(')')
if pos0 == -1 or pos1 != len(value) -1:
raise ValueError
transform_type = value[:pos0]
values = [float(x) for x in value[pos0+1:-1].split(',')]
transforms.append((transform_type, values))
##############################################
return transforms
@classmethod
def to_xml(cls, value):
return 'matrix({})'.format(' '.join([str(x) for x in value.to_list()])) # Fixme: to func
####################################################################################################
......@@ -789,30 +893,58 @@ class PathDataAttribute(StringAttribute):
##############################################
def from_xml(self, value):
@classmethod
def from_xml(cls, value):
# Replace comma separator by space
value = value.replace(',', ' ')
# Add space after letter
data_path = ''
for c in value:
data_path += c
if c.isalpha:
data_path += ' '
# Convert float
parts = []
for part in split_space_list(value):
if not(len(part) == 1 and part.isalpha):
part = float(part)
parts.append(part)
parts = split_space_list(value)
commands = []
command = None # last command
number_of_args = None
i = 0
while i < len(parts):
part = parts[i]
command = part
command_lower = command.lower()
if command_lower in self.COMMANDS:
number_of_args = self.NUMBER_OF_ARGS[command_lower]
if number_of_args % 2:
raise ValueError
next_i = i+number_of_args+1
values = [float(x) for x in parts[i+1:next_i]]
#! points = [Vector2D(values[2*i], values[2*i+1]) for i in range(number_of_args / 2)]
points = values
commands.append((command, points))
i = next_i
else:
raise ValueError
if isinstance(part, str):
command = part
command_lower = command.lower()
if command_lower not in cls.COMMANDS:
raise ValueError('Invalid path instruction')
number_of_args = cls.NUMBER_OF_ARGS[command_lower]
# else repeated instruction
next_i = i + number_of_args + 1
values = parts[i+1:next_i]
#! points = [Vector2D(values[2*i], values[2*i+1]) for i in range(number_of_args / 2)]
points = values
commands.append((command, points))
i = next_i
return commands
##############################################
@classmethod
def to_xml(cls, value):
path_data = ''
for command in value:
if path_data:
path_data += ' '
path_data += ' '.join(list(command[0]) + [str(x) for x in command[1]])
return path_data
####################################################################################################
class Path(PathMixin, SvgElementMixin, XmlObjectAdaptator):
......@@ -910,7 +1042,15 @@ class Stop(XmlObjectAdaptator):
####################################################################################################
class Text(DeltaMixin, FontMixin, ColorMixin, SvgElementMixin, XmlObjectAdaptator):
class Style(TextXmlObjectAdaptator):
"""Defines style"""
__tag__ = 'style'
####################################################################################################
class Text(PositionMixin, DeltaMixin, FontMixin, ColorMixin, SvgElementMixin, TextXmlObjectAdaptator):
"""Defines a text"""
......@@ -924,6 +1064,12 @@ class Text(DeltaMixin, FontMixin, ColorMixin, SvgElementMixin, XmlObjectAdaptato
# textLength="a target length for the text that the SVG viewer will attempt to display the text between by adjusting the spacing and/or the glyphs. (default: The text's normal length)"
# lengthAdjust="tells the viewer what to adjust to try to accomplish rendering the text if the length is specified. The two values are 'spacing' and 'spacingAndGlyphs'"
__attributes__ = (
# Fixme: common ???
StringAttribute('_class', 'class', None),
StringAttribute('style'),
)
####################################################################################################
class TextRef(XmlObjectAdaptator):
......
......@@ -24,8 +24,8 @@ import logging
from Patro.Common.Xml.Objectivity import StringAttribute, XmlObjectAdaptator
from Patro.Common.Xml.XmlFile import XmlFileMixin
from Patro.Pattern.Measurement import Measurements
from Patro.Pattern.PersonalData import Gender
from Patro.Measurement.ValentinaMeasurement import ValentinaMeasurements
from Patro.Measurement.PersonalData import Gender
####################################################################################################
......@@ -76,7 +76,7 @@ class VitFile(XmlFileMixin):
def __init__(self, path):
XmlFileMixin.__init__(self, path)
self._measurements = Measurements()
self._measurements = ValentinaMeasurements()
self._read()
##############################################
......@@ -116,5 +116,3 @@ class VitFile(XmlFileMixin):
measurements.add(**xml_measurement.to_dict())
else:
raise NotImplementedError
measurements.eval()
......@@ -54,6 +54,145 @@ from Patro.GeometryEngine.Vector import Vector2D
####################################################################################################
class ValentinaBuiltInVariables:
# defined in libs/ifc/ifcdef.cpp
current_length = 'CurrentLength'
current_seam_allowance = 'CurrentSeamAllowance'
angle_line = 'AngleLine_'
increment = 'Increment_'
line = 'Line_'
measurement = 'M_'
seg = 'Seg_'
arc = 'ARC_'
elarc = 'ELARC_'
spl = 'SPL_'
angle1 = 'Angle1'
angle2 = 'Angle2'
c1_length = 'C1Length'
c2_length = 'C2Length'
radius = 'Radius'
rotation = 'Rotation'
spl_path = 'SplPath'
angle1_arc = angle1 + arc
angle1_elarc = angle1 + elarc
angle1_spl_path = angle1 + spl_path
angle1_spl = angle1 + spl
angle2_arc = angle2 + arc
angle2_elarc = angle2 + elarc
angle2_spl_path = angle2 + spl_path
angle2_spl = angle2 + spl
c1_length_spl_path = c1_length + spl_path
c1_length_spl = c1_length + spl
c2_length_spl_path = c2_length + spl_path
c2_length_spl = c2_length + spl
radius1_elarc = radius + '1' + elarc
radius2_elarc = radius + '2' + elarc
radius_arc = radius + arc
rotation_elarc = rotation + elarc
####################################################################################################
VALENTINA_ATTRIBUTES = (
'aScale',
'angle',
'angle1',
'angle2',
'arc',
'axisP1',
'axisP2',
'axisType',
'baseLineP1',
'baseLineP2',
'basePoint',
'c1Center',
'c1Radius',
'c2Center',
'c2Radius',
'cCenter',
'cRadius',
'center',
'closed',
'color',
'crossPoint',
'curve',
'curve1',
'curve2',
'cut',
'dartP1',
'dartP2',
'dartP3',
'duplicate',
'firstArc',
'firstPoint',
'firstToCountour',
'forbidFlipping',
'forceFlipping',
'hCrossPoint',
'height',
'idObject',
'inLayout',
'kAsm1',
'kAsm2',
'kCurve',
'lastToCountour',
'length',
'length1',
'length2',
'lineColor',
'mx',
'mx1',
'mx2',
'my',
'my1',
'my2',
'name',
'name1',
'name2',
'p1Line',
'p1Line1',
'p1Line2',
'p2Line',
'p2Line1',
'p2Line2',
'pShoulder',
'pSpline',
'pathPoint',
'penStyle',
'placeLabelType',
'point1',
'point2',
'point3',
'point4',
'radius',
'radius1',
'radius2',
'rotationAngle',
'secondArc',
'secondPoint',
'showLabel',
'showLabel1',
'showLabel2',
'suffix',
'tangent',
'thirdPoint',
'type',
'typeLine',
'vCrossPoint',
'version',
'width',
'x',
'y',
)
####################################################################################################
class MxMyMixin:
__attributes__ = (
......@@ -675,81 +814,3 @@ class DetailNode(XmlObjectAdaptator):
StringAttribute('type'),
BoolAttribute('reverse'),
)
####################################################################################################
# angle
# angle1
# angle2
# arc
# axisP1
# axisP2
# axisType
# baseLineP1
# baseLineP2
# basePoint
# c1Center
# c1Radius
# c2Center
# c2Radius
# cCenter
# center
# color
# cRadius
# crossPoint
# curve
# curve1
# curve2
# dartP1
# dartP2
# dartP3
# duplicate
# firstArc
# firstPoint
# hCrossPoint
# id
# idObject
# length
# length1
# length2
# lineColor
# mx
# mx1
# mx2
# my
# my1
# my2
# name
# name1
# name2
# object (group)
# p1Line
# p1Line1
# p1Line2
# p2Line
# p2Line1
# p2Line2
# point1
# point2
# point3
# point4
# pShoulder
# pSpline
# radius
# radius1
# radius2
# rotationAngle
# secondArc
# secondPoint
# spline
# splinePath
# suffix
# tangent
# thirdPoint
# tool
# type
# typeLine
# vCrossPoint
# visible (group)
# x
# y
......@@ -20,16 +20,22 @@
####################################################################################################
from math import log, sqrt # pow
from math import log, sqrt, pow
from .BoundingBox import bounding_box_from_points
from Patro.Common.Math.Root import quadratic_root, cubic_root
from .Interpolation import interpolate_two_points
from .Primitive import Primitive2D, ReversablePrimitiveMixin
from .Line import Line2D
from .Primitive import Primitive3P, Primitive4P, Primitive2DMixin
from .Transformation import AffineTransformation
from .Vector import Vector2D
####################################################################################################
class QuadraticBezier2D(Primitive2D, ReversablePrimitiveMixin):
# Fixme: implement intersection
####################################################################################################
class QuadraticBezier2D(Primitive2DMixin, Primitive3P):
"""Class to implements 2D Quadratic Bezier Curve."""
......@@ -39,24 +45,7 @@ class QuadraticBezier2D(Primitive2D, ReversablePrimitiveMixin):
def __init__(self, p0, p1, p2):
self._p0 = Vector2D(p0)
self._p1 = Vector2D(p1)
self._p2 = Vector2D(p2)
##############################################
def clone(self):
return self.__class__(self._p0, self._p1, self._p2)
##############################################
def bounding_box(self):
return bounding_box_from_points((self._p0, self._p1, self._p2))
##############################################
def reverse(self):
return self.__class__(self._p2, self._p1, self._p0)
Primitive3P.__init__(self, p0, p1, p2)
##############################################
......@@ -65,40 +54,6 @@ class QuadraticBezier2D(Primitive2D, ReversablePrimitiveMixin):
##############################################
@property
def p0(self):
return self._p0
@p0.setter
def p0(self, value):
self._p0 = value
@property
def p1(self):
return self._p1
@p1.setter
def p1(self, value):
self._p1 = value
@property
def p2(self):
return self._p2
@p2.setter
def p2(self, value):
self._p2 = value
@property
def start_point(self):
return self._p0
@property
def end_point(self):
return self._p2
##############################################
@property
def length(self):
......@@ -143,11 +98,13 @@ class QuadraticBezier2D(Primitive2D, ReversablePrimitiveMixin):
##############################################
def interpolated_length(self):
def interpolated_length(self, dt=None):
# Length of the curve obtained via line interpolation
dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude()
if dt is None:
dt = self.LineInterpolationPrecision / (self.end_point - self.start_point).magnitude()
length = 0
t = 0
while t < 1:
......@@ -159,6 +116,60 @@ class QuadraticBezier2D(Primitive2D, ReversablePrimitiveMixin):
##############################################
def length_at_t(self, t, cache=False):
"""Compute the length of the curve at *t*."""
if cache: # lookup cache
if not hasattr(self, '_length_cache'):
self._length_cache = {}
length = self._length_cache.get(t, None)
if length is not None:
return length
length = self.split_at_t(t).length
if cache: # save
self._length_cache[t] = length
return length
##############################################
def t_at_length(self, length, precision=1e-6):
"""Compute t for the given length. Length must lie in [0, curve length] range]. """
if length < 0:
raise ValueError('Negative length')
if length == 0:
return 0
curve_length = self.length # Fixme: cache ?
if (curve_length - length) <= precision:
return 1
if length > curve_length:
raise ValueError('Out of length')
# Search t for length using dichotomy
# convergence rate :
# 10 iterations corresponds to curve length / 1024
# 16 / 65536
# start range
inf = 0
sup = 1
while True:
middle = (sup + inf) / 2
length_at_middle = self.length_at_t(middle, cache=True) # Fixme: out of memory, use LRU ???
# exit condition
if abs(length_at_middle - length) <= precision:
return middle
elif length_at_middle < length:
inf = middle
else: # length < length_at_middle
sup = middle
##############################################
def point_at_t(self, t):
# if 0 < t or 1 < t:
# raise ValueError()
......@@ -171,12 +182,37 @@ class QuadraticBezier2D(Primitive2D, ReversablePrimitiveMixin):
"""Split the curve at given position"""
p01 = interpolate_two_points(self._p0, self._p1, t)
p12 = interpolate_two_points(self._p1, self._p2, t)
p = interpolate_two_points(p01, p12, t) # p = p012
# p = self.point_at_t(t)
if t <= 0:
return None, self
elif t >= 1:
return self, None
else:
p01 = interpolate_two_points(self._p0, self._p1, t)
p12 = interpolate_two_points(self._p1, self._p2, t)
p = interpolate_two_points(p01, p12, t) # p = p012
# p = self.point_at_t(t)
return (QuadraticBezier2D(self._p0, p01, p), QuadraticBezier2D(p, p12, self._p2))
##############################################
def split_at_two_t(self, t1, t2):
return (QuadraticBezier2D(self._p0, p01, p), QuadraticBezier2D(p, p12, self._p2))
if t1 == t2:
return self.point_at_t(t1)
if t2 < t1:
# Fixme: raise ?
t1, t2 = t2, t1
# curve = self
# if t1 > 0:
curve = self.split_at_t(t1)[1] # right
if t2 < 1:
# Interpolate the parameter at t2 in the new curve
t = (t2 - t1) / (1 - t1)
curve = curve.split_at_t(t)[0] # left
return curve
##############################################
......@@ -208,6 +244,88 @@ class QuadraticBezier2D(Primitive2D, ReversablePrimitiveMixin):
u = 1 - t
return (self._p1 - self._p0) * u + (self._p2 - self._p1) * t
##############################################
def _map_to_line(self, line):
transformation = AffineTransformation.Rotation(-line.v.orientation)
# Fixme: use __vector_cls__
transformation *= AffineTransformation.Translation(Vector2D(0, -line.p.y))
# Fixme: better API ?
return self.clone().transform(transformation)
##############################################
def intersect_line(self, line):
"""Find the intersections of the curve with a line."""
# Algorithm:
# Apply a transformation to the curve that maps the line onto the X-axis.
# Then we only need to test the Y-values for a zero.
# t, p0, p1, p2, p3 = symbols('t p0 p1 p2 p3')
# u = 1 - t
# B = p0 * u**2 + p1 * 2*t*u + p2 * t**2
# collect(expand(B), t)
# solveset(B, t)
curve = self._map_to_line(line)
p0 = curve.p0.y
p1 = curve.p1.y
p2 = curve.p2.y
return quadratic_root(
p2 - 2*p1 + p0, # t**2
2*(p1 - p0), # t
p0,
)
### a = p0 - 2*p1 + p2 # t**2
### # b = 2*(-p0 + p1) # t
### b = -p0 + p1 # was / 2 !!!
### c = p0
###
### # discriminant = b**2 - 4*a*c
### # discriminant = 4 * (p1**2 - p0*p2)
### discriminant = p1**2 - p0*p2 # was / 4 !!!
###
### if discriminant < 0:
### return None
### elif discriminant == 0:
### return -b / a # dropped 2
### else:
### # dropped 2
### y1 = (-b - sqrt(discriminant)) / a
### y2 = (-b + sqrt(discriminant)) / a
### return y1, y2
##############################################
def fat_line(self):
line = Line2D.from_two_points(self._p0, self._p3)
d1 = line.distance_to_line(self._p1)
d_min = min(0, d1 / 2)
d_max = max(0, d1 / 2)
return (line, d_min, d_max)
##############################################
def non_parametric_curve(self, line):
"""Return the non-parametric Bezier curve D(ti, di(t)) where di(t) is the distance of the curve from
the baseline of the fat-line, ti is equally spaced in [0, 1].
"""
ts = np.arange(0, 1, 1/(self.number_of_points-1))
distances = [line.distance_to_line(p) for p in self.points]
points = [Vector2D(t, d) for t, f in zip(ts, distances)]
return self.__class__(*points)
####################################################################################################
_Sqrt3 = sqrt(3)
......@@ -215,7 +333,7 @@ _Div18Sqrt3 = 18 / _Sqrt3
_OneThird = 1 / 3
_Sqrt3Div36 = _Sqrt3 / 36
class CubicBezier2D(QuadraticBezier2D):
class CubicBezier2D(Primitive4P, QuadraticBezier2D):
"""Class to implements 2D Cubic Bezier Curve."""
......@@ -225,23 +343,7 @@ class CubicBezier2D(QuadraticBezier2D):
def __init__(self, p0, p1, p2, p3):
QuadraticBezier2D.__init__(self, p0, p1, p2)
self._p3 = Vector2D(p3)
##############################################
def clone(self):
return self.__class__(self._p0, self._p1, self._p2, self._p3)
##############################################
def bounding_box(self):
return bounding_box_from_points((self._p0, self._p1, self._p2, self._p3))
##############################################
def reverse(self):
return self.__class__(self._p3, self._p2, self._p1, self._p0)
Primitive4P.__init__(self, p0, p1, p2, p3)
##############################################
......@@ -250,20 +352,6 @@ class CubicBezier2D(QuadraticBezier2D):
##############################################
@property
def p3(self):
return self._p3
@p3.setter
def p3(self, value):
self._p3 = value
@property
def end_point(self):
return self._p3
##############################################
@property
def length(self):
return self.adaptive_length_approximation()
......@@ -354,3 +442,347 @@ class CubicBezier2D(QuadraticBezier2D):
def tangent_at(self, t):
u = 1 - t
return (self._p1 - self._p0) * u**2 + (self._p2 - self._p1) * 2 * t * u + (self._p3 - self._p2) * t**2
##############################################
def intersect_line(self, line):
"""Find the intersections of the curve with a line."""
# Algorithm: same as for quadratic
# t, p0, p1, p2, p3, p4 = symbols('t p0 p1 p2 p3 p4')
# u = 1 - t
# B = p0 * u**3 +
# p1 * 3 * u**2 * t +
# p2 * 3 * u * t**2 +
# p3 * t**3
# B = p0 +
# (p1 - p0) * 3 * t +
# (p2 - p1 * 2 + p0) * 3 * t**2 +
# (p3 - p2 * 3 + p1 * 3 - p0) * t**3
# solveset(B, t)
curve = self._map_to_line(line)
p0 = curve.p0.y
p1 = curve.p1.y
p2 = curve.p2.y
p3 = curve.p3.y
return cubic_root(
p3 - 3*p2 + 3*p1 - p0,
3 * (p2 - p1 * 2 + p0),
3 * (p1 - p0),
p0,
)
##############################################
def fat_line(self):
line = Line2D.from_two_points(self._p0, self._p3)
d1 = line.distance_to_line(self._p1)
d2 = line.distance_to_line(self._p2)
if d1*d2 > 0:
factor = 3 / 4
else:
factor = 4 / 9
d_min = factor * min(0, d1, d2)
d_max = factor * max(0, d1, d2)
return (line, d_min, d_max)
##############################################
def _clipping_convex_hull(self):
line_03 = Line2D(self._p0, self._p3)
d1 = line_03.distance_to_line(self._p1)
d2 = line_03.distance_to_line(self._p2)
# Check if p1 and p2 are on the same side of the line [p0, p3]
if d1 * d2 < 0:
# p1 and p2 lie on different sides of [p0, p3].
# The hull is a quadrilateral and line [p0, p3] is not part of the hull.
# The top part includes p1, we will reverse it later if that is not the case.
hull = [
[self._p0, self._p1, self._p3], # top part
[self._p0, self._p2, self._p3] # bottom part
]
flip = d1 < 0
else:
# p1 and p2 lie on the same sides of [p0, p3]. The hull can be a triangle or a
# quadrilateral and line [p0, p3] is part of the hull. Check if the hull is a triangle
# or a quadrilateral. Also, if at least one of the distances for p1 or p2, from line
# [p0, p3] is zero then hull must at most have 3 vertices.
# Fixme: check cross product
y0, y1, y2, y3 = [p.y for p in self.points]
if abs(d1) < abs(d2):
pmax = p2;
# apex is y0 in this case, and the other apex point is y3
# vector yapex -> yapex2 or base vector which is already part of the hull
# V30xV10 * V10xV20
cross_product = ((y1 - y0) - (y3 - y0)/3) * (2*(y1 - y0) - (y2 - y0)) /3
else:
pmax = p1;
# apex is y3 and the other apex point is y0
# vector yapex -> yapex2 or base vector which is already part of the hull
# V32xV30 * V32xV31
cross_product = ((y3 - y2) - (y3 - y0)/3) * (2*(y3 - y2) - (y3 + y1)) /3
# Compare cross products of these vectors to determine if the point is in the triangle
# [p3, pmax, p0], or if it is a quadrilateral.
has_null_distance = d1 == 0 or d2 == 0 # Fixme: don't need to compute cross_product
if cross_product < 0 or has_null_distance:
# hull is a triangle
hull = [
[self._p0, pmax, self._p3], # top part is a triangle
[self._p0, self._p3], # bottom part is just an edge
]
else:
hull = [
[self._p0, self._p1, self._p2, self._p3], # top part is a quadrilateral
[self._p0, self._p3], # bottom part is just an edge
]
flip = d1 < 0 if d1 else d2 < 0
if flip:
hull.reverse()
return hull
##############################################
@staticmethod
def _clip_convex_hull(hull_top, hull_bottom, d_min, d_max) :
# Top /----
# / ---/
# / /
# d_max -------------------*---
# / / t_max
# t_min / /
# d_min -------*---------------
# / /
# / ----/ Bottom
# p0 /----
if (hull_top[0].y < d_min):
# Left of hull is below d_min,
# walk through the hull until it enters the region between d_min and d_max
return self._clip_convex_hull_part(hull_top, True, d_min);
elif (hull_bottom[0].y > d_max) :
# Left of hull is above d_max,
# walk through the hull until it enters the region between d_min and d_max
return self._clip_convex_hull_part(hull_bottom, False, d_max);
else :
# Left of hull is between d_min and d_max, no clipping possible
return hull_top[0].x; # Fixme: 0 ???
##############################################
@staticmethod
def _clip_convex_hull_part(part, top, threshold) :
"""Clip the bottom or top part of the convex hull.
*part* is a list of points, *top* is a boolean flag to indicate if it corresponds to the top
part, *threshold* is d_min if top part else d_max.
"""
# Walk on the edges
px = part[0].x;
py = part[0].y;
for i in range(1, len(part)):
qx = part[i].x;
qy = part[i].y;
if (qy >= threshold if top else qy <= threshold):
# compute a linear interpolation
# threshold = s * (t - px) + py
# t = (threshold - py) / s + px
return px + (threshold - py) * (qx - px) / (qy - py);
px = qx;
py = qy;
return None; # no intersection
##############################################
@staticmethod
def _instersect_curve(
curve1, curve2,
t_min=0, t_max=1,
u_min=0, u_max=1,
old_delta_t=1,
reverse=False, # flag to indicate that 1 <-> 2 when we store locations
recursion=0, # number of recursions
recursion_limit=32,
t_limit=0.8,
locations=[],
) :
# Code inspired from
# https://github.com/paperjs/paper.js/blob/master/src/path/Curve.js
# http://nbviewer.jupyter.org/gist/hkrish/0a128f21a5b9e5a7a914 The Bezier Clipping Algorithm
# https://gist.github.com/hkrish/5ef0f2da7f9882341ee5 hkrish/bezclip_manual.py
# Note:
# see https://github.com/paperjs/paper.js/issues/565
# It was determined that more than 20 recursions are needed sometimes, depending on the
# delta_t threshold values further below when determining which curve converges the
# least. He also recommended a threshold of 0.5 instead of the initial 0.8
if recursion > recursion_limit:
return
tolerance = 1e-5
epsilon = 1e-10
# t_min_new = 0.
# t_max_new = 0.
# delta_t = 0.
# NOTE: the recursion threshold of 4 is needed to prevent this issue from occurring:
# https://github.com/paperjs/paper.js/issues/571
# when two curves share an end point
if curve1.p0.x == curve1.p3.x and u_max - u_min <= epsilon and recursion > 4:
# The fat-line of curve1 has converged to a point, the clipping is not reliable.
# Return the value we have even though we will miss the precision.
t_max_new = t_min_new = (t_max + t_min) / 2
delta_t = 0
else :
# Compute the fat-line for curve1:
# a baseline and two offsets which completely encloses the curve
fatline, d_min, d_max = curve1.fat_line()
# Calculate a non-parametric bezier curve D(ti, di(t)) where di(t) is the distance of curve2 from
# the baseline, ti is equally spaced in [0, 1]
non_parametric_curve = curve2.non_parametric_curve(fatline)
# Get the top and bottom parts of the convex-hull
top, bottom = non_parametric_curve._clip_convex_hull()
# Clip the convex-hull with d_min and d_max
t_min_clip = self.clip_convex_hull(top, bottom, d_min, d_max);
top.reverse()
bottom.reverse()
t_max_clip = clipConvexHull(top, bottom, d_min, d_max);
# No intersections if one of the t values is None
if t_min_clip is None or t_max_clip is None:
return
# Clip curve2 with the fat-line for curve1
curve2 = curve2.split_at_two_t(t_min_clip, t_max_clip)
delta_t = t_max_clip - t_min_clip
# t_min and t_max are within the range [0, 1]
# We need to project it to the original parameter range
t_min_new = t_max * t_min_clip + t_min * (1 - t_min_clip)
t_max_new = t_max * t_max_clip + t_min * (1 - t_max_clip)
delta_t_new = t_max_new - t_min_new
delta_u = u_max - u_min
# Check if we need to subdivide the curves
if old_delta_t > t_limit and delta_t > t_limit:
# Subdivide the curve which has converged the least.
args = (delta_t, not reverse, recursion+1, recursion_limit, t_limit, locations)
if delta_u < delta_t_new: # curve2 < curve1
parts = curve1.split_at_t(0.5)
t = t_min_new + delta_t_new / 2
self._intersect_curve(curve2, parts[0], u_min, u_max, t_min_new, t, *args)
self._intersect_curve(curve2, parts[1], u_min, u_max, t, t_max_new, *args)
else :
parts = curve2.split_at_t(0.5)
t = u_min + delta_u / 2
self._intersect_curve(parts[0], curve1, u_min, t, t_min_new, t_max_new, *args)
self._intersect_curve(parts[1], curve1, t, u_max, t_min_new, t_max_new, *args)
elif max(delta_u, delta_t_new) < tolerance:
# We have isolated the intersection with sufficient precision
t1 = t_min_new + delta_t_new / 2
t2 = u_min + delta_u / 2
if reverse:
t1, t2 = t2, t1
p1 = curve1.point_at_t(t1)
p2 = curve2.point_at_t(t2)
locations.append([t1, point1, t2, point2])
else:
args = (delta_t, not reverse, recursion+1, recursion_limit, t_limit)
self._intersect_curve(curve2, curve1, locations, u_min, u_max, t_min_new, t_max_new, *args)
##############################################
def is_flat_enough(self, flatness):
"""Determines if a curve is sufficiently flat, meaning it appears as a straight line and has
curve-time that is enough linear, as specified by the given *flatness* parameter.
*flatness* is the maximum error allowed for the straight line to deviate from the curve.
"""
# Reference:
# Kaspar Fischer and Roger Willcocks http://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf
# PostScript Language Reference. Addison- Wesley, third edition, 1999
# We define the flatness of the curve as the argmax of the distance from the curve to the
# line passing by the start and stop point.
#
# flatness = argmax(d(t)) for t in [0, 1] where d(t) = | B(t) - L(t) |
#
# L = (1-t)*P0 + t*P1
#
# Let
# u = 3*P1 - 2*P0 - P3
# v = 3*P2 - P0 - 2*P3
#
# d(t) = (1-t)**2 * t * (3*P1 - 2*P0 - P3) + (1-t) * t**2 * (3*P2 - P0 - 2*P3)
# = (1-t)**2 * t * u + (1-t) * t**2 * v
#
# d(t)**2 = (1 - t)**2 * t**2 * (((1 - t)*ux + t*vx)**2 + ((1 - t)*uy + t*vy)**2
#
# argmax((1 - t)**2 * t**2) = 1/16
# argmax((1 - t)*a + t*b) = argmax(a, b)
#
# flatness**2 = argmax(d(t)**2) <= 1/16 * (argmax(ux**2, vx**2) + argmax(uy**2, vy**2))
#
# argmax(ux**2, vx**2) + argmax(uy**2, vy**2) is thus an upper bound of 16 * flatness**2
# x0, y0 = list(self._p0)
# x1, y1 = list(self._p1)
# x2, y2 = list(self._p2)
# x3, y3 = list(self._p3)
# ux = 3*x1 - 2*x0 - x3
# uy = 3*y1 - 2*y0 - y3
# vx = 3*x2 - 2*x3 - x0
# vy = 3*y2 - 2*y3 - y0
u = 3*P1 - 2*P0 - P3
v = 3*P2 - 2*P3 - P0
return max(u.x**2, v.x**2) + max(u.y**2, v.y**2) <= 16 * flatness**2
##############################################
@property
def area(self):
"""Compute the area delimited by the curve and the segment across the start and stop point."""
# Reference: http://objectmix.com/graphics/133553-area-closed-bezier-curve.html BUT DEAD LINK
# Proof using divergence theorem ???
# Fixme: any proof !
x0, y0 = list(self._p0)
x1, y1 = list(self._p1)
x2, y2 = list(self._p2)
x3, y3 = list(self._p3)
return (3 * ((y3 - y0) * (x1 + x2) - (x3 - x0) * (y1 + y2)
+ y1 * (x0 - x2) - x1 * (y0 - y2)
+ y3 * (x2 + x0 / 3) - x3 * (y2 + y0 / 3)) / 20)
......@@ -18,11 +18,83 @@
#
####################################################################################################
"""Module to compute bounding box and convex hull for a set of points.
"""
####################################################################################################
__all__ = [
'bounding_box_from_points',
'convex_hull',
]
####################################################################################################
import functools
####################################################################################################
def bounding_box_from_points(points):
"""Return the bounding box of the list of points."""
bounding_box = points[0].bounding_box()
for point in points[1:]:
bounding_box |= point.bounding_box()
bounding_box = None
for point in points:
if bounding_box is None:
bounding_box = point.bounding_box
else:
bounding_box |= point.bounding_box
return bounding_box
####################################################################################################
def _sort_point_for_graham_scan(points):
def sort_by_y(p0, p1):
return p0.x < p1.x if (p0.y == p1.y) else p0.y < p1.y
# sort by ascending y
sorted_points = sorted(points, key=functools.cmp_to_key(sort_by_y))
# sort by ascending slope with p0
p0 = sorted_points[0]
x0 = p0.x
y0 = p0.y
def slope(p):
# return (p - p0).tan
return (p.y - y0) / (p.x - x0)
def sort_by_slope(p0, p1):
s0 = slope(p0)
s1 = slope(p1)
return p0.x < p1.x if (s0 == s1) else s0 < s1
return sorted_points[0] + sorted(sorted_points[1:], key=cmp_to_key(sort_by_slope))
####################################################################################################
def _ccw(p1, p2, p3):
# Three points are a counter-clockwise turn if ccw > 0, clockwise if ccw < 0, and collinear if
# ccw = 0 because ccw is a determinant that gives twice the signed area of the triangle formed
# by p1, p2 and p3.
return (p2.x - p1.x)*(p3.y - p1.y) - (p2.y - p1.y)*(p3.x - p1.x)
####################################################################################################
def convex_hull(points):
"""Return the convex hull of the list of points using Graham Scan algorithm."""
# Reference: Graham Scan Algorithm
# https://en.wikipedia.org/wiki/Graham_scan
# convex_hull is a stack of points beginning with the leftmost point.
convex_hull = []
sorted_points = _sort_point_for_graham_scan(points)
for p in sorted_points:
# if we turn clockwise to reach this point, pop the last point from the stack, else, append this point to it.
while len(convex_hull) > 1 and _ccw(convex_hull[-1], convex_hull[-2], p) >= 0: # Fixme: check
convex_hull.pop()
convex_hull.append(p)
# the stack is now a representation of the convex hull, return it.
return convex_hull
......@@ -20,28 +20,108 @@
####################################################################################################
from math import sqrt, radians, cos, sin
from math import sqrt, radians, cos, sin, fabs, pi
import numpy as np
from IntervalArithmetic import Interval, Interval2D
from IntervalArithmetic import Interval
from .Primitive import Primitive2D
from Patro.Common.Math.Functions import sign
from .Line import Line2D
from .Primitive import Primitive, Primitive2DMixin
from .Segment import Segment2D
from .Vector import Vector2D
####################################################################################################
class Circle2D(Primitive2D):
class DomainMixin:
##############################################
@property
def domain(self):
return self._domain
@domain.setter
def domain(self, interval):
if interval is not None and interval.length < 360:
self._domain = Interval(interval)
else:
self._domain = None
##############################################
@property
def is_closed(self):
return self._domain is None
##############################################
def start_stop_point(self, start=True):
if self._domain is not None:
angle = self.domain.inf if start else self.domain.sup
return self.point_at_angle(angle)
else:
return None
##############################################
@property
def start_point(self):
return self.start_stop_point(start=True)
##############################################
@property
def stop_point(self):
return self.start_stop_point(start=False)
####################################################################################################
class Circle2D(Primitive2DMixin, DomainMixin, Primitive):
"""Class to implements 2D Circle."""
#######################################
##############################################
@classmethod
def from_two_points(cls, center, point):
"""Construct a circle from a center point and passing by another point"""
return cls(center, (point - center).magnitude)
##############################################
@classmethod
def from_triangle_circumcenter(cls, triangle):
"""Construct a circle passing by three point"""
return cls.from_two_points(triangle.circumcenter, triangle.p0)
##############################################
@classmethod
def from_triangle_in_circle(cls, triangle):
"""Construct the in circle of a triangle"""
return triangle.in_circle
##############################################
# Fixme: tangent constructs ...
##############################################
def __init__(self, center, radius, domain=None, diameter=False):
"""Construct a 2D circle from a center point and a radius.
def __init__(self, center, radius, domain):
If the circle is not closed, *domain* is an interval in degrees.
"""
if diameter:
radius /= 2
self._radius = radius
self._center = Vector2D(center)
self._domain = Interval(domain)
self.center = center
self.domain = domain # Fixme: name ???
##############################################
......@@ -51,7 +131,7 @@ class Circle2D(Primitive2D):
@center.setter
def center(self, value):
self._center = value
self._center = Vector2D(value)
@property
def radius(self):
......@@ -62,38 +142,202 @@ class Circle2D(Primitive2D):
self._radius = value
@property
def domain(self):
return self._domain
def diameter(self):
return self._radius * 2
@domain.setter
def domain(self, value):
self._domain = value
##############################################
@property
def eccentricity(self):
return 1
@property
def perimeter(self):
return 2 * pi * self._radius
@property
def area(self):
return pi * self._radius**2
##############################################
def point_at_angle(self, angle):
return Vector2D.from_polar(self._radius, angle) + self._center
##############################################
def tangent_at_angle(self, angle):
point = Vector2D.from_polar(self._radius, angle) + self._center
tangent = (point - self._center).normal
return Line2D(point, tangent)
##############################################
@property
def bounding_box(self):
return self._center.bounding_box.enlarge(self._radius)
##############################################
def is_point_inside(self, point):
return (point - self._center).magnitude_square <= self._radius**2
return self._center.bounding_box().enlarge(self._radius)
##############################################
def intersect_segment(self, segment):
# Fixme: check domain !!!
# http://mathworld.wolfram.com/Circle-LineIntersection.html
# Reference: Rhoad et al. 1984, p. 429
# Rhoad, R.; Milauskas, G.; and Whipple, R. Geometry for Enjoyment and Challenge,
# rev. ed. Evanston, IL: McDougal, Littell & Company, 1984.
# Definitions
# dx = x1 - x0
# dy = y1 - y0
# D = x0 * y1 - x1 * y0
# Equations
# x**2 + y**2 = r**2
# dx * y = dy * x - D
dx = segment.vector.x
dy = segment.vector.y
dr2 = dx**2 + dy**2
p0 = segment.p0 - self.center
p1 = segment.p1 - self.center
D = p0.cross_product(p1)
# from sympy import *
# x, y, dx, dy, D, r = symbols('x y dx dy D r')
# system = [x**2 + y**2 - r**2, dx*y - dy*x + D]
# vars = [x, y]
# solution = nonlinsolve(system, vars)
# solution.subs(dx**2 + dy**2, dr**2)
discriminant = self.radius**2 * dr2 - D**2
if discriminant < 0:
return None
elif discriminant == 0: # tangent line
x = ( D * dy ) / dr2
y = (- D * dx ) / dr2
return Vector2D(x, y) + self.center
else: # intersection
x_a = D * dy
y_a = -D * dx
x_b = sign(dy) * dx * sqrt(discriminant)
y_b = fabs(dy) * sqrt(discriminant)
x0 = (x_a - x_b) / dr2
y0 = (y_a - y_b) / dr2
x1 = (x_a + x_b) / dr2
y1 = (y_a + y_b) / dr2
p0 = Vector2D(x0, y0) + self.center
p1 = Vector2D(x1, y1) + self.center
return p0, p1
##############################################
def intersect_circle(self, circle):
# Fixme: check domain !!!
# http://mathworld.wolfram.com/Circle-CircleIntersection.html
v = circle.center - self.center
d = sign(v.x) * v.magnitude
# Equations
# x**2 + y**2 = R**2
# (x-d)**2 + y**2 = r**2
x = (d**2 - circle.radius**2 + self.radius**2) / (2*d)
y2 = self.radius**2 - x**2
if y2 < 0:
return None
else:
p = self.center + v.normalise() * x
if y2 == 0:
return p
else:
n = v.normal() * sqrt(y2)
return p - n, p - n
##############################################
def bezier_approximation(self):
# http://spencermortensen.com/articles/bezier-circle/
# > First approximation:
#
# 1) The endpoints of the cubic Bézier curve must coincide with the endpoints of the
# circular arc, and their first derivatives must agree there.
#
# 2) The midpoint of the cubic Bézier curve must lie on the circle.
#
# B(t) = (1-t)**3 * P0 + 3*(1-t)**2*t * P1 + 3*(1-t)*t**2 * P2 + t**3 * P3
#
# For an unitary circle : P0 = (0,1) P1 = (c,1) P2 = (1,c) P3 = (1, 0)
#
# The second constraint provides the value of c = 4/3 * (sqrt(2) - 1)
#
# The maximum radial drift is 0.027253 % with this approximation.
# In this approximation, the Bézier curve always falls outside the circle, except
# momentarily when it dips in to touch the circle at the midpoint and endpoints.
#
# >Better approximation:
#
# 2) The maximum radial distance from the circle to the Bézier curve must be as small as
# possible.
#
# The first constraint yields the parametric form of the Bézier curve:
# B(t) = (x,y), where:
# x(t) = 3*c*(1-t)**2*t + 3*(1-t)*t**2 + t**3
# y(t) = 3*c*t**2*(1-t) + 3*t*(1-t)**2 + (1-t)**3
#
# The radial distance from the arc to the Bézier curve is: d(t) = sqrt(x**2 + y**2) - 1
#
# The Bézier curve touches the right circular arc at its initial endpoint, then drifts
# outside the arc, inside, outside again, and finally returns to touch the arc at its
# endpoint.
#
# roots of d : 0, (3*c +- sqrt(-9*c**2 - 24*c + 16) - 2)/(6*c - 4), 1
#
# This radial distance function, d(t), has minima at t = 0, 1/2, 1,
# and maxima at t = 1/2 +- sqrt(12 - 20*c - 3*c**22)/(4 - 6*c)
#
# Because the Bézier curve is symmetric about t = 1/2 , the two maxima have the same
# value. The radial deviation is minimized when the magnitude of this maximum is equal to
# the magnitude of the minimum at t = 1/2.
#
# This gives the ideal value for c = 0.551915024494
# The maximum radial drift is 0.019608 % with this approximation.
# P0 = (0,1) P1 = (c,1) P2 = (1,c) P3 = (1,0)
# P0 = (1,0) P1 = (1,-c) P2 = (c,-1) P3 = (0,-1)
# P0 = (0,-1) P1 = (-c,-1) P2 = (-1,-c) P3 = (-1,0)
# P0 = (-1,0) P1 = (-1,c) P2 = (-c,1) P3 = (0,1)
raise NotImplementedError
####################################################################################################
class Conic2D(Primitive2D):
class Conic2D(Primitive2DMixin, DomainMixin, Primitive):
"""Class to implements 2D Conic."""
#######################################
def __init__(self, x_radius, y_radius, center, angle, domain):
def __init__(self, center, x_radius, y_radius, angle, domain=None):
self.center = center
self._x_radius = x_radius
self._y_radius = y_radius
self._center = Vector2D(center)
self._angle = angle
self._domain = Interval(domain)
self.domain = Interval(domain)
##############################################
......@@ -103,7 +347,7 @@ class Conic2D(Primitive2D):
@center.setter
def center(self, value):
self._center = value
self._center = Vector2D(value)
@property
def x_radius(self):
......@@ -129,14 +373,6 @@ class Conic2D(Primitive2D):
def angle(self, value):
self._angle = value
@property
def domain(self):
return self._domain
@domain.setter
def domain(self, value):
self._domain = value
##############################################
@property
......@@ -179,3 +415,29 @@ class Conic2D(Primitive2D):
(B/2, C, E/2),
(D/2, E/2, F),
))
##############################################
def point_at_angle(self, angle):
raise NotImplementedError
##############################################
@property
def bounding_box(self):
raise NotImplementedError
##############################################
def is_point_inside(self, point):
raise NotImplementedError
##############################################
def intersect_segment(self, segment):
raise NotImplementedError
##############################################
def intersect_conic(self, conic):
raise NotImplementedError
......@@ -22,12 +22,12 @@
from Patro.Common.IterTools import pairwise
from .Primitive import Primitive2D
from .Primitive import Primitive, Primitive2DMixin
from .Vector import Vector2D
####################################################################################################
class Line2D(Primitive2D):
class Line2D(Primitive2DMixin, Primitive):
"""Class to implement 2D Line."""
......@@ -68,10 +68,19 @@ class Line2D(Primitive2D):
##############################################
def point_at_s(self, s):
@property
def is_infinite(self):
return True
##############################################
def interpolate(self, s):
"""Return the Point corresponding to the curvilinear abscissa s"""
return self.p + (self.v * s)
point_at_s = interpolate
point_at_t = interpolate
##############################################
def compute_distance_between_abscissae(self, s0, s1):
......@@ -106,9 +115,9 @@ class Line2D(Primitive2D):
# Fixme: is_parallel_to
def is_parallel(self, other):
def is_parallel(self, other, cross=False):
"""Self is parallel to other"""
return self.v.is_parallel(other.v)
return self.v.is_parallel(other.v, cross)
##############################################
......@@ -134,7 +143,7 @@ class Line2D(Primitive2D):
"""Return the orthogonal line at abscissa s"""
point = self.point_at_s(s)
point = self.interpolate(s)
vector = self.v.normal()
return self.__class__(point, vector)
......@@ -147,17 +156,20 @@ class Line2D(Primitive2D):
# l1 = p1 + s1*v1
# l2 = p2 + s2*v2
# delta = p2 - p1 = s2*v2 - s1*v1
# delta x v1 = s2*v2 x v1 = s2 * - v1 x v2
# delta x v2 = s1*v1 x v2 = s1 * v1 x v2
if l1.is_parallel(l2):
# at intersection l1 = l2
# p2 + s2*v2 = p1 + s1*v1
# delta = p2 - p1 = s1*v1 - s2*v2
# delta x v1 = - s2 * v2 x v1 = s2 * v1 x v2
# delta x v2 = s1 * v1 x v2
test, cross = l1.is_parallel(l2, cross=True)
if test:
return (None, None)
else:
denominator = 1. / l1.v.cross(l2.v)
denominator = 1. / cross
delta = l2.p - l1.p
s1 = delta.cross(l2.v) * denominator
s2 = delta.cross(l1.v) * -denominator
s2 = delta.cross(l1.v) * denominator
return (s1, s2)
##############################################
......@@ -166,11 +178,11 @@ class Line2D(Primitive2D):
"""Return the intersection Point between self and other"""
s0, s1 = self.intersection_abscissae(other)
if s0 is None:
s1, s2 = self.intersection_abscissae(other)
if s1 is None:
return None
else:
return self.point_at_s(s0)
return self.interpolate(s1)
##############################################
......@@ -191,9 +203,24 @@ class Line2D(Primitive2D):
"""Return the distance of a point to the line"""
# Reference: https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
# Line equation: a*x + b*y + c = 0
# d = |a*x + b*y + c| / sqrt(a**2 + b**2)
# Vx*y - Vy*x + c = 0
# c = Vy*X0 - Vx*Y0
# d = (vx*(y - y0) - vy*(x - x0)) / |V|
# d = V x (P - P0) / |V|
# x0 = self.p.x
# y0 = self.p.y
# vx = self.v.x
# vy = self.v.y
# return (self.v.x*(point.y - self.p.y) - self.v.y*(point.x - self.p.x)) / self.v.magnitude
delta = point - self.p
d = delta.deviation_with(self.v)
return d
##############################################
......
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# 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/>.
#
####################################################################################################
####################################################################################################
import math
from Patro.Common.Math.Functions import sign
from .Primitive import PrimitiveNP, Primitive2DMixin
from .Segment import Segment2D
from .Triangle import Triangle2D
####################################################################################################
class Polygon2D(Primitive2DMixin, PrimitiveNP):
"""Class to implements 2D Polygon."""
##############################################
# def __new__(cls, *points):
# # remove consecutive duplicates
# no_duplicate = []
# for point in points:
# if no_duplicate and point == no_duplicate[-1]:
# continue
# no_duplicate.append(point)
# if len(no_duplicate) > 1 and no_duplicate[-1] == no_duplicate[0]:
# no_duplicate.pop() # last point was same as first
# # remove collinear points
# i = -3
# while i < len(no_duplicate) - 3 and len(no_duplicate) > 2:
# a, b, c = no_duplicate[i], no_duplicate[i + 1], no_duplicate[i + 2]
# if Point.is_collinear(a, b, c):
# no_duplicate.pop(i + 1)
# if a == c:
# no_duplicate.pop(i)
# else:
# i += 1
# if len(vertices) > 3:
# return GeometryEntity.__new__(cls, *vertices, **kwargs)
# elif len(vertices) == 3:
# return Triangle(*vertices, **kwargs)
# elif len(vertices) == 2:
# return Segment(*vertices, **kwargs)
# else:
# return Point(*vertices, **kwargs)
##############################################
def __init__(self, *points):
if len(points) < 3:
raise ValueError('Polygon require at least 3 vertexes')
PrimitiveNP.__init__(self, points)
self._edges = None
self._is_simple = None
self._is_convex = None
##############################################
@property
def is_closed(self):
return True
##############################################
@property
def is_triangle(self):
return self.number_of_points == 3
def to_triangle(self):
if self.is_triangle:
return Triangle2D(*self.points)
else:
raise ValueError('Polygon is not a triangle')
##############################################
# barycenter
# momentum
##############################################
@property
def edges(self):
if self._edges is None:
N = self.number_of_points
for i in range(N):
j = (i+1) % N
edge = Segment2D(self._points[i], self._points[j])
self._edges.append(edge)
return iter(self._edges)
##############################################
def _test_is_simple(self):
edges = list(self.edges)
# intersections = []
# Test for edge intersection
for edge1 in edges:
for edge2 in edges:
if edge1 != edge2:
# Fixme: recompute line for edge
intersection, intersect = edge1.intersection(edge2)
if intersect:
common_vertex = edge1.share_vertex_with(edge2)
if common_vertex is not None:
if common_vertex == intersection:
continue
else:
# degenerated case where a vertex lie on an edge
return False
else:
# two edge intersect
# intersections.append(intersection)
return False
##############################################
def _test_is_convex(self):
# https://en.wikipedia.org/wiki/Convex_polygon
# http://mathworld.wolfram.com/ConvexPolygon.html
if not self.is_simple:
return False
edges = list(self.edges)
# a polygon is convex if all turns from one edge vector to the next have the same sense
# sign = edges[-1].perp_dot(edges[0])
sign0 = sign(edges[-1].cross(edges[0]))
for i in range(len(edges)):
if sign(edges[i].cross(edges[i+1])) != sign0:
return False
return True
##############################################
@property
def is_simple(self):
"""Test if the polygon is simple, i.e. if it doesn't self-intersect."""
if self._is_simple is None:
self._is_simple = self._test_is_simple()
return self._is_simple
##############################################
@property
def is_convex(self):
if self._is_convex is None:
self._is_convex = self._test_is_convex()
return self._is_convex
@property
def is_concave(self):
return not self.is_convex
##############################################
@property
def perimeter(self):
return sum([edge.magnitude for edge in self.edges])
##############################################
@property
def area(self):
if not self.is_simple:
return None
# http://mathworld.wolfram.com/PolygonArea.html
# A = 1/2 (x1*y2 - x2*y1 + x2*y3 - x3*y2 + ... + x(n-1)*yn - xn*y(n-1) + xn*y1 - x1*yn)
# determinant
area = self._points[-1].cross(self._points[0])
for i in range(self.number_of_points):
area *= self._points[i].cross(self._points[i+1])
# area of a convex polygon is defined to be positive if the points are arranged in a
# counterclockwise order, and negative if they are in clockwise order (Beyer 1987).
return abs(area) / 2
##############################################
def _crossing_number_test(self, point):
"""Crossing number test for a point in a polygon."""
# Wm. Randolph Franklin, "PNPOLY - Point Inclusion in Polygon Test" Web Page (2000)
# https://www.ecse.rpi.edu/Homepages/wrf/research/geom/pnpoly.html
crossing_number = 0
x = point.x
y = point.y
for edge in self.edges:
if ((edge.p0.y <= y < edge.p1.y) or # upward crossing
(edge.p1.y <= y < edge.p0.y)): # downward crossing
xi = edge.p0.x + (y - edge.p0.y) / edge.vector.slope
if x < xi:
crossing_number += 1
# Fixme: even/odd func
return (crossing_number & 1) == 1 # odd => in
##############################################
def _winding_number_test(self, point):
"""Winding number test for a point in a polygon."""
# more accurate than crossing number test
# http://geomalgorithms.com/a03-_inclusion.html#wn_PnPoly()
winding_number = 0
y = point.y
for edge in self.edges:
if edge.p0.y <= y:
if edge.p1.y > y: # upward crossing
if edge.is_left(point):
winding_number += 1
else:
if edge.p1.y <= y: # downward crossing
if edge.is_right(point):
winding_number -= 1
return winding_number > 0
##############################################
def is_point_inside(self, point):
# http://geomalgorithms.com/a03-_inclusion.html
# http://paulbourke.net/geometry/polygonmesh/#insidepoly
# Fixme: bounding box test
return self._winding_number_test(point)
......@@ -18,55 +18,164 @@
#
####################################################################################################
__all__ = [
'Primitive',
'Primitive2DMixin',
'Primitive1P',
'Primitive2P',
'Primitive3P',
'Primitive4P',
]
####################################################################################################
import collections
from .BoundingBox import bounding_box_from_points
####################################################################################################
# Fixme:
# length, interpolate path
# area
####################################################################################################
class Primitive:
"""Base class for geometric primitive"""
__dimension__ = None # in [2, 3] for 2D / 3D primitive
# __dimension__ = None # in [2, 3] for 2D / 3D primitive
__vector_cls__ = None
##############################################
def clone(self):
@property
def dimension(self):
"""Dimension in [2, 3] for 2D / 3D primitive"""
raise NotImplementedError
##############################################
def bounding_box(self):
# Fixme: infinite primitive
@property
def is_infinite(self):
"""True if the primitive has infinite extend like a line"""
return False
##############################################
@property
def is_closed(self):
"""True if the primitive is a closed path."""
return False
##############################################
@property
def number_of_points(self):
"""Number of points which define the primitive."""
raise NotImplementedError
def __len__(self):
return self.number_of_points
##############################################
@property
def is_reversable(self):
def is_reversible(self):
"""True if the order of the points is reversible"""
# Fixme: True if number_of_points > 1 ???
return False
##############################################
@property
def points(self):
raise NotImplementedError
##############################################
# @points.setter
# def points(self):
# raise NotImplementedError
def _set_points(self, points):
raise NotImplementedError
##############################################
def __repr__(self):
return self.__class__.__name__ + str([str(p) for p in self.points])
##############################################
def clone(self):
return self.__class__(*self.points)
##############################################
@property
def bounding_box(self):
"""Bounding box of the primitive.
Return None if primitive is infinite.
"""
# Fixme: cache
if self.is_infinite:
return None
else:
return bounding_box_from_points(self.points)
##############################################
def reverse(self):
return self
##############################################
def transform(self, transformation):
# for point in self.points:
# point *= transformation # don't work
self._set_points([transformation*p for p in self.points])
####################################################################################################
class Primitive2D:
__dimension__ = 2
class Primitive2DMixin:
# __dimension__ = 2
__vector_cls__ = None # Fixme: due to import, done in module's __init__.py
@property
def dimension(self):
return 2
####################################################################################################
class ReversablePrimitiveMixin:
class ReversiblePrimitiveMixin:
##############################################
@property
def is_reversable(self):
return True
def is_reversible(self):
True
##############################################
def reverse(self):
@property
def reversed_points(self):
raise NotImplementedError
# return reversed(list(self.points))
##############################################
def reverse(self):
return self.__class__(*self.reversed_points)
##############################################
......@@ -78,8 +187,244 @@ class ReversablePrimitiveMixin:
def end_point(self):
raise NotImplementedError
####################################################################################################
class Primitive1P(Primitive):
##############################################
def __init__(self, p0):
self.p0 = p0
##############################################
@property
def number_of_points(self):
return 1
@property
def p0(self):
return self._p0
@p0.setter
def p0(self, value):
self._p0 = self.__vector_cls__(value)
##############################################
@property
def points(self):
return iter(self._p0) # Fixme: efficiency ???
@property
def reversed_points(self):
return self.points
##############################################
def _set_points(self, points):
self._p0 = points
####################################################################################################
class Primitive2P(Primitive1P, ReversiblePrimitiveMixin):
##############################################
def __init__(self, p0, p1):
# We don't call super().__init__(p0) for speed
self.p0 = p0
self.p1 = p1
##############################################
# Redundant code ... until we don't use self._points = []
@property
def number_of_points(self):
return 2
@property
def p1(self):
return self._p1
@p1.setter
def p1(self, value):
self._p1 = self.__vector_cls__(value)
##############################################
@property
def start_point(self):
return self._p0
@property
def end_point(self):
return self._p1
##############################################
@property
def points(self):
return iter((self._p0, self._p1))
@property
def reversed_points(self):
return iter((self._p1, self._p0))
##############################################
def _set_points(self, points):
self._p0, self._p1 = points
##############################################
def interpolate(self, t):
"""Return the linear interpolate of two points."""
return self._p0 * (1 - t) + self._p1 * t
####################################################################################################
class Primitive3P(Primitive2P):
##############################################
def __init__(self, p0, p1, p2):
self.p0 = p0
self.p1 = p1
self.p2 = p2
##############################################
@property
def number_of_points(self):
return 3
@property
def p2(self):
return self._p2
@p2.setter
def p2(self, value):
self._p2 = self.__vector_cls__(value)
##############################################
@property
def end_point(self):
return self._p2
##############################################
@property
def points(self):
return iter((self._p0, self._p1, self._p2))
@property
def reversed_points(self):
return iter((self._p2, self._p1, self._p0))
##############################################
def _set_points(self, points):
self._p0, self._p1, self._p2 = points
####################################################################################################
class Primitive4P(Primitive3P):
##############################################
def __init__(self, p0, p1, p2, p3):
self.p0 = p0
self.p1 = p1
self.p2 = p2
self.p3 = p3
##############################################
@property
def number_of_points(self):
return 4
@property
def p3(self):
return self._p3
@p3.setter
def p3(self, value):
self._p3 = self.__vector_cls__(value)
##############################################
@property
def end_point(self):
return self._p3
##############################################
@property
def points(self):
return iter((self._p0, self._p1, self._p2, self._p3))
@property
def reversed_points(self):
return iter((self._p3, self._p2, self._p1, self._p0))
##############################################
def _set_points(self, points):
self._p0, self._p1, self._p2, self._p3 = points
####################################################################################################
class PrimitiveNP(Primitive, ReversiblePrimitiveMixin):
##############################################
def __init__(self, *points):
if len(points) == 1 and isinstance(points[0], collections.Iterable):
points = points[0]
self._points = [self.__vector_cls__(p) for p in points]
##############################################
@property
def number_of_points(self):
return len(self._points)
##############################################
@property
def start_point(self):
return self._points[0]
@property
def end_point(self):
return self._points[-1]
##############################################
@property
def points(self):
return iter(self._points)
@property
def reversed_points(self):
return reversed(self._points)
##############################################
def _set_points(self, points):
self._points = points
##############################################
def iter_on_points(self):
for point in self.start_point, self.start_point:
yield point
def __getitem__(self, _slice):
return self._points[_slice]
####################################################################################################
#
# Patro - A Python library to make patterns for fashion design
# Copyright (C) 2017 Fabrice Salvaire
#
# 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/>.
#
####################################################################################################
####################################################################################################
import math
from .Primitive import Primitive2P, Primitive2DMixin
from .Segment import Segment2D
####################################################################################################
class Rectangle2D(Primitive2DMixin, Primitive2P):
"""Class to implements 2D Rectangle."""
##############################################
def __init__(self, p0, p1):
# if p1 == p0:
# raise ValueError('Rectangle reduced to a point')
Primitive2P.__init__(self, p0, p1)
##############################################
@classmethod
def from_point_and_offset(self, p0, v):
return cls(p0, p0+v)
@classmethod
def from_point_and_radius(self, p0, v):
return cls(p0-v, p0+v)
##############################################
@property
def is_closed(self):
return True
##############################################
@property
def p01(self):
return self.__vector_cls__(self._p0.x, self._p1.y)
@property
def p10(self):
return self.__vector_cls__(self._p1.x, self._p0.y)
@property
def edges(self):
p0 = self._p0
p1 = self.p01
p2 = self._p1
p3 = self.p10
return (
Segment2D(p0, p1),
Segment2D(p1, p2),
Segment2D(p2, p3),
Segment2D(p3, p0),
)
##############################################
@property
def diagonal(self):
return self._p1 - self._p0
##############################################
@property
def perimeter(self):
d = self.diagonal
return 2*(abs(d.x) + abs(d.y))
##############################################
@property
def area(self):
d = self.diagonal
return abs(d.x * d.y)
##############################################
def is_point_inside(self, point):
bounding_box = self.bounding_box
return (point.x in bounding_box.x and
point.y in bounding_box.y)
......@@ -21,17 +21,18 @@
####################################################################################################
# from .Interpolation import interpolate_two_points
from .BoundingBox import bounding_box_from_points
from .Line import Line2D
from .Primitive import Primitive2D, ReversablePrimitiveMixin
from .Primitive import Primitive2P, Primitive2DMixin
from .Triangle import triangle_orientation
from .Vector import Vector2D
####################################################################################################
class Segment2D(Primitive2D, ReversablePrimitiveMixin):
class Segment2D(Primitive2DMixin, Primitive2P):
"""2D Segment"""
"""Class to implement 2D Segment"""
# Fixme: _p0 versus p0
#######################################
......@@ -39,49 +40,7 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
"""Construct a :class:`Segment2D` between two points."""
self._p0 = Vector2D(p0)
self._p1 = Vector2D(p1)
##############################################
def clone(self):
return self.__class__(self._p0, self._p1)
##############################################
def bounding_box(self):
return bounding_box_from_points((self._p0, self._p1))
##############################################
def reverse(self):
return self.__class__(self._p1, self._p0)
##############################################
@property
def p0(self):
return self._p0
@p0.setter
def p0(self, value):
self._p0 = value
@property
def p1(self):
return self._p1
@p1.setter
def p1(self, value):
self._p1 = value
@property
def start_point(self):
return self._p0
@property
def end_point(self):
return self._p1
Primitive2P.__init__(self, p0, p1)
##############################################
......@@ -100,19 +59,23 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
##############################################
@property
def cross_product(self):
return self._p0.cross(self._p1)
##############################################
def to_line(self):
return Line2D.from_two_points(self._p1, self._p0)
##############################################
def point_at_t(self, t):
# return interpolate_two_points(self._p0, self._p1)
return self._p0 * (1 - t) + self._p1 * t
point_at_t = Primitive2P.interpolate
##############################################
def intersect(self, segment2):
def intersect_with(self, segment2):
"""Checks if the line segments intersect.
return 1 if there is an intersection
......@@ -131,3 +94,69 @@ class Segment2D(Primitive2D, ReversablePrimitiveMixin):
return (((ccw11 * ccw12 < 0) and (ccw21 * ccw22 < 0))
# one ccw value is zero to detect an intersection
or (ccw11 * ccw12 * ccw21 * ccw22 == 0))
##############################################
def intersection(self, segment2):
# P = (1-t) * Pa0 + t * Pa1 = Pa0 + t * (Pa1 - Pa0)
# = (1-u) * Pb0 + u * Pb1 = Pb0 + u * (Pb1 - Pb0)
line1 = self.to_line()
line2 = segment2.to_line()
s1, s2 = line1.intersection_abscissae(line2)
if s1 is None:
return None
else:
intersect = (0 <= s1 <= 1) and (0 <= s2 <= 1)
return self.interpolate(s1), intersect
##############################################
def share_vertex_with(self, segment2):
# return (
# self._p0 == segment2._p0 or
# self._p0 == segment2._p1 or
# self._p1 == segment2._p0 or
# self._p1 == segment2._p1
# )
if (self._p0 == segment2._p0 or
self._p0 == segment2._p1):
return self._p0
elif (self._p1 == segment2._p0 or
self._p1 == segment2._p1):
return self._p1
else:
return None
##############################################
def side_of(self, point):
"""Tests if a point is left/on/right of a line.
> 0 if point is left of the line
= 0 if point is on the line
< 0 if point is right of the line
"""
v1 = self.vector
v2 = point - self._p0
return v1.cross(v2)
##############################################
def left_of(self, point):
"""Tests if a point is left a line"""
return self.side_of(point) > 0
def right_of(self, point):
"""Tests if a point is right a line"""
return self.side_of(point) < 0
def is_collinear(self, point):
"""Tests if a point is on line"""
return self.side_of(point) == 0
......@@ -80,6 +80,11 @@ class Transformation:
##############################################
def to_list(self):
return list(self._m.flat)
##############################################
def same_dimension(self, other):
return self.__size__ == other.dimension
......@@ -129,7 +134,7 @@ class Transformation2D(Transformation):
@classmethod
def Scale(cls, x_scale, y_scale):
return cls(np.array((x_scale, 0), (0, y_scale)))
return cls(np.array(((x_scale, 0), (0, y_scale))))
##############################################
......@@ -164,15 +169,6 @@ class AffineTransformation(Transformation):
##############################################
@classmethod
def Rotation(cls, angle):
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Rotation(angle).array
return transformation
##############################################
@classmethod
def RotationAt(cls, center, angle):
......@@ -191,6 +187,33 @@ class AffineTransformation(Transformation):
def translation_part(self):
return self._m[:self.__dimension__,-1]
####################################################################################################
class AffineTransformation2D(AffineTransformation):
__dimension__ = 2
__size__ = 3
##############################################
@classmethod
def Rotation(cls, angle):
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Rotation(angle).array
return transformation
##############################################
@classmethod
def Scale(cls, x_scale, y_scale):
# Fixme: others, use *= ?
transformation = cls.Identity()
transformation.matrix_part[...] = Transformation2D.Scale(x_scale, y_scale).array
return transformation
#######################################
def __mul__(self, obj):
......@@ -206,12 +229,6 @@ class AffineTransformation(Transformation):
####################################################################################################
class AffineTransformation2D(AffineTransformation):
__dimension__ = 2
__size__ = 3
####################################################################################################
# The matrix to rotate an angle θ about the axis defined by unit vector (l, m, n) is
# l*l*(1-c) + c , m*l*(1-c) - n*s , n*l*(1-c) + m*s
# l*m*(1-c) + n*s , m*m*(1-c) + c , n*m*(1-c) - l*s
......
......@@ -20,6 +20,13 @@
####################################################################################################
import math
from .Primitive import Primitive3P, Primitive2DMixin
from .Line import Line2D
####################################################################################################
def triangle_orientation(p0, p1, p2):
"""Return the triangle orientation defined by the three points."""
......@@ -47,3 +54,235 @@ def triangle_orientation(p0, p1, p2):
# p1 is between p0 and p2
else:
return 1
####################################################################################################
def same_side(p1, p2, a, b):
"""Return True if the points p1 and p2 lie on the same side of the edge [a, b]."""
v = b - a
cross1 = v.cross(p1 - a)
cross2 = v.cross(p2 - a)
# return cross1.dot(cross2) >= 0
return cross1*cross2 >= 0
####################################################################################################
class Triangle2D(Primitive2DMixin, Primitive3P):
"""Class to implements 2D Triangle."""
##############################################
def __init__(self, p0, p1, p2):
if (p1 - p0).is_parallel((p2 - p0)):
raise ValueError('Flat triangle')
Primitive3P.__init__(self, p0, p1, p2)
# self._p10 = None
# self._p21 = None
# self._p02 = None
##############################################
@property
def is_closed(self):
return True
##############################################
@property
def edges(self):
# Fixme: circular import, Segment import triangle_orientation
from .Segment import Segment2D
p0 = self._p0
p1 = self._p1
p2 = self._p2
return (
Segment2D(p0, p1),
Segment2D(p1, p2),
Segment2D(p2, p0),
)
##############################################
@property
def bisector_vector0(self):
return (self._p1 - self._p0) + (self._p2 - self._p0)
@property
def bisector_vector1(self):
return (self._p0 - self._p1) + (self._p2 - self._p1)
@property
def bisector_vector2(self):
return (self._p1 - self._p2) + (self._p0 - self._p2)
##############################################
@property
def bisector_line0(self):
return Line2D(self._p0, self.bisector_vector0)
@property
def bisector_line1(self):
return Line2D(self._p1, self.bisector_vector1)
@property
def bisector_line2(self):
return Line2D(self._p2, self.bisector_vector2)
##############################################
def _cache_length(self):
if not hasattr(self._p10):
self._p10 = (self._p1 - self._p0).magnitude
self._p21 = (self._p2 - self._p1).magnitude
self._p02 = (self._p0 - self._p2).magnitude
##############################################
def _cache_angle(self):
if not hasattr(self._a10):
self._a10 = (self._p1 - self._p0).orientation
self._a21 = (self._p2 - self._p1).orientation
self._a02 = (self._p0 - self._p2).orientation
##############################################
@property
def perimeter(self):
self._cache_length()
return self._p10 + self._p21 + self._p02
##############################################
@property
def area(self):
# using height
# = base * height / 2
# using edge length
# = \frac{1}{4} \sqrt{(a+b+c)(-a+b+c)(a-b+c)(a+b-c)} = \sqrt{p(p-a)(p-b)(p-c)}
# using sinus law
# = \frac{1}{2} a b \sin\gamma
# using coordinate
# = \frac{1}{2} \left\|{ \overrightarrow{AB} \wedge \overrightarrow{AC}}
# = \dfrac{1}{2} \big| x_A y_C - x_A y_B + x_B y_A - x_B y_C + x_C y_B - x_C y_A \big|
return .5 * math.fabs((self._p1 - self._p0).cross(self._p2 - self._p0))
##############################################
@property
def is_equilateral(self):
self._cache_length()
# all sides have the same length and angle = 60
return (self._p10 == self._p21 and
self._p21 == self._p02)
##############################################
@property
def is_scalene(self):
self._cache_length()
# all sides have different lengths
return (self._p10 != self._p21 and
self._p21 != self._p02 and
self._p02 != self._p10)
##############################################
@property
def is_isosceles(self):
self._cache_length()
# two sides of equal length
return not(self.is_equilateral) and not(self.is_scalene)
##############################################
@property
def is_right(self):
self._cache_angle()
# one angle = 90
raise NotImplementedError
##############################################
@property
def is_obtuse(self):
self._cache_angle()
# one angle > 90
return max(self._a10, self._a21, self._a02) > 90
##############################################
@property
def is_acute(self):
self._cache_angle()
# all angle < 90
return max(self._a10, self._a21, self._a02) < 90
##############################################
@property
def is_oblique(self):
return not self.is_equilateral
##############################################
@property
def orthocenter(self):
# intersection of the altitudes
raise NotImplementedError
##############################################
@property
def centroid(self):
# intersection of the medians
raise NotImplementedError
##############################################
@property
def circumcenter(self):
# intersection of the perpendiculars at middle
raise NotImplementedError
##############################################
@property
def in_circle(self):
# intersection of the bisectors
raise NotImplementedError # return circle
##############################################
def is_point_inside(self, point):
# Reference:
# http://mathworld.wolfram.com/TriangleInterior.html
return (
same_side(point, self._p0, self._p1, self._p2) and
same_side(point, self._p1, self._p0, self._p2) and
same_side(point, self._p2, self._p0, self._p1)
)