1 Star 0 Fork 0

KeyToon9/blender-addons

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
mesh_bsurfaces.py 200.26 KB
一键复制 编辑 原始数据 按行查看 历史
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554
# ##### BEGIN GPL LICENSE BLOCK #####
#
# 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 2
# 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
"name": "Bsurfaces GPL Edition",
"author": "Eclectiel, Vladimir Spivak (cwolf3d)",
"version": (1, 8, 0),
"blender": (2, 80, 0),
"location": "View3D EditMode > Sidebar > Edit Tab",
"description": "Modeling and retopology tool",
"doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/bsurfaces.html",
"category": "Mesh",
}
import bpy
import bmesh
from bpy_extras import object_utils
import operator
from mathutils import Matrix, Vector
from mathutils.geometry import (
intersect_line_line,
intersect_point_line,
)
from math import (
degrees,
pi,
sqrt,
)
from bpy.props import (
BoolProperty,
FloatProperty,
IntProperty,
StringProperty,
PointerProperty,
EnumProperty,
FloatVectorProperty,
)
from bpy.types import (
Operator,
Panel,
PropertyGroup,
AddonPreferences,
)
# ----------------------------
# GLOBAL
global_shade_smooth = False
global_mesh_object = ""
global_gpencil_object = ""
global_curve_object = ""
# ----------------------------
# Panels
class VIEW3D_PT_tools_SURFSK_mesh(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Edit'
bl_label = "Bsurfaces"
def draw(self, context):
layout = self.layout
bs = context.scene.bsurfaces
col = layout.column(align=True)
row = layout.row()
row.separator()
col.operator("mesh.surfsk_init", text="Initialize (Add BSurface mesh)")
col.operator("mesh.surfsk_add_modifiers", text="Add Mirror and others modifiers")
col.label(text="Mesh of BSurface:")
col.prop(bs, "SURFSK_mesh", text="")
if bs.SURFSK_mesh != None:
try: mesh_object = bs.SURFSK_mesh
except: pass
try: col.prop(mesh_object.data.materials[0], "diffuse_color")
except: pass
try: col.prop(mesh_object.modifiers['Shrinkwrap'], "offset")
except: pass
try: col.prop(mesh_object, "show_in_front")
except: pass
try: col.prop(bs, "SURFSK_shade_smooth")
except: pass
try: col.prop(mesh_object, "show_wire")
except: pass
col.label(text="Guide strokes:")
col.row().prop(bs, "SURFSK_guide", expand=True)
if bs.SURFSK_guide == 'GPencil':
col.prop(bs, "SURFSK_gpencil", text="")
col.separator()
if bs.SURFSK_guide == 'Curve':
col.prop(bs, "SURFSK_curve", text="")
col.separator()
col.separator()
col.operator("mesh.surfsk_add_surface", text="Add Surface")
col.operator("mesh.surfsk_edit_surface", text="Edit Surface")
col.separator()
if bs.SURFSK_guide == 'GPencil':
col.operator("gpencil.surfsk_add_strokes", text="Add Strokes")
col.operator("gpencil.surfsk_edit_strokes", text="Edit Strokes")
col.separator()
col.operator("gpencil.surfsk_strokes_to_curves", text="Strokes to curves")
if bs.SURFSK_guide == 'Annotation':
col.operator("gpencil.surfsk_add_annotation", text="Add Annotation")
col.separator()
col.operator("gpencil.surfsk_annotations_to_curves", text="Annotation to curves")
if bs.SURFSK_guide == 'Curve':
col.operator("curve.surfsk_edit_curve", text="Edit curve")
col.separator()
col.label(text="Initial settings:")
col.prop(bs, "SURFSK_edges_U")
col.prop(bs, "SURFSK_edges_V")
col.prop(bs, "SURFSK_cyclic_cross")
col.prop(bs, "SURFSK_cyclic_follow")
col.prop(bs, "SURFSK_loops_on_strokes")
col.prop(bs, "SURFSK_automatic_join")
col.prop(bs, "SURFSK_keep_strokes")
class VIEW3D_PT_tools_SURFSK_curve(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = "curve_edit"
bl_category = 'Edit'
bl_label = "Bsurfaces"
@classmethod
def poll(cls, context):
return context.active_object
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
row = layout.row()
row.separator()
col.operator("curve.surfsk_first_points", text="Set First Points")
col.operator("curve.switch_direction", text="Switch Direction")
col.operator("curve.surfsk_reorder_splines", text="Reorder Splines")
# ----------------------------
# Returns the type of strokes used
def get_strokes_type(context):
strokes_type = "NO_STROKES"
strokes_num = 0
# Check if they are annotation
if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
try:
strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
strokes_num = len(strokes)
if strokes_num > 0:
strokes_type = "GP_ANNOTATION"
except:
strokes_type = "NO_STROKES"
# Check if they are grease pencil
if context.scene.bsurfaces.SURFSK_guide == 'GPencil':
try:
global global_gpencil_object
gpencil = bpy.data.objects[global_gpencil_object]
strokes = gpencil.data.layers.active.active_frame.strokes
strokes_num = len(strokes)
if strokes_num > 0:
strokes_type = "GP_STROKES"
except:
strokes_type = "NO_STROKES"
# Check if they are curves, if there aren't grease pencil strokes
if context.scene.bsurfaces.SURFSK_guide == 'Curve':
try:
global global_curve_object
ob = bpy.data.objects[global_curve_object]
if ob.type == "CURVE":
strokes_type = "EXTERNAL_CURVE"
strokes_num = len(ob.data.splines)
# Check if there is any non-bezier spline
for i in range(len(ob.data.splines)):
if ob.data.splines[i].type != "BEZIER":
strokes_type = "CURVE_WITH_NON_BEZIER_SPLINES"
break
else:
strokes_type = "EXTERNAL_NO_CURVE"
except:
strokes_type = "NO_STROKES"
# Check if they are mesh
try:
global global_mesh_object
self.main_object = bpy.data.objects[global_mesh_object]
total_vert_sel = len([v for v in self.main_object.data.vertices if v.select])
# Check if there is a single stroke without any selection in the object
if strokes_num == 1 and total_vert_sel == 0:
if strokes_type == "EXTERNAL_CURVE":
strokes_type = "SINGLE_CURVE_STROKE_NO_SELECTION"
elif strokes_type == "GP_STROKES":
strokes_type = "SINGLE_GP_STROKE_NO_SELECTION"
if strokes_num == 0 and total_vert_sel > 0:
strokes_type = "SELECTION_ALONE"
except:
pass
return strokes_type
# ----------------------------
# Surface generator operator
class MESH_OT_SURFSK_add_surface(Operator):
bl_idname = "mesh.surfsk_add_surface"
bl_label = "Bsurfaces add surface"
bl_description = "Generates surfaces from grease pencil strokes, bezier curves or loose edges"
bl_options = {'REGISTER', 'UNDO'}
is_crosshatch: BoolProperty(
default=False
)
is_fill_faces: BoolProperty(
default=False
)
selection_U_exists: BoolProperty(
default=False
)
selection_V_exists: BoolProperty(
default=False
)
selection_U2_exists: BoolProperty(
default=False
)
selection_V2_exists: BoolProperty(
default=False
)
selection_V_is_closed: BoolProperty(
default=False
)
selection_U_is_closed: BoolProperty(
default=False
)
selection_V2_is_closed: BoolProperty(
default=False
)
selection_U2_is_closed: BoolProperty(
default=False
)
edges_U: IntProperty(
name="Cross",
description="Number of face-loops crossing the strokes",
default=1,
min=1,
max=200
)
edges_V: IntProperty(
name="Follow",
description="Number of face-loops following the strokes",
default=1,
min=1,
max=200
)
cyclic_cross: BoolProperty(
name="Cyclic Cross",
description="Make cyclic the face-loops crossing the strokes",
default=False
)
cyclic_follow: BoolProperty(
name="Cyclic Follow",
description="Make cyclic the face-loops following the strokes",
default=False
)
loops_on_strokes: BoolProperty(
name="Loops on strokes",
description="Make the loops match the paths of the strokes",
default=False
)
automatic_join: BoolProperty(
name="Automatic join",
description="Join automatically vertices of either surfaces generated "
"by crosshatching, or from the borders of closed shapes",
default=False
)
join_stretch_factor: FloatProperty(
name="Stretch",
description="Amount of stretching or shrinking allowed for "
"edges when joining vertices automatically",
default=1,
min=0,
max=3,
subtype='FACTOR'
)
keep_strokes: BoolProperty(
name="Keep strokes",
description="Keeps the sketched strokes or curves after adding the surface",
default=False
)
strokes_type: StringProperty()
initial_global_undo_state: BoolProperty()
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
row = layout.row()
if not self.is_fill_faces:
row.separator()
if not self.is_crosshatch:
if not self.selection_U_exists:
col.prop(self, "edges_U")
row.separator()
if not self.selection_V_exists:
col.prop(self, "edges_V")
row.separator()
row.separator()
if not self.selection_U_exists:
if not (
(self.selection_V_exists and not self.selection_V_is_closed) or
(self.selection_V2_exists and not self.selection_V2_is_closed)
):
col.prop(self, "cyclic_cross")
if not self.selection_V_exists:
if not (
(self.selection_U_exists and not self.selection_U_is_closed) or
(self.selection_U2_exists and not self.selection_U2_is_closed)
):
col.prop(self, "cyclic_follow")
col.prop(self, "loops_on_strokes")
col.prop(self, "automatic_join")
if self.automatic_join:
row.separator()
col.separator()
row.separator()
col.prop(self, "join_stretch_factor")
col.prop(self, "keep_strokes")
# Get an ordered list of a chain of vertices
def get_ordered_verts(self, ob, all_selected_edges_idx, all_selected_verts_idx,
first_vert_idx, middle_vertex_idx, closing_vert_idx):
# Order selected vertices.
verts_ordered = []
if closing_vert_idx is not None:
verts_ordered.append(ob.data.vertices[closing_vert_idx])
verts_ordered.append(ob.data.vertices[first_vert_idx])
prev_v = first_vert_idx
prev_ed = None
finish_while = False
while True:
edges_non_matched = 0
for i in all_selected_edges_idx:
if ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[0] == prev_v and \
ob.data.edges[i].vertices[1] in all_selected_verts_idx:
verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[1]])
prev_v = ob.data.edges[i].vertices[1]
prev_ed = ob.data.edges[i]
elif ob.data.edges[i] != prev_ed and ob.data.edges[i].vertices[1] == prev_v and \
ob.data.edges[i].vertices[0] in all_selected_verts_idx:
verts_ordered.append(ob.data.vertices[ob.data.edges[i].vertices[0]])
prev_v = ob.data.edges[i].vertices[0]
prev_ed = ob.data.edges[i]
else:
edges_non_matched += 1
if edges_non_matched == len(all_selected_edges_idx):
finish_while = True
if finish_while:
break
if closing_vert_idx is not None:
verts_ordered.append(ob.data.vertices[closing_vert_idx])
if middle_vertex_idx is not None:
verts_ordered.append(ob.data.vertices[middle_vertex_idx])
verts_ordered.reverse()
return tuple(verts_ordered)
# Calculates length of a chain of points.
def get_chain_length(self, object, verts_ordered):
matrix = object.matrix_world
edges_lengths = []
edges_lengths_sum = 0
for i in range(0, len(verts_ordered)):
if i == 0:
prev_v_co = matrix @ verts_ordered[i].co
else:
v_co = matrix @ verts_ordered[i].co
v_difs = [prev_v_co[0] - v_co[0], prev_v_co[1] - v_co[1], prev_v_co[2] - v_co[2]]
edge_length = abs(sqrt(v_difs[0] * v_difs[0] + v_difs[1] * v_difs[1] + v_difs[2] * v_difs[2]))
edges_lengths.append(edge_length)
edges_lengths_sum += edge_length
prev_v_co = v_co
return edges_lengths, edges_lengths_sum
# Calculates the proportion of the edges of a chain of edges, relative to the full chain length.
def get_edges_proportions(self, edges_lengths, edges_lengths_sum, use_boundaries, fixed_edges_num):
edges_proportions = []
if use_boundaries:
verts_count = 1
for l in edges_lengths:
edges_proportions.append(l / edges_lengths_sum)
verts_count += 1
else:
verts_count = 1
for _n in range(0, fixed_edges_num):
edges_proportions.append(1 / fixed_edges_num)
verts_count += 1
return edges_proportions
# Calculates the angle between two pairs of points in space
def orientation_difference(self, points_A_co, points_B_co):
# each parameter should be a list with two elements,
# and each element should be a x,y,z coordinate
vec_A = points_A_co[0] - points_A_co[1]
vec_B = points_B_co[0] - points_B_co[1]
angle = vec_A.angle(vec_B)
if angle > 0.5 * pi:
angle = abs(angle - pi)
return angle
# Calculate the which vert of verts_idx list is the nearest one
# to the point_co coordinates, and the distance
def shortest_distance(self, object, point_co, verts_idx):
matrix = object.matrix_world
for i in range(0, len(verts_idx)):
dist = (point_co - matrix @ object.data.vertices[verts_idx[i]].co).length
if i == 0:
prev_dist = dist
nearest_vert_idx = verts_idx[i]
shortest_dist = dist
if dist < prev_dist:
prev_dist = dist
nearest_vert_idx = verts_idx[i]
shortest_dist = dist
return nearest_vert_idx, shortest_dist
# Returns the index of the opposite vert tip in a chain, given a vert tip index
# as parameter, and a multidimentional list with all pairs of tips
def opposite_tip(self, vert_tip_idx, all_chains_tips_idx):
opposite_vert_tip_idx = None
for i in range(0, len(all_chains_tips_idx)):
if vert_tip_idx == all_chains_tips_idx[i][0]:
opposite_vert_tip_idx = all_chains_tips_idx[i][1]
if vert_tip_idx == all_chains_tips_idx[i][1]:
opposite_vert_tip_idx = all_chains_tips_idx[i][0]
return opposite_vert_tip_idx
# Simplifies a spline and returns the new points coordinates
def simplify_spline(self, spline_coords, segments_num):
simplified_spline = []
points_between_segments = round(len(spline_coords) / segments_num)
simplified_spline.append(spline_coords[0])
for i in range(1, segments_num):
simplified_spline.append(spline_coords[i * points_between_segments])
simplified_spline.append(spline_coords[len(spline_coords) - 1])
return simplified_spline
# Returns a list with the coords of the points distributed over the splines
# passed to this method according to the proportions parameter
def distribute_pts(self, surface_splines, proportions):
# Calculate the length of each final surface spline
surface_splines_lengths = []
surface_splines_parsed = []
for sp_idx in range(0, len(surface_splines)):
# Calculate spline length
surface_splines_lengths.append(0)
for i in range(0, len(surface_splines[sp_idx].bezier_points)):
if i == 0:
prev_p = surface_splines[sp_idx].bezier_points[i]
else:
p = surface_splines[sp_idx].bezier_points[i]
edge_length = (prev_p.co - p.co).length
surface_splines_lengths[sp_idx] += edge_length
prev_p = p
# Calculate vertex positions with appropriate edge proportions, and ordered, for each spline
for sp_idx in range(0, len(surface_splines)):
surface_splines_parsed.append([])
surface_splines_parsed[sp_idx].append(surface_splines[sp_idx].bezier_points[0].co)
prev_p_co = surface_splines[sp_idx].bezier_points[0].co
p_idx = 0
for prop_idx in range(len(proportions) - 1):
target_length = surface_splines_lengths[sp_idx] * proportions[prop_idx]
partial_segment_length = 0
finish_while = False
while True:
# if not it'll pass the p_idx as an index below and crash
if p_idx < len(surface_splines[sp_idx].bezier_points):
p_co = surface_splines[sp_idx].bezier_points[p_idx].co
new_dist = (prev_p_co - p_co).length
# The new distance that could have the partial segment if
# it is still shorter than the target length
potential_segment_length = partial_segment_length + new_dist
# If the potential is still shorter, keep adding
if potential_segment_length < target_length:
partial_segment_length = potential_segment_length
p_idx += 1
prev_p_co = p_co
# If the potential is longer than the target, calculate the target
# (a point between the last two points), and assign
elif potential_segment_length > target_length:
remaining_dist = target_length - partial_segment_length
vec = p_co - prev_p_co
vec.normalize()
intermediate_co = prev_p_co + (vec * remaining_dist)
surface_splines_parsed[sp_idx].append(intermediate_co)
partial_segment_length += remaining_dist
prev_p_co = intermediate_co
finish_while = True
# If the potential is equal to the target, assign
elif potential_segment_length == target_length:
surface_splines_parsed[sp_idx].append(p_co)
prev_p_co = p_co
finish_while = True
if finish_while:
break
# last point of the spline
surface_splines_parsed[sp_idx].append(
surface_splines[sp_idx].bezier_points[len(surface_splines[sp_idx].bezier_points) - 1].co
)
return surface_splines_parsed
# Counts the number of faces that belong to each edge
def edge_face_count(self, ob):
ed_keys_count_dict = {}
for face in ob.data.polygons:
for ed_keys in face.edge_keys:
if ed_keys not in ed_keys_count_dict:
ed_keys_count_dict[ed_keys] = 1
else:
ed_keys_count_dict[ed_keys] += 1
edge_face_count = []
for i in range(len(ob.data.edges)):
edge_face_count.append(0)
for i in range(len(ob.data.edges)):
ed = ob.data.edges[i]
v1 = ed.vertices[0]
v2 = ed.vertices[1]
if (v1, v2) in ed_keys_count_dict:
edge_face_count[i] = ed_keys_count_dict[(v1, v2)]
elif (v2, v1) in ed_keys_count_dict:
edge_face_count[i] = ed_keys_count_dict[(v2, v1)]
return edge_face_count
# Fills with faces all the selected vertices which form empty triangles or quads
def fill_with_faces(self, object):
all_selected_verts_count = self.main_object_selected_verts_count
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
# Calculate average length of selected edges
all_selected_verts = []
original_sel_edges_count = 0
for ed in object.data.edges:
if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
coords = []
coords.append(object.data.vertices[ed.vertices[0]].co)
coords.append(object.data.vertices[ed.vertices[1]].co)
original_sel_edges_count += 1
if not ed.vertices[0] in all_selected_verts:
all_selected_verts.append(ed.vertices[0])
if not ed.vertices[1] in all_selected_verts:
all_selected_verts.append(ed.vertices[1])
tuple(all_selected_verts)
# Check if there is any edge selected. If not, interrupt the script
if original_sel_edges_count == 0 and all_selected_verts_count > 0:
return 0
# Get all edges connected to selected verts
all_edges_around_sel_verts = []
edges_connected_to_sel_verts = {}
verts_connected_to_every_vert = {}
for ed_idx in range(len(object.data.edges)):
ed = object.data.edges[ed_idx]
include_edge = False
if ed.vertices[0] in all_selected_verts:
if not ed.vertices[0] in edges_connected_to_sel_verts:
edges_connected_to_sel_verts[ed.vertices[0]] = []
edges_connected_to_sel_verts[ed.vertices[0]].append(ed_idx)
include_edge = True
if ed.vertices[1] in all_selected_verts:
if not ed.vertices[1] in edges_connected_to_sel_verts:
edges_connected_to_sel_verts[ed.vertices[1]] = []
edges_connected_to_sel_verts[ed.vertices[1]].append(ed_idx)
include_edge = True
if include_edge is True:
all_edges_around_sel_verts.append(ed_idx)
# Get all connected verts to each vert
if not ed.vertices[0] in verts_connected_to_every_vert:
verts_connected_to_every_vert[ed.vertices[0]] = []
if not ed.vertices[1] in verts_connected_to_every_vert:
verts_connected_to_every_vert[ed.vertices[1]] = []
verts_connected_to_every_vert[ed.vertices[0]].append(ed.vertices[1])
verts_connected_to_every_vert[ed.vertices[1]].append(ed.vertices[0])
# Get all verts connected to faces
all_verts_part_of_faces = []
all_edges_faces_count = []
all_edges_faces_count += self.edge_face_count(object)
# Get only the selected edges that have faces attached.
count_faces_of_edges_around_sel_verts = {}
selected_verts_with_faces = []
for ed_idx in all_edges_around_sel_verts:
count_faces_of_edges_around_sel_verts[ed_idx] = all_edges_faces_count[ed_idx]
if all_edges_faces_count[ed_idx] > 0:
ed = object.data.edges[ed_idx]
if not ed.vertices[0] in selected_verts_with_faces:
selected_verts_with_faces.append(ed.vertices[0])
if not ed.vertices[1] in selected_verts_with_faces:
selected_verts_with_faces.append(ed.vertices[1])
all_verts_part_of_faces.append(ed.vertices[0])
all_verts_part_of_faces.append(ed.vertices[1])
tuple(selected_verts_with_faces)
# Discard unneeded verts from calculations
participating_verts = []
movable_verts = []
for v_idx in all_selected_verts:
vert_has_edges_with_one_face = False
# Check if the actual vert has at least one edge connected to only one face
for ed_idx in edges_connected_to_sel_verts[v_idx]:
if count_faces_of_edges_around_sel_verts[ed_idx] == 1:
vert_has_edges_with_one_face = True
# If the vert has two or less edges connected and the vert is not part of any face.
# Or the vert is part of any face and at least one of
# the connected edges has only one face attached to it.
if (len(edges_connected_to_sel_verts[v_idx]) == 2 and
v_idx not in all_verts_part_of_faces) or \
len(edges_connected_to_sel_verts[v_idx]) == 1 or \
(v_idx in all_verts_part_of_faces and
vert_has_edges_with_one_face):
participating_verts.append(v_idx)
if v_idx not in all_verts_part_of_faces:
movable_verts.append(v_idx)
# Remove from movable verts list those that are part of closed geometry (ie: triangles, quads)
for mv_idx in movable_verts:
freeze_vert = False
mv_connected_verts = verts_connected_to_every_vert[mv_idx]
for actual_v_idx in all_selected_verts:
count_shared_neighbors = 0
checked_verts = []
for mv_conn_v_idx in mv_connected_verts:
if mv_idx != actual_v_idx:
if mv_conn_v_idx in verts_connected_to_every_vert[actual_v_idx] and \
mv_conn_v_idx not in checked_verts:
count_shared_neighbors += 1
checked_verts.append(mv_conn_v_idx)
if actual_v_idx in mv_connected_verts:
freeze_vert = True
break
if count_shared_neighbors == 2:
freeze_vert = True
break
if freeze_vert:
break
if freeze_vert:
movable_verts.remove(mv_idx)
# Calculate merge distance for participating verts
shortest_edge_length = None
for ed in object.data.edges:
if ed.vertices[0] in movable_verts and ed.vertices[1] in movable_verts:
v1 = object.data.vertices[ed.vertices[0]]
v2 = object.data.vertices[ed.vertices[1]]
length = (v1.co - v2.co).length
if shortest_edge_length is None:
shortest_edge_length = length
else:
if length < shortest_edge_length:
shortest_edge_length = length
if shortest_edge_length is not None:
edges_merge_distance = shortest_edge_length * 0.5
else:
edges_merge_distance = 0
# Get together the verts near enough. They will be merged later
remaining_verts = []
remaining_verts += participating_verts
for v1_idx in participating_verts:
if v1_idx in remaining_verts and v1_idx in movable_verts:
verts_to_merge = []
coords_verts_to_merge = {}
verts_to_merge.append(v1_idx)
v1_co = object.data.vertices[v1_idx].co
coords_verts_to_merge[v1_idx] = (v1_co[0], v1_co[1], v1_co[2])
for v2_idx in remaining_verts:
if v1_idx != v2_idx:
v2_co = object.data.vertices[v2_idx].co
dist = (v1_co - v2_co).length
if dist <= edges_merge_distance: # Add the verts which are near enough
verts_to_merge.append(v2_idx)
coords_verts_to_merge[v2_idx] = (v2_co[0], v2_co[1], v2_co[2])
for vm_idx in verts_to_merge:
remaining_verts.remove(vm_idx)
if len(verts_to_merge) > 1:
# Calculate middle point of the verts to merge.
sum_x_co = 0
sum_y_co = 0
sum_z_co = 0
movable_verts_to_merge_count = 0
for i in range(len(verts_to_merge)):
if verts_to_merge[i] in movable_verts:
v_co = object.data.vertices[verts_to_merge[i]].co
sum_x_co += v_co[0]
sum_y_co += v_co[1]
sum_z_co += v_co[2]
movable_verts_to_merge_count += 1
middle_point_co = [
sum_x_co / movable_verts_to_merge_count,
sum_y_co / movable_verts_to_merge_count,
sum_z_co / movable_verts_to_merge_count
]
# Check if any vert to be merged is not movable
shortest_dist = None
are_verts_not_movable = False
verts_not_movable = []
for v_merge_idx in verts_to_merge:
if v_merge_idx in participating_verts and v_merge_idx not in movable_verts:
are_verts_not_movable = True
verts_not_movable.append(v_merge_idx)
if are_verts_not_movable:
# Get the vert connected to faces, that is nearest to
# the middle point of the movable verts
shortest_dist = None
for vcf_idx in verts_not_movable:
dist = abs((object.data.vertices[vcf_idx].co -
Vector(middle_point_co)).length)
if shortest_dist is None:
shortest_dist = dist
nearest_vert_idx = vcf_idx
else:
if dist < shortest_dist:
shortest_dist = dist
nearest_vert_idx = vcf_idx
coords = object.data.vertices[nearest_vert_idx].co
target_point_co = [coords[0], coords[1], coords[2]]
else:
target_point_co = middle_point_co
# Move verts to merge to the middle position
for v_merge_idx in verts_to_merge:
if v_merge_idx in movable_verts: # Only move the verts that are not part of faces
object.data.vertices[v_merge_idx].co[0] = target_point_co[0]
object.data.vertices[v_merge_idx].co[1] = target_point_co[1]
object.data.vertices[v_merge_idx].co[2] = target_point_co[2]
# Perform "Remove Doubles" to weld all the disconnected verts
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
bpy.ops.mesh.remove_doubles(threshold=0.0001)
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
# Get all the definitive selected edges, after weldding
selected_edges = []
edges_per_vert = {} # Number of faces of each selected edge
for ed in object.data.edges:
if object.data.vertices[ed.vertices[0]].select and object.data.vertices[ed.vertices[1]].select:
selected_edges.append(ed.index)
# Save all the edges that belong to each vertex.
if not ed.vertices[0] in edges_per_vert:
edges_per_vert[ed.vertices[0]] = []
if not ed.vertices[1] in edges_per_vert:
edges_per_vert[ed.vertices[1]] = []
edges_per_vert[ed.vertices[0]].append(ed.index)
edges_per_vert[ed.vertices[1]].append(ed.index)
# Check if all the edges connected to each vert have two faces attached to them.
# To discard them later and make calculations faster
a = []
a += self.edge_face_count(object)
tuple(a)
verts_surrounded_by_faces = {}
for v_idx in edges_per_vert:
edges_with_two_faces_count = 0
for ed_idx in edges_per_vert[v_idx]:
if a[ed_idx] == 2:
edges_with_two_faces_count += 1
if edges_with_two_faces_count == len(edges_per_vert[v_idx]):
verts_surrounded_by_faces[v_idx] = True
else:
verts_surrounded_by_faces[v_idx] = False
# Get all the selected vertices
selected_verts_idx = []
for v in object.data.vertices:
if v.select:
selected_verts_idx.append(v.index)
# Get all the faces of the object
all_object_faces_verts_idx = []
for face in object.data.polygons:
face_verts = []
face_verts.append(face.vertices[0])
face_verts.append(face.vertices[1])
face_verts.append(face.vertices[2])
if len(face.vertices) == 4:
face_verts.append(face.vertices[3])
all_object_faces_verts_idx.append(face_verts)
# Deselect all vertices
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
# Make a dictionary with the verts related to each vert
related_key_verts = {}
for ed_idx in selected_edges:
ed = object.data.edges[ed_idx]
if not verts_surrounded_by_faces[ed.vertices[0]]:
if not ed.vertices[0] in related_key_verts:
related_key_verts[ed.vertices[0]] = []
if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
related_key_verts[ed.vertices[0]].append(ed.vertices[1])
if not verts_surrounded_by_faces[ed.vertices[1]]:
if not ed.vertices[1] in related_key_verts:
related_key_verts[ed.vertices[1]] = []
if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
related_key_verts[ed.vertices[1]].append(ed.vertices[0])
# Get groups of verts forming each face
faces_verts_idx = []
for v1 in related_key_verts: # verts-1 ....
for v2 in related_key_verts: # verts-2
if v1 != v2:
related_verts_in_common = []
v2_in_rel_v1 = False
v1_in_rel_v2 = False
for rel_v1 in related_key_verts[v1]:
# Check if related verts of verts-1 are related verts of verts-2
if rel_v1 in related_key_verts[v2]:
related_verts_in_common.append(rel_v1)
if v2 in related_key_verts[v1]:
v2_in_rel_v1 = True
if v1 in related_key_verts[v2]:
v1_in_rel_v2 = True
repeated_face = False
# If two verts have two related verts in common, they form a quad
if len(related_verts_in_common) == 2:
# Check if the face is already saved
all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
for f_verts in all_faces_to_check_idx:
repeated_verts = 0
if len(f_verts) == 4:
if v1 in f_verts:
repeated_verts += 1
if v2 in f_verts:
repeated_verts += 1
if related_verts_in_common[0] in f_verts:
repeated_verts += 1
if related_verts_in_common[1] in f_verts:
repeated_verts += 1
if repeated_verts == len(f_verts):
repeated_face = True
break
if not repeated_face:
faces_verts_idx.append(
[v1, related_verts_in_common[0], v2, related_verts_in_common[1]]
)
# If Two verts have one related vert in common and
# they are related to each other, they form a triangle
elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
# Check if the face is already saved.
all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
for f_verts in all_faces_to_check_idx:
repeated_verts = 0
if len(f_verts) == 3:
if v1 in f_verts:
repeated_verts += 1
if v2 in f_verts:
repeated_verts += 1
if related_verts_in_common[0] in f_verts:
repeated_verts += 1
if repeated_verts == len(f_verts):
repeated_face = True
break
if not repeated_face:
faces_verts_idx.append([v1, related_verts_in_common[0], v2])
# Keep only the faces that don't overlap by ignoring quads
# that overlap with two adjacent triangles
faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
all_faces_to_check_idx = faces_verts_idx + all_object_faces_verts_idx
for i in range(len(faces_verts_idx)):
for t in range(len(all_faces_to_check_idx)):
if i != t:
verts_in_common = 0
if len(faces_verts_idx[i]) == 4 and len(all_faces_to_check_idx[t]) == 3:
for v_idx in all_faces_to_check_idx[t]:
if v_idx in faces_verts_idx[i]:
verts_in_common += 1
# If it doesn't have all it's vertices repeated in the other face
if verts_in_common == 3:
if i not in faces_to_not_include_idx:
faces_to_not_include_idx.append(i)
# Build faces discarding the ones in faces_to_not_include
me = object.data
bm = bmesh.new()
bm.from_mesh(me)
num_faces_created = 0
for i in range(len(faces_verts_idx)):
if i not in faces_to_not_include_idx:
bm.faces.new([bm.verts[v] for v in faces_verts_idx[i]])
num_faces_created += 1
bm.to_mesh(me)
bm.free()
for v_idx in selected_verts_idx:
self.main_object.data.vertices[v_idx].select = True
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
self.update()
return num_faces_created
# Crosshatch skinning
def crosshatch_surface_invoke(self, ob_original_splines):
self.is_crosshatch = False
self.crosshatch_merge_distance = 0
objects_to_delete = [] # duplicated strokes to be deleted.
# If the main object uses modifiers deactivate them temporarily until the surface is joined
# (without this the surface verts merging with the main object doesn't work well)
self.modifiers_prev_viewport_state = []
if len(self.main_object.modifiers) > 0:
for m_idx in range(len(self.main_object.modifiers)):
self.modifiers_prev_viewport_state.append(
self.main_object.modifiers[m_idx].show_viewport
)
self.main_object.modifiers[m_idx].show_viewport = False
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
ob_original_splines.select_set(True)
bpy.context.view_layer.objects.active = ob_original_splines
if len(ob_original_splines.data.splines) >= 2:
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
ob_splines = bpy.context.object
ob_splines.name = "SURFSKIO_NE_STR"
# Get estimative merge distance (sum up the distances from the first point to
# all other points, then average them and then divide them)
first_point_dist_sum = 0
first_dist = 0
second_dist = 0
coords_first_pt = ob_splines.data.splines[0].bezier_points[0].co
for i in range(len(ob_splines.data.splines)):
sp = ob_splines.data.splines[i]
if coords_first_pt != sp.bezier_points[0].co:
first_dist = (coords_first_pt - sp.bezier_points[0].co).length
if coords_first_pt != sp.bezier_points[len(sp.bezier_points) - 1].co:
second_dist = (coords_first_pt - sp.bezier_points[len(sp.bezier_points) - 1].co).length
first_point_dist_sum += first_dist + second_dist
if i == 0:
if first_dist != 0:
shortest_dist = first_dist
elif second_dist != 0:
shortest_dist = second_dist
if shortest_dist > first_dist and first_dist != 0:
shortest_dist = first_dist
if shortest_dist > second_dist and second_dist != 0:
shortest_dist = second_dist
self.crosshatch_merge_distance = shortest_dist / 20
# Recalculation of merge distance
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
ob_calc_merge_dist = bpy.context.object
ob_calc_merge_dist.name = "SURFSKIO_CALC_TMP"
objects_to_delete.append(ob_calc_merge_dist)
# Smooth out strokes a little to improve crosshatch detection
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
for i in range(4):
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Convert curves into mesh
ob_calc_merge_dist.data.resolution_u = 12
bpy.ops.object.convert(target='MESH', keep_original=False)
# Find "intersection-nodes"
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
threshold=self.crosshatch_merge_distance)
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Remove verts with less than three edges
verts_edges_count = {}
for ed in ob_calc_merge_dist.data.edges:
v = ed.vertices
if v[0] not in verts_edges_count:
verts_edges_count[v[0]] = 0
if v[1] not in verts_edges_count:
verts_edges_count[v[1]] = 0
verts_edges_count[v[0]] += 1
verts_edges_count[v[1]] += 1
nodes_verts_coords = []
for v_idx in verts_edges_count:
v = ob_calc_merge_dist.data.vertices[v_idx]
if verts_edges_count[v_idx] < 3:
v.select = True
# Remove them
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
# Remove doubles to discard very near verts from calculations of distance
bpy.ops.mesh.remove_doubles(
'INVOKE_REGION_WIN',
threshold=self.crosshatch_merge_distance * 4.0
)
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Get all coords of the resulting nodes
nodes_verts_coords = [(v.co[0], v.co[1], v.co[2]) for
v in ob_calc_merge_dist.data.vertices]
# Check if the strokes are a crosshatch
if len(nodes_verts_coords) >= 3:
self.is_crosshatch = True
shortest_dist = None
for co_1 in nodes_verts_coords:
for co_2 in nodes_verts_coords:
if co_1 != co_2:
dist = (Vector(co_1) - Vector(co_2)).length
if shortest_dist is not None:
if dist < shortest_dist:
shortest_dist = dist
else:
shortest_dist = dist
self.crosshatch_merge_distance = shortest_dist / 3
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
ob_splines.select_set(True)
bpy.context.view_layer.objects.active = ob_splines
# Deselect all points
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Smooth splines in a localized way, to eliminate "saw-teeth"
# like shapes when there are many points
for sp in ob_splines.data.splines:
angle_sum = 0
angle_limit = 2 # Degrees
for t in range(len(sp.bezier_points)):
# Because on each iteration it checks the "next two points"
# of the actual. This way it doesn't go out of range
if t <= len(sp.bezier_points) - 3:
p1 = sp.bezier_points[t]
p2 = sp.bezier_points[t + 1]
p3 = sp.bezier_points[t + 2]
vec_1 = p1.co - p2.co
vec_2 = p2.co - p3.co
if p2.co != p1.co and p2.co != p3.co:
angle = vec_1.angle(vec_2)
angle_sum += degrees(angle)
if angle_sum >= angle_limit: # If sum of angles is grater than the limit
if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
p1.select_control_point = True
p1.select_left_handle = True
p1.select_right_handle = True
p2.select_control_point = True
p2.select_left_handle = True
p2.select_right_handle = True
if (p1.co - p2.co).length <= self.crosshatch_merge_distance:
p3.select_control_point = True
p3.select_left_handle = True
p3.select_right_handle = True
angle_sum = 0
sp.bezier_points[0].select_control_point = False
sp.bezier_points[0].select_left_handle = False
sp.bezier_points[0].select_right_handle = False
sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = False
sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = False
sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = False
# Smooth out strokes a little to improve crosshatch detection
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
for i in range(15):
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Simplify the splines
for sp in ob_splines.data.splines:
angle_sum = 0
sp.bezier_points[0].select_control_point = True
sp.bezier_points[0].select_left_handle = True
sp.bezier_points[0].select_right_handle = True
sp.bezier_points[len(sp.bezier_points) - 1].select_control_point = True
sp.bezier_points[len(sp.bezier_points) - 1].select_left_handle = True
sp.bezier_points[len(sp.bezier_points) - 1].select_right_handle = True
angle_limit = 15 # Degrees
for t in range(len(sp.bezier_points)):
# Because on each iteration it checks the "next two points"
# of the actual. This way it doesn't go out of range
if t <= len(sp.bezier_points) - 3:
p1 = sp.bezier_points[t]
p2 = sp.bezier_points[t + 1]
p3 = sp.bezier_points[t + 2]
vec_1 = p1.co - p2.co
vec_2 = p2.co - p3.co
if p2.co != p1.co and p2.co != p3.co:
angle = vec_1.angle(vec_2)
angle_sum += degrees(angle)
# If sum of angles is grater than the limit
if angle_sum >= angle_limit:
p1.select_control_point = True
p1.select_left_handle = True
p1.select_right_handle = True
p2.select_control_point = True
p2.select_left_handle = True
p2.select_right_handle = True
p3.select_control_point = True
p3.select_left_handle = True
p3.select_right_handle = True
angle_sum = 0
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all(action='INVERT')
bpy.ops.curve.delete(type='VERT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
objects_to_delete.append(ob_splines)
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Check if the strokes are a crosshatch
if self.is_crosshatch:
all_points_coords = []
for i in range(len(ob_splines.data.splines)):
all_points_coords.append([])
all_points_coords[i] = [Vector((x, y, z)) for
x, y, z in [bp.co for
bp in ob_splines.data.splines[i].bezier_points]]
all_intersections = []
checked_splines = []
for i in range(len(all_points_coords)):
for t in range(len(all_points_coords[i]) - 1):
bp1_co = all_points_coords[i][t]
bp2_co = all_points_coords[i][t + 1]
for i2 in range(len(all_points_coords)):
if i != i2 and i2 not in checked_splines:
for t2 in range(len(all_points_coords[i2]) - 1):
bp3_co = all_points_coords[i2][t2]
bp4_co = all_points_coords[i2][t2 + 1]
intersec_coords = intersect_line_line(
bp1_co, bp2_co, bp3_co, bp4_co
)
if intersec_coords is not None:
dist = (intersec_coords[0] - intersec_coords[1]).length
if dist <= self.crosshatch_merge_distance * 1.5:
_temp_co, percent1 = intersect_point_line(
intersec_coords[0], bp1_co, bp2_co
)
if (percent1 >= -0.02 and percent1 <= 1.02):
_temp_co, percent2 = intersect_point_line(
intersec_coords[1], bp3_co, bp4_co
)
if (percent2 >= -0.02 and percent2 <= 1.02):
# Format: spline index, first point index from
# corresponding segment, percentage from first point of
# actual segment, coords of intersection point
all_intersections.append(
(i, t, percent1,
ob_splines.matrix_world @ intersec_coords[0])
)
all_intersections.append(
(i2, t2, percent2,
ob_splines.matrix_world @ intersec_coords[1])
)
checked_splines.append(i)
# Sort list by spline, then by corresponding first point index of segment,
# and then by percentage from first point of segment: elements 0 and 1 respectively
all_intersections.sort(key=operator.itemgetter(0, 1, 2))
self.crosshatch_strokes_coords = {}
for i in range(len(all_intersections)):
if not all_intersections[i][0] in self.crosshatch_strokes_coords:
self.crosshatch_strokes_coords[all_intersections[i][0]] = []
self.crosshatch_strokes_coords[all_intersections[i][0]].append(
all_intersections[i][3]
) # Save intersection coords
else:
self.is_crosshatch = False
# Delete all duplicates
bpy.ops.object.delete({"selected_objects": objects_to_delete})
# If the main object has modifiers, turn their "viewport view status" to
# what it was before the forced deactivation above
if len(self.main_object.modifiers) > 0:
for m_idx in range(len(self.main_object.modifiers)):
self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
self.update()
return
# Part of the Crosshatch process that is repeated when the operator is tweaked
def crosshatch_surface_execute(self, context):
# If the main object uses modifiers deactivate them temporarily until the surface is joined
# (without this the surface verts merging with the main object doesn't work well)
self.modifiers_prev_viewport_state = []
if len(self.main_object.modifiers) > 0:
for m_idx in range(len(self.main_object.modifiers)):
self.modifiers_prev_viewport_state.append(self.main_object.modifiers[m_idx].show_viewport)
self.main_object.modifiers[m_idx].show_viewport = False
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
me_name = "SURFSKIO_STK_TMP"
me = bpy.data.meshes.new(me_name)
all_verts_coords = []
all_edges = []
for st_idx in self.crosshatch_strokes_coords:
for co_idx in range(len(self.crosshatch_strokes_coords[st_idx])):
coords = self.crosshatch_strokes_coords[st_idx][co_idx]
all_verts_coords.append(coords)
if co_idx > 0:
all_edges.append((len(all_verts_coords) - 2, len(all_verts_coords) - 1))
me.from_pydata(all_verts_coords, all_edges, [])
ob = object_utils.object_data_add(context, me)
ob.location = (0.0, 0.0, 0.0)
ob.rotation_euler = (0.0, 0.0, 0.0)
ob.scale = (1.0, 1.0, 1.0)
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
ob.select_set(True)
bpy.context.view_layer.objects.active = ob
# Get together each vert and its nearest, to the middle position
verts = ob.data.vertices
checked_verts = []
for i in range(len(verts)):
shortest_dist = None
if i not in checked_verts:
for t in range(len(verts)):
if i != t and t not in checked_verts:
dist = (verts[i].co - verts[t].co).length
if shortest_dist is not None:
if dist < shortest_dist:
shortest_dist = dist
nearest_vert = t
else:
shortest_dist = dist
nearest_vert = t
middle_location = (verts[i].co + verts[nearest_vert].co) / 2
verts[i].co = middle_location
verts[nearest_vert].co = middle_location
checked_verts.append(i)
checked_verts.append(nearest_vert)
# Calculate average length between all the generated edges
ob = bpy.context.object
lengths_sum = 0
for ed in ob.data.edges:
v1 = ob.data.vertices[ed.vertices[0]]
v2 = ob.data.vertices[ed.vertices[1]]
lengths_sum += (v1.co - v2.co).length
edges_count = len(ob.data.edges)
# possible division by zero here
average_edge_length = lengths_sum / edges_count if edges_count != 0 else 0.0001
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='SELECT')
bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN',
threshold=average_edge_length / 15.0)
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
final_points_ob = bpy.context.view_layer.objects.active
# Make a dictionary with the verts related to each vert
related_key_verts = {}
for ed in final_points_ob.data.edges:
if not ed.vertices[0] in related_key_verts:
related_key_verts[ed.vertices[0]] = []
if not ed.vertices[1] in related_key_verts:
related_key_verts[ed.vertices[1]] = []
if not ed.vertices[1] in related_key_verts[ed.vertices[0]]:
related_key_verts[ed.vertices[0]].append(ed.vertices[1])
if not ed.vertices[0] in related_key_verts[ed.vertices[1]]:
related_key_verts[ed.vertices[1]].append(ed.vertices[0])
# Get groups of verts forming each face
faces_verts_idx = []
for v1 in related_key_verts: # verts-1 ....
for v2 in related_key_verts: # verts-2
if v1 != v2:
related_verts_in_common = []
v2_in_rel_v1 = False
v1_in_rel_v2 = False
for rel_v1 in related_key_verts[v1]:
# Check if related verts of verts-1 are related verts of verts-2
if rel_v1 in related_key_verts[v2]:
related_verts_in_common.append(rel_v1)
if v2 in related_key_verts[v1]:
v2_in_rel_v1 = True
if v1 in related_key_verts[v2]:
v1_in_rel_v2 = True
repeated_face = False
# If two verts have two related verts in common, they form a quad
if len(related_verts_in_common) == 2:
# Check if the face is already saved
for f_verts in faces_verts_idx:
repeated_verts = 0
if len(f_verts) == 4:
if v1 in f_verts:
repeated_verts += 1
if v2 in f_verts:
repeated_verts += 1
if related_verts_in_common[0] in f_verts:
repeated_verts += 1
if related_verts_in_common[1] in f_verts:
repeated_verts += 1
if repeated_verts == len(f_verts):
repeated_face = True
break
if not repeated_face:
faces_verts_idx.append([v1, related_verts_in_common[0],
v2, related_verts_in_common[1]])
# If Two verts have one related vert in common and they are
# related to each other, they form a triangle
elif v2_in_rel_v1 and v1_in_rel_v2 and len(related_verts_in_common) == 1:
# Check if the face is already saved.
for f_verts in faces_verts_idx:
repeated_verts = 0
if len(f_verts) == 3:
if v1 in f_verts:
repeated_verts += 1
if v2 in f_verts:
repeated_verts += 1
if related_verts_in_common[0] in f_verts:
repeated_verts += 1
if repeated_verts == len(f_verts):
repeated_face = True
break
if not repeated_face:
faces_verts_idx.append([v1, related_verts_in_common[0], v2])
# Keep only the faces that don't overlap by ignoring
# quads that overlap with two adjacent triangles
faces_to_not_include_idx = [] # Indices of faces_verts_idx to eliminate
for i in range(len(faces_verts_idx)):
for t in range(len(faces_verts_idx)):
if i != t:
verts_in_common = 0
if len(faces_verts_idx[i]) == 4 and len(faces_verts_idx[t]) == 3:
for v_idx in faces_verts_idx[t]:
if v_idx in faces_verts_idx[i]:
verts_in_common += 1
# If it doesn't have all it's vertices repeated in the other face
if verts_in_common == 3:
if i not in faces_to_not_include_idx:
faces_to_not_include_idx.append(i)
# Build surface
all_surface_verts_co = []
for i in range(len(final_points_ob.data.vertices)):
coords = final_points_ob.data.vertices[i].co
all_surface_verts_co.append([coords[0], coords[1], coords[2]])
# Verts of each face.
all_surface_faces = []
for i in range(len(faces_verts_idx)):
if i not in faces_to_not_include_idx:
face = []
for v_idx in faces_verts_idx[i]:
face.append(v_idx)
all_surface_faces.append(face)
# Build the mesh
surf_me_name = "SURFSKIO_surface"
me_surf = bpy.data.meshes.new(surf_me_name)
me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
ob_surface = object_utils.object_data_add(context, me_surf)
ob_surface.location = (0.0, 0.0, 0.0)
ob_surface.rotation_euler = (0.0, 0.0, 0.0)
ob_surface.scale = (1.0, 1.0, 1.0)
# Delete final points temporal object
bpy.ops.object.delete({"selected_objects": [final_points_ob]})
# Delete isolated verts if there are any
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
ob_surface.select_set(True)
bpy.context.view_layer.objects.active = ob_surface
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.select_face_by_sides(type='NOTEQUAL')
bpy.ops.mesh.delete()
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Join crosshatch results with original mesh
# Calculate a distance to merge the verts of the crosshatch surface to the main object
edges_length_sum = 0
for ed in ob_surface.data.edges:
edges_length_sum += (
ob_surface.data.vertices[ed.vertices[0]].co -
ob_surface.data.vertices[ed.vertices[1]].co
).length
# Make dictionary with all the verts connected to each vert, on the new surface object.
surface_connected_verts = {}
for ed in ob_surface.data.edges:
if not ed.vertices[0] in surface_connected_verts:
surface_connected_verts[ed.vertices[0]] = []
surface_connected_verts[ed.vertices[0]].append(ed.vertices[1])
if ed.vertices[1] not in surface_connected_verts:
surface_connected_verts[ed.vertices[1]] = []
surface_connected_verts[ed.vertices[1]].append(ed.vertices[0])
# Duplicate the new surface object, and use shrinkwrap to
# calculate later the nearest verts to the main object
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
final_ob_duplicate = bpy.context.view_layer.objects.active
bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
shrinkwrap_modifier = final_ob_duplicate.modifiers[-1]
shrinkwrap_modifier.wrap_method = "NEAREST_VERTEX"
shrinkwrap_modifier.target = self.main_object
bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier=shrinkwrap_modifier.name)
# Make list with verts of original mesh as index and coords as value
main_object_verts_coords = []
for v in self.main_object.data.vertices:
coords = self.main_object.matrix_world @ v.co
# To avoid problems when taking "-0.00" as a different value as "0.00"
for c in range(len(coords)):
if "%.3f" % coords[c] == "-0.00":
coords[c] = 0
main_object_verts_coords.append(["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]])
tuple(main_object_verts_coords)
# Determine which verts will be merged, snap them to the nearest verts
# on the original verts, and get them selected
crosshatch_verts_to_merge = []
if self.automatic_join:
for i in range(len(ob_surface.data.vertices)-1):
# Calculate the distance from each of the connected verts to the actual vert,
# and compare it with the distance they would have if joined.
# If they don't change much, that vert can be joined
merge_actual_vert = True
try:
if len(surface_connected_verts[i]) < 4:
for c_v_idx in surface_connected_verts[i]:
points_original = []
points_original.append(ob_surface.data.vertices[c_v_idx].co)
points_original.append(ob_surface.data.vertices[i].co)
points_target = []
points_target.append(ob_surface.data.vertices[c_v_idx].co)
points_target.append(final_ob_duplicate.data.vertices[i].co)
vec_A = points_original[0] - points_original[1]
vec_B = points_target[0] - points_target[1]
dist_A = (points_original[0] - points_original[1]).length
dist_B = (points_target[0] - points_target[1]).length
if not (
points_original[0] == points_original[1] or
points_target[0] == points_target[1]
): # If any vector's length is zero
angle = vec_A.angle(vec_B) / pi
else:
angle = 0
# Set a range of acceptable variation in the connected edges
if dist_B > dist_A * 1.7 * self.join_stretch_factor or \
dist_B < dist_A / 2 / self.join_stretch_factor or \
angle >= 0.15 * self.join_stretch_factor:
merge_actual_vert = False
break
else:
merge_actual_vert = False
except:
self.report({'WARNING'},
"Crosshatch set incorrectly")
if merge_actual_vert:
coords = final_ob_duplicate.data.vertices[i].co
# To avoid problems when taking "-0.000" as a different value as "0.00"
for c in range(len(coords)):
if "%.3f" % coords[c] == "-0.00":
coords[c] = 0
comparison_coords = ["%.3f" % coords[0], "%.3f" % coords[1], "%.3f" % coords[2]]
if comparison_coords in main_object_verts_coords:
# Get the index of the vert with those coords in the main object
main_object_related_vert_idx = main_object_verts_coords.index(comparison_coords)
if self.main_object.data.vertices[main_object_related_vert_idx].select is True or \
self.main_object_selected_verts_count == 0:
ob_surface.data.vertices[i].co = final_ob_duplicate.data.vertices[i].co
ob_surface.data.vertices[i].select = True
crosshatch_verts_to_merge.append(i)
# Make sure the vert in the main object is selected,
# in case it wasn't selected and the "join crosshatch" option is active
self.main_object.data.vertices[main_object_related_vert_idx].select = True
# Delete duplicated object
bpy.ops.object.delete({"selected_objects": [final_ob_duplicate]})
# Join crosshatched surface and main object
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
ob_surface.select_set(True)
self.main_object.select_set(True)
bpy.context.view_layer.objects.active = self.main_object
bpy.ops.object.join('INVOKE_REGION_WIN')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Perform Remove doubles to merge verts
if not (self.automatic_join is False and self.main_object_selected_verts_count == 0):
bpy.ops.mesh.remove_doubles(threshold=0.0001)
bpy.ops.mesh.select_all(action='DESELECT')
# If the main object has modifiers, turn their "viewport view status"
# to what it was before the forced deactivation above
if len(self.main_object.modifiers) > 0:
for m_idx in range(len(self.main_object.modifiers)):
self.main_object.modifiers[m_idx].show_viewport = self.modifiers_prev_viewport_state[m_idx]
self.update()
return {'FINISHED'}
def rectangular_surface(self, context):
# Selected edges
all_selected_edges_idx = []
all_selected_verts = []
all_verts_idx = []
for ed in self.main_object.data.edges:
if ed.select:
all_selected_edges_idx.append(ed.index)
# Selected vertices
if not ed.vertices[0] in all_selected_verts:
all_selected_verts.append(self.main_object.data.vertices[ed.vertices[0]])
if not ed.vertices[1] in all_selected_verts:
all_selected_verts.append(self.main_object.data.vertices[ed.vertices[1]])
# All verts (both from each edge) to determine later
# which are at the tips (those not repeated twice)
all_verts_idx.append(ed.vertices[0])
all_verts_idx.append(ed.vertices[1])
# Identify the tips and "middle-vertex" that separates U from V, if there is one
all_chains_tips_idx = []
for v_idx in all_verts_idx:
if all_verts_idx.count(v_idx) < 2:
all_chains_tips_idx.append(v_idx)
edges_connected_to_tips = []
for ed in self.main_object.data.edges:
if (ed.vertices[0] in all_chains_tips_idx or ed.vertices[1] in all_chains_tips_idx) and \
not (ed.vertices[0] in all_verts_idx and ed.vertices[1] in all_verts_idx):
edges_connected_to_tips.append(ed)
# Check closed selections
# List with groups of three verts, where the first element of the pair is
# the unselected vert of a closed selection and the other two elements are the
# selected neighbor verts (it will be useful to determine which selection chain
# the unselected vert belongs to, and determine the "middle-vertex")
single_unselected_verts_and_neighbors = []
# To identify a "closed" selection (a selection that is a closed chain except
# for one vertex) find the vertex in common that have the edges connected to tips.
# If there is a vertex in common, that one is the unselected vert that closes
# the selection or is a "middle-vertex"
single_unselected_verts = []
for ed in edges_connected_to_tips:
for ed_b in edges_connected_to_tips:
if ed != ed_b:
if ed.vertices[0] == ed_b.vertices[0] and \
not self.main_object.data.vertices[ed.vertices[0]].select and \
ed.vertices[0] not in single_unselected_verts:
# The second element is one of the tips of the selected
# vertices of the closed selection
single_unselected_verts_and_neighbors.append(
[ed.vertices[0], ed.vertices[1], ed_b.vertices[1]]
)
single_unselected_verts.append(ed.vertices[0])
break
elif ed.vertices[0] == ed_b.vertices[1] and \
not self.main_object.data.vertices[ed.vertices[0]].select and \
ed.vertices[0] not in single_unselected_verts:
single_unselected_verts_and_neighbors.append(
[ed.vertices[0], ed.vertices[1], ed_b.vertices[0]]
)
single_unselected_verts.append(ed.vertices[0])
break
elif ed.vertices[1] == ed_b.vertices[0] and \
not self.main_object.data.vertices[ed.vertices[1]].select and \
ed.vertices[1] not in single_unselected_verts:
single_unselected_verts_and_neighbors.append(
[ed.vertices[1], ed.vertices[0], ed_b.vertices[1]]
)
single_unselected_verts.append(ed.vertices[1])
break
elif ed.vertices[1] == ed_b.vertices[1] and \
not self.main_object.data.vertices[ed.vertices[1]].select and \
ed.vertices[1] not in single_unselected_verts:
single_unselected_verts_and_neighbors.append(
[ed.vertices[1], ed.vertices[0], ed_b.vertices[0]]
)
single_unselected_verts.append(ed.vertices[1])
break
middle_vertex_idx = None
tips_to_discard_idx = []
# Check if there is a "middle-vertex", and get its index
for i in range(0, len(single_unselected_verts_and_neighbors)):
actual_chain_verts = self.get_ordered_verts(
self.main_object, all_selected_edges_idx,
all_verts_idx, single_unselected_verts_and_neighbors[i][1],
None, None
)
if single_unselected_verts_and_neighbors[i][2] != \
actual_chain_verts[len(actual_chain_verts) - 1].index:
middle_vertex_idx = single_unselected_verts_and_neighbors[i][0]
tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][1])
tips_to_discard_idx.append(single_unselected_verts_and_neighbors[i][2])
# List with pairs of verts that belong to the tips of each selection chain (row)
verts_tips_same_chain_idx = []
if len(all_chains_tips_idx) >= 2:
checked_v = []
for i in range(0, len(all_chains_tips_idx)):
if all_chains_tips_idx[i] not in checked_v:
v_chain = self.get_ordered_verts(
self.main_object, all_selected_edges_idx,
all_verts_idx, all_chains_tips_idx[i],
middle_vertex_idx, None
)
verts_tips_same_chain_idx.append([v_chain[0].index, v_chain[len(v_chain) - 1].index])
checked_v.append(v_chain[0].index)
checked_v.append(v_chain[len(v_chain) - 1].index)
# Selection tips (vertices).
verts_tips_parsed_idx = []
if len(all_chains_tips_idx) >= 2:
for spec_v_idx in all_chains_tips_idx:
if (spec_v_idx not in tips_to_discard_idx):
verts_tips_parsed_idx.append(spec_v_idx)
# Identify the type of selection made by the user
if middle_vertex_idx is not None:
# If there are 4 tips (two selection chains), and
# there is only one single unselected vert (the middle vert)
if len(all_chains_tips_idx) == 4 and len(single_unselected_verts_and_neighbors) == 1:
selection_type = "TWO_CONNECTED"
else:
# The type of the selection was not identified, the script stops.
self.report({'WARNING'}, "The selection isn't valid.")
self.stopping_errors = True
return{'CANCELLED'}
else:
if len(all_chains_tips_idx) == 2: # If there are 2 tips
selection_type = "SINGLE"
elif len(all_chains_tips_idx) == 4: # If there are 4 tips
selection_type = "TWO_NOT_CONNECTED"
elif len(all_chains_tips_idx) == 0:
if len(self.main_splines.data.splines) > 1:
selection_type = "NO_SELECTION"
else:
# If the selection was not identified and there is only one stroke,
# there's no possibility to build a surface, so the script is interrupted
self.report({'WARNING'}, "The selection isn't valid.")
self.stopping_errors = True
return{'CANCELLED'}
else:
# The type of the selection was not identified, the script stops
self.report({'WARNING'}, "The selection isn't valid.")
self.stopping_errors = True
return{'CANCELLED'}
# If the selection type is TWO_NOT_CONNECTED and there is only one stroke, stop the script
if selection_type == "TWO_NOT_CONNECTED" and len(self.main_splines.data.splines) == 1:
self.report({'WARNING'},
"At least two strokes are needed when there are two not connected selections")
self.stopping_errors = True
return{'CANCELLED'}
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
self.main_splines.select_set(True)
bpy.context.view_layer.objects.active = self.main_splines
# Enter editmode for the new curve (converted from grease pencil strokes), to smooth it out
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
bpy.ops.curve.smooth('INVOKE_REGION_WIN')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
self.selection_U_exists = False
self.selection_U2_exists = False
self.selection_V_exists = False
self.selection_V2_exists = False
self.selection_U_is_closed = False
self.selection_U2_is_closed = False
self.selection_V_is_closed = False
self.selection_V2_is_closed = False
# Define what vertices are at the tips of each selection and are not the middle-vertex
if selection_type == "TWO_CONNECTED":
self.selection_U_exists = True
self.selection_V_exists = True
closing_vert_U_idx = None
closing_vert_V_idx = None
closing_vert_U2_idx = None
closing_vert_V2_idx = None
# Determine which selection is Selection-U and which is Selection-V
points_A = []
points_B = []
points_first_stroke_tips = []
points_A.append(
self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[0]].co
)
points_A.append(
self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
)
points_B.append(
self.main_object.matrix_world @ self.main_object.data.vertices[verts_tips_parsed_idx[1]].co
)
points_B.append(
self.main_object.matrix_world @ self.main_object.data.vertices[middle_vertex_idx].co
)
points_first_stroke_tips.append(
self.main_splines.data.splines[0].bezier_points[0].co
)
points_first_stroke_tips.append(
self.main_splines.data.splines[0].bezier_points[
len(self.main_splines.data.splines[0].bezier_points) - 1
].co
)
angle_A = self.orientation_difference(points_A, points_first_stroke_tips)
angle_B = self.orientation_difference(points_B, points_first_stroke_tips)
if angle_A < angle_B:
first_vert_U_idx = verts_tips_parsed_idx[0]
first_vert_V_idx = verts_tips_parsed_idx[1]
else:
first_vert_U_idx = verts_tips_parsed_idx[1]
first_vert_V_idx = verts_tips_parsed_idx[0]
elif selection_type == "SINGLE" or selection_type == "TWO_NOT_CONNECTED":
first_sketched_point_first_stroke_co = self.main_splines.data.splines[0].bezier_points[0].co
last_sketched_point_first_stroke_co = \
self.main_splines.data.splines[0].bezier_points[
len(self.main_splines.data.splines[0].bezier_points) - 1
].co
first_sketched_point_last_stroke_co = \
self.main_splines.data.splines[
len(self.main_splines.data.splines) - 1
].bezier_points[0].co
if len(self.main_splines.data.splines) > 1:
first_sketched_point_second_stroke_co = self.main_splines.data.splines[1].bezier_points[0].co
last_sketched_point_second_stroke_co = \
self.main_splines.data.splines[1].bezier_points[
len(self.main_splines.data.splines[1].bezier_points) - 1
].co
single_unselected_neighbors = [] # Only the neighbors of the single unselected verts
for verts_neig_idx in single_unselected_verts_and_neighbors:
single_unselected_neighbors.append(verts_neig_idx[1])
single_unselected_neighbors.append(verts_neig_idx[2])
all_chains_tips_and_middle_vert = []
for v_idx in all_chains_tips_idx:
if v_idx not in single_unselected_neighbors:
all_chains_tips_and_middle_vert.append(v_idx)
all_chains_tips_and_middle_vert += single_unselected_verts
all_participating_verts = all_chains_tips_and_middle_vert + all_verts_idx
# The tip of the selected vertices nearest to the first point of the first sketched stroke
nearest_tip_to_first_st_first_pt_idx, shortest_distance_to_first_stroke = \
self.shortest_distance(
self.main_object,
first_sketched_point_first_stroke_co,
all_chains_tips_and_middle_vert
)
# If the nearest tip is not from a closed selection, get the opposite tip vertex index
if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
nearest_tip_to_first_st_first_pt_opposite_idx = \
self.opposite_tip(
nearest_tip_to_first_st_first_pt_idx,
verts_tips_same_chain_idx
)
# The tip of the selected vertices nearest to the last point of the first sketched stroke
nearest_tip_to_first_st_last_pt_idx, _temp_dist = \
self.shortest_distance(
self.main_object,
last_sketched_point_first_stroke_co,
all_chains_tips_and_middle_vert
)
# The tip of the selected vertices nearest to the first point of the last sketched stroke
nearest_tip_to_last_st_first_pt_idx, shortest_distance_to_last_stroke = \
self.shortest_distance(
self.main_object,
first_sketched_point_last_stroke_co,
all_chains_tips_and_middle_vert
)
if len(self.main_splines.data.splines) > 1:
# The selected vertex nearest to the first point of the second sketched stroke
# (This will be useful to determine the direction of the closed
# selection V when extruding along strokes)
nearest_vert_to_second_st_first_pt_idx, _temp_dist = \
self.shortest_distance(
self.main_object,
first_sketched_point_second_stroke_co,
all_verts_idx
)
# The selected vertex nearest to the first point of the second sketched stroke
# (This will be useful to determine the direction of the closed
# selection V2 when extruding along strokes)
nearest_vert_to_second_st_last_pt_idx, _temp_dist = \
self.shortest_distance(
self.main_object,
last_sketched_point_second_stroke_co,
all_verts_idx
)
# Determine if the single selection will be treated as U or as V
edges_sum = 0
for i in all_selected_edges_idx:
edges_sum += (
(self.main_object.matrix_world @
self.main_object.data.vertices[self.main_object.data.edges[i].vertices[0]].co) -
(self.main_object.matrix_world @
self.main_object.data.vertices[self.main_object.data.edges[i].vertices[1]].co)
).length
average_edge_length = edges_sum / len(all_selected_edges_idx)
# Get shortest distance from the first point of the last stroke to any participating vertex
_temp_idx, shortest_distance_to_last_stroke = \
self.shortest_distance(
self.main_object,
first_sketched_point_last_stroke_co,
all_participating_verts
)
# If the beginning of the first stroke is near enough, and its orientation
# difference with the first edge of the nearest selection chain is not too high,
# interpret things as an "extrude along strokes" instead of "extrude through strokes"
if shortest_distance_to_first_stroke < average_edge_length / 4 and \
shortest_distance_to_last_stroke < average_edge_length and \
len(self.main_splines.data.splines) > 1:
self.selection_U_exists = False
self.selection_V_exists = True
# If the first selection is not closed
if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
self.selection_V_is_closed = False
closing_vert_U_idx = None
closing_vert_U2_idx = None
closing_vert_V_idx = None
closing_vert_V2_idx = None
first_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
if selection_type == "TWO_NOT_CONNECTED":
self.selection_V2_exists = True
first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
else:
self.selection_V_is_closed = True
closing_vert_V_idx = nearest_tip_to_first_st_first_pt_idx
# Get the neighbors of the first (unselected) vert of the closed selection U.
vert_neighbors = []
for verts in single_unselected_verts_and_neighbors:
if verts[0] == nearest_tip_to_first_st_first_pt_idx:
vert_neighbors.append(verts[1])
vert_neighbors.append(verts[2])
break
verts_V = self.get_ordered_verts(
self.main_object, all_selected_edges_idx,
all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
)
for i in range(0, len(verts_V)):
if verts_V[i].index == nearest_vert_to_second_st_first_pt_idx:
# If the vertex nearest to the first point of the second stroke
# is in the first half of the selected verts
if i >= len(verts_V) / 2:
first_vert_V_idx = vert_neighbors[1]
break
else:
first_vert_V_idx = vert_neighbors[0]
break
if selection_type == "TWO_NOT_CONNECTED":
self.selection_V2_exists = True
# If the second selection is not closed
if nearest_tip_to_first_st_last_pt_idx not in single_unselected_verts or \
nearest_tip_to_first_st_last_pt_idx == middle_vertex_idx:
self.selection_V2_is_closed = False
closing_vert_V2_idx = None
first_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
else:
self.selection_V2_is_closed = True
closing_vert_V2_idx = nearest_tip_to_first_st_last_pt_idx
# Get the neighbors of the first (unselected) vert of the closed selection U
vert_neighbors = []
for verts in single_unselected_verts_and_neighbors:
if verts[0] == nearest_tip_to_first_st_last_pt_idx:
vert_neighbors.append(verts[1])
vert_neighbors.append(verts[2])
break
verts_V2 = self.get_ordered_verts(
self.main_object, all_selected_edges_idx,
all_verts_idx, vert_neighbors[0], middle_vertex_idx, None
)
for i in range(0, len(verts_V2)):
if verts_V2[i].index == nearest_vert_to_second_st_last_pt_idx:
# If the vertex nearest to the first point of the second stroke
# is in the first half of the selected verts
if i >= len(verts_V2) / 2:
first_vert_V2_idx = vert_neighbors[1]
break
else:
first_vert_V2_idx = vert_neighbors[0]
break
else:
self.selection_V2_exists = False
else:
self.selection_U_exists = True
self.selection_V_exists = False
# If the first selection is not closed
if nearest_tip_to_first_st_first_pt_idx not in single_unselected_verts or \
nearest_tip_to_first_st_first_pt_idx == middle_vertex_idx:
self.selection_U_is_closed = False
closing_vert_U_idx = None
points_tips = []
points_tips.append(
self.main_object.matrix_world @
self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
)
points_tips.append(
self.main_object.matrix_world @
self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_opposite_idx].co
)
points_first_stroke_tips = []
points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
points_first_stroke_tips.append(
self.main_splines.data.splines[0].bezier_points[
len(self.main_splines.data.splines[0].bezier_points) - 1
].co
)
vec_A = points_tips[0] - points_tips[1]
vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
# Compare the direction of the selection and the first
# grease pencil stroke to determine which is the "first" vertex of the selection
if vec_A.dot(vec_B) < 0:
first_vert_U_idx = nearest_tip_to_first_st_first_pt_opposite_idx
else:
first_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
else:
self.selection_U_is_closed = True
closing_vert_U_idx = nearest_tip_to_first_st_first_pt_idx
# Get the neighbors of the first (unselected) vert of the closed selection U
vert_neighbors = []
for verts in single_unselected_verts_and_neighbors:
if verts[0] == nearest_tip_to_first_st_first_pt_idx:
vert_neighbors.append(verts[1])
vert_neighbors.append(verts[2])
break
points_first_and_neighbor = []
points_first_and_neighbor.append(
self.main_object.matrix_world @
self.main_object.data.vertices[nearest_tip_to_first_st_first_pt_idx].co
)
points_first_and_neighbor.append(
self.main_object.matrix_world @
self.main_object.data.vertices[vert_neighbors[0]].co
)
points_first_stroke_tips = []
points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[0].co)
points_first_stroke_tips.append(self.main_splines.data.splines[0].bezier_points[1].co)
vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
vec_B = points_first_stroke_tips[0] - points_first_stroke_tips[1]
# Compare the direction of the selection and the first grease pencil stroke to
# determine which is the vertex neighbor to the first vertex (unselected) of
# the closed selection. This will determine the direction of the closed selection
if vec_A.dot(vec_B) < 0:
first_vert_U_idx = vert_neighbors[1]
else:
first_vert_U_idx = vert_neighbors[0]
if selection_type == "TWO_NOT_CONNECTED":
self.selection_U2_exists = True
# If the second selection is not closed
if nearest_tip_to_last_st_first_pt_idx not in single_unselected_verts or \
nearest_tip_to_last_st_first_pt_idx == middle_vertex_idx:
self.selection_U2_is_closed = False
closing_vert_U2_idx = None
first_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
else:
self.selection_U2_is_closed = True
closing_vert_U2_idx = nearest_tip_to_last_st_first_pt_idx
# Get the neighbors of the first (unselected) vert of the closed selection U
vert_neighbors = []
for verts in single_unselected_verts_and_neighbors:
if verts[0] == nearest_tip_to_last_st_first_pt_idx:
vert_neighbors.append(verts[1])
vert_neighbors.append(verts[2])
break
points_first_and_neighbor = []
points_first_and_neighbor.append(
self.main_object.matrix_world @
self.main_object.data.vertices[nearest_tip_to_last_st_first_pt_idx].co
)
points_first_and_neighbor.append(
self.main_object.matrix_world @
self.main_object.data.vertices[vert_neighbors[0]].co
)
points_last_stroke_tips = []
points_last_stroke_tips.append(
self.main_splines.data.splines[
len(self.main_splines.data.splines) - 1
].bezier_points[0].co
)
points_last_stroke_tips.append(
self.main_splines.data.splines[
len(self.main_splines.data.splines) - 1
].bezier_points[1].co
)
vec_A = points_first_and_neighbor[0] - points_first_and_neighbor[1]
vec_B = points_last_stroke_tips[0] - points_last_stroke_tips[1]
# Compare the direction of the selection and the last grease pencil stroke to
# determine which is the vertex neighbor to the first vertex (unselected) of
# the closed selection. This will determine the direction of the closed selection
if vec_A.dot(vec_B) < 0:
first_vert_U2_idx = vert_neighbors[1]
else:
first_vert_U2_idx = vert_neighbors[0]
else:
self.selection_U2_exists = False
elif selection_type == "NO_SELECTION":
self.selection_U_exists = False
self.selection_V_exists = False
# Get an ordered list of the vertices of Selection-U
verts_ordered_U = []
if self.selection_U_exists:
verts_ordered_U = self.get_ordered_verts(
self.main_object, all_selected_edges_idx,
all_verts_idx, first_vert_U_idx,
middle_vertex_idx, closing_vert_U_idx
)
# Get an ordered list of the vertices of Selection-U2
verts_ordered_U2 = []
if self.selection_U2_exists:
verts_ordered_U2 = self.get_ordered_verts(
self.main_object, all_selected_edges_idx,
all_verts_idx, first_vert_U2_idx,
middle_vertex_idx, closing_vert_U2_idx
)
# Get an ordered list of the vertices of Selection-V
verts_ordered_V = []
if self.selection_V_exists:
verts_ordered_V = self.get_ordered_verts(
self.main_object, all_selected_edges_idx,
all_verts_idx, first_vert_V_idx,
middle_vertex_idx, closing_vert_V_idx
)
verts_ordered_V_indices = [x.index for x in verts_ordered_V]
# Get an ordered list of the vertices of Selection-V2
verts_ordered_V2 = []
if self.selection_V2_exists:
verts_ordered_V2 = self.get_ordered_verts(
self.main_object, all_selected_edges_idx,
all_verts_idx, first_vert_V2_idx,
middle_vertex_idx, closing_vert_V2_idx
)
# Check if when there are two-not-connected selections both have the same
# number of verts. If not terminate the script
if ((self.selection_U2_exists and len(verts_ordered_U) != len(verts_ordered_U2)) or
(self.selection_V2_exists and len(verts_ordered_V) != len(verts_ordered_V2))):
# Display a warning
self.report({'WARNING'}, "Both selections must have the same number of edges")
self.stopping_errors = True
return{'CANCELLED'}
# Calculate edges U proportions
# Sum selected edges U lengths
edges_lengths_U = []
edges_lengths_sum_U = 0
if self.selection_U_exists:
edges_lengths_U, edges_lengths_sum_U = self.get_chain_length(
self.main_object,
verts_ordered_U
)
if self.selection_U2_exists:
edges_lengths_U2, edges_lengths_sum_U2 = self.get_chain_length(
self.main_object,
verts_ordered_U2
)
# Sum selected edges V lengths
edges_lengths_V = []
edges_lengths_sum_V = 0
if self.selection_V_exists:
edges_lengths_V, edges_lengths_sum_V = self.get_chain_length(
self.main_object,
verts_ordered_V
)
if self.selection_V2_exists:
edges_lengths_V2, edges_lengths_sum_V2 = self.get_chain_length(
self.main_object,
verts_ordered_V2
)
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.subdivide('INVOKE_REGION_WIN',
number_cuts=bpy.context.scene.bsurfaces.SURFSK_precision)
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Proportions U
edges_proportions_U = []
edges_proportions_U = self.get_edges_proportions(
edges_lengths_U, edges_lengths_sum_U,
self.selection_U_exists, self.edges_U
)
verts_count_U = len(edges_proportions_U) + 1
if self.selection_U2_exists:
edges_proportions_U2 = []
edges_proportions_U2 = self.get_edges_proportions(
edges_lengths_U2, edges_lengths_sum_U2,
self.selection_U2_exists, self.edges_V
)
# Proportions V
edges_proportions_V = []
edges_proportions_V = self.get_edges_proportions(
edges_lengths_V, edges_lengths_sum_V,
self.selection_V_exists, self.edges_V
)
if self.selection_V2_exists:
edges_proportions_V2 = []
edges_proportions_V2 = self.get_edges_proportions(
edges_lengths_V2, edges_lengths_sum_V2,
self.selection_V2_exists, self.edges_V
)
# Cyclic Follow: simplify sketched curves, make them Cyclic, and complete
# the actual sketched curves with a "closing segment"
if self.cyclic_follow and not self.selection_V_exists and not \
((self.selection_U_exists and not self.selection_U_is_closed) or
(self.selection_U2_exists and not self.selection_U2_is_closed)):
simplified_spline_coords = []
simplified_curve = []
ob_simplified_curve = []
splines_first_v_co = []
for i in range(len(self.main_splines.data.splines)):
# Create a curve object for the actual spline "cyclic extension"
simplified_curve.append(bpy.data.curves.new('SURFSKIO_simpl_crv', 'CURVE'))
ob_simplified_curve.append(bpy.data.objects.new('SURFSKIO_simpl_crv', simplified_curve[i]))
bpy.context.collection.objects.link(ob_simplified_curve[i])
simplified_curve[i].dimensions = "3D"
spline_coords = []
for bp in self.main_splines.data.splines[i].bezier_points:
spline_coords.append(bp.co)
# Simplification
simplified_spline_coords.append(self.simplify_spline(spline_coords, 5))
# Get the coordinates of the first vert of the actual spline
splines_first_v_co.append(simplified_spline_coords[i][0])
# Generate the spline
spline = simplified_curve[i].splines.new('BEZIER')
# less one because one point is added when the spline is created
spline.bezier_points.add(len(simplified_spline_coords[i]) - 1)
for p in range(0, len(simplified_spline_coords[i])):
spline.bezier_points[p].co = simplified_spline_coords[i][p]
spline.use_cyclic_u = True
spline_bp_count = len(spline.bezier_points)
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
ob_simplified_curve[i].select_set(True)
bpy.context.view_layer.objects.active = ob_simplified_curve[i]
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Select the "closing segment", and subdivide it
ob_simplified_curve[i].data.splines[0].bezier_points[0].select_control_point = True
ob_simplified_curve[i].data.splines[0].bezier_points[0].select_left_handle = True
ob_simplified_curve[i].data.splines[0].bezier_points[0].select_right_handle = True
ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_control_point = True
ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_left_handle = True
ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].select_right_handle = True
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
segments = sqrt(
(ob_simplified_curve[i].data.splines[0].bezier_points[0].co -
ob_simplified_curve[i].data.splines[0].bezier_points[spline_bp_count - 1].co).length /
self.average_gp_segment_length
)
for t in range(2):
bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=segments)
# Delete the other vertices and make it non-cyclic to
# keep only the needed verts of the "closing segment"
bpy.ops.curve.select_all(action='INVERT')
bpy.ops.curve.delete(type='VERT')
ob_simplified_curve[i].data.splines[0].use_cyclic_u = False
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Add the points of the "closing segment" to the original curve from grease pencil stroke
first_new_index = len(self.main_splines.data.splines[i].bezier_points)
self.main_splines.data.splines[i].bezier_points.add(
len(ob_simplified_curve[i].data.splines[0].bezier_points) - 1
)
for t in range(1, len(ob_simplified_curve[i].data.splines[0].bezier_points)):
self.main_splines.data.splines[i].bezier_points[t - 1 + first_new_index].co = \
ob_simplified_curve[i].data.splines[0].bezier_points[t].co
# Delete the temporal curve
bpy.ops.object.delete({"selected_objects": [ob_simplified_curve[i]]})
# Get the coords of the points distributed along the sketched strokes,
# with proportions-U of the first selection
pts_on_strokes_with_proportions_U = self.distribute_pts(
self.main_splines.data.splines,
edges_proportions_U
)
sketched_splines_parsed = []
if self.selection_U2_exists:
# Initialize the multidimensional list with the proportions of all the segments
proportions_loops_crossing_strokes = []
for i in range(len(pts_on_strokes_with_proportions_U)):
proportions_loops_crossing_strokes.append([])
for t in range(len(pts_on_strokes_with_proportions_U[0])):
proportions_loops_crossing_strokes[i].append(None)
# Calculate the proportions of each segment of the loops-U from pts_on_strokes_with_proportions_U
for lp in range(len(pts_on_strokes_with_proportions_U[0])):
loop_segments_lengths = []
for st in range(len(pts_on_strokes_with_proportions_U)):
# When on the first stroke, add the segment from the selection to the dirst stroke
if st == 0:
loop_segments_lengths.append(
((self.main_object.matrix_world @ verts_ordered_U[lp].co) -
pts_on_strokes_with_proportions_U[0][lp]).length
)
# For all strokes except for the last, calculate the distance
# from the actual stroke to the next
if st != len(pts_on_strokes_with_proportions_U) - 1:
loop_segments_lengths.append(
(pts_on_strokes_with_proportions_U[st][lp] -
pts_on_strokes_with_proportions_U[st + 1][lp]).length
)
# When on the last stroke, add the segments
# from the last stroke to the second selection
if st == len(pts_on_strokes_with_proportions_U) - 1:
loop_segments_lengths.append(
(pts_on_strokes_with_proportions_U[st][lp] -
(self.main_object.matrix_world @ verts_ordered_U2[lp].co)).length
)
# Calculate full loop length
loop_seg_lengths_sum = 0
for i in range(len(loop_segments_lengths)):
loop_seg_lengths_sum += loop_segments_lengths[i]
# Fill the multidimensional list with the proportions of all the segments
for st in range(len(pts_on_strokes_with_proportions_U)):
proportions_loops_crossing_strokes[st][lp] = \
loop_segments_lengths[st] / loop_seg_lengths_sum
# Calculate proportions for each stroke
for st in range(len(pts_on_strokes_with_proportions_U)):
actual_stroke_spline = []
# Needs to be a list for the "distribute_pts" method
actual_stroke_spline.append(self.main_splines.data.splines[st])
# Calculate the proportions for the actual stroke.
actual_edges_proportions_U = []
for i in range(len(edges_proportions_U)):
proportions_sum = 0
# Sum the proportions of this loop up to the actual.
for t in range(0, st + 1):
proportions_sum += proportions_loops_crossing_strokes[t][i]
# i + 1, because proportions_loops_crossing_strokes refers to loops,
# and the proportions refer to edges, so we start at the element 1
# of proportions_loops_crossing_strokes instead of element 0
actual_edges_proportions_U.append(
edges_proportions_U[i] -
((edges_proportions_U[i] - edges_proportions_U2[i]) * proportions_sum)
)
points_actual_spline = self.distribute_pts(actual_stroke_spline, actual_edges_proportions_U)
sketched_splines_parsed.append(points_actual_spline[0])
else:
sketched_splines_parsed = pts_on_strokes_with_proportions_U
# If the selection type is "TWO_NOT_CONNECTED" replace the
# points of the last spline with the points in the "target" selection
if selection_type == "TWO_NOT_CONNECTED":
if self.selection_U2_exists:
for i in range(0, len(sketched_splines_parsed[len(sketched_splines_parsed) - 1])):
sketched_splines_parsed[len(sketched_splines_parsed) - 1][i] = \
self.main_object.matrix_world @ verts_ordered_U2[i].co
# Create temporary curves along the "control-points" found
# on the sketched curves and the mesh selection
mesh_ctrl_pts_name = "SURFSKIO_ctrl_pts"
me = bpy.data.meshes.new(mesh_ctrl_pts_name)
ob_ctrl_pts = bpy.data.objects.new(mesh_ctrl_pts_name, me)
ob_ctrl_pts.data = me
bpy.context.collection.objects.link(ob_ctrl_pts)
cyclic_loops_U = []
first_verts = []
second_verts = []
last_verts = []
for i in range(0, verts_count_U):
vert_num_in_spline = 1
if self.selection_U_exists:
ob_ctrl_pts.data.vertices.add(1)
last_v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
last_v.co = self.main_object.matrix_world @ verts_ordered_U[i].co
vert_num_in_spline += 1
for t in range(0, len(sketched_splines_parsed)):
ob_ctrl_pts.data.vertices.add(1)
v = ob_ctrl_pts.data.vertices[len(ob_ctrl_pts.data.vertices) - 1]
v.co = sketched_splines_parsed[t][i]
if vert_num_in_spline > 1:
ob_ctrl_pts.data.edges.add(1)
ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[0] = \
len(ob_ctrl_pts.data.vertices) - 2
ob_ctrl_pts.data.edges[len(ob_ctrl_pts.data.edges) - 1].vertices[1] = \
len(ob_ctrl_pts.data.vertices) - 1
if t == 0:
first_verts.append(v.index)
if t == 1:
second_verts.append(v.index)
if t == len(sketched_splines_parsed) - 1:
last_verts.append(v.index)
last_v = v
vert_num_in_spline += 1
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
ob_ctrl_pts.select_set(True)
bpy.context.view_layer.objects.active = ob_ctrl_pts
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Determine which loops-U will be "Cyclic"
for i in range(0, len(first_verts)):
# When there is Cyclic Cross there is no need of
# Automatic Join, (and there are at least three strokes)
if self.automatic_join and not self.cyclic_cross and \
selection_type != "TWO_CONNECTED" and len(self.main_splines.data.splines) >= 3:
v = ob_ctrl_pts.data.vertices
first_point_co = v[first_verts[i]].co
second_point_co = v[second_verts[i]].co
last_point_co = v[last_verts[i]].co
# Coordinates of the point in the center of both the first and last verts.
verts_center_co = [
(first_point_co[0] + last_point_co[0]) / 2,
(first_point_co[1] + last_point_co[1]) / 2,
(first_point_co[2] + last_point_co[2]) / 2
]
vec_A = second_point_co - first_point_co
vec_B = second_point_co - Vector(verts_center_co)
# Calculate the length of the first segment of the loop,
# and the length it would have after moving the first vert
# to the middle position between first and last
length_original = (second_point_co - first_point_co).length
length_target = (second_point_co - Vector(verts_center_co)).length
angle = vec_A.angle(vec_B) / pi
# If the target length doesn't stretch too much, and the
# its angle doesn't change to much either
if length_target <= length_original * 1.03 * self.join_stretch_factor and \
angle <= 0.008 * self.join_stretch_factor and not self.selection_U_exists:
cyclic_loops_U.append(True)
# Move the first vert to the center coordinates
ob_ctrl_pts.data.vertices[first_verts[i]].co = verts_center_co
# Select the last verts from Cyclic loops, for later deletion all at once
v[last_verts[i]].select = True
else:
cyclic_loops_U.append(False)
else:
# If "Cyclic Cross" is active then "all" crossing curves become cyclic
if self.cyclic_cross and not self.selection_U_exists and not \
((self.selection_V_exists and not self.selection_V_is_closed) or
(self.selection_V2_exists and not self.selection_V2_is_closed)):
cyclic_loops_U.append(True)
else:
cyclic_loops_U.append(False)
# The cyclic_loops_U list needs to be reversed.
cyclic_loops_U.reverse()
# Delete the previously selected (last_)verts.
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.mesh.delete('INVOKE_REGION_WIN', type='VERT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Create curves from control points.
bpy.ops.object.convert('INVOKE_REGION_WIN', target='CURVE', keep_original=False)
ob_curves_surf = bpy.context.view_layer.objects.active
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.spline_type_set('INVOKE_REGION_WIN', type='BEZIER')
bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
# Make Cyclic the splines designated as Cyclic.
for i in range(0, len(cyclic_loops_U)):
ob_curves_surf.data.splines[i].use_cyclic_u = cyclic_loops_U[i]
# Get the coords of all points on first loop-U, for later comparison with its
# subdivided version, to know which points of the loops-U are crossed by the
# original strokes. The indices will be the same for the other loops-U
if self.loops_on_strokes:
coords_loops_U_control_points = []
for p in ob_ctrl_pts.data.splines[0].bezier_points:
coords_loops_U_control_points.append(["%.4f" % p.co[0], "%.4f" % p.co[1], "%.4f" % p.co[2]])
tuple(coords_loops_U_control_points)
# Calculate number of edges-V in case option "Loops on strokes" is active or inactive
if self.loops_on_strokes and not self.selection_V_exists:
edges_V_count = len(self.main_splines.data.splines) * self.edges_V
else:
edges_V_count = len(edges_proportions_V)
# The Follow precision will vary depending on the number of Follow face-loops
precision_multiplier = round(2 + (edges_V_count / 15))
curve_cuts = bpy.context.scene.bsurfaces.SURFSK_precision * precision_multiplier
# Subdivide the curves
bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=curve_cuts)
# The verts position shifting that happens with splines subdivision.
# For later reorder splines points
verts_position_shift = curve_cuts + 1
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Reorder coordinates of the points of each spline to put the first point of
# the spline starting at the position it was the first point before sudividing
# the curve. And make a new curve object per spline (to handle memory better later)
splines_U_objects = []
for i in range(len(ob_curves_surf.data.splines)):
spline_U_curve = bpy.data.curves.new('SURFSKIO_spline_U_' + str(i), 'CURVE')
ob_spline_U = bpy.data.objects.new('SURFSKIO_spline_U_' + str(i), spline_U_curve)
bpy.context.collection.objects.link(ob_spline_U)
spline_U_curve.dimensions = "3D"
# Add points to the spline in the new curve object
ob_spline_U.data.splines.new('BEZIER')
for t in range(len(ob_curves_surf.data.splines[i].bezier_points)):
if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
if t + verts_position_shift <= len(ob_curves_surf.data.splines[i].bezier_points) - 1:
point_index = t + verts_position_shift
else:
point_index = t + verts_position_shift - len(ob_curves_surf.data.splines[i].bezier_points)
else:
point_index = t
# to avoid adding the first point since it's added when the spline is created
if t > 0:
ob_spline_U.data.splines[0].bezier_points.add(1)
ob_spline_U.data.splines[0].bezier_points[t].co = \
ob_curves_surf.data.splines[i].bezier_points[point_index].co
if cyclic_loops_U[i] is True and not self.selection_U_exists: # If the loop is cyclic
# Add a last point at the same location as the first one
ob_spline_U.data.splines[0].bezier_points.add(1)
ob_spline_U.data.splines[0].bezier_points[len(ob_spline_U.data.splines[0].bezier_points) - 1].co = \
ob_spline_U.data.splines[0].bezier_points[0].co
else:
ob_spline_U.data.splines[0].use_cyclic_u = False
splines_U_objects.append(ob_spline_U)
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
ob_spline_U.select_set(True)
bpy.context.view_layer.objects.active = ob_spline_U
# When option "Loops on strokes" is active each "Cross" loop will have
# its own proportions according to where the original strokes "touch" them
if self.loops_on_strokes:
# Get the indices of points where the original strokes "touch" loops-U
points_U_crossed_by_strokes = []
for i in range(len(splines_U_objects[0].data.splines[0].bezier_points)):
bp = splines_U_objects[0].data.splines[0].bezier_points[i]
if ["%.4f" % bp.co[0], "%.4f" % bp.co[1], "%.4f" % bp.co[2]] in coords_loops_U_control_points:
points_U_crossed_by_strokes.append(i)
# Make a dictionary with the number of the edge, in the selected chain V, corresponding to each stroke
edge_order_number_for_splines = {}
if self.selection_V_exists:
# For two-connected selections add a first hypothetic stroke at the beginning.
if selection_type == "TWO_CONNECTED":
edge_order_number_for_splines[0] = 0
for i in range(len(self.main_splines.data.splines)):
sp = self.main_splines.data.splines[i]
v_idx, _dist_temp = self.shortest_distance(
self.main_object,
sp.bezier_points[0].co,
verts_ordered_V_indices
)
# Get the position (edges count) of the vert v_idx in the selected chain V
edge_idx_in_chain = verts_ordered_V_indices.index(v_idx)
# For two-connected selections the strokes go after the
# hypothetic stroke added before, so the index adds one per spline
if selection_type == "TWO_CONNECTED":
spline_number = i + 1
else:
spline_number = i
edge_order_number_for_splines[spline_number] = edge_idx_in_chain
# Get the first and last verts indices for later comparison
if i == 0:
first_v_idx = v_idx
elif i == len(self.main_splines.data.splines) - 1:
last_v_idx = v_idx
if self.selection_V_is_closed:
# If there is no last stroke on the last vertex (same as first vertex),
# add a hypothetic spline at last vert order
if first_v_idx != last_v_idx:
edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
len(verts_ordered_V_indices) - 1
else:
if self.cyclic_cross:
edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
len(verts_ordered_V_indices) - 2
edge_order_number_for_splines[(len(self.main_splines.data.splines) - 1) + 1] = \
len(verts_ordered_V_indices) - 1
else:
edge_order_number_for_splines[len(self.main_splines.data.splines) - 1] = \
len(verts_ordered_V_indices) - 1
# Get the coords of the points distributed along the
# "crossing curves", with appropriate proportions-V
surface_splines_parsed = []
for i in range(len(splines_U_objects)):
sp_ob = splines_U_objects[i]
# If "Loops on strokes" option is active, calculate the proportions for each loop-U
if self.loops_on_strokes:
# Segments distances from stroke to stroke
dist = 0
full_dist = 0
segments_distances = []
for t in range(len(sp_ob.data.splines[0].bezier_points)):
bp = sp_ob.data.splines[0].bezier_points[t]
if t == 0:
last_p = bp.co
else:
actual_p = bp.co
dist += (last_p - actual_p).length
if t in points_U_crossed_by_strokes:
segments_distances.append(dist)
full_dist += dist
dist = 0
last_p = actual_p
# Calculate Proportions.
used_edges_proportions_V = []
for t in range(len(segments_distances)):
if self.selection_V_exists:
if t == 0:
order_number_last_stroke = 0
segment_edges_length_V = 0
segment_edges_length_V2 = 0
for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
segment_edges_length_V += edges_lengths_V[order]
if self.selection_V2_exists:
segment_edges_length_V2 += edges_lengths_V2[order]
for order in range(order_number_last_stroke, edge_order_number_for_splines[t + 1]):
# Calculate each "sub-segment" (the ones between each stroke) length
if self.selection_V2_exists:
proportion_sub_seg = (edges_lengths_V2[order] -
((edges_lengths_V2[order] - edges_lengths_V[order]) /
len(splines_U_objects) * i)) / (segment_edges_length_V2 -
(segment_edges_length_V2 - segment_edges_length_V) /
len(splines_U_objects) * i)
sub_seg_dist = segments_distances[t] * proportion_sub_seg
else:
proportion_sub_seg = edges_lengths_V[order] / segment_edges_length_V
sub_seg_dist = segments_distances[t] * proportion_sub_seg
used_edges_proportions_V.append(sub_seg_dist / full_dist)
order_number_last_stroke = edge_order_number_for_splines[t + 1]
else:
for _c in range(self.edges_V):
# Calculate each "sub-segment" (the ones between each stroke) length
sub_seg_dist = segments_distances[t] / self.edges_V
used_edges_proportions_V.append(sub_seg_dist / full_dist)
actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
surface_splines_parsed.append(actual_spline[0])
else:
if self.selection_V2_exists:
used_edges_proportions_V = []
for p in range(len(edges_proportions_V)):
used_edges_proportions_V.append(
edges_proportions_V2[p] -
((edges_proportions_V2[p] -
edges_proportions_V[p]) / len(splines_U_objects) * i)
)
else:
used_edges_proportions_V = edges_proportions_V
actual_spline = self.distribute_pts(sp_ob.data.splines, used_edges_proportions_V)
surface_splines_parsed.append(actual_spline[0])
# Set the verts of the first and last splines to the locations
# of the respective verts in the selections
if self.selection_V_exists:
for i in range(0, len(surface_splines_parsed[0])):
surface_splines_parsed[len(surface_splines_parsed) - 1][i] = \
self.main_object.matrix_world @ verts_ordered_V[i].co
if selection_type == "TWO_NOT_CONNECTED":
if self.selection_V2_exists:
for i in range(0, len(surface_splines_parsed[0])):
surface_splines_parsed[0][i] = self.main_object.matrix_world @ verts_ordered_V2[i].co
# When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"),
# merge the verts of the tips of the loops when they are "near enough"
if self.automatic_join and selection_type != "TWO_CONNECTED":
# Join the tips of "Follow" loops that are near enough and must be "closed"
if not self.selection_V_exists and len(edges_proportions_U) >= 3:
for i in range(len(surface_splines_parsed[0])):
sp = surface_splines_parsed
loop_segment_dist = (sp[0][i] - sp[1][i]).length
verts_middle_position_co = [
(sp[0][i][0] + sp[len(sp) - 1][i][0]) / 2,
(sp[0][i][1] + sp[len(sp) - 1][i][1]) / 2,
(sp[0][i][2] + sp[len(sp) - 1][i][2]) / 2
]
points_original = []
points_original.append(sp[1][i])
points_original.append(sp[0][i])
points_target = []
points_target.append(sp[1][i])
points_target.append(Vector(verts_middle_position_co))
vec_A = points_original[0] - points_original[1]
vec_B = points_target[0] - points_target[1]
# check for zero angles, not sure if it is a great fix
if vec_A.length != 0 and vec_B.length != 0:
angle = vec_A.angle(vec_B) / pi
edge_new_length = (Vector(verts_middle_position_co) - sp[1][i]).length
else:
angle = 0
edge_new_length = 0
# If after moving the verts to the middle point, the segment doesn't stretch too much
if edge_new_length <= loop_segment_dist * 1.5 * \
self.join_stretch_factor and angle < 0.25 * self.join_stretch_factor:
# Avoid joining when the actual loop must be merged with the original mesh
if not (self.selection_U_exists and i == 0) and \
not (self.selection_U2_exists and i == len(surface_splines_parsed[0]) - 1):
# Change the coords of both verts to the middle position
surface_splines_parsed[0][i] = verts_middle_position_co
surface_splines_parsed[len(surface_splines_parsed) - 1][i] = verts_middle_position_co
# Delete object with control points and object from grease pencil conversion
bpy.ops.object.delete({"selected_objects": [ob_ctrl_pts]})
bpy.ops.object.delete({"selected_objects": splines_U_objects})
# Generate surface
# Get all verts coords
all_surface_verts_co = []
for i in range(0, len(surface_splines_parsed)):
# Get coords of all verts and make a list with them
for pt_co in surface_splines_parsed[i]:
all_surface_verts_co.append(pt_co)
# Define verts for each face
all_surface_faces = []
for i in range(0, len(all_surface_verts_co) - len(surface_splines_parsed[0])):
if ((i + 1) / len(surface_splines_parsed[0]) != int((i + 1) / len(surface_splines_parsed[0]))):
all_surface_faces.append(
[i + 1, i, i + len(surface_splines_parsed[0]),
i + len(surface_splines_parsed[0]) + 1]
)
# Build the mesh
surf_me_name = "SURFSKIO_surface"
me_surf = bpy.data.meshes.new(surf_me_name)
me_surf.from_pydata(all_surface_verts_co, [], all_surface_faces)
ob_surface = object_utils.object_data_add(context, me_surf)
ob_surface.location = (0.0, 0.0, 0.0)
ob_surface.rotation_euler = (0.0, 0.0, 0.0)
ob_surface.scale = (1.0, 1.0, 1.0)
# Select all the "unselected but participating" verts, from closed selection
# or double selections with middle-vertex, for later join with remove doubles
for v_idx in single_unselected_verts:
self.main_object.data.vertices[v_idx].select = True
# Join the new mesh to the main object
ob_surface.select_set(True)
self.main_object.select_set(True)
bpy.context.view_layer.objects.active = self.main_object
bpy.ops.object.join('INVOKE_REGION_WIN')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.mesh.remove_doubles('INVOKE_REGION_WIN', threshold=0.0001)
bpy.ops.mesh.normals_make_consistent('INVOKE_REGION_WIN', inside=False)
bpy.ops.mesh.select_all('INVOKE_REGION_WIN', action='DESELECT')
self.update()
return{'FINISHED'}
def update(self):
try:
global global_shade_smooth
if global_shade_smooth:
bpy.ops.object.shade_smooth()
else:
bpy.ops.object.shade_flat()
bpy.context.scene.bsurfaces.SURFSK_shade_smooth = global_shade_smooth
except:
pass
return{'FINISHED'}
def execute(self, context):
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
try:
global global_mesh_object
global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
bpy.data.objects[global_mesh_object].select_set(True)
self.main_object = bpy.data.objects[global_mesh_object]
bpy.context.view_layer.objects.active = self.main_object
bsurfaces_props = bpy.context.scene.bsurfaces
except:
self.report({'WARNING'}, "Specify the name of the object with retopology")
return{"CANCELLED"}
bpy.context.view_layer.objects.active = self.main_object
self.update()
if not self.is_fill_faces:
bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
value='True, False, False')
# Build splines from the "last saved splines".
last_saved_curve = bpy.data.curves.new('SURFSKIO_last_crv', 'CURVE')
self.main_splines = bpy.data.objects.new('SURFSKIO_last_crv', last_saved_curve)
bpy.context.collection.objects.link(self.main_splines)
last_saved_curve.dimensions = "3D"
for sp in self.last_strokes_splines_coords:
spline = self.main_splines.data.splines.new('BEZIER')
# less one because one point is added when the spline is created
spline.bezier_points.add(len(sp) - 1)
for p in range(0, len(sp)):
spline.bezier_points[p].co = [sp[p][0], sp[p][1], sp[p][2]]
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
self.main_splines.select_set(True)
bpy.context.view_layer.objects.active = self.main_splines
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
# Important to make it vector first and then automatic, otherwise the
# tips handles get too big and distort the shrinkwrap results later
bpy.ops.curve.handle_type_set(type='VECTOR')
bpy.ops.curve.handle_type_set('INVOKE_REGION_WIN', type='AUTOMATIC')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
self.main_splines.name = "SURFSKIO_temp_strokes"
if self.is_crosshatch:
strokes_for_crosshatch = True
strokes_for_rectangular_surface = False
else:
strokes_for_rectangular_surface = True
strokes_for_crosshatch = False
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
if strokes_for_rectangular_surface:
self.rectangular_surface(context)
elif strokes_for_crosshatch:
self.crosshatch_surface_execute(context)
#Set Shade smooth to new polygons
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
global global_shade_smooth
if global_shade_smooth:
bpy.ops.object.shade_smooth()
else:
bpy.ops.object.shade_flat()
# Delete main splines
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
if self.keep_strokes:
self.main_splines.name = "keep_strokes"
self.main_splines.data.bevel_depth = 0.001
if "keep_strokes_material" in bpy.data.materials :
self.main_splines.data.materials.append(bpy.data.materials["keep_strokes_material"])
else:
mat = bpy.data.materials.new("keep_strokes_material")
mat.diffuse_color = (1, 0, 0, 0)
mat.specular_color = (1, 0, 0)
mat.specular_intensity = 0.0
mat.roughness = 0.0
self.main_splines.data.materials.append(mat)
else:
bpy.ops.object.delete({"selected_objects": [self.main_splines]})
# Delete grease pencil strokes
if self.strokes_type == "GP_STROKES" and not self.stopping_errors:
try:
bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
except:
pass
# Delete annotations
if self.strokes_type == "GP_ANNOTATION" and not self.stopping_errors:
try:
bpy.context.annotation_data.layers.active.clear()
except:
pass
bsurfaces_props = bpy.context.scene.bsurfaces
bsurfaces_props.SURFSK_edges_U = self.edges_U
bsurfaces_props.SURFSK_edges_V = self.edges_V
bsurfaces_props.SURFSK_cyclic_cross = self.cyclic_cross
bsurfaces_props.SURFSK_cyclic_follow = self.cyclic_follow
bsurfaces_props.SURFSK_automatic_join = self.automatic_join
bsurfaces_props.SURFSK_loops_on_strokes = self.loops_on_strokes
bsurfaces_props.SURFSK_keep_strokes = self.keep_strokes
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
self.main_object.select_set(True)
bpy.context.view_layer.objects.active = self.main_object
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
self.update()
return{'FINISHED'}
def invoke(self, context, event):
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bsurfaces_props = bpy.context.scene.bsurfaces
self.cyclic_cross = bsurfaces_props.SURFSK_cyclic_cross
self.cyclic_follow = bsurfaces_props.SURFSK_cyclic_follow
self.automatic_join = bsurfaces_props.SURFSK_automatic_join
self.loops_on_strokes = bsurfaces_props.SURFSK_loops_on_strokes
self.keep_strokes = bsurfaces_props.SURFSK_keep_strokes
try:
global global_mesh_object
global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
bpy.data.objects[global_mesh_object].select_set(True)
self.main_object = bpy.data.objects[global_mesh_object]
bpy.context.view_layer.objects.active = self.main_object
except:
self.report({'WARNING'}, "Specify the name of the object with retopology")
return{"CANCELLED"}
self.update()
self.main_object_selected_verts_count = len([v for v in self.main_object.data.vertices if v.select])
bpy.ops.wm.context_set_value(data_path='tool_settings.mesh_select_mode',
value='True, False, False')
self.edges_U = bsurfaces_props.SURFSK_edges_U
self.edges_V = bsurfaces_props.SURFSK_edges_V
self.is_fill_faces = False
self.stopping_errors = False
self.last_strokes_splines_coords = []
# Determine the type of the strokes
self.strokes_type = get_strokes_type(context)
# Check if it will be used grease pencil strokes or curves
# If there are strokes to be used
if self.strokes_type == "GP_STROKES" or self.strokes_type == "EXTERNAL_CURVE" or self.strokes_type == "GP_ANNOTATION":
if self.strokes_type == "GP_STROKES":
# Convert grease pencil strokes to curve
global global_gpencil_object
gp = bpy.data.objects[global_gpencil_object]
self.original_curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
self.using_external_curves = False
elif self.strokes_type == "GP_ANNOTATION":
# Convert grease pencil strokes to curve
gp = bpy.context.annotation_data
self.original_curve = conver_gpencil_to_curve(self, context, gp, 'Annotation')
self.using_external_curves = False
elif self.strokes_type == "EXTERNAL_CURVE":
global global_curve_object
self.original_curve = bpy.data.objects[global_curve_object]
self.using_external_curves = True
# Make sure there are no objects left from erroneous
# executions of this operator, with the reserved names used here
for o in bpy.data.objects:
if o.name.find("SURFSKIO_") != -1:
bpy.ops.object.delete({"selected_objects": [o]})
bpy.context.view_layer.objects.active = self.original_curve
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
self.temporary_curve = bpy.context.view_layer.objects.active
# Deselect all points of the curve
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Delete splines with only a single isolated point
for i in range(len(self.temporary_curve.data.splines)):
sp = self.temporary_curve.data.splines[i]
if len(sp.bezier_points) == 1:
sp.bezier_points[0].select_control_point = True
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.delete(type='VERT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
self.temporary_curve.select_set(True)
bpy.context.view_layer.objects.active = self.temporary_curve
# Set a minimum number of points for crosshatch
minimum_points_num = 15
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Check if the number of points of each curve has at least the number of points
# of minimum_points_num, which is a bit more than the face-loops limit.
# If not, subdivide to reach at least that number of points
for i in range(len(self.temporary_curve.data.splines)):
sp = self.temporary_curve.data.splines[i]
if len(sp.bezier_points) < minimum_points_num:
for bp in sp.bezier_points:
bp.select_control_point = True
if (len(sp.bezier_points) - 1) != 0:
# Formula to get the number of cuts that will make a curve
# of N number of points have near to "minimum_points_num"
# points, when subdividing with this number of cuts
subdivide_cuts = int(
(minimum_points_num - len(sp.bezier_points)) /
(len(sp.bezier_points) - 1)
) + 1
else:
subdivide_cuts = 0
bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Detect if the strokes are a crosshatch and do it if it is
self.crosshatch_surface_invoke(self.temporary_curve)
if not self.is_crosshatch:
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
self.temporary_curve.select_set(True)
bpy.context.view_layer.objects.active = self.temporary_curve
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Set a minimum number of points for rectangular surfaces
minimum_points_num = 60
# Check if the number of points of each curve has at least the number of points
# of minimum_points_num, which is a bit more than the face-loops limit.
# If not, subdivide to reach at least that number of points
for i in range(len(self.temporary_curve.data.splines)):
sp = self.temporary_curve.data.splines[i]
if len(sp.bezier_points) < minimum_points_num:
for bp in sp.bezier_points:
bp.select_control_point = True
if (len(sp.bezier_points) - 1) != 0:
# Formula to get the number of cuts that will make a curve of
# N number of points have near to "minimum_points_num" points,
# when subdividing with this number of cuts
subdivide_cuts = int(
(minimum_points_num - len(sp.bezier_points)) /
(len(sp.bezier_points) - 1)
) + 1
else:
subdivide_cuts = 0
bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Save coordinates of the actual strokes (as the "last saved splines")
for sp_idx in range(len(self.temporary_curve.data.splines)):
self.last_strokes_splines_coords.append([])
for bp_idx in range(len(self.temporary_curve.data.splines[sp_idx].bezier_points)):
coords = self.temporary_curve.matrix_world @ \
self.temporary_curve.data.splines[sp_idx].bezier_points[bp_idx].co
self.last_strokes_splines_coords[sp_idx].append([coords[0], coords[1], coords[2]])
# Check for cyclic splines, put the first and last points in the middle of their actual positions
for sp_idx in range(len(self.temporary_curve.data.splines)):
if self.temporary_curve.data.splines[sp_idx].use_cyclic_u is True:
first_p_co = self.last_strokes_splines_coords[sp_idx][0]
last_p_co = self.last_strokes_splines_coords[sp_idx][
len(self.last_strokes_splines_coords[sp_idx]) - 1
]
target_co = [
(first_p_co[0] + last_p_co[0]) / 2,
(first_p_co[1] + last_p_co[1]) / 2,
(first_p_co[2] + last_p_co[2]) / 2
]
self.last_strokes_splines_coords[sp_idx][0] = target_co
self.last_strokes_splines_coords[sp_idx][
len(self.last_strokes_splines_coords[sp_idx]) - 1
] = target_co
tuple(self.last_strokes_splines_coords)
# Estimation of the average length of the segments between
# each point of the grease pencil strokes.
# Will be useful to determine whether a curve should be made "Cyclic"
segments_lengths_sum = 0
segments_count = 0
random_spline = self.temporary_curve.data.splines[0].bezier_points
for i in range(0, len(random_spline)):
if i != 0 and len(random_spline) - 1 >= i:
segments_lengths_sum += (random_spline[i - 1].co - random_spline[i].co).length
segments_count += 1
self.average_gp_segment_length = segments_lengths_sum / segments_count
# Delete temporary strokes curve object
bpy.ops.object.delete({"selected_objects": [self.temporary_curve]})
# Set again since "execute()" will turn it again to its initial value
self.execute(context)
if not self.stopping_errors:
# Delete grease pencil strokes
if self.strokes_type == "GP_STROKES":
try:
bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
except:
pass
# Delete annotation strokes
elif self.strokes_type == "GP_ANNOTATION":
try:
bpy.context.annotation_data.layers.active.clear()
except:
pass
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.object.delete({"selected_objects": [self.original_curve]})
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
return {"FINISHED"}
else:
return{"CANCELLED"}
elif self.strokes_type == "SELECTION_ALONE":
self.is_fill_faces = True
created_faces_count = self.fill_with_faces(self.main_object)
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
if created_faces_count == 0:
self.report({'WARNING'}, "There aren't any strokes attached to the object")
return {"CANCELLED"}
else:
return {"FINISHED"}
if self.strokes_type == "EXTERNAL_NO_CURVE":
self.report({'WARNING'}, "The secondary object is not a Curve.")
return{"CANCELLED"}
elif self.strokes_type == "MORE_THAN_ONE_EXTERNAL":
self.report({'WARNING'}, "There shouldn't be more than one secondary object selected.")
return{"CANCELLED"}
elif self.strokes_type == "SINGLE_GP_STROKE_NO_SELECTION" or \
self.strokes_type == "SINGLE_CURVE_STROKE_NO_SELECTION":
self.report({'WARNING'}, "It's needed at least one stroke and one selection, or two strokes.")
return{"CANCELLED"}
elif self.strokes_type == "NO_STROKES":
self.report({'WARNING'}, "There aren't any strokes attached to the object")
return{"CANCELLED"}
elif self.strokes_type == "CURVE_WITH_NON_BEZIER_SPLINES":
self.report({'WARNING'}, "All splines must be Bezier.")
return{"CANCELLED"}
else:
return{"CANCELLED"}
# ----------------------------
# Init operator
class MESH_OT_SURFSK_init(Operator):
bl_idname = "mesh.surfsk_init"
bl_label = "Bsurfaces initialize"
bl_description = "Add an empty mesh object with useful settings"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bs = bpy.context.scene.bsurfaces
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
global global_shade_smooth
global global_mesh_object
global global_gpencil_object
if bs.SURFSK_mesh == None:
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
mesh = bpy.data.meshes.new('BSurfaceMesh')
mesh_object = object_utils.object_data_add(context, mesh)
mesh_object.select_set(True)
bpy.context.view_layer.objects.active = mesh_object
mesh_object.show_all_edges = True
mesh_object.display_type = 'SOLID'
mesh_object.show_wire = True
global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
if global_shade_smooth:
bpy.ops.object.shade_smooth()
else:
bpy.ops.object.shade_flat()
color_red = [1.0, 0.0, 0.0, 0.3]
material = makeMaterial("BSurfaceMesh", color_red)
mesh_object.data.materials.append(material)
bpy.ops.object.modifier_add(type='SHRINKWRAP')
modifier = mesh_object.modifiers["Shrinkwrap"]
if self.active_object is not None:
modifier.target = self.active_object
modifier.wrap_method = 'TARGET_PROJECT'
modifier.wrap_mode = 'OUTSIDE_SURFACE'
modifier.show_on_cage = True
global_mesh_object = mesh_object.name
bpy.context.scene.bsurfaces.SURFSK_mesh = bpy.data.objects[global_mesh_object]
bpy.context.scene.tool_settings.snap_elements = {'FACE'}
bpy.context.scene.tool_settings.use_snap = True
bpy.context.scene.tool_settings.use_snap_self = False
bpy.context.scene.tool_settings.use_snap_align_rotation = True
bpy.context.scene.tool_settings.use_snap_project = True
bpy.context.scene.tool_settings.use_snap_rotate = True
bpy.context.scene.tool_settings.use_snap_scale = True
bpy.context.scene.tool_settings.use_mesh_automerge = True
bpy.context.scene.tool_settings.double_threshold = 0.01
if context.scene.bsurfaces.SURFSK_guide == 'GPencil' and bs.SURFSK_gpencil == None:
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.gpencil_add(radius=1.0, align='WORLD', location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0), type='EMPTY')
bpy.context.scene.tool_settings.gpencil_stroke_placement_view3d = 'SURFACE'
gpencil_object = bpy.context.scene.objects[bpy.context.scene.objects[-1].name]
gpencil_object.select_set(True)
bpy.context.view_layer.objects.active = gpencil_object
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
global_gpencil_object = gpencil_object.name
bpy.context.scene.bsurfaces.SURFSK_gpencil = bpy.data.objects[global_gpencil_object]
gpencil_object.data.stroke_depth_order = '3D'
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
if context.scene.bsurfaces.SURFSK_guide == 'Annotation':
bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
def invoke(self, context, event):
if bpy.context.active_object:
self.active_object = bpy.context.active_object
else:
self.active_object = None
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Add modifiers operator
class MESH_OT_SURFSK_add_modifiers(Operator):
bl_idname = "mesh.surfsk_add_modifiers"
bl_label = "Add Mirror and others modifiers"
bl_description = "Add modifiers: Mirror, Shrinkwrap, Subdivision, Solidify"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bs = bpy.context.scene.bsurfaces
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
if bs.SURFSK_mesh == None:
self.report({'ERROR_INVALID_INPUT'}, "Please select Mesh of BSurface or click Initialize")
else:
mesh_object = bs.SURFSK_mesh
try:
mesh_object.select_set(True)
except:
self.report({'ERROR_INVALID_INPUT'}, "Mesh of BSurface does not exist")
return {"CANCEL"}
bpy.context.view_layer.objects.active = mesh_object
try:
shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
if self.active_object is not None and self.active_object != mesh_object:
shrinkwrap.target = self.active_object
shrinkwrap.wrap_method = 'TARGET_PROJECT'
shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
shrinkwrap.show_on_cage = True
shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
except:
bpy.ops.object.modifier_add(type='SHRINKWRAP')
shrinkwrap = mesh_object.modifiers["Shrinkwrap"]
if self.active_object is not None and self.active_object != mesh_object:
shrinkwrap.target = self.active_object
shrinkwrap.wrap_method = 'TARGET_PROJECT'
shrinkwrap.wrap_mode = 'OUTSIDE_SURFACE'
shrinkwrap.show_on_cage = True
shrinkwrap.offset = bpy.context.scene.bsurfaces.SURFSK_Shrinkwrap_offset
try:
mirror = mesh_object.modifiers["Mirror"]
mirror.use_clip = True
except:
bpy.ops.object.modifier_add(type='MIRROR')
mirror = mesh_object.modifiers["Mirror"]
mirror.use_clip = True
try:
_subsurf = mesh_object.modifiers["Subdivision"]
except:
bpy.ops.object.modifier_add(type='SUBSURF')
_subsurf = mesh_object.modifiers["Subdivision"]
try:
solidify = mesh_object.modifiers["Solidify"]
solidify.thickness = 0.01
except:
bpy.ops.object.modifier_add(type='SOLIDIFY')
solidify = mesh_object.modifiers["Solidify"]
solidify.thickness = 0.01
return {"FINISHED"}
def invoke(self, context, event):
if bpy.context.active_object:
self.active_object = bpy.context.active_object
else:
self.active_object = None
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Edit surface operator
class MESH_OT_SURFSK_edit_surface(Operator):
bl_idname = "mesh.surfsk_edit_surface"
bl_label = "Bsurfaces edit surface"
bl_description = "Edit surface mesh"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.context.scene.bsurfaces.SURFSK_mesh.select_set(True)
bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_mesh
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
bpy.ops.wm.tool_set_by_id(name="builtin.select")
def invoke(self, context, event):
try:
global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
bpy.data.objects[global_mesh_object].select_set(True)
self.main_object = bpy.data.objects[global_mesh_object]
bpy.context.view_layer.objects.active = self.main_object
except:
self.report({'WARNING'}, "Specify the name of the object with retopology")
return{"CANCELLED"}
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Add strokes operator
class GPENCIL_OT_SURFSK_add_strokes(Operator):
bl_idname = "gpencil.surfsk_add_strokes"
bl_label = "Bsurfaces add strokes"
bl_description = "Add the grease pencil strokes"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_gpencil
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='PAINT_GPENCIL')
bpy.ops.wm.tool_set_by_id(name="builtin_brush.Draw")
return{"FINISHED"}
def invoke(self, context, event):
try:
bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
except:
self.report({'WARNING'}, "Specify the name of the object with strokes")
return{"CANCELLED"}
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Edit strokes operator
class GPENCIL_OT_SURFSK_edit_strokes(Operator):
bl_idname = "gpencil.surfsk_edit_strokes"
bl_label = "Bsurfaces edit strokes"
bl_description = "Edit the grease pencil strokes"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil
gpencil_object.select_set(True)
bpy.context.view_layer.objects.active = gpencil_object
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT_GPENCIL')
try:
bpy.ops.gpencil.select_all(action='SELECT')
except:
pass
def invoke(self, context, event):
try:
bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
except:
self.report({'WARNING'}, "Specify the name of the object with strokes")
return{"CANCELLED"}
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Convert annotation to curves operator
class GPENCIL_OT_SURFSK_annotation_to_curves(Operator):
bl_idname = "gpencil.surfsk_annotations_to_curves"
bl_label = "Convert annotation to curves"
bl_description = "Convert annotation to curves for editing"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
# Convert annotation to curve
curve = conver_gpencil_to_curve(self, context, None, 'Annotation')
if curve != None:
# Delete annotation strokes
try:
bpy.context.annotation_data.layers.active.clear()
except:
pass
# Clean up curves
curve.select_set(True)
bpy.context.view_layer.objects.active = curve
bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
return {"FINISHED"}
def invoke(self, context, event):
try:
strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
_strokes_num = len(strokes)
except:
self.report({'WARNING'}, "Not active annotation")
return{"CANCELLED"}
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Convert strokes to curves operator
class GPENCIL_OT_SURFSK_strokes_to_curves(Operator):
bl_idname = "gpencil.surfsk_strokes_to_curves"
bl_label = "Convert strokes to curves"
bl_description = "Convert grease pencil strokes to curves for editing"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
# Convert grease pencil strokes to curve
gp = bpy.context.scene.bsurfaces.SURFSK_gpencil
curve = conver_gpencil_to_curve(self, context, gp, 'GPensil')
if curve != None:
# Delete grease pencil strokes
try:
bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
except:
pass
# Clean up curves
curve.select_set(True)
bpy.context.view_layer.objects.active = curve
bpy.ops.wm.tool_set_by_id(name="builtin.select_box")
return {"FINISHED"}
def invoke(self, context, event):
try:
bpy.context.scene.bsurfaces.SURFSK_gpencil.select_set(True)
except:
self.report({'WARNING'}, "Specify the name of the object with strokes")
return{"CANCELLED"}
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Add annotation
class GPENCIL_OT_SURFSK_add_annotation(Operator):
bl_idname = "gpencil.surfsk_add_annotation"
bl_label = "Bsurfaces add annotation"
bl_description = "Add annotation"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.wm.tool_set_by_id(name="builtin.annotate")
bpy.context.scene.tool_settings.annotation_stroke_placement_view3d = 'SURFACE'
return{"FINISHED"}
def invoke(self, context, event):
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Edit curve operator
class CURVE_OT_SURFSK_edit_curve(Operator):
bl_idname = "curve.surfsk_edit_curve"
bl_label = "Bsurfaces edit curve"
bl_description = "Edit curve"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
bpy.context.view_layer.objects.active = bpy.context.scene.bsurfaces.SURFSK_curve
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='EDIT')
def invoke(self, context, event):
try:
bpy.context.scene.bsurfaces.SURFSK_curve.select_set(True)
except:
self.report({'WARNING'}, "Specify the name of the object with curve")
return{"CANCELLED"}
self.execute(context)
return {"FINISHED"}
# ----------------------------
# Reorder splines
class CURVE_OT_SURFSK_reorder_splines(Operator):
bl_idname = "curve.surfsk_reorder_splines"
bl_label = "Bsurfaces reorder splines"
bl_description = "Defines the order of the splines by using grease pencil strokes"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
objects_to_delete = []
# Convert grease pencil strokes to curve.
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.gpencil.convert('INVOKE_REGION_WIN', type='CURVE', use_link_strokes=False)
for ob in bpy.context.selected_objects:
if ob != bpy.context.view_layer.objects.active and ob.name.startswith("GP_Layer"):
GP_strokes_curve = ob
# GP_strokes_curve = bpy.context.object
objects_to_delete.append(GP_strokes_curve)
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
GP_strokes_curve.select_set(True)
bpy.context.view_layer.objects.active = GP_strokes_curve
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='SELECT')
bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=100)
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
GP_strokes_mesh = bpy.context.object
objects_to_delete.append(GP_strokes_mesh)
GP_strokes_mesh.data.resolution_u = 1
bpy.ops.object.convert(target='MESH', keep_original=False)
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
self.main_curve.select_set(True)
bpy.context.view_layer.objects.active = self.main_curve
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
curves_duplicate_1 = bpy.context.object
objects_to_delete.append(curves_duplicate_1)
minimum_points_num = 500
# Some iterations since the subdivision operator
# has a limit of 100 subdivisions per iteration
for x in range(round(minimum_points_num / 100)):
# Check if the number of points of each curve has at least the number of points
# of minimum_points_num. If not, subdivide to reach at least that number of points
for i in range(len(curves_duplicate_1.data.splines)):
sp = curves_duplicate_1.data.splines[i]
if len(sp.bezier_points) < minimum_points_num:
for bp in sp.bezier_points:
bp.select_control_point = True
if (len(sp.bezier_points) - 1) != 0:
# Formula to get the number of cuts that will make a curve of N
# number of points have near to "minimum_points_num" points,
# when subdividing with this number of cuts
subdivide_cuts = int(
(minimum_points_num - len(sp.bezier_points)) /
(len(sp.bezier_points) - 1)
) + 1
else:
subdivide_cuts = 0
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.subdivide('INVOKE_REGION_WIN', number_cuts=subdivide_cuts)
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.object.duplicate('INVOKE_REGION_WIN')
curves_duplicate_2 = bpy.context.object
objects_to_delete.append(curves_duplicate_2)
# Duplicate the duplicate and add Shrinkwrap to it, with the grease pencil strokes curve as target
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
curves_duplicate_2.select_set(True)
bpy.context.view_layer.objects.active = curves_duplicate_2
bpy.ops.object.modifier_add('INVOKE_REGION_WIN', type='SHRINKWRAP')
curves_duplicate_2.modifiers["Shrinkwrap"].wrap_method = "NEAREST_VERTEX"
curves_duplicate_2.modifiers["Shrinkwrap"].target = GP_strokes_mesh
bpy.ops.object.modifier_apply('INVOKE_REGION_WIN', modifier='Shrinkwrap')
# Get the distance of each vert from its original position to its position with Shrinkwrap
nearest_points_coords = {}
for st_idx in range(len(curves_duplicate_1.data.splines)):
for bp_idx in range(len(curves_duplicate_1.data.splines[st_idx].bezier_points)):
bp_1_co = curves_duplicate_1.matrix_world @ \
curves_duplicate_1.data.splines[st_idx].bezier_points[bp_idx].co
bp_2_co = curves_duplicate_2.matrix_world @ \
curves_duplicate_2.data.splines[st_idx].bezier_points[bp_idx].co
if bp_idx == 0:
shortest_dist = (bp_1_co - bp_2_co).length
nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
"%.4f" % bp_2_co[1],
"%.4f" % bp_2_co[2])
dist = (bp_1_co - bp_2_co).length
if dist < shortest_dist:
nearest_points_coords[st_idx] = ("%.4f" % bp_2_co[0],
"%.4f" % bp_2_co[1],
"%.4f" % bp_2_co[2])
shortest_dist = dist
# Get all coords of GP strokes points, for comparison
GP_strokes_coords = []
for st_idx in range(len(GP_strokes_curve.data.splines)):
GP_strokes_coords.append(
[("%.4f" % x if "%.4f" % x != "-0.00" else "0.00",
"%.4f" % y if "%.4f" % y != "-0.00" else "0.00",
"%.4f" % z if "%.4f" % z != "-0.00" else "0.00") for
x, y, z in [bp.co for bp in GP_strokes_curve.data.splines[st_idx].bezier_points]]
)
# Check the point of the GP strokes with the same coords as
# the nearest points of the curves (with shrinkwrap)
# Dictionary with GP stroke index as index, and a list as value.
# The list has as index the point index of the GP stroke
# nearest to the spline, and as value the spline index
GP_connection_points = {}
for gp_st_idx in range(len(GP_strokes_coords)):
GPvert_spline_relationship = {}
for splines_st_idx in range(len(nearest_points_coords)):
if nearest_points_coords[splines_st_idx] in GP_strokes_coords[gp_st_idx]:
GPvert_spline_relationship[
GP_strokes_coords[gp_st_idx].index(nearest_points_coords[splines_st_idx])
] = splines_st_idx
GP_connection_points[gp_st_idx] = GPvert_spline_relationship
# Get the splines new order
splines_new_order = []
for i in GP_connection_points:
dict_keys = sorted(GP_connection_points[i].keys()) # Sort dictionaries by key
for k in dict_keys:
splines_new_order.append(GP_connection_points[i][k])
# Reorder
curve_original_name = self.main_curve.name
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
self.main_curve.select_set(True)
bpy.context.view_layer.objects.active = self.main_curve
self.main_curve.name = "SURFSKIO_CRV_ORD"
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
for _sp_idx in range(len(self.main_curve.data.splines)):
self.main_curve.data.splines[0].bezier_points[0].select_control_point = True
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.separate('EXEC_REGION_WIN')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
# Get the names of the separated splines objects in the original order
splines_unordered = {}
for o in bpy.data.objects:
if o.name.find("SURFSKIO_CRV_ORD") != -1:
spline_order_string = o.name.partition(".")[2]
if spline_order_string != "" and int(spline_order_string) > 0:
spline_order_index = int(spline_order_string) - 1
splines_unordered[spline_order_index] = o.name
# Join all splines objects in final order
for order_idx in splines_new_order:
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.data.objects[splines_unordered[order_idx]].select_set(True)
bpy.data.objects["SURFSKIO_CRV_ORD"].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects["SURFSKIO_CRV_ORD"]
bpy.ops.object.join('INVOKE_REGION_WIN')
# Go back to the original name of the curves object.
bpy.context.object.name = curve_original_name
# Delete all unused objects
bpy.ops.object.delete({"selected_objects": objects_to_delete})
bpy.ops.object.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.data.objects[curve_original_name].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[curve_original_name]
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
try:
bpy.context.scene.bsurfaces.SURFSK_gpencil.data.layers.active.clear()
except:
pass
return {"FINISHED"}
def invoke(self, context, event):
self.main_curve = bpy.context.object
there_are_GP_strokes = False
try:
# Get the active grease pencil layer
strokes_num = len(self.main_curve.grease_pencil.layers.active.active_frame.strokes)
if strokes_num > 0:
there_are_GP_strokes = True
except:
pass
if there_are_GP_strokes:
self.execute(context)
self.report({'INFO'}, "Splines have been reordered")
else:
self.report({'WARNING'}, "Draw grease pencil strokes to connect splines")
return {"FINISHED"}
# ----------------------------
# Set first points operator
class CURVE_OT_SURFSK_first_points(Operator):
bl_idname = "curve.surfsk_first_points"
bl_label = "Bsurfaces set first points"
bl_description = "Set the selected points as the first point of each spline"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
splines_to_invert = []
# Check non-cyclic splines to invert
for i in range(len(self.main_curve.data.splines)):
b_points = self.main_curve.data.splines[i].bezier_points
if i not in self.cyclic_splines: # Only for non-cyclic splines
if b_points[len(b_points) - 1].select_control_point:
splines_to_invert.append(i)
# Reorder points of cyclic splines, and set all handles to "Automatic"
# Check first selected point
cyclic_splines_new_first_pt = {}
for i in self.cyclic_splines:
sp = self.main_curve.data.splines[i]
for t in range(len(sp.bezier_points)):
bp = sp.bezier_points[t]
if bp.select_control_point or bp.select_right_handle or bp.select_left_handle:
cyclic_splines_new_first_pt[i] = t
break # To take only one if there are more
# Reorder
for spline_idx in cyclic_splines_new_first_pt:
sp = self.main_curve.data.splines[spline_idx]
spline_old_coords = []
for bp_old in sp.bezier_points:
coords = (bp_old.co[0], bp_old.co[1], bp_old.co[2])
left_handle_type = str(bp_old.handle_left_type)
left_handle_length = float(bp_old.handle_left.length)
left_handle_xyz = (
float(bp_old.handle_left.x),
float(bp_old.handle_left.y),
float(bp_old.handle_left.z)
)
right_handle_type = str(bp_old.handle_right_type)
right_handle_length = float(bp_old.handle_right.length)
right_handle_xyz = (
float(bp_old.handle_right.x),
float(bp_old.handle_right.y),
float(bp_old.handle_right.z)
)
spline_old_coords.append(
[coords, left_handle_type,
right_handle_type, left_handle_length,
right_handle_length, left_handle_xyz,
right_handle_xyz]
)
for t in range(len(sp.bezier_points)):
bp = sp.bezier_points
if t + cyclic_splines_new_first_pt[spline_idx] + 1 <= len(bp) - 1:
new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1
else:
new_index = t + cyclic_splines_new_first_pt[spline_idx] + 1 - len(bp)
bp[t].co = Vector(spline_old_coords[new_index][0])
bp[t].handle_left.length = spline_old_coords[new_index][3]
bp[t].handle_right.length = spline_old_coords[new_index][4]
bp[t].handle_left_type = "FREE"
bp[t].handle_right_type = "FREE"
bp[t].handle_left.x = spline_old_coords[new_index][5][0]
bp[t].handle_left.y = spline_old_coords[new_index][5][1]
bp[t].handle_left.z = spline_old_coords[new_index][5][2]
bp[t].handle_right.x = spline_old_coords[new_index][6][0]
bp[t].handle_right.y = spline_old_coords[new_index][6][1]
bp[t].handle_right.z = spline_old_coords[new_index][6][2]
bp[t].handle_left_type = spline_old_coords[new_index][1]
bp[t].handle_right_type = spline_old_coords[new_index][2]
# Invert the non-cyclic splines designated above
for i in range(len(splines_to_invert)):
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
self.main_curve.data.splines[splines_to_invert[i]].bezier_points[0].select_control_point = True
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
bpy.ops.curve.switch_direction()
bpy.ops.curve.select_all('INVOKE_REGION_WIN', action='DESELECT')
# Keep selected the first vert of each spline
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
for i in range(len(self.main_curve.data.splines)):
if not self.main_curve.data.splines[i].use_cyclic_u:
bp = self.main_curve.data.splines[i].bezier_points[0]
else:
bp = self.main_curve.data.splines[i].bezier_points[
len(self.main_curve.data.splines[i].bezier_points) - 1
]
bp.select_control_point = True
bp.select_right_handle = True
bp.select_left_handle = True
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
return {'FINISHED'}
def invoke(self, context, event):
self.main_curve = bpy.context.object
# Check if all curves are Bezier, and detect which ones are cyclic
self.cyclic_splines = []
for i in range(len(self.main_curve.data.splines)):
if self.main_curve.data.splines[i].type != "BEZIER":
self.report({'WARNING'}, "All splines must be Bezier type")
return {'CANCELLED'}
else:
if self.main_curve.data.splines[i].use_cyclic_u:
self.cyclic_splines.append(i)
self.execute(context)
self.report({'INFO'}, "First points have been set")
return {'FINISHED'}
# Add-ons Preferences Update Panel
# Define Panel classes for updating
panels = (
VIEW3D_PT_tools_SURFSK_mesh,
VIEW3D_PT_tools_SURFSK_curve
)
def conver_gpencil_to_curve(self, context, pencil, type):
newCurve = bpy.data.curves.new(type + '_curve', type='CURVE')
newCurve.dimensions = '3D'
CurveObject = object_utils.object_data_add(context, newCurve)
error = False
if type == 'GPensil':
try:
strokes = pencil.data.layers.active.active_frame.strokes
except:
error = True
CurveObject.location = pencil.location
CurveObject.rotation_euler = pencil.rotation_euler
CurveObject.scale = pencil.scale
elif type == 'Annotation':
try:
strokes = bpy.context.annotation_data.layers.active.active_frame.strokes
except:
error = True
CurveObject.location = (0.0, 0.0, 0.0)
CurveObject.rotation_euler = (0.0, 0.0, 0.0)
CurveObject.scale = (1.0, 1.0, 1.0)
if not error:
for i, _stroke in enumerate(strokes):
stroke_points = strokes[i].points
data_list = [ (point.co.x, point.co.y, point.co.z)
for point in stroke_points ]
points_to_add = len(data_list)-1
flat_list = []
for point in data_list:
flat_list.extend(point)
spline = newCurve.splines.new(type='BEZIER')
spline.bezier_points.add(points_to_add)
spline.bezier_points.foreach_set("co", flat_list)
for point in spline.bezier_points:
point.handle_left_type="AUTO"
point.handle_right_type="AUTO"
return CurveObject
else:
return None
def update_panel(self, context):
message = "Bsurfaces GPL Edition: Updating Panel locations has failed"
try:
for panel in panels:
if "bl_rna" in panel.__dict__:
bpy.utils.unregister_class(panel)
for panel in panels:
category = context.preferences.addons[__name__].preferences.category
if category != 'Tool':
panel.bl_category = context.preferences.addons[__name__].preferences.category
else:
context.preferences.addons[__name__].preferences.category = 'Edit'
panel.bl_category = 'Edit'
raise ValueError("You can not install add-ons in the Tool panel")
bpy.utils.register_class(panel)
except Exception as e:
print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
pass
def makeMaterial(name, diffuse):
if name in bpy.data.materials:
material = bpy.data.materials[name]
material.diffuse_color = diffuse
else:
material = bpy.data.materials.new(name)
material.diffuse_color = diffuse
return material
def update_mesh(self, context):
try:
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.update()
global global_mesh_object
global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
bpy.data.objects[global_mesh_object].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[global_mesh_object]
except:
print("Select mesh object")
def update_gpencil(self, context):
try:
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.update()
global global_gpencil_object
global_gpencil_object = bpy.context.scene.bsurfaces.SURFSK_gpencil.name
bpy.data.objects[global_gpencil_object].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[global_gpencil_object]
except:
print("Select gpencil object")
def update_curve(self, context):
try:
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.update()
global global_curve_object
global_curve_object = bpy.context.scene.bsurfaces.SURFSK_curve.name
bpy.data.objects[global_curve_object].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[global_curve_object]
except:
print("Select curve object")
def update_shade_smooth(self, context):
try:
global global_shade_smooth
global_shade_smooth = bpy.context.scene.bsurfaces.SURFSK_shade_smooth
contex_mode = bpy.context.mode
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set('INVOKE_REGION_WIN', mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
global global_mesh_object
global_mesh_object = bpy.context.scene.bsurfaces.SURFSK_mesh.name
bpy.data.objects[global_mesh_object].select_set(True)
if global_shade_smooth:
bpy.ops.object.shade_smooth()
else:
bpy.ops.object.shade_flat()
if contex_mode == "EDIT_MESH":
bpy.ops.object.editmode_toggle('INVOKE_REGION_WIN')
except:
print("Select mesh object")
class BsurfPreferences(AddonPreferences):
# this must match the addon name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __name__
category: StringProperty(
name="Tab Category",
description="Choose a name for the category of the panel",
default="Edit",
update=update_panel
)
def draw(self, context):
layout = self.layout
row = layout.row()
col = row.column()
col.label(text="Tab Category:")
col.prop(self, "category", text="")
# Properties
class BsurfacesProps(PropertyGroup):
SURFSK_guide: EnumProperty(
name="Guide:",
items=[
('Annotation', 'Annotation', 'Annotation'),
('GPencil', 'GPencil', 'GPencil'),
('Curve', 'Curve', 'Curve')
],
default="Annotation"
)
SURFSK_edges_U: IntProperty(
name="Cross",
description="Number of face-loops crossing the strokes",
default=5,
min=1,
max=200
)
SURFSK_edges_V: IntProperty(
name="Follow",
description="Number of face-loops following the strokes",
default=1,
min=1,
max=200
)
SURFSK_cyclic_cross: BoolProperty(
name="Cyclic Cross",
description="Make cyclic the face-loops crossing the strokes",
default=False
)
SURFSK_cyclic_follow: BoolProperty(
name="Cyclic Follow",
description="Make cyclic the face-loops following the strokes",
default=False
)
SURFSK_keep_strokes: BoolProperty(
name="Keep strokes",
description="Keeps the sketched strokes or curves after adding the surface",
default=False
)
SURFSK_automatic_join: BoolProperty(
name="Automatic join",
description="Join automatically vertices of either surfaces "
"generated by crosshatching, or from the borders of closed shapes",
default=True
)
SURFSK_loops_on_strokes: BoolProperty(
name="Loops on strokes",
description="Make the loops match the paths of the strokes",
default=True
)
SURFSK_precision: IntProperty(
name="Precision",
description="Precision level of the surface calculation",
default=2,
min=1,
max=100
)
SURFSK_mesh: PointerProperty(
name="Mesh of BSurface",
type=bpy.types.Object,
description="Mesh of BSurface",
update=update_mesh,
)
SURFSK_gpencil: PointerProperty(
name="GreasePencil object",
type=bpy.types.Object,
description="GreasePencil object",
update=update_gpencil,
)
SURFSK_curve: PointerProperty(
name="Curve object",
type=bpy.types.Object,
description="Curve object",
update=update_curve,
)
SURFSK_shade_smooth: BoolProperty(
name="Shade smooth",
description="Render and display faces smooth, using interpolated Vertex Normals",
default=False,
update=update_shade_smooth,
)
classes = (
MESH_OT_SURFSK_init,
MESH_OT_SURFSK_add_modifiers,
MESH_OT_SURFSK_add_surface,
MESH_OT_SURFSK_edit_surface,
GPENCIL_OT_SURFSK_add_strokes,
GPENCIL_OT_SURFSK_edit_strokes,
GPENCIL_OT_SURFSK_strokes_to_curves,
GPENCIL_OT_SURFSK_annotation_to_curves,
GPENCIL_OT_SURFSK_add_annotation,
CURVE_OT_SURFSK_edit_curve,
CURVE_OT_SURFSK_reorder_splines,
CURVE_OT_SURFSK_first_points,
BsurfPreferences,
BsurfacesProps
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
for panel in panels:
bpy.utils.register_class(panel)
bpy.types.Scene.bsurfaces = PointerProperty(type=BsurfacesProps)
update_panel(None, bpy.context)
def unregister():
for panel in panels:
bpy.utils.unregister_class(panel)
for cls in classes:
bpy.utils.unregister_class(cls)
del bpy.types.Scene.bsurfaces
if __name__ == "__main__":
register()
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/keytoon/blender-addons.git
git@gitee.com:keytoon/blender-addons.git
keytoon
blender-addons
blender-addons
master

搜索帮助