Compare commits
87 Commits
8d3b3af882
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d558fac2c3 | |||
| e8da519d29 | |||
| cd87a0b7f4 | |||
| c0624c0a62 | |||
| e531217cb7 | |||
| b6ba83eb5d | |||
| 05a289dd65 | |||
| 82d0939a18 | |||
| 220a1e2bb3 | |||
| b9a2124026 | |||
| 84ff943f92 | |||
| 24f6b76f06 | |||
| 9fcada260e | |||
| b9f661cdb3 | |||
| 375487c10c | |||
| 92b4baa285 | |||
| 18443b60c3 | |||
| dcfafb18d1 | |||
| 118bc51cc5 | |||
| df56a54b66 | |||
| 9cce8199c3 | |||
| e50134ce32 | |||
| c81d2c0c43 | |||
| 26214a704d | |||
| 3e54fa46a6 | |||
| 17ff7a8017 | |||
| 9999f3d0ad | |||
| 3609236da9 | |||
| e9e727c66f | |||
| bc87ae1acc | |||
| 0171785b42 | |||
| f011e2ca94 | |||
| 5fdad504da | |||
| 0cc06364e8 | |||
| 5c7611ad40 | |||
| 66971eaa7a | |||
| 98824c1680 | |||
| 080659ab95 | |||
| 250853d7d0 | |||
| 18d6d98e07 | |||
| 7930705d01 | |||
| d8966cc035 | |||
| e406e8d9b2 | |||
| bb64e4d41e | |||
| 6060c74b17 | |||
| 970281e10a | |||
| bcf7d557b1 | |||
| d9589e99f5 | |||
| f8d1cfe3fe | |||
| 264327432d | |||
| 6a13ede6b7 | |||
| edaf83229b | |||
| 6fee7bd143 | |||
| 61923e1b2b | |||
| e2d66a5d64 | |||
| 1c3b0f3919 | |||
| 2a838aee93 | |||
| f457db93e7 | |||
| 975071c995 | |||
| ac7b2f2ee5 | |||
| 2386366566 | |||
| 7fbda8c289 | |||
| f208e7fc00 | |||
| 662ce87e98 | |||
| 46970fd4f0 | |||
| eff0878f53 | |||
| 3c28d2e29c | |||
| f1860ae85d | |||
| da0fd365f2 | |||
| cd626b0707 | |||
| dd5ccec881 | |||
| 95678d4394 | |||
| 067cb56584 | |||
| b10760a704 | |||
| e56ee2cb8f | |||
| 238d7d062b | |||
| 01b6501a0c | |||
| 02a00a9b4a | |||
| 13a5e1eb7a | |||
| e1b63aa4e6 | |||
| 827bd8d4d7 | |||
| 68b9d14453 | |||
| c993935b17 | |||
| de57c320c2 | |||
| 8184f559fc | |||
| 8f691e37c4 | |||
| a597b58c93 |
@@ -26,6 +26,12 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
|
||||||
|
# Rhino-Testdateien (rhino/-Ordner)
|
||||||
|
rhino/*.3dm
|
||||||
|
rhino/*.3dm.thumb.png
|
||||||
|
rhino/*.3dmbak
|
||||||
|
rhino/dossier.project.json
|
||||||
|
|
||||||
# Claude Code
|
# Claude Code
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# DOSSIER C# Plugin — Build & Install
|
||||||
|
|
||||||
|
Das Plugin (`.rhp`) bootstrappt beim Rhino-Start die Python-Module und
|
||||||
|
registriert native Commands (`dWall`, `dDoor`, `dSlab`, …).
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install dotnet@7
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder direkt von Microsoft: https://dotnet.microsoft.com/download/dotnet/7.0
|
||||||
|
|
||||||
|
RhinoCommon wird beim ersten Build automatisch via NuGet geladen.
|
||||||
|
|
||||||
|
## Repo-Pfad setzen (nach Neuinstallation wichtig)
|
||||||
|
|
||||||
|
Das Plugin sucht das Repo in dieser Reihenfolge:
|
||||||
|
|
||||||
|
1. Env-Var `DOSSIER_HOME`
|
||||||
|
2. Datei `~/.dossier_home` (eine Zeile: absoluter Pfad zum Repo-Root)
|
||||||
|
3. Hardcoded Fallback `/Users/karim/STUDIO/DOSSIER`
|
||||||
|
|
||||||
|
Einfachste Variante — einmalig nach dem Klonen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "/Users/karim/PROJECTS/DOSSIER" > ~/.dossier_home
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne das findet das Plugin `rhino/startup.py` nicht und bootet nicht.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd csharp/DOSSIER
|
||||||
|
./build.sh # Release → bin/Release/net7.0/DOSSIER.rhp
|
||||||
|
./build.sh debug # Debug-Build mit Symbols
|
||||||
|
./build.sh clean # bin/ + obj/ löschen
|
||||||
|
./build.sh install # Build + yak install in Rhino-User-Plugin-Pfad
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation in Rhino (einmalig nach Build)
|
||||||
|
|
||||||
|
Mac Rhino 8 unterstützt kein Drag & Drop für `.rhp`-Dateien.
|
||||||
|
|
||||||
|
1. Rhino 8 öffnen
|
||||||
|
2. Befehl: `PluginManager`
|
||||||
|
3. Button **Install…** → `csharp/DOSSIER/bin/Release/net7.0/DOSSIER.rhp`
|
||||||
|
4. Rhino neu starten
|
||||||
|
|
||||||
|
Der Pfad bleibt in Rhinos Settings-XML registriert. Bei späteren Builds
|
||||||
|
einfach wieder in denselben Output-Pfad bauen — Rhino lädt den neuen Stand
|
||||||
|
automatisch beim nächsten Start.
|
||||||
|
|
||||||
|
## Startup-Eintrag (Python-Bootstrap)
|
||||||
|
|
||||||
|
Der Launcher trägt den Startup-Eintrag automatisch ein. Für manuelle
|
||||||
|
Dev-Setups ohne Launcher:
|
||||||
|
|
||||||
|
Rhino → Options → General → „Run these commands every time a model is opened":
|
||||||
|
|
||||||
|
```
|
||||||
|
_-RunPythonScript "/Users/karim/PROJECTS/DOSSIER/rhino/startup.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
(Mit Dash, mit Quotes, voller Pfad — siehe CLAUDE.md für Details warum.)
|
||||||
|
|
||||||
|
## Nach Neuinstallation Mac — Checkliste
|
||||||
|
|
||||||
|
- [ ] Repo klonen: `git clone https://git.kgva.ch/karim/DOSSIER.git`
|
||||||
|
- [ ] `echo "/Users/karim/PROJECTS/DOSSIER" > ~/.dossier_home`
|
||||||
|
- [ ] `brew install dotnet@7 node`
|
||||||
|
- [ ] `cd DOSSIER && npm install && npm run build`
|
||||||
|
- [ ] `cd csharp/DOSSIER && ./build.sh install`
|
||||||
|
- [ ] Rhino öffnen → PluginManager → Install → `.rhp` registrieren
|
||||||
|
- [ ] Rhino neu starten → DOSSIER bootstrappt (Panels + Commands)
|
||||||
|
- [ ] `~/Library/Application Support/RhinoPanel/override_presets.json` von Backup zurückkopieren
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
dist/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
.vs/
|
||||||
|
.idea/
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
//
|
||||||
|
// BIM-Commands: jeweils Wrapper auf das Python-Script in rhino/aliases/cmd/.
|
||||||
|
// Naming-Convention: d-Prefix + englischer BIM-Begriff (VisualARQ-Stil).
|
||||||
|
// Klassen-Guids sind frei generiert (uuidgen) — wichtig nur dass sie
|
||||||
|
// stabil bleiben, damit Rhino sie ueber Sessions wiedererkennt.
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace DOSSIER.Cmd;
|
||||||
|
|
||||||
|
[Guid("9A87B609-719F-468B-AF2A-6E59A9B61062")]
|
||||||
|
public class DWall : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dWall";
|
||||||
|
protected override string ScriptRelativePath => "cmd/wand.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("80278984-16B8-485B-8876-3D63806BCA58")]
|
||||||
|
public class DDoor : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dDoor";
|
||||||
|
protected override string ScriptRelativePath => "cmd/tuer.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("20D22047-03FA-4CF3-ACF3-3424A109BD91")]
|
||||||
|
public class DWindow : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dWindow";
|
||||||
|
protected override string ScriptRelativePath => "cmd/fenster.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("536641ED-93D4-4D49-A028-9F2C4EEE2A24")]
|
||||||
|
public class DSlab : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dSlab";
|
||||||
|
protected override string ScriptRelativePath => "cmd/decke.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("CF196C6A-EEAE-478C-8EB0-C69B6F7B9942")]
|
||||||
|
public class DStair : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dStair";
|
||||||
|
protected override string ScriptRelativePath => "cmd/treppe.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("B4A19B8B-4056-428B-BA2E-DF69A2A8DA9A")]
|
||||||
|
public class DColumn : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dColumn";
|
||||||
|
protected override string ScriptRelativePath => "cmd/stuetze.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("CCDF2D03-1FBD-4BC3-A06E-6D3FEEE575AB")]
|
||||||
|
public class DBeam : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dBeam";
|
||||||
|
protected override string ScriptRelativePath => "cmd/traeger.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("6ADF4344-0C05-48D1-BB2A-B330E3057CE4")]
|
||||||
|
public class DRoom : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dRoom";
|
||||||
|
protected override string ScriptRelativePath => "cmd/raum.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("A9D471FD-CB75-4C4E-8236-3C8B9A491266")]
|
||||||
|
public class DSymbol : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dSymbol";
|
||||||
|
protected override string ScriptRelativePath => "cmd/symbol.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("5388E3A7-B40E-40CE-B958-4A294B1E9F4F")]
|
||||||
|
public class DTag : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dTag";
|
||||||
|
protected override string ScriptRelativePath => "cmd/stempel.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("F0A5E3B0-F77E-4316-B521-294979F1E9CA")]
|
||||||
|
public class DRoof : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dRoof";
|
||||||
|
protected override string ScriptRelativePath => "cmd/dach.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("404E4389-F8BF-4BAE-A972-60EADB33941C")]
|
||||||
|
public class DVoid : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dVoid";
|
||||||
|
protected override string ScriptRelativePath => "cmd/aussparung.py";
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace DOSSIER.Cmd;
|
||||||
|
|
||||||
|
[Guid("07F23908-EF40-4A98-A550-C8D8A1F80A7F")]
|
||||||
|
public class DJoin : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dJoin";
|
||||||
|
protected override string ScriptRelativePath => "cmd/smart_join.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("69DBE84C-5E44-4155-84CB-D67329B64830")]
|
||||||
|
public class DSplit : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dSplit";
|
||||||
|
protected override string ScriptRelativePath => "cmd/smart_split.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("38E80D26-5270-45C6-B5F3-2E2179545C47")]
|
||||||
|
public class DPipette : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dPipette";
|
||||||
|
protected override string ScriptRelativePath => "cmd/pipette.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("F2C8B5A1-9D4E-4F73-B2C6-1A8E7D3F5C42")]
|
||||||
|
public class DSection : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dSection";
|
||||||
|
protected override string ScriptRelativePath => "cmd/section.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("66647D04-F324-459F-82B9-0FD82307FA93")]
|
||||||
|
public class DKeys : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dKeys";
|
||||||
|
protected override string ScriptRelativePath => "cmd/dkeys.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("93406D93-E9AC-424D-BFBD-3B7A542A85A7")]
|
||||||
|
public class DWelcome : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dWelcome";
|
||||||
|
protected override string ScriptRelativePath => "cmd/dwelcome.py";
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace DOSSIER.Cmd;
|
||||||
|
|
||||||
|
[Guid("4498B184-E064-4049-8B43-873721ECEE71")]
|
||||||
|
public class DPlan : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dPlan";
|
||||||
|
protected override string ScriptRelativePath => "view/plan.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("D6089B7C-C513-4A39-A62B-5A5E91764A18")]
|
||||||
|
public class D3D : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "d3D";
|
||||||
|
protected override string ScriptRelativePath => "view/persp3d.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("BA89B2DE-2301-4E0D-8542-3BDF393BF7A7")]
|
||||||
|
public class DMaterial : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dMaterial";
|
||||||
|
protected override string ScriptRelativePath => "view/material.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("A802824C-BC9B-405B-88A4-77125AA7D5A9")]
|
||||||
|
public class DLevelUp : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dLevelUp";
|
||||||
|
protected override string ScriptRelativePath => "view/geschoss_up.py";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("A034FF6F-0BCC-48D7-9AC9-8447D5718D32")]
|
||||||
|
public class DLevelDown : DossierPythonCommand
|
||||||
|
{
|
||||||
|
public override string EnglishName => "dLevelDown";
|
||||||
|
protected override string ScriptRelativePath => "view/geschoss_down.py";
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<AssemblyName>DOSSIER</AssemblyName>
|
||||||
|
<RootNamespace>DOSSIER</RootNamespace>
|
||||||
|
<Version>0.2.0</Version>
|
||||||
|
<Title>DOSSIER</Title>
|
||||||
|
<Company>Karim Gabriele Varano</Company>
|
||||||
|
<Description>DOSSIER — Architektur-Studio-Plugin fuer Rhino 8. Bootstrappt beim Plugin-Load die Python-Module (Panels, Aliases, View-Modes, Welcome) und registriert native Commands (dWall, dDoor, dStair, ...) als saubere Wrapper auf die jeweiligen Python-Scripts.</Description>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
|
||||||
|
<!-- Rhino-Plugin-Output: .rhp statt .dll -->
|
||||||
|
<TargetExt>.rhp</TargetExt>
|
||||||
|
<NoWarn>NU1701;NETSDK1086</NoWarn>
|
||||||
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
|
|
||||||
|
<!-- Kein Konflikt mit Rhinos Eto/WPF -->
|
||||||
|
<UseWindowsForms>false</UseWindowsForms>
|
||||||
|
<UseWpf>false</UseWpf>
|
||||||
|
|
||||||
|
<!-- Plugin-Metadaten (sichtbar im _PluginManager) -->
|
||||||
|
<AssemblyTitle>DOSSIER</AssemblyTitle>
|
||||||
|
<Copyright>Copyright (C) 2026 Karim Gabriele Varano. AGPL-3.0-or-later.</Copyright>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="RhinoCommon" Version="8.0.23304.9001" IncludeAssets="compile;build" />
|
||||||
|
<!-- Rhino-CPython3-Runtime — direkt aus dem App-Bundle linken.
|
||||||
|
Erlaubt RhinoCode-API ohne den Umweg ueber _-RunPythonScript-Command. -->
|
||||||
|
<Reference Include="Rhino.Runtime.Code">
|
||||||
|
<HintPath>/Applications/Rhino 8.app/Contents/Frameworks/RhCore.framework/Versions/A/Resources/Rhino.Runtime.Code.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Plugin-Guid als Assembly-Attribut (Rhino registriert Plugin via dieser ID) -->
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
|
||||||
|
<_Parameter1>Rhino.PlugIns.DescriptionType.Address</_Parameter1>
|
||||||
|
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||||
|
<_Parameter2>-</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
|
||||||
|
<_Parameter1>Rhino.PlugIns.DescriptionType.Email</_Parameter1>
|
||||||
|
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||||
|
<_Parameter2>karim@gabrielevarano.ch</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
|
||||||
|
<_Parameter1>Rhino.PlugIns.DescriptionType.Organization</_Parameter1>
|
||||||
|
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||||
|
<_Parameter2>Karim Gabriele Varano</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="Rhino.PlugIns.PlugInDescriptionAttribute">
|
||||||
|
<_Parameter1>Rhino.PlugIns.DescriptionType.WebSite</_Parameter1>
|
||||||
|
<_Parameter1_IsLiteral>true</_Parameter1_IsLiteral>
|
||||||
|
<_Parameter2>https://github.com/karimgvarano/DOSSIER</_Parameter2>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.InteropServices.GuidAttribute">
|
||||||
|
<_Parameter1>e8a4d2c1-6b3f-4e89-9c5a-1d2e3f4a5b6c</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace DOSSIER;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Locator-Pfade fuer das DOSSIER-Repo. Reihenfolge:
|
||||||
|
/// 1. Env-Var DOSSIER_HOME
|
||||||
|
/// 2. File ~/.dossier_home (eine Zeile mit dem Pfad)
|
||||||
|
/// 3. Hardcoded Fallback /Users/karim/STUDIO/DOSSIER (Dev-Setup)
|
||||||
|
/// </summary>
|
||||||
|
internal static class DossierPaths
|
||||||
|
{
|
||||||
|
private const string FallbackRoot = "/Users/karim/STUDIO/DOSSIER";
|
||||||
|
private const string MarkerFile = ".dossier_home";
|
||||||
|
|
||||||
|
private static string? _cachedRoot;
|
||||||
|
|
||||||
|
public static string? Root
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_cachedRoot is not null) return _cachedRoot;
|
||||||
|
_cachedRoot = ResolveRoot();
|
||||||
|
return _cachedRoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RhinoDir => Path.Combine(Root ?? FallbackRoot, "rhino");
|
||||||
|
|
||||||
|
public static string AliasDir => Path.Combine(RhinoDir, "aliases");
|
||||||
|
|
||||||
|
public static string CmdDir => Path.Combine(AliasDir, "cmd");
|
||||||
|
|
||||||
|
public static string ViewDir => Path.Combine(AliasDir, "view");
|
||||||
|
|
||||||
|
public static string StartupPy => Path.Combine(RhinoDir, "startup.py");
|
||||||
|
|
||||||
|
private static string? ResolveRoot()
|
||||||
|
{
|
||||||
|
var env = Environment.GetEnvironmentVariable("DOSSIER_HOME");
|
||||||
|
if (!string.IsNullOrEmpty(env) && Directory.Exists(env)) return env;
|
||||||
|
|
||||||
|
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
var marker = Path.Combine(home, MarkerFile);
|
||||||
|
if (File.Exists(marker))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var p = File.ReadAllText(marker).Trim();
|
||||||
|
if (!string.IsNullOrEmpty(p) && Directory.Exists(p)) return p;
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Directory.Exists(FallbackRoot)) return FallbackRoot;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
using Rhino;
|
||||||
|
using Rhino.PlugIns;
|
||||||
|
|
||||||
|
namespace DOSSIER;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DOSSIER-Plugin. Drei Aufgaben:
|
||||||
|
/// 1. Bootstrappt beim Plugin-Load die Python-Module: Panels, Aliases,
|
||||||
|
/// View-Modes, BeginCommand-Hook, Welcome-Screen (alles ueber rhino/startup.py).
|
||||||
|
/// 2. Registriert native Rhino-Commands (dWall, dDoor, dStair, ...) die
|
||||||
|
/// jeweils das passende Python-Script in rhino/aliases/cmd/ ausfuehren.
|
||||||
|
/// 3. Loest das Echo-/Autocomplete-Problem der frueheren Keyboard-Macros
|
||||||
|
/// (jetzt zeigt die History "dWall" statt "_-RunPythonScript ...").
|
||||||
|
///
|
||||||
|
/// Installation: Plugin via _PluginManager → Install... registrieren. Beim
|
||||||
|
/// naechsten Rhino-Start laeuft DOSSIER automatisch. Kein zusaetzlicher
|
||||||
|
/// StartupCommands-XML-Eintrag noetig.
|
||||||
|
/// </summary>
|
||||||
|
public class DossierPlugin : PlugIn
|
||||||
|
{
|
||||||
|
public DossierPlugin() { Instance = this; }
|
||||||
|
|
||||||
|
public static DossierPlugin Instance { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>Plugin bei jedem Rhino-Start automatisch laden — default ist
|
||||||
|
/// "WhenNeeded" (erst beim ersten Command-Aufruf). Wir brauchen aber
|
||||||
|
/// AtStartup, damit OnLoad → startup.py-Bootstrap immer feuert.</summary>
|
||||||
|
public override PlugInLoadTime LoadTime => PlugInLoadTime.AtStartup;
|
||||||
|
|
||||||
|
protected override LoadReturnCode OnLoad(ref string errorMessage)
|
||||||
|
{
|
||||||
|
var root = DossierPaths.Root;
|
||||||
|
if (root is null)
|
||||||
|
{
|
||||||
|
errorMessage =
|
||||||
|
"DOSSIER Root nicht gefunden. Setze Env-Var DOSSIER_HOME " +
|
||||||
|
"auf den DOSSIER-Repo-Ordner (z.B. /Users/karim/STUDIO/DOSSIER) " +
|
||||||
|
"oder leg ein File ~/.dossier_home an.";
|
||||||
|
return LoadReturnCode.ErrorShowDialog;
|
||||||
|
}
|
||||||
|
RhinoApp.WriteLine($"[DOSSIER] Plugin geladen (root={root})");
|
||||||
|
|
||||||
|
// Python-Bootstrap deferred auf Idle — OnLoad feuert vor Eto-UI-Init,
|
||||||
|
// Panels brauchen aber MainWindow + Idle-Event. PythonRunner.RunDeferred
|
||||||
|
// wartet auf naechstes Idle und ruft dann startup.py auf.
|
||||||
|
PythonRunner.RunDeferred(DossierPaths.StartupPy, "startup");
|
||||||
|
|
||||||
|
return LoadReturnCode.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
using System.IO;
|
||||||
|
using Rhino;
|
||||||
|
using Rhino.Commands;
|
||||||
|
|
||||||
|
namespace DOSSIER;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abstrakte Basis fuer alle DOSSIER-Commands die ein Python-Script
|
||||||
|
/// ausfuehren. Subklasse setzt nur EnglishName + ScriptRelativePath.
|
||||||
|
///
|
||||||
|
/// Mechanik: Rhino erlaubt kein synchrones Command-in-Command-Nesting fuer
|
||||||
|
/// _-RunPythonScript. PythonRunner.RunDeferred wartet auf das naechste Idle-
|
||||||
|
/// Event und versucht dann zuerst die RhinoCode-API (keine Echo) und faellt
|
||||||
|
/// auf _-RunPythonScript zurueck. Der Outer-Command beendet sauber mit Success,
|
||||||
|
/// das Python-Script laeuft direkt danach. Ergebnis fuer den User: sauberer
|
||||||
|
/// Command-Name in der History (z.B. "dWall") statt "_-RunPythonScript ...".
|
||||||
|
/// </summary>
|
||||||
|
public abstract class DossierPythonCommand : Command
|
||||||
|
{
|
||||||
|
/// <summary>z.B. "cmd/wand.py" oder "view/plan.py" — relativ zu rhino/aliases/.</summary>
|
||||||
|
protected abstract string ScriptRelativePath { get; }
|
||||||
|
|
||||||
|
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
|
||||||
|
{
|
||||||
|
var scriptPath = Path.Combine(DossierPaths.AliasDir, ScriptRelativePath);
|
||||||
|
if (!File.Exists(scriptPath))
|
||||||
|
{
|
||||||
|
RhinoApp.WriteLine($"[DOSSIER] FEHLER: Script nicht gefunden: {scriptPath}");
|
||||||
|
return Result.Failure;
|
||||||
|
}
|
||||||
|
PythonRunner.RunDeferred(scriptPath, EnglishName);
|
||||||
|
return Result.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Rhino;
|
||||||
|
|
||||||
|
namespace DOSSIER;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Geteilter Python-Script-Runner fuer Plugin-Startup (startup.py) und
|
||||||
|
/// Commands (cmd/*.py). Primaer ueber Rhino.Runtime.Code-API (CPython3),
|
||||||
|
/// Fallback ueber _-RunPythonScript-Command.
|
||||||
|
/// </summary>
|
||||||
|
internal static class PythonRunner
|
||||||
|
{
|
||||||
|
/// <summary>Fuehrt das Script aus. Versucht RhinoCode-API zuerst,
|
||||||
|
/// faellt auf _-RunPythonScript zurueck. Labels nur fuer Logs.</summary>
|
||||||
|
public static bool Run(string scriptPath, string label)
|
||||||
|
{
|
||||||
|
if (!File.Exists(scriptPath))
|
||||||
|
{
|
||||||
|
RhinoApp.WriteLine($"[DOSSIER] {label}: Script nicht gefunden: {scriptPath}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (TryRunViaRhinoCode(scriptPath, label)) return true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RhinoApp.RunScript($"_-RunPythonScript \"{scriptPath}\"", echo: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
RhinoApp.WriteLine($"[DOSSIER] {label} RunScript: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Wie Run, aber defern auf das naechste Idle-Event.
|
||||||
|
/// Erlaubt safe Invocation aus Plugin-OnLoad oder Command-RunCommand
|
||||||
|
/// (Rhino mag kein direktes _-RunPythonScript aus diesen Kontexten).</summary>
|
||||||
|
public static void RunDeferred(string scriptPath, string label)
|
||||||
|
{
|
||||||
|
EventHandler? handler = null;
|
||||||
|
handler = (sender, e) =>
|
||||||
|
{
|
||||||
|
RhinoApp.Idle -= handler;
|
||||||
|
Run(scriptPath, label);
|
||||||
|
};
|
||||||
|
RhinoApp.Idle += handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryRunViaRhinoCode(string scriptPath, string label)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var spec = new Rhino.Runtime.Code.Languages.LanguageSpec("*.*.python", "3.*");
|
||||||
|
var lang = Rhino.Runtime.Code.RhinoCode.Languages.QueryLatest(spec);
|
||||||
|
if (lang == null) return false;
|
||||||
|
|
||||||
|
// RhinoCode.CreateCode(text) setzt __file__/sys.path NICHT automatisch
|
||||||
|
// — die DOSSIER-Scripts erwarten beides. Injizieren vorne rein.
|
||||||
|
var pathLit = scriptPath.Replace("\\", "/");
|
||||||
|
var preamble =
|
||||||
|
"import sys, os\n" +
|
||||||
|
$"__file__ = r'{pathLit}'\n" +
|
||||||
|
"sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n" +
|
||||||
|
"sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))\n";
|
||||||
|
var code = lang.CreateCode(preamble + File.ReadAllText(scriptPath));
|
||||||
|
var ctx = new Rhino.Runtime.Code.Execution.RunContext();
|
||||||
|
code.Run(ctx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
RhinoApp.WriteLine($"[DOSSIER] {label} RhinoCode: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+144
@@ -0,0 +1,144 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# DOSSIER — Build-Skript
|
||||||
|
# Baut das C#-Plugin (.rhp) das beim Rhino-Start die Python-Module
|
||||||
|
# bootstrappt (Panels, Aliases, Welcome) und Native-Commands registriert
|
||||||
|
# (dWall, dDoor, dStair, dSlab, ...).
|
||||||
|
#
|
||||||
|
# === Voraussetzungen ===
|
||||||
|
# 1. .NET 7 SDK installiert. Auf Mac:
|
||||||
|
# brew install dotnet@7
|
||||||
|
# Oder direkt von Microsoft:
|
||||||
|
# https://dotnet.microsoft.com/download/dotnet/7.0
|
||||||
|
#
|
||||||
|
# 2. RhinoCommon NuGet-Package wird beim ersten Build automatisch geladen.
|
||||||
|
#
|
||||||
|
# === Build ===
|
||||||
|
# ./build.sh — Release-Build, output in bin/Release/net7.0/
|
||||||
|
# ./build.sh debug — Debug-Build mit Symbols
|
||||||
|
# ./build.sh clean — bin/obj loeschen
|
||||||
|
# ./build.sh install — Build + ins Rhino Plug-In-Verzeichnis kopieren
|
||||||
|
#
|
||||||
|
# === Installation in Rhino (einmalig, auf Mac) ===
|
||||||
|
# WICHTIG: Mac Rhino 8 unterstuetzt KEIN Drag-Drop fuer .rhp-Plugins
|
||||||
|
# (der Drag landet im Datei-Oeffnen-Handler, nicht im Plugin-Loader).
|
||||||
|
#
|
||||||
|
# Richtiger Weg:
|
||||||
|
# 1. Rhino 8 oeffnen
|
||||||
|
# 2. Command-Prompt: PluginManager
|
||||||
|
# (oder Tools-Menue → Options → Plug-Ins)
|
||||||
|
# 3. Button "Install..." → browse zur .rhp
|
||||||
|
# bin/Release/net7.0/DOSSIER.rhp
|
||||||
|
# 4. Open → Rhino registriert das Plugin
|
||||||
|
# 5. Rhino restart — DOSSIER bootstrappt (Panels/Aliases/Welcome) +
|
||||||
|
# Commands dWall/dDoor/... sind verfuegbar
|
||||||
|
#
|
||||||
|
# Pfad bleibt in Rhinos settings-XML registriert. Bei spaeteren Builds
|
||||||
|
# einfach in den gleichen Output-Pfad bauen — Rhino laedt den neuen Stand
|
||||||
|
# automatisch beim naechsten Start.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# --- Param-Parsing ---
|
||||||
|
MODE="${1:-release}"
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
debug|Debug)
|
||||||
|
CONFIG="Debug"
|
||||||
|
;;
|
||||||
|
release|Release|"")
|
||||||
|
CONFIG="Release"
|
||||||
|
;;
|
||||||
|
clean)
|
||||||
|
echo "==> Loesche bin/ + obj/"
|
||||||
|
rm -rf bin obj
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
CONFIG="Release"
|
||||||
|
DO_INSTALL=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [release|debug|clean|install]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- dotnet-Check ---
|
||||||
|
if ! command -v dotnet &>/dev/null; then
|
||||||
|
echo "FEHLER: dotnet nicht installiert."
|
||||||
|
echo "Install: brew install dotnet@7"
|
||||||
|
echo " oder https://dotnet.microsoft.com/download/dotnet/7.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Build ---
|
||||||
|
echo "==> Build: $CONFIG"
|
||||||
|
dotnet build -c "$CONFIG"
|
||||||
|
|
||||||
|
OUTPUT="$SCRIPT_DIR/bin/$CONFIG/net7.0/DOSSIER.rhp"
|
||||||
|
if [ ! -f "$OUTPUT" ]; then
|
||||||
|
echo "FEHLER: Build-Output nicht gefunden: $OUTPUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> .rhp Output: $OUTPUT"
|
||||||
|
|
||||||
|
# --- Yak-Paket bauen ---
|
||||||
|
# yak (Rhinos Package Manager) ist im Rhino-App-Bundle dabei. Wir packen .rhp +
|
||||||
|
# manifest.yml in ein .yak-Archiv das der Launcher bundlet + via "yak install"
|
||||||
|
# in den User-Plugin-Pfad legt. Dort wird's von Rhino aus dem trusted Yak-
|
||||||
|
# Verzeichnis geladen.
|
||||||
|
YAK="/Applications/Rhino 8.app/Contents/Resources/bin/yak"
|
||||||
|
DIST_DIR="$SCRIPT_DIR/dist"
|
||||||
|
mkdir -p "$DIST_DIR"
|
||||||
|
if [ -x "$YAK" ]; then
|
||||||
|
BUILD_DIR="$SCRIPT_DIR/bin/$CONFIG/net7.0"
|
||||||
|
pushd "$BUILD_DIR" >/dev/null
|
||||||
|
# yak spec failed mit Exit-1 wenn manifest.yml schon existiert — kein Fehler
|
||||||
|
"$YAK" spec --input DOSSIER.rhp >/dev/null 2>&1 || true
|
||||||
|
rm -f dossier-*.yak
|
||||||
|
YAK_OUT=$("$YAK" build 2>&1 | grep -oE '/.*\.yak$' | head -1)
|
||||||
|
popd >/dev/null
|
||||||
|
if [ -n "$YAK_OUT" ] && [ -f "$YAK_OUT" ]; then
|
||||||
|
# Versionierter Filename rein damit Launcher die Version vom Filename ablesen kann
|
||||||
|
YAK_NAME=$(basename "$YAK_OUT")
|
||||||
|
# Alte .yak im dist/ wegraeumen
|
||||||
|
rm -f "$DIST_DIR"/dossier-*.yak
|
||||||
|
cp -v "$YAK_OUT" "$DIST_DIR/$YAK_NAME"
|
||||||
|
# Stabilen Symlink fuer Launcher (immer 'dossier.yak') zusaetzlich
|
||||||
|
ln -sf "$YAK_NAME" "$DIST_DIR/dossier.yak"
|
||||||
|
# Version separat als Textdatei (extrahiert aus manifest.yml im .yak)
|
||||||
|
VERSION=$(grep '^version:' "$BUILD_DIR/manifest.yml" | awk '{print $2}')
|
||||||
|
echo -n "$VERSION" > "$DIST_DIR/dossier-version.txt"
|
||||||
|
echo "==> .yak Output: $DIST_DIR/$YAK_NAME (version=$VERSION)"
|
||||||
|
else
|
||||||
|
echo "WARN: yak build hat keinen Output produziert"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARN: yak CLI nicht gefunden ($YAK) — kein .yak-Paket gebaut"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Install: lokales Test-Install via yak ---
|
||||||
|
# Fuer Dev-Iteration: installiert das frische .yak direkt in den Rhino-User-
|
||||||
|
# Plugin-Pfad (~/Library/Application Support/McNeel/Rhinoceros/packages/8.0/).
|
||||||
|
# In Production macht der Launcher das automatisch beim ersten Rhino-Start.
|
||||||
|
if [ -n "$DO_INSTALL" ]; then
|
||||||
|
# Alte Manuell-Install-Standorte aufraeumen
|
||||||
|
OLD_MANUAL="$HOME/Library/Application Support/Dossier/Plugin"
|
||||||
|
for old in "/Applications/Rhino 8.app/Contents/PlugIns/DOSSIER.rhp" \
|
||||||
|
"/Applications/Rhino 8.app/Contents/PlugIns/DossierCommands.rhp" \
|
||||||
|
"$OLD_MANUAL/DOSSIER.rhp"; do
|
||||||
|
if [ -f "$old" ]; then rm -v "$old"; fi
|
||||||
|
done
|
||||||
|
if [ -x "$YAK" ] && [ -f "$DIST_DIR/dossier.yak" ]; then
|
||||||
|
# yak install nimmt Quelle als Verzeichnis (treats local dir as source server)
|
||||||
|
"$YAK" install dossier --source "$DIST_DIR" 2>&1 | sed 's/^/ /'
|
||||||
|
echo "==> Yak-Install fertig. Rhino restart noetig (Plugin laedt on-demand beim ersten Command)."
|
||||||
|
echo "==> StartupCommands-XML-Eintrag wird vom Launcher gesetzt — fuer Dev manuell pruefen:"
|
||||||
|
echo " Options → General → Run these commands every time a model is opened"
|
||||||
|
echo " soll enthalten: _-RunPythonScript \"$SCRIPT_DIR/../../rhino/startup.py\""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK."
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
import js from '@eslint/js'
|
import js from '@eslint/js'
|
||||||
import globals from 'globals'
|
import globals from 'globals'
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "dossier-launcher",
|
"name": "dossier-launcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# clean-rhino.sh — setzt DOSSIER in Rhino zurueck auf "frisch installiert" Zustand.
|
||||||
|
# Damit kann das Setup im Launcher (Settings → Setup tab) jederzeit von Null
|
||||||
|
# durchgespielt werden.
|
||||||
|
#
|
||||||
|
# Aufgaben:
|
||||||
|
# 1. yak uninstall dossier (Plugin raus)
|
||||||
|
# 2. Window-Layout-Datei loeschen (workspaces/<guid>.xml)
|
||||||
|
# 3. StartupCommands-XML-Eintrag entfernen (Python-Bootstrap-Trigger)
|
||||||
|
#
|
||||||
|
# Bleibt unangetastet:
|
||||||
|
# - dossier_settings.json (User-Praeferenzen, Tags, etc.)
|
||||||
|
# - launcher recent.json
|
||||||
|
# - alles ausserhalb DOSSIER
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RHINO_APP="/Applications/Rhino 8.app"
|
||||||
|
YAK="$RHINO_APP/Contents/Resources/bin/yak"
|
||||||
|
SETTINGS_XML="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"
|
||||||
|
WORKSPACES_DIR="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces"
|
||||||
|
LAYOUT_GUID="b6b68c03-3031-4899-bca2-fe6e425146fc"
|
||||||
|
|
||||||
|
# --- Safety: Rhino muss zu sein ---
|
||||||
|
if pgrep -f "Rhino 8.app/Contents/MacOS/Rhinoceros$" >/dev/null; then
|
||||||
|
echo "FEHLER: Rhino laeuft. Bitte beenden und nochmal."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 1. Yak uninstall (idempotent — meldet 'package not installed' wenn schon weg) ---
|
||||||
|
echo "==> 1. Yak uninstall dossier"
|
||||||
|
if [ -x "$YAK" ]; then
|
||||||
|
"$YAK" uninstall dossier 2>&1 | sed 's/^/ /' || true
|
||||||
|
else
|
||||||
|
echo " WARN: yak nicht gefunden — skip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 2. Window-Layout-Datei loeschen ---
|
||||||
|
echo "==> 2. Window-Layout-Datei loeschen"
|
||||||
|
LAYOUT_FILE="$WORKSPACES_DIR/$LAYOUT_GUID.xml"
|
||||||
|
if [ -f "$LAYOUT_FILE" ]; then
|
||||||
|
rm -v "$LAYOUT_FILE" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo " schon weg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 3. StartupCommands-Eintrag aus XML entfernen ---
|
||||||
|
echo "==> 3. StartupCommands-Eintrag entfernen"
|
||||||
|
if [ -f "$SETTINGS_XML" ]; then
|
||||||
|
# sed: matche genau unsere DOSSIER-Zeile und loesche
|
||||||
|
# (egal welcher Pfad — solange startup.py drin steht)
|
||||||
|
if grep -q 'StartupCommands.*startup.py' "$SETTINGS_XML"; then
|
||||||
|
# macOS sed braucht leeres Backup-Suffix
|
||||||
|
sed -i '' '/<entry key="StartupCommands">.*startup\.py.*<\/entry>/d' "$SETTINGS_XML"
|
||||||
|
echo " entfernt"
|
||||||
|
else
|
||||||
|
echo " schon weg"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " WARN: Rhino-settings-XML nicht gefunden"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Clean fertig. Naechster Schritt:"
|
||||||
|
echo " → Launcher → Settings → Setup → 'Setup starten'"
|
||||||
@@ -3,6 +3,7 @@ name = "dossier-launcher"
|
|||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
description = "Dossier — Projekt-Launcher fuer Rhino"
|
description = "Dossier — Projekt-Launcher fuer Rhino"
|
||||||
authors = ["Karim Gabriele Varano"]
|
authors = ["Karim Gabriele Varano"]
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
// Verhindert ein extra Konsolen-Fenster auf Windows im Release-Build.
|
// Verhindert ein extra Konsolen-Fenster auf Windows im Release-Build.
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
@@ -292,11 +294,26 @@ fn plugin_loaded_marker_path() -> PathBuf {
|
|||||||
dossier_dir().join("plugin_loaded.flag")
|
dossier_dir().join("plugin_loaded.flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn splash_owner_marker_path() -> PathBuf {
|
||||||
|
// Vor Launch von Rhino schreibt Launcher diesen Marker → Plugin-Splash
|
||||||
|
// (rhino/_startup_splash.py) prueft beim Show ob Marker frisch (<30s)
|
||||||
|
// ist und skippt dann, damit nicht beide Splashes gleichzeitig laufen.
|
||||||
|
dossier_dir().join("splash_owner_launcher.flag")
|
||||||
|
}
|
||||||
|
|
||||||
fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> {
|
fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), String> {
|
||||||
let settings = load_settings();
|
let settings = load_settings();
|
||||||
// XML-Edit nur sinnvoll wenn Rhino nicht laeuft (sonst ueberschreibt's
|
// Setup-Schritte nur wenn Rhino NICHT laeuft — sonst ueberschreibt Rhino
|
||||||
// beim Beenden) UND der Eintrag fuer den naechsten Start eh schon greift.
|
// unsere XML-Edits beim Beenden, und yak install kann die in-use .rhp
|
||||||
|
// nicht ersetzen.
|
||||||
if settings.auto_load_plugin && !is_rhino_running() {
|
if settings.auto_load_plugin && !is_rhino_running() {
|
||||||
|
// Schritt 1: .rhp via yak installieren/aktualisieren. Soft-Fail wenn
|
||||||
|
// .yak nicht gebundelt ist (Dev-Setup ohne build.sh ausgefuehrt) —
|
||||||
|
// Bootstrap via XML reicht alleine, Commands fehlen nur.
|
||||||
|
if let Err(e) = ensure_rhino_plugin_installed() {
|
||||||
|
eprintln!("[DOSSIER] Plugin-Install skip: {e}");
|
||||||
|
}
|
||||||
|
// Schritt 2: StartupCommands-XML fuer Python-Bootstrap setzen.
|
||||||
let startup_path = settings.plugin_startup_path
|
let startup_path = settings.plugin_startup_path
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(default_plugin_startup_path);
|
.unwrap_or_else(default_plugin_startup_path);
|
||||||
@@ -312,8 +329,11 @@ fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), Stri
|
|||||||
// Splash NUR zeigen wenn Auto-Load aktiv (sonst gibt's nichts zu warten).
|
// Splash NUR zeigen wenn Auto-Load aktiv (sonst gibt's nichts zu warten).
|
||||||
let show_splash = settings.auto_load_plugin;
|
let show_splash = settings.auto_load_plugin;
|
||||||
let marker = plugin_loaded_marker_path();
|
let marker = plugin_loaded_marker_path();
|
||||||
|
let owner_marker = splash_owner_marker_path();
|
||||||
if show_splash {
|
if show_splash {
|
||||||
let _ = fs::remove_file(&marker);
|
let _ = fs::remove_file(&marker);
|
||||||
|
// Owner-Marker: signalisiert dem Plugin-Splash dass Launcher uebernimmt
|
||||||
|
let _ = fs::write(&owner_marker, b"launcher");
|
||||||
if let Some(splash) = app.get_webview_window("splash") {
|
if let Some(splash) = app.get_webview_window("splash") {
|
||||||
let _ = splash.show();
|
let _ = splash.show();
|
||||||
}
|
}
|
||||||
@@ -342,6 +362,7 @@ fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), Stri
|
|||||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||||
}
|
}
|
||||||
let _ = fs::remove_file(&marker);
|
let _ = fs::remove_file(&marker);
|
||||||
|
let _ = fs::remove_file(&owner_marker);
|
||||||
if let Some(splash) = app_clone.get_webview_window("splash") {
|
if let Some(splash) = app_clone.get_webview_window("splash") {
|
||||||
let _ = splash.hide();
|
let _ = splash.hide();
|
||||||
}
|
}
|
||||||
@@ -453,11 +474,264 @@ fn ensure_rhino_startup_command(startup_path: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Rhino-Plugin Install via Yak =====
|
||||||
|
// Mac Rhino lehnt Auto-Load fuer dritte Plugins ab (egal welche Methode).
|
||||||
|
// Wir installieren `.yak` aber trotzdem in den User-Plugin-Pfad — beim ersten
|
||||||
|
// dWall/dDoor/... laedt Rhino das Plugin on-demand und cached es danach.
|
||||||
|
// Der Python-Bootstrap (Panels/Aliases) laeuft parallel via StartupCommands-
|
||||||
|
// XML (ensure_rhino_startup_command). Beide Pfade zusammen = full setup.
|
||||||
|
|
||||||
|
fn yak_binary_path() -> PathBuf {
|
||||||
|
PathBuf::from("/Applications/Rhino 8.app/Contents/Resources/bin/yak")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rhino_packages_dir() -> PathBuf {
|
||||||
|
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
|
||||||
|
home.join("Library/Application Support/McNeel/Rhinoceros/packages/8.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn installed_dossier_version() -> Option<String> {
|
||||||
|
let manifest = rhino_packages_dir().join("DOSSIER/manifest.txt");
|
||||||
|
fs::read_to_string(&manifest).ok().map(|s| s.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// .yak + version.txt im App-Bundle (Production) oder Repo-dist/ (Dev).
|
||||||
|
fn bundled_plugin_paths() -> (PathBuf, PathBuf) {
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
|
||||||
|
let yak = contents_dir.join("Resources/plugin/dossier.yak");
|
||||||
|
let ver = contents_dir.join("Resources/plugin/dossier-version.txt");
|
||||||
|
if yak.is_file() && ver.is_file() {
|
||||||
|
return (yak, ver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dev-Fallback: Repo-Pfad
|
||||||
|
let repo = PathBuf::from("/Users/karim/STUDIO/DOSSIER/csharp/DOSSIER/dist");
|
||||||
|
(repo.join("dossier.yak"), repo.join("dossier-version.txt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bundled_plugin_version() -> Option<String> {
|
||||||
|
let (_, ver_path) = bundled_plugin_paths();
|
||||||
|
fs::read_to_string(&ver_path).ok().map(|s| s.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installiert/aktualisiert das DOSSIER-Plugin via yak. Idempotent — skip wenn
|
||||||
|
// installierte Version == gebundelte Version. Soft-Fail wenn .yak fehlt oder
|
||||||
|
// yak-CLI nicht vorhanden (Logging, kein Error — Bootstrap via XML laeuft eh).
|
||||||
|
fn ensure_rhino_plugin_installed() -> Result<(), String> {
|
||||||
|
let yak = yak_binary_path();
|
||||||
|
if !yak.is_file() {
|
||||||
|
return Err(format!("yak CLI nicht gefunden: {}", yak.display()));
|
||||||
|
}
|
||||||
|
let (yak_pkg, _) = bundled_plugin_paths();
|
||||||
|
if !yak_pkg.is_file() {
|
||||||
|
return Err(format!(
|
||||||
|
"DOSSIER .yak-Paket nicht gefunden: {} (build.sh in csharp/DOSSIER ausfuehren)",
|
||||||
|
yak_pkg.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let bundled_ver = bundled_plugin_version();
|
||||||
|
let installed_ver = installed_dossier_version();
|
||||||
|
if let (Some(b), Some(i)) = (&bundled_ver, &installed_ver) {
|
||||||
|
if b == i {
|
||||||
|
return Ok(()); // schon aktuell — kein Re-Install
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_rhino_running() {
|
||||||
|
return Err(
|
||||||
|
"Rhino laeuft — Plugin-Update kann erst nach Rhino-Quit installiert werden."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let pkg_dir = yak_pkg.parent().ok_or_else(|| "Plugin-Pfad ohne Parent".to_string())?;
|
||||||
|
let output = Command::new(&yak)
|
||||||
|
.arg("install")
|
||||||
|
.arg("dossier")
|
||||||
|
.arg("--source")
|
||||||
|
.arg(pkg_dir)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("yak install: {e}"))?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"yak install fehlgeschlagen: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"[DOSSIER] Plugin via yak installiert: {} → {}",
|
||||||
|
installed_ver.unwrap_or_else(|| "(nicht installiert)".into()),
|
||||||
|
bundled_ver.unwrap_or_else(|| "(unbekannt)".into())
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn open_rhino(app: tauri::AppHandle, path3dm: String) -> Result<(), String> {
|
fn open_rhino(app: tauri::AppHandle, path3dm: String) -> Result<(), String> {
|
||||||
open_rhino_internal(&app, &path3dm)
|
open_rhino_internal(&app, &path3dm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn install_rhino_plugin() -> Result<String, String> {
|
||||||
|
ensure_rhino_plugin_installed()?;
|
||||||
|
Ok(installed_dossier_version().unwrap_or_else(|| "(version unbekannt)".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Window-Layout Installation =====
|
||||||
|
// Rhino-Workspaces sind XML-Dateien im User-Pfad benannt nach Layout-GUID.
|
||||||
|
// Wir bundlen DOSSIERs Master-Layout(s) im Repo unter rhino/workspaces/ und
|
||||||
|
// kopieren sie beim Init in Rhinos Workspaces-Folder, damit startup.py das
|
||||||
|
// Layout per `_-WindowLayout "<name>" _Enter` direkt anwenden kann.
|
||||||
|
|
||||||
|
fn rhino_workspaces_dir() -> PathBuf {
|
||||||
|
let home = std::env::var("HOME").map(PathBuf::from).unwrap_or_default();
|
||||||
|
home.join("Library/Application Support/McNeel/Rhinoceros/8.0/settings/Scheme__Default/workspaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bundled_workspaces_dir() -> PathBuf {
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(contents_dir) = exe.parent().and_then(|p| p.parent()) {
|
||||||
|
let bundled = contents_dir.join("Resources/rhino/workspaces");
|
||||||
|
if bundled.is_dir() {
|
||||||
|
return bundled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathBuf::from("/Users/karim/STUDIO/DOSSIER/rhino/workspaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kopiert alle Workspace-XMLs aus dem bundle in Rhinos Workspaces-Folder.
|
||||||
|
// Vorhandene Files werden ueberschrieben (User-Customizations gehen verloren —
|
||||||
|
// dafuer ist's reproduzierbar). Returns Anzahl kopierter Files.
|
||||||
|
fn ensure_window_layout_installed() -> Result<usize, String> {
|
||||||
|
let src = bundled_workspaces_dir();
|
||||||
|
if !src.is_dir() {
|
||||||
|
return Err(format!("Workspace-Quelle fehlt: {}", src.display()));
|
||||||
|
}
|
||||||
|
let dst = rhino_workspaces_dir();
|
||||||
|
fs::create_dir_all(&dst)
|
||||||
|
.map_err(|e| format!("Workspace-Zielordner erstellen: {e}"))?;
|
||||||
|
if is_rhino_running() {
|
||||||
|
return Err("Rhino laeuft — bitte erst beenden (Layout-Datei wird sonst ueberschrieben).".into());
|
||||||
|
}
|
||||||
|
let mut count = 0;
|
||||||
|
for entry in fs::read_dir(&src).map_err(|e| format!("Workspace-Quelle lesen: {e}"))? {
|
||||||
|
let entry = entry.map_err(|e| format!("DirEntry: {e}"))?;
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("xml")).unwrap_or(false) {
|
||||||
|
let file_name = path.file_name().ok_or("FileName")?;
|
||||||
|
let dst_path = dst.join(file_name);
|
||||||
|
fs::copy(&path, &dst_path)
|
||||||
|
.map_err(|e| format!("Workspace kopieren ({}): {e}", file_name.to_string_lossy()))?;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== DOSSIER INIT =====
|
||||||
|
// Komplettes Setup auf einem neuen PC: Plugin via Yak + StartupCommands-XML +
|
||||||
|
// Window-Layout-Files. Returns Status pro Schritt fuer das Frontend-Dialog.
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone, Debug)]
|
||||||
|
struct InitStep {
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
status: String, // "ok" | "error" | "skipped"
|
||||||
|
detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone, Debug)]
|
||||||
|
struct InitResult {
|
||||||
|
steps: Vec<InitStep>,
|
||||||
|
overall_ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone, Debug)]
|
||||||
|
struct InitStatus {
|
||||||
|
plugin_installed: bool,
|
||||||
|
startup_cmd_set: bool,
|
||||||
|
layout_installed: bool,
|
||||||
|
initialized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn check_dossier_initialized() -> InitStatus {
|
||||||
|
let plugin_installed = installed_dossier_version().is_some();
|
||||||
|
let startup_cmd_set = fs::read_to_string(rhino_settings_xml_path())
|
||||||
|
.map(|s| s.contains("StartupCommands") && s.contains("startup.py"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
let layout_installed = rhino_workspaces_dir()
|
||||||
|
.join("b6b68c03-3031-4899-bca2-fe6e425146fc.xml")
|
||||||
|
.is_file();
|
||||||
|
InitStatus {
|
||||||
|
plugin_installed,
|
||||||
|
startup_cmd_set,
|
||||||
|
layout_installed,
|
||||||
|
// initialized = ALLE drei vorhanden. So zeigt der Dialog auch nach
|
||||||
|
// teilweisem Clean (z.B. nur layout geloescht) noch an.
|
||||||
|
initialized: plugin_installed && startup_cmd_set && layout_installed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn dossier_init() -> Result<InitResult, String> {
|
||||||
|
if is_rhino_running() {
|
||||||
|
return Err("Rhino laeuft — bitte erst beenden, dann Init nochmal starten.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut steps = Vec::new();
|
||||||
|
let mut overall_ok = true;
|
||||||
|
|
||||||
|
// Schritt 1: Plugin via Yak installieren/aktualisieren
|
||||||
|
let (status, detail) = match ensure_rhino_plugin_installed() {
|
||||||
|
Ok(()) => {
|
||||||
|
let v = installed_dossier_version().unwrap_or_else(|| "(?)".into());
|
||||||
|
("ok".into(), format!("Version {v}"))
|
||||||
|
}
|
||||||
|
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||||
|
};
|
||||||
|
steps.push(InitStep {
|
||||||
|
id: "plugin".into(),
|
||||||
|
label: "DOSSIER-Plugin via Yak installieren".into(),
|
||||||
|
status, detail,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schritt 2: StartupCommands-XML eintragen (Python-Bootstrap)
|
||||||
|
let startup_path = default_plugin_startup_path();
|
||||||
|
let (status, detail) = if !Path::new(&startup_path).is_file() {
|
||||||
|
overall_ok = false;
|
||||||
|
("error".into(), format!("startup.py nicht gefunden: {startup_path}"))
|
||||||
|
} else {
|
||||||
|
match ensure_rhino_startup_command(&startup_path) {
|
||||||
|
Ok(()) => ("ok".into(), startup_path.clone()),
|
||||||
|
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
steps.push(InitStep {
|
||||||
|
id: "startup".into(),
|
||||||
|
label: "Python-Bootstrap (StartupCommands-XML)".into(),
|
||||||
|
status, detail,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schritt 3: Window-Layout-Files in Rhinos Workspaces-Folder kopieren
|
||||||
|
let (status, detail) = match ensure_window_layout_installed() {
|
||||||
|
Ok(n) => ("ok".into(), format!("{n} Layout-Datei(en) installiert")),
|
||||||
|
Err(e) => { overall_ok = false; ("error".into(), e) }
|
||||||
|
};
|
||||||
|
steps.push(InitStep {
|
||||||
|
id: "layout".into(),
|
||||||
|
label: "Window-Layout in Rhino kopieren".into(),
|
||||||
|
status, detail,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(InitResult { steps, overall_ok })
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn trigger_plugin_load_now() -> Result<(), String> {
|
fn trigger_plugin_load_now() -> Result<(), String> {
|
||||||
// Schreibt den `_RunPythonScript <pfad>` Eintrag in Rhinos Startup-Command-
|
// Schreibt den `_RunPythonScript <pfad>` Eintrag in Rhinos Startup-Command-
|
||||||
@@ -911,6 +1185,9 @@ pub fn run() {
|
|||||||
read_project_config,
|
read_project_config,
|
||||||
open_rhino,
|
open_rhino,
|
||||||
trigger_plugin_load_now,
|
trigger_plugin_load_now,
|
||||||
|
install_rhino_plugin,
|
||||||
|
dossier_init,
|
||||||
|
check_dossier_initialized,
|
||||||
get_default_plugin_startup_path,
|
get_default_plugin_startup_path,
|
||||||
show_in_finder,
|
show_in_finder,
|
||||||
is_rhino_running,
|
is_rhino_running,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
// Tauri 2 Konvention: main.rs ist nur Einstieg, Logik in lib.rs (fuer Mobile-
|
// Tauri 2 Konvention: main.rs ist nur Einstieg, Logik in lib.rs (fuer Mobile-
|
||||||
// Unterstuetzung und damit `tauri::generate_context!` korrekt aufgeloest wird).
|
// Unterstuetzung und damit `tauri::generate_context!` korrekt aufgeloest wird).
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@@ -56,7 +56,9 @@
|
|||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"../../dist": "dist",
|
"../../dist": "dist",
|
||||||
"../../rhino": "rhino"
|
"../../rhino": "rhino",
|
||||||
|
"../../csharp/DOSSIER/dist/dossier.yak": "plugin/dossier.yak",
|
||||||
|
"../../csharp/DOSSIER/dist/dossier-version.txt": "plugin/dossier-version.txt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
import React, { useEffect, useState, useMemo, useCallback } from 'react'
|
import React, { useEffect, useState, useMemo, useCallback } from 'react'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
@@ -124,6 +126,13 @@ export default function App() {
|
|||||||
invoke('list_window_layouts').then(setLayouts).catch(() => {})
|
invoke('list_window_layouts').then(setLayouts).catch(() => {})
|
||||||
invoke('read_dossier_settings').then(ds => setActiveLayout(ds?.windowLayout || '')).catch(() => {})
|
invoke('read_dossier_settings').then(ds => setActiveLayout(ds?.windowLayout || '')).catch(() => {})
|
||||||
invoke('read_settings').then(s => setTags(s?.tags || [])).catch(() => {})
|
invoke('read_settings').then(s => setTags(s?.tags || [])).catch(() => {})
|
||||||
|
// Auto-Open Setup-Dialog wenn DOSSIER nicht initialisiert ist (z.B. nach
|
||||||
|
// clean-rhino.sh oder auf einem neuen Mac).
|
||||||
|
invoke('check_dossier_initialized')
|
||||||
|
.then(st => {
|
||||||
|
if (!st?.initialized) { setSettingsTab('setup'); setSettingsOpen(true) }
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// File-Meta laden sobald recent sich aendert
|
// File-Meta laden sobald recent sich aendert
|
||||||
@@ -748,6 +757,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
|
|||||||
<header style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
<header style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
<span>Einstellungen</span>
|
<span>Einstellungen</span>
|
||||||
<div style={{ display: 'flex', gap: 4, marginLeft: 'auto', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 4, marginLeft: 'auto', flexWrap: 'wrap' }}>
|
||||||
|
<TabBtn active={tab === 'setup'} onClick={() => setTab('setup')}>Setup</TabBtn>
|
||||||
<TabBtn active={tab === 'rhino'} onClick={() => setTab('rhino')}>Rhino</TabBtn>
|
<TabBtn active={tab === 'rhino'} onClick={() => setTab('rhino')}>Rhino</TabBtn>
|
||||||
<TabBtn active={tab === 'view'} onClick={() => setTab('view')}>View</TabBtn>
|
<TabBtn active={tab === 'view'} onClick={() => setTab('view')}>View</TabBtn>
|
||||||
<TabBtn active={tab === 'ebenen'} onClick={() => setTab('ebenen')}>Ebenen</TabBtn>
|
<TabBtn active={tab === 'ebenen'} onClick={() => setTab('ebenen')}>Ebenen</TabBtn>
|
||||||
@@ -757,6 +767,7 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="body">
|
<div className="body">
|
||||||
|
{tab === 'setup' && <SetupSettings />}
|
||||||
{tab === 'rhino' && <RhinoSettings />}
|
{tab === 'rhino' && <RhinoSettings />}
|
||||||
{tab === 'view' && <ViewSettings />}
|
{tab === 'view' && <ViewSettings />}
|
||||||
{tab === 'ebenen' && <EbenenSchemaSettings />}
|
{tab === 'ebenen' && <EbenenSchemaSettings />}
|
||||||
@@ -772,6 +783,148 @@ function SettingsDialog({ initialTab = 'rhino', onClose }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SetupSettings() {
|
||||||
|
const [running, setRunning] = useState(false)
|
||||||
|
const [result, setResult] = useState(null) // { steps, overall_ok }
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [rhinoBusy, setRhinoBusy] = useState(false)
|
||||||
|
const [status, setStatus] = useState(null) // { plugin_installed, startup_cmd_set, layout_installed, initialized }
|
||||||
|
const [rhinoApp, setRhinoApp] = useState('')
|
||||||
|
const [startupPath, setStartupPath] = useState('')
|
||||||
|
|
||||||
|
// Live-Check: ob Rhino laeuft (Init kann nicht laufen wenn ja)
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
const tick = () => {
|
||||||
|
invoke('is_rhino_running')
|
||||||
|
.then(v => { if (!cancelled) setRhinoBusy(!!v) })
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
tick()
|
||||||
|
const id = setInterval(tick, 2000)
|
||||||
|
return () => { cancelled = true; clearInterval(id) }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Initialer State-Check + erkannte Rhino-Konfig
|
||||||
|
const refreshStatus = useCallback(() => {
|
||||||
|
invoke('check_dossier_initialized').then(setStatus).catch(() => {})
|
||||||
|
}, [])
|
||||||
|
useEffect(() => {
|
||||||
|
refreshStatus()
|
||||||
|
invoke('read_settings').then(s => setRhinoApp(s?.rhinoApp || 'Rhinoceros 8')).catch(() => {})
|
||||||
|
invoke('get_default_plugin_startup_path').then(setStartupPath).catch(() => {})
|
||||||
|
}, [refreshStatus])
|
||||||
|
|
||||||
|
const runInit = async () => {
|
||||||
|
setRunning(true); setError(null); setResult(null)
|
||||||
|
try {
|
||||||
|
const r = await invoke('dossier_init')
|
||||||
|
setResult(r)
|
||||||
|
refreshStatus()
|
||||||
|
} catch (e) {
|
||||||
|
setError(typeof e === 'string' ? e : (e?.message || String(e)))
|
||||||
|
} finally {
|
||||||
|
setRunning(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dot = (ok) => (
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block', width: 8, height: 8, borderRadius: 4,
|
||||||
|
background: ok ? 'var(--accent)' : '#e87b6b', marginRight: 8,
|
||||||
|
}} />
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ marginTop: 0 }}>DOSSIER einrichten</h3>
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5 }}>
|
||||||
|
Setzt DOSSIER auf einem frischen Mac komplett auf: installiert das C#-Plugin in Rhino via Yak,
|
||||||
|
traegt den Python-Bootstrap als Startup-Command ein, und kopiert das DOSSIER-Window-Layout in Rhinos
|
||||||
|
Workspaces-Folder. Idempotent — kann mehrfach ausgefuehrt werden.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Erkannte Konfiguration */}
|
||||||
|
<div style={{
|
||||||
|
marginTop: 14, padding: 10, background: 'rgba(255,255,255,0.04)',
|
||||||
|
border: '1px solid var(--border)', borderRadius: 6, fontSize: 11,
|
||||||
|
}}>
|
||||||
|
<div style={{ color: 'var(--text-muted)', marginBottom: 6 }}>Erkannte Konfiguration:</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 4 }}>
|
||||||
|
<span style={{ color: 'var(--text-muted)' }}>Rhino-App:</span>
|
||||||
|
<span>{rhinoApp || '(nicht gesetzt)'}</span>
|
||||||
|
<span style={{ color: 'var(--text-muted)' }}>startup.py:</span>
|
||||||
|
<span style={{ wordBreak: 'break-all' }}>{startupPath || '(nicht gefunden)'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: 6, color: 'var(--text-muted)', fontSize: 10 }}>
|
||||||
|
(Aendern unter Settings → Rhino)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Aktueller Install-Status (live) */}
|
||||||
|
{status && (
|
||||||
|
<div style={{ marginTop: 12, fontSize: 11 }}>
|
||||||
|
<div style={{ color: 'var(--text-muted)', marginBottom: 4 }}>Status:</div>
|
||||||
|
<div>{dot(status.plugin_installed)}DOSSIER-Plugin (.rhp) installiert</div>
|
||||||
|
<div>{dot(status.startup_cmd_set)}Python-Bootstrap in Rhino-StartupCommands</div>
|
||||||
|
<div>{dot(status.layout_installed)}Window-Layout in Rhino-Workspaces</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 14 }}>
|
||||||
|
Hinweis: Rhino muss waehrend des Setups <strong>geschlossen</strong> sein.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<button
|
||||||
|
className="primary pill"
|
||||||
|
onClick={runInit}
|
||||||
|
disabled={running || rhinoBusy}
|
||||||
|
title={rhinoBusy ? 'Rhino laeuft — bitte beenden' : 'Setup starten'}
|
||||||
|
>
|
||||||
|
{running ? 'Setup laeuft…' : 'Setup starten'}
|
||||||
|
</button>
|
||||||
|
{rhinoBusy && (
|
||||||
|
<span style={{ fontSize: 11, color: '#e87b6b' }}>
|
||||||
|
Rhino laeuft — bitte beenden.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<ul style={{ listStyle: 'none', padding: 0, marginTop: 20, borderTop: '1px solid var(--border)' }}>
|
||||||
|
{result.steps.map(s => (
|
||||||
|
<li key={s.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 10,
|
||||||
|
padding: '10px 0', borderBottom: '1px solid var(--border)' }}>
|
||||||
|
<span style={{ width: 16, fontSize: 14,
|
||||||
|
color: s.status === 'ok' ? 'var(--accent)' : '#e87b6b' }}>
|
||||||
|
{s.status === 'ok' ? '✓' : '✗'}
|
||||||
|
</span>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: 12 }}>{s.label}</div>
|
||||||
|
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2,
|
||||||
|
wordBreak: 'break-all' }}>
|
||||||
|
{s.detail}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li style={{ padding: '10px 0', fontSize: 11,
|
||||||
|
color: result.overall_ok ? 'var(--accent)' : '#e87b6b' }}>
|
||||||
|
{result.overall_ok
|
||||||
|
? '✓ Alle Schritte erfolgreich. Rhino oeffnen — Plugin laedt bei erstem dWall/dDoor/...-Aufruf, startup.py bootstrappt automatisch.'
|
||||||
|
: '✗ Mindestens ein Schritt ist fehlgeschlagen. Details oben.'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p style={{ color: '#e87b6b', marginTop: 12, fontSize: 12 }}>{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function TabBtn({ active, onClick, children }) {
|
function TabBtn({ active, onClick, children }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
// Material-Symbols-Outlined-style Icons als Inline-SVG. Keine Font-Loads,
|
// Material-Symbols-Outlined-style Icons als Inline-SVG. Keine Font-Loads,
|
||||||
// kein Codepoint-Mapping — sauber zu themen via currentColor + stroke-width.
|
// kein Codepoint-Mapping — sauber zu themen via currentColor + stroke-width.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { checkForAppUpdate, installAppUpdate, skipUpdateVersion, isTauri } from "../utils/updater.js";
|
import { checkForAppUpdate, installAppUpdate, skipUpdateVersion, isTauri } from "../utils/updater.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
// Shared helpers fuer den Tauri-Updater. Verwendet vom Auto-Check Modal
|
// Shared helpers fuer den Tauri-Updater. Verwendet vom Auto-Check Modal
|
||||||
// (UpdateNotifier) und dem manuellen Check in den Einstellungen.
|
// (UpdateNotifier) und dem manuellen Check in den Einstellungen.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// Copyright (C) 2026 Karim Gabriele Varano
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "dossier",
|
"name": "dossier",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#! python3
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""Loescht ALLE Custom-Display-Modes (User-erstellte) — laesst die Rhino-
|
||||||
|
Built-ins (Wireframe, Shaded, Rendered, Ghosted, XRay, Technical, Artistic,
|
||||||
|
Pen, Monochrome, Arctic, Raytraced) in Ruhe.
|
||||||
|
|
||||||
|
Loescht auch Orphan-Modes ohne Namen (die manchmal bei abgebrochenen
|
||||||
|
Imports hierbleiben und Rhino zum Crash bringen wenn man sie anklickt).
|
||||||
|
|
||||||
|
Vorgehen:
|
||||||
|
_RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_clean_display_modes.py
|
||||||
|
"""
|
||||||
|
from Rhino.Display import DisplayModeDescription
|
||||||
|
|
||||||
|
BUILTIN_NAMES = {
|
||||||
|
"Wireframe", "Shaded", "Rendered", "Ghosted",
|
||||||
|
"X-Ray", "XRay", "X Ray",
|
||||||
|
"Technical", "Artistic", "Pen", "Monochrome",
|
||||||
|
"Arctic", "Raytraced",
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted = []
|
||||||
|
kept = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for dm in list(DisplayModeDescription.GetDisplayModes()):
|
||||||
|
name_en = name_local = None
|
||||||
|
try: name_en = dm.EnglishName
|
||||||
|
except Exception: pass
|
||||||
|
try: name_local = dm.LocalName
|
||||||
|
except Exception: pass
|
||||||
|
name_display = name_en or name_local or "(Orphan, kein Name)"
|
||||||
|
is_builtin = (name_en in BUILTIN_NAMES) or (name_local in BUILTIN_NAMES)
|
||||||
|
|
||||||
|
if is_builtin:
|
||||||
|
kept.append(name_display)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Custom oder Orphan → loeschen
|
||||||
|
try:
|
||||||
|
dm_id = dm.Id
|
||||||
|
ok = DisplayModeDescription.DeleteDisplayMode(dm_id)
|
||||||
|
if ok:
|
||||||
|
deleted.append("{} ({})".format(name_display, dm_id))
|
||||||
|
else:
|
||||||
|
errors.append("{} → DeleteDisplayMode returned False".format(name_display))
|
||||||
|
except Exception as ex:
|
||||||
|
errors.append("{} → {}".format(name_display, ex))
|
||||||
|
|
||||||
|
print("[CLEAN] Display-Modes gesaeubert.")
|
||||||
|
print("[CLEAN] Built-ins behalten ({}):".format(len(kept)))
|
||||||
|
for n in kept:
|
||||||
|
print(" ✓ {}".format(n))
|
||||||
|
print("")
|
||||||
|
print("[CLEAN] Geloescht ({}):".format(len(deleted)))
|
||||||
|
for n in deleted:
|
||||||
|
print(" × {}".format(n))
|
||||||
|
if errors:
|
||||||
|
print("")
|
||||||
|
print("[CLEAN] Fehler ({}):".format(len(errors)))
|
||||||
|
for e in errors:
|
||||||
|
print(" ! {}".format(e))
|
||||||
|
print("")
|
||||||
|
print("[CLEAN] Fertig. Jetzt _reset_panels.py laufen lassen damit der")
|
||||||
|
print("[CLEAN] Plugin den 'Dossier Plan' aus dem Template neu importiert.")
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#! python3
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""Boundary/Hatch-Inspector — zeigt was Rhino setzt wenn du via
|
||||||
|
Properties-Panel die Section-Boundary aenderst.
|
||||||
|
|
||||||
|
Vorgehen:
|
||||||
|
1. Objekt selektieren
|
||||||
|
2. In Rhinos Properties → Section Style → Custom → Boundary verstellen
|
||||||
|
(Farbe ändern, Visible toggeln, Width setzen)
|
||||||
|
3. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_obj_boundary.py
|
||||||
|
4. Output schicken — speziell die Boundary-Properties
|
||||||
|
"""
|
||||||
|
import Rhino
|
||||||
|
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
if not objs:
|
||||||
|
print("[INSPECT] Bitte Objekt selektieren")
|
||||||
|
else:
|
||||||
|
obj = objs[0]
|
||||||
|
a = obj.Attributes
|
||||||
|
print("[INSPECT] Object {}".format(str(obj.Id)[:8]))
|
||||||
|
print("")
|
||||||
|
print("=== Attributes.SectionAttributesSource ===")
|
||||||
|
try: print(" =", a.SectionAttributesSource)
|
||||||
|
except Exception as ex: print(" err:", ex)
|
||||||
|
print("")
|
||||||
|
print("=== Attributes.GetCustomSectionStyle() — alle Props ===")
|
||||||
|
try:
|
||||||
|
css = a.GetCustomSectionStyle()
|
||||||
|
if css is None:
|
||||||
|
print(" None (kein Custom-SectionStyle)")
|
||||||
|
else:
|
||||||
|
for n in sorted(dir(css)):
|
||||||
|
if n.startswith("_"): continue
|
||||||
|
try:
|
||||||
|
v = getattr(css, n)
|
||||||
|
if callable(v): continue
|
||||||
|
sv = str(v)
|
||||||
|
if len(sv) > 80: sv = sv[:77] + "..."
|
||||||
|
print(" {} = {}".format(n, sv))
|
||||||
|
except Exception as ex:
|
||||||
|
print(" {} <unreadable: {}>".format(n, ex))
|
||||||
|
except Exception as ex:
|
||||||
|
print(" err:", ex)
|
||||||
|
print("")
|
||||||
|
print("=== Layer.GetCustomSectionStyle (Layer-Default) ===")
|
||||||
|
try:
|
||||||
|
lyr = doc.Layers[a.LayerIndex]
|
||||||
|
print(" Layer:", lyr.FullPath)
|
||||||
|
if hasattr(lyr, "GetCustomSectionStyle"):
|
||||||
|
css = lyr.GetCustomSectionStyle()
|
||||||
|
if css is None:
|
||||||
|
print(" Layer hat KEIN Custom-SectionStyle")
|
||||||
|
else:
|
||||||
|
for n in sorted(dir(css)):
|
||||||
|
if n.startswith("_"): continue
|
||||||
|
try:
|
||||||
|
v = getattr(css, n)
|
||||||
|
if callable(v): continue
|
||||||
|
sv = str(v)
|
||||||
|
if len(sv) > 80: sv = sv[:77] + "..."
|
||||||
|
print(" {} = {}".format(n, sv))
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print(" err:", ex)
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#! python3
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""Dumpt ALLE Section-/Hatch-relevanten Properties des selektierten Objekts.
|
||||||
|
So sehen wir was Rhino's eigene Section-Style-UI tatsaechlich setzt vs.
|
||||||
|
was unser Plugin-Code setzt.
|
||||||
|
|
||||||
|
Vorgehen:
|
||||||
|
1. Ein 3D-Objekt selektieren (Wand, Box, ...)
|
||||||
|
2. In Rhinos Properties-Panel manuell SectionStyle → Custom mit spezifischen
|
||||||
|
Werten setzen (z.B. Pattern Color=Gruen, Pattern Rotation=20, Pattern
|
||||||
|
Scale=2.4, Boundary Color=Rot, Boundary Width Scale=6) → Apply
|
||||||
|
3. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_obj_section.py
|
||||||
|
4. Output an Claude
|
||||||
|
"""
|
||||||
|
import Rhino
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(v):
|
||||||
|
if v is None: return "None"
|
||||||
|
s = str(v)
|
||||||
|
if len(s) > 80: s = s[:77] + "..."
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _dump_group(css, prefix, title):
|
||||||
|
"""Dumpt Properties auf css deren Name mit `prefix` (case-insens) anfaengt."""
|
||||||
|
print("--- {} ---".format(title))
|
||||||
|
p_lower = prefix.lower()
|
||||||
|
found = False
|
||||||
|
for n in sorted(dir(css)):
|
||||||
|
if n.startswith("_"): continue
|
||||||
|
if p_lower not in n.lower(): continue
|
||||||
|
try:
|
||||||
|
v = getattr(css, n)
|
||||||
|
if callable(v): continue
|
||||||
|
found = True
|
||||||
|
print(" {:32s} = {}".format(n, _fmt(v)))
|
||||||
|
except Exception as ex:
|
||||||
|
print(" {:32s} = <unreadable: {}>".format(n, ex))
|
||||||
|
if not found:
|
||||||
|
print(" (nichts)")
|
||||||
|
|
||||||
|
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
if not objs:
|
||||||
|
print("[INSPECT] Bitte ein Objekt selektieren")
|
||||||
|
else:
|
||||||
|
obj = objs[0]
|
||||||
|
a = obj.Attributes
|
||||||
|
print("[INSPECT] Object: {} (Id={})".format(type(obj).__name__, obj.Id))
|
||||||
|
|
||||||
|
# SectionAttributesSource (FromLayer / FromObject)
|
||||||
|
print("")
|
||||||
|
print("=== Attributes ===")
|
||||||
|
try:
|
||||||
|
print(" SectionAttributesSource =", a.SectionAttributesSource)
|
||||||
|
except Exception as ex:
|
||||||
|
print(" SectionAttributesSource err:", ex)
|
||||||
|
try:
|
||||||
|
print(" HatchBackgroundFillColor =", a.HatchBackgroundFillColor)
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
print(" HatchBoundaryVisible =", a.HatchBoundaryVisible)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# Custom SectionStyle aus Object
|
||||||
|
print("")
|
||||||
|
print("=== Object.GetCustomSectionStyle() ===")
|
||||||
|
css = None
|
||||||
|
if hasattr(a, "GetCustomSectionStyle"):
|
||||||
|
try:
|
||||||
|
css = a.GetCustomSectionStyle()
|
||||||
|
except Exception as ex:
|
||||||
|
print(" err:", ex)
|
||||||
|
|
||||||
|
if css is None:
|
||||||
|
print(" None (kein Custom-SectionStyle set)")
|
||||||
|
else:
|
||||||
|
print(" Type:", type(css).__name__)
|
||||||
|
print("")
|
||||||
|
# Gruppierte Property-Dumps damit Mapping zu Rhino-UI klar wird
|
||||||
|
_dump_group(css, "Hatch", "Hatch (Pattern, Color, Scale, Rotation)")
|
||||||
|
print("")
|
||||||
|
_dump_group(css, "Boundary", "Boundary (Visible, Color, Width)")
|
||||||
|
print("")
|
||||||
|
_dump_group(css, "Background", "Background (FillColor, FillMode)")
|
||||||
|
print("")
|
||||||
|
# Section-spezifisch (SectionFillRule etc.)
|
||||||
|
print("--- Misc Section ---")
|
||||||
|
for n in ("SectionFillRule", "Name", "Id", "HasUserData", "Index"):
|
||||||
|
if hasattr(css, n):
|
||||||
|
try: print(" {:32s} = {}".format(n, _fmt(getattr(css, n))))
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# Layer-Default SectionStyle als Vergleich
|
||||||
|
print("")
|
||||||
|
print("=== Layer.GetCustomSectionStyle (Layer-Default) ===")
|
||||||
|
try:
|
||||||
|
lyr = doc.Layers[a.LayerIndex]
|
||||||
|
print(" Layer:", lyr.FullPath, "Color:", lyr.Color)
|
||||||
|
if hasattr(lyr, "GetCustomSectionStyle"):
|
||||||
|
l_css = lyr.GetCustomSectionStyle()
|
||||||
|
if l_css is None:
|
||||||
|
print(" Layer hat KEIN Custom-SectionStyle")
|
||||||
|
else:
|
||||||
|
_dump_group(l_css, "Hatch", "Layer.Hatch")
|
||||||
|
_dump_group(l_css, "Boundary", "Layer.Boundary")
|
||||||
|
_dump_group(l_css, "Background", "Layer.Background")
|
||||||
|
except Exception as ex:
|
||||||
|
print(" err:", ex)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#! python3
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""One-Shot-Diagnose: dumpt alle Properties + Werte des 'Dossier Plan'
|
||||||
|
Display-Modes und exportiert ihn als ini neben dem Skript.
|
||||||
|
|
||||||
|
Vorgehen:
|
||||||
|
1. In Rhinos Display-Mode-Editor: 'Show HiddenLines' AUS schalten +
|
||||||
|
Apply
|
||||||
|
2. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_inspect_plan_mode.py
|
||||||
|
3. Resultat: zeigt alle Hidden/Tangent/Silhouette-Properties +
|
||||||
|
/tmp/dossier_plan_inspect.ini
|
||||||
|
|
||||||
|
So koennen wir sehen welche Property-Namen Mac Rhino tatsaechlich hat.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from Rhino.Display import DisplayModeDescription
|
||||||
|
|
||||||
|
target_name = "Dossier Plan"
|
||||||
|
dmd = None
|
||||||
|
for dm in DisplayModeDescription.GetDisplayModes():
|
||||||
|
if dm.EnglishName == target_name or dm.LocalName == target_name:
|
||||||
|
dmd = dm; break
|
||||||
|
|
||||||
|
if dmd is None:
|
||||||
|
print("[INSPECT] 'Dossier Plan' not found")
|
||||||
|
else:
|
||||||
|
attrs = dmd.DisplayAttributes
|
||||||
|
print("[INSPECT] Mode gefunden: {} (Id={})".format(dmd.EnglishName, dmd.Id))
|
||||||
|
print("")
|
||||||
|
print("=== ALLE DisplayAttributes Properties mit Werten ===")
|
||||||
|
for n in sorted(dir(attrs)):
|
||||||
|
if n.startswith("_"): continue
|
||||||
|
try:
|
||||||
|
v = getattr(attrs, n)
|
||||||
|
if callable(v): continue
|
||||||
|
sv = str(v)
|
||||||
|
if len(sv) > 80: sv = sv[:77] + "..."
|
||||||
|
print(" {} = {}".format(n, sv))
|
||||||
|
except Exception as ex:
|
||||||
|
print(" {} = <unreadable: {}>".format(n, ex))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("=== Sub-Objekt Properties (ALLE) ===")
|
||||||
|
# Erst alle Sub-Objekt-Properties autodetect (anything mit "+" im String)
|
||||||
|
sub_names = set()
|
||||||
|
for n in dir(attrs):
|
||||||
|
if n.startswith("_"): continue
|
||||||
|
try:
|
||||||
|
v = getattr(attrs, n)
|
||||||
|
if callable(v): continue
|
||||||
|
if "DisplayPipelineAttributes+" in str(v):
|
||||||
|
sub_names.add(n)
|
||||||
|
except Exception: pass
|
||||||
|
# Plus die expliziten Kandidaten
|
||||||
|
for hard in ("CurveSettings", "ObjectSettings", "ShadingSettings",
|
||||||
|
"MeshSpecificAttributes", "SubObjectDisplayMode",
|
||||||
|
"ViewSpecificAttributes"):
|
||||||
|
if hasattr(attrs, hard): sub_names.add(hard)
|
||||||
|
|
||||||
|
for sub_name in sorted(sub_names):
|
||||||
|
try:
|
||||||
|
sub = getattr(attrs, sub_name)
|
||||||
|
print(" --- {} ---".format(sub_name))
|
||||||
|
for n in sorted(dir(sub)):
|
||||||
|
if n.startswith("_"): continue
|
||||||
|
try:
|
||||||
|
v = getattr(sub, n)
|
||||||
|
if callable(v): continue
|
||||||
|
sv = str(v)
|
||||||
|
if len(sv) > 80: sv = sv[:77] + "..."
|
||||||
|
print(" {} = {}".format(n, sv))
|
||||||
|
except Exception as ex:
|
||||||
|
print(" {} = <unreadable: {}>".format(n, ex))
|
||||||
|
except Exception as ex:
|
||||||
|
print(" {} couldn't be inspected: {}".format(sub_name, ex))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("=== ini-Export ===")
|
||||||
|
# In den Desktop schreiben damit der User die Datei einfach manuell
|
||||||
|
# oeffnen + mir den Inhalt schicken kann (in /tmp gehts manchmal verloren).
|
||||||
|
ini_path = os.path.expanduser("~/Desktop/dossier_plan_inspect.ini")
|
||||||
|
try:
|
||||||
|
ok = DisplayModeDescription.ExportToFile(dmd, ini_path)
|
||||||
|
print(" Export OK: {} → {}".format(ok, ini_path))
|
||||||
|
if ok and os.path.exists(ini_path):
|
||||||
|
with open(ini_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
content = f.read()
|
||||||
|
print(" ini-Inhalt ({} chars) — siehe Datei auf dem Desktop.".format(len(content)))
|
||||||
|
# Falls Rhinos Log das Print durchlaesst, hier ueberhaupt rein
|
||||||
|
print("===INI-START===")
|
||||||
|
for line in content.split("\n"):
|
||||||
|
print(line)
|
||||||
|
print("===INI-END===")
|
||||||
|
except Exception as ex:
|
||||||
|
print(" Export-Fehler:", ex)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""Hilfsscript: alle Dossier-Panel-Registrierungs-Flags clearen + Module
|
"""Hilfsscript: alle Dossier-Panel-Registrierungs-Flags clearen + Module
|
||||||
neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos
|
neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos
|
||||||
Panel-Manager cached die Icon-Bindung pro GUID — fuer NEUE Icons hilft
|
Panel-Manager cached die Icon-Bindung pro GUID — fuer NEUE Icons hilft
|
||||||
|
|||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
#
|
||||||
|
# Nuke + Reset Rhino-8 Settings (Mac). Backupt vorher in einen Ordner mit
|
||||||
|
# Zeitstempel — verlorene Settings koennen daraus rekonstruiert werden.
|
||||||
|
#
|
||||||
|
# Was geht VERLOREN:
|
||||||
|
# - Alle Custom-Display-Modes (Dossier Plan, DOSSIER2D, etc.)
|
||||||
|
# - Window-Layouts, Toolbar-Customizations
|
||||||
|
# - Custom-Keyboard-Shortcuts
|
||||||
|
# - Tab-Panel-Positions
|
||||||
|
#
|
||||||
|
# Was bleibt:
|
||||||
|
# - Lizenz (License Manager Ordner wird NICHT angefasst)
|
||||||
|
# - .3dm Templates
|
||||||
|
# - Scripts unter scripts/
|
||||||
|
# - Plugin-Einstellungen (in Plug-ins/-Unterordnern)
|
||||||
|
#
|
||||||
|
# Vorgehen:
|
||||||
|
# 1. Rhino komplett quitten (Cmd+Q)
|
||||||
|
# 2. ./rhino/_reset_rhino_settings.sh
|
||||||
|
# 3. Rhino neu starten
|
||||||
|
# 4. _RunPythonScript .../_reset_panels.py → Plan-Mode aus Template
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SETTINGS_DIR="$HOME/Library/Application Support/McNeel/Rhinoceros/8.0/settings"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
BACKUP="$SETTINGS_DIR.backup-$TIMESTAMP"
|
||||||
|
|
||||||
|
if [ ! -d "$SETTINGS_DIR" ]; then
|
||||||
|
echo "[RESET] Settings-Ordner nicht gefunden: $SETTINGS_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check ob Rhino läuft
|
||||||
|
if pgrep -x "Rhinoceros" > /dev/null; then
|
||||||
|
echo "[RESET] FEHLER: Rhino läuft noch. Bitte erst Cmd+Q drücken."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[RESET] Backup → $BACKUP"
|
||||||
|
mv "$SETTINGS_DIR" "$BACKUP"
|
||||||
|
echo "[RESET] Settings nun zurückgesetzt."
|
||||||
|
echo "[RESET] Beim nächsten Rhino-Start werden Defaults regeneriert."
|
||||||
|
echo "[RESET] Backup liegt unter: $BACKUP"
|
||||||
|
echo ""
|
||||||
|
echo "[RESET] Nächste Schritte:"
|
||||||
|
echo " 1. Rhino starten"
|
||||||
|
echo " 2. _RunPythonScript /Users/karim/STUDIO/DOSSIER/rhino/_reset_panels.py"
|
||||||
|
echo " → Dossier-Plan wird aus Template neu erstellt"
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
#! python 3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""
|
||||||
|
_startup_splash.py
|
||||||
|
Petrol-grüner Splash-Screen waehrend des DOSSIER-Plugin-Startups.
|
||||||
|
Borderless Eto-Form mit WebView + Inline-HTML im selben Stil wie der
|
||||||
|
Launcher-Splash. Bedeckt visuell die 3+ Sekunden waehrend Rhino die
|
||||||
|
Panels registriert + WindowLayout neu anwendet.
|
||||||
|
|
||||||
|
Wird von startup.py beim ersten Idle gezeigt und nach Layout-Apply
|
||||||
|
(oder Timeout) wieder versteckt.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import Rhino
|
||||||
|
import scriptcontext as sc
|
||||||
|
|
||||||
|
_SPLASH_KEY = "_dossier_startup_splash"
|
||||||
|
_SPLASH_SHOWN_AT_KEY = "_dossier_startup_splash_shown_at"
|
||||||
|
_SAFETY_TIMEOUT_SEC = 12.0 # spaetestens nach 12s wegmachen, falls Hide-Hook nicht feuert
|
||||||
|
|
||||||
|
# Marker den der Launcher direkt vor `open -a Rhinoceros` schreibt, damit
|
||||||
|
# Plugin-Splash NICHT zusaetzlich zum Launcher-Splash erscheint.
|
||||||
|
_OWNER_MARKER = os.path.expanduser(
|
||||||
|
"~/Library/Application Support/ch.gabrielevarano.Dossier/splash_owner_launcher.flag"
|
||||||
|
)
|
||||||
|
_OWNER_FRESH_SEC = 30.0 # Stale-Schutz falls Launcher crasht
|
||||||
|
|
||||||
|
|
||||||
|
_SPLASH_HTML = '''<!DOCTYPE html>
|
||||||
|
<html lang="de"><head><meta charset="utf-8"/><title>Dossier laedt</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
|
||||||
|
--paper: #fff; --paper-mute: rgba(255,255,255,0.72); --paper-faint: rgba(255,255,255,0.45);
|
||||||
|
--font-display: Krungthep, 'Archivo Black', sans-serif;
|
||||||
|
--font-mono: 'DM Mono', 'Menlo', monospace;
|
||||||
|
}
|
||||||
|
html, body { margin:0; padding:0; width:100%; height:100%; background:transparent !important;
|
||||||
|
color:var(--paper); overflow:hidden; font-family:var(--font-mono); user-select:none;
|
||||||
|
-webkit-user-select:none; cursor:default; }
|
||||||
|
.frame { box-sizing:border-box; width:100%; height:100%; padding:22px 26px;
|
||||||
|
display:grid; grid-template-rows:auto 1fr auto; gap:0;
|
||||||
|
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
|
||||||
|
border-radius:16px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.18); }
|
||||||
|
.brand-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px; }
|
||||||
|
.brand { font-family:var(--font-display); font-size:28px; letter-spacing:-0.01em;
|
||||||
|
line-height:1; color:var(--paper); }
|
||||||
|
.brand-dot { color:var(--accent-deep); }
|
||||||
|
.version { font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em;
|
||||||
|
color:var(--paper-mute); text-transform:uppercase; }
|
||||||
|
.status-row { align-self:end; display:flex; align-items:center; gap:10px;
|
||||||
|
margin-top:18px; font-size:11px; letter-spacing:0.10em; color:var(--paper);
|
||||||
|
text-transform:uppercase; }
|
||||||
|
.dot-pulse { width:7px; height:7px; border-radius:50%; background:var(--paper); }
|
||||||
|
.bar { position:relative; height:2px; width:100%; background:rgba(255,255,255,0.28);
|
||||||
|
border-radius:2px; margin-top:12px; }
|
||||||
|
.meta-row { display:flex; align-items:baseline; justify-content:space-between; gap:12px;
|
||||||
|
margin-top:10px; font-size:9px; letter-spacing:0.14em; color:var(--paper-faint);
|
||||||
|
text-transform:uppercase; }
|
||||||
|
</style></head><body>
|
||||||
|
<div class="frame">
|
||||||
|
<div class="brand-row">
|
||||||
|
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
|
||||||
|
<div class="version">Rhino 8 Plugin</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="status-row">
|
||||||
|
<span class="dot-pulse"></span>
|
||||||
|
<span>Plugin laedt — Panels werden platziert</span>
|
||||||
|
</div>
|
||||||
|
<div class="bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="meta-row">
|
||||||
|
<span>AGPL-3.0 · Karim Gabriele Varano</span>
|
||||||
|
<span>CPython 3.9</span>
|
||||||
|
</div>
|
||||||
|
</div></body></html>
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _try_borderless_mac(form):
|
||||||
|
"""Mac-spezifisch: direkter NSWindow-Zugriff via Eto.ControlObject um
|
||||||
|
titlebar/Decorations komplett zu killen.
|
||||||
|
|
||||||
|
Eto.Mac.Forms.EtoWindow IST-A NSWindow (Xamarin.Mac-Subclass).
|
||||||
|
StyleMask ist ein .NET-Enum-Property — Python.NET 3 verlangt explizite
|
||||||
|
Enum-Konversion (kein impliziter int → Enum cast mehr). Wir leiten
|
||||||
|
den Enum-Typ zur Laufzeit aus dem Getter ab und konstruieren den
|
||||||
|
Borderless-Wert via System.Enum.ToObject."""
|
||||||
|
nswindow = getattr(form, "ControlObject", None)
|
||||||
|
if nswindow is None:
|
||||||
|
print("[SPLASH] keine ControlObject auf Form")
|
||||||
|
return False
|
||||||
|
print("[SPLASH] ControlObject type:", str(type(nswindow)))
|
||||||
|
|
||||||
|
import System
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
# NSWindowStyleMaskBorderless = 0
|
||||||
|
# NSWindowStyleMaskTitled = 1, FullSizeContentView = 32768
|
||||||
|
try:
|
||||||
|
current = nswindow.StyleMask
|
||||||
|
style_type = type(current)
|
||||||
|
borderless = System.Enum.ToObject(style_type, 0)
|
||||||
|
nswindow.StyleMask = borderless
|
||||||
|
print("[SPLASH] StyleMask=0 (Borderless) applied")
|
||||||
|
ok = True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] StyleMask Enum:", ex)
|
||||||
|
# Fallback: FullSizeContentView (32768) + TitlebarAppearsTransparent
|
||||||
|
# damit Content unter die (transparente) Titlebar reicht
|
||||||
|
try:
|
||||||
|
current = nswindow.StyleMask
|
||||||
|
style_type = type(current)
|
||||||
|
full = System.Enum.ToObject(style_type, 1 | 32768)
|
||||||
|
nswindow.StyleMask = full
|
||||||
|
print("[SPLASH] StyleMask=Titled|FullSize set (Fallback)")
|
||||||
|
ok = True
|
||||||
|
except Exception as ex2:
|
||||||
|
print("[SPLASH] StyleMask Fallback:", ex2)
|
||||||
|
|
||||||
|
# Titlebar transparent + Titel unsichtbar
|
||||||
|
def _set_prop(prop, value, log=False):
|
||||||
|
try:
|
||||||
|
setattr(nswindow, prop, value)
|
||||||
|
if log: print("[SPLASH] {}={} OK".format(prop, value))
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
if log: print("[SPLASH] {}:".format(prop), ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
_set_prop("TitlebarAppearsTransparent", True, True)
|
||||||
|
# NSWindowTitleHidden = 1
|
||||||
|
try:
|
||||||
|
tv_type = type(nswindow.TitleVisibility)
|
||||||
|
nswindow.TitleVisibility = System.Enum.ToObject(tv_type, 1)
|
||||||
|
print("[SPLASH] TitleVisibility=Hidden OK")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] TitleVisibility:", ex)
|
||||||
|
_set_prop("IsOpaque", False)
|
||||||
|
_set_prop("HasShadow", True)
|
||||||
|
_set_prop("MovableByWindowBackground", True)
|
||||||
|
|
||||||
|
# Clear NSWindow background damit rounded corners aus dem HTML sichtbar
|
||||||
|
# werden. Xamarin.Mac exponiert NSColor.Clear als statische Property.
|
||||||
|
try:
|
||||||
|
from AppKit import NSColor as _NSC
|
||||||
|
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
|
||||||
|
if clear is not None:
|
||||||
|
nswindow.BackgroundColor = clear
|
||||||
|
print("[SPLASH] BackgroundColor=Clear OK")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] BackgroundColor Clear:", ex)
|
||||||
|
|
||||||
|
# Force-Paint: Splash MUSS sichtbar sein BEVOR Rhino den Script-Thread
|
||||||
|
# weiter belegt. Python-Script blockiert sonst die Main-Loop und der
|
||||||
|
# Splash wuerde erst nach Script-Ende paintet werden — viel zu spaet.
|
||||||
|
try: nswindow.OrderFrontRegardless()
|
||||||
|
except Exception: pass
|
||||||
|
try: nswindow.DisplayIfNeeded()
|
||||||
|
except Exception: pass
|
||||||
|
try: nswindow.Display()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def _try_transparent_webview_mac(wv):
|
||||||
|
"""WKWebView transparent machen damit der NSWindow-Hintergrund (oder
|
||||||
|
nichts) durchscheint und runde Ecken sichtbar werden. wv.ControlObject
|
||||||
|
ist die WKWebView."""
|
||||||
|
wk = getattr(wv, "ControlObject", None)
|
||||||
|
if wk is None:
|
||||||
|
print("[SPLASH] WebView: keine ControlObject"); return
|
||||||
|
print("[SPLASH] WebView ControlObject type:", str(type(wk)))
|
||||||
|
|
||||||
|
# KVC: setValue:forKey:@"drawsBackground" → @NO. Funktioniert sowohl bei
|
||||||
|
# WebView (alt) als auch WKWebView (NSObject KVC). Das ist der zuverlaessige
|
||||||
|
# Weg WebView-Hintergrund komplett zu entfernen, besser als UnderPageBg.
|
||||||
|
try:
|
||||||
|
from Foundation import NSNumber, NSString
|
||||||
|
try:
|
||||||
|
wk.SetValueForKey(NSNumber.FromBoolean(False), NSString("drawsBackground"))
|
||||||
|
print("[SPLASH] WebView drawsBackground=NO via KVC OK")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] KVC drawsBackground:", ex)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] Foundation import:", ex)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from AppKit import NSColor as _NSC
|
||||||
|
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
|
||||||
|
if clear is not None:
|
||||||
|
try: wk.UnderPageBackgroundColor = clear
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
layer = getattr(wk, "Layer", None)
|
||||||
|
if layer is not None:
|
||||||
|
layer.BackgroundColor = clear.CGColor
|
||||||
|
layer.Opaque = False
|
||||||
|
print("[SPLASH] WebView Layer transparent OK")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] WebView Layer:", ex)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] WebView NSColor:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch_to_main(fn):
|
||||||
|
"""Fuehrt fn beim naechsten Rhino-Idle-Event aus. Mac Eto/AppKit
|
||||||
|
erfordert UI-Mutationen auf dem Main-Thread; threading.Timer-Callbacks
|
||||||
|
laufen im falschen Thread und Close() crasht oder no-op't dort."""
|
||||||
|
handler_ref = [None]
|
||||||
|
def _idle(sender, e):
|
||||||
|
try: Rhino.RhinoApp.Idle -= handler_ref[0]
|
||||||
|
except Exception: pass
|
||||||
|
try: fn()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] dispatched fn:", ex)
|
||||||
|
handler_ref[0] = _idle
|
||||||
|
try: Rhino.RhinoApp.Idle += _idle
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] idle subscribe:", ex)
|
||||||
|
try: fn()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
|
def _install_safety_timeout():
|
||||||
|
"""Registriert Idle-Handler der periodisch prueft ob _SAFETY_TIMEOUT_SEC
|
||||||
|
erreicht ist. Cleanup-self wenn Splash bereits zu."""
|
||||||
|
handler_ref = [None]
|
||||||
|
def _idle(sender, e):
|
||||||
|
try:
|
||||||
|
if sc.sticky.get(_SPLASH_KEY) is None:
|
||||||
|
try: Rhino.RhinoApp.Idle -= handler_ref[0]
|
||||||
|
except Exception: pass
|
||||||
|
return
|
||||||
|
shown_at = sc.sticky.get(_SPLASH_SHOWN_AT_KEY) or 0
|
||||||
|
if shown_at and (time.time() - shown_at) >= _SAFETY_TIMEOUT_SEC:
|
||||||
|
try: Rhino.RhinoApp.Idle -= handler_ref[0]
|
||||||
|
except Exception: pass
|
||||||
|
print("[SPLASH] safety-timeout — auto-hide")
|
||||||
|
try: _hide_main()
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
handler_ref[0] = _idle
|
||||||
|
try: Rhino.RhinoApp.Idle += _idle
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] safety install:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _launcher_owns_splash():
|
||||||
|
"""True wenn Launcher direkt vor Rhino-Launch einen frischen Owner-
|
||||||
|
Marker geschrieben hat. Verhindert doppelte Splashes."""
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(_OWNER_MARKER):
|
||||||
|
return False
|
||||||
|
age = time.time() - os.path.getmtime(_OWNER_MARKER)
|
||||||
|
if age <= _OWNER_FRESH_SEC:
|
||||||
|
return True
|
||||||
|
except Exception: pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def show():
|
||||||
|
"""Zeigt den Splash. Idempotent — zweiter Aufruf bringt das bestehende
|
||||||
|
Fenster nur in den Vordergrund. Auto-Hide nach _SAFETY_TIMEOUT_SEC
|
||||||
|
als Fallback via Idle-Polling (NICHT threading.Timer — Mac UI braucht
|
||||||
|
Main-Thread). Skipt wenn Launcher seinen eigenen Splash zeigt."""
|
||||||
|
if _launcher_owns_splash():
|
||||||
|
print("[SPLASH] Launcher zeigt eigenen Splash — skip"); return
|
||||||
|
if sc.sticky.get(_SPLASH_KEY) is not None:
|
||||||
|
print("[SPLASH] schon offen — skip"); return
|
||||||
|
try:
|
||||||
|
import Eto.Forms as ef
|
||||||
|
import Eto.Drawing as ed
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] Eto-Import:", ex); return
|
||||||
|
try:
|
||||||
|
form = ef.Form()
|
||||||
|
form.Title = "" # leerer Titel hilft bei Mac-Titlebar-Reduktion
|
||||||
|
# Versuche WindowStyle.None (Eto-API, funktioniert nicht immer auf Mac)
|
||||||
|
try: form.WindowStyle = getattr(ef.WindowStyle, "None")
|
||||||
|
except Exception: pass
|
||||||
|
# Alle Window-Chrome-Optionen aus
|
||||||
|
for attr, val in (
|
||||||
|
("Resizable", False), ("Minimizable", False),
|
||||||
|
("Maximizable", False), ("Closeable", False),
|
||||||
|
("ShowInTaskbar", False), ("Topmost", True),
|
||||||
|
):
|
||||||
|
try: setattr(form, attr, val)
|
||||||
|
except Exception: pass
|
||||||
|
try: form.Size = ed.Size(420, 160)
|
||||||
|
except Exception: pass
|
||||||
|
# Transparent so dass WebView's eigene rounded gradient sichtbar wird
|
||||||
|
try:
|
||||||
|
form.BackgroundColor = ed.Colors.Transparent
|
||||||
|
except Exception:
|
||||||
|
try: form.BackgroundColor = ed.Color(0.37, 0.66, 0.59)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
wv = ef.WebView()
|
||||||
|
try:
|
||||||
|
# WebView selber transparent damit das Form-Hintergrund durchscheint
|
||||||
|
wv.BackgroundColor = ed.Colors.Transparent
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
wv.LoadHtml(_SPLASH_HTML)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] LoadHtml:", ex)
|
||||||
|
form.Content = wv
|
||||||
|
|
||||||
|
# Center on screen
|
||||||
|
try:
|
||||||
|
screen = ef.Screen.PrimaryScreen
|
||||||
|
sb = screen.Bounds
|
||||||
|
x = int(sb.X + (sb.Width - form.Size.Width) / 2)
|
||||||
|
y = int(sb.Y + (sb.Height - form.Size.Height) / 2 - 100)
|
||||||
|
form.Location = ed.Point(x, y)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] center:", ex)
|
||||||
|
|
||||||
|
try: form.Show()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] Show:", ex); return
|
||||||
|
|
||||||
|
# Mac-spezifischer Borderless-Hack — MUSS nach Show() laufen damit
|
||||||
|
# die NSWindow existiert
|
||||||
|
try:
|
||||||
|
if _try_borderless_mac(form):
|
||||||
|
print("[SPLASH] Borderless (Mac NSWindow) applied")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] borderless-mac:", ex)
|
||||||
|
# WebView transparent (rounded corners via HTML border-radius)
|
||||||
|
try: _try_transparent_webview_mac(wv)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] webview-clear:", ex)
|
||||||
|
# Event-Loop einmal explizit pumpen damit Splash gepainted wird
|
||||||
|
# bevor das Script weiter blockiert (sonst sieht Nutzer die Panels
|
||||||
|
# zuerst entstehen und Splash erscheint erst danach).
|
||||||
|
try:
|
||||||
|
ef.Application.Instance.RunIteration()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sc.sticky[_SPLASH_KEY] = form
|
||||||
|
sc.sticky[_SPLASH_SHOWN_AT_KEY] = time.time()
|
||||||
|
print("[SPLASH] visible")
|
||||||
|
# Safety-Timeout via Idle-Polling (Main-Thread, Mac-safe)
|
||||||
|
_install_safety_timeout()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] show:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _hide_main():
|
||||||
|
"""Synchroner Close — MUSS auf Main-Thread laufen. Nur intern aufrufen,
|
||||||
|
extern hide() verwenden."""
|
||||||
|
form = sc.sticky.get(_SPLASH_KEY)
|
||||||
|
if form is None:
|
||||||
|
return
|
||||||
|
sc.sticky[_SPLASH_KEY] = None
|
||||||
|
sc.sticky[_SPLASH_SHOWN_AT_KEY] = None
|
||||||
|
try: form.Close()
|
||||||
|
except Exception:
|
||||||
|
try: form.Visible = False
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SPLASH] hide visible:", ex)
|
||||||
|
print("[SPLASH] hidden")
|
||||||
|
|
||||||
|
|
||||||
|
def hide():
|
||||||
|
"""Versteckt + entsorgt den Splash. Idempotent + thread-safe —
|
||||||
|
dispatcht auf Rhino-Main-Thread via Idle-Event."""
|
||||||
|
if sc.sticky.get(_SPLASH_KEY) is None:
|
||||||
|
return
|
||||||
|
_dispatch_to_main(_hide_main)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
about.py
|
about.py
|
||||||
About-Dialog als Eto-Form + WebView. Vom DOSSIER-Logo-Klick in der
|
About-Dialog als Eto-Form + WebView. Vom DOSSIER-Logo-Klick in der
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'aussparung'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("aussparung")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'dach'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("dach")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'decke'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("decke")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Alias 'dkeys': oeffnet DOSSIER Shortcuts-Cheatsheet
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
import welcome
|
||||||
|
welcome.show_cheatsheet()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Alias 'dwelcome': zeigt DOSSIER Welcome-Screen manuell (force-mode,
|
||||||
|
# ignoriert version-marker + optout)
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
import welcome
|
||||||
|
welcome._show_welcome_now()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'fenster'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("fenster")
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Pipette / Einstellungen-übernehmen: User klickt ein Source-Objekt, dessen
|
||||||
|
# Attribute werden zur aktuellen Default-Einstellung gemacht — der naechste
|
||||||
|
# gezeichnete Curve/Rectangle/etc. erbt sie automatisch.
|
||||||
|
#
|
||||||
|
# Was uebernommen wird:
|
||||||
|
# 1. Layer → wird zum Current Layer
|
||||||
|
# 2. Color (wenn per-Object Override) → wird Current Object-Color
|
||||||
|
# 3. Linetype (per-Object) → Current
|
||||||
|
# 4. PlotWeight (per-Object) → Current
|
||||||
|
# 5. Fuer DOSSIER-Elemente (wand_axis, treppe_axis, etc.) → spezifische
|
||||||
|
# UserStrings (Dicke, Modus, Breite, Stufen etc.) werden in sticky
|
||||||
|
# gespeichert als _last_* → nachste Create-Wand/Treppe etc. nimmt sie.
|
||||||
|
# 6. Bei Hatch-Quelle → wechselt auf den Curve dahinter (Hatch hat selten
|
||||||
|
# direkt Sinn als Pipette-Quelle, eher der gefuellte Rahmen).
|
||||||
|
import scriptcontext as sc
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Input.Custom as ric
|
||||||
|
import Rhino.DocObjects as rdoc
|
||||||
|
from Rhino.Input import GetResult
|
||||||
|
|
||||||
|
|
||||||
|
# Welche UserStrings pro DOSSIER-Type als sticky _last_* gespeichert werden,
|
||||||
|
# damit das naechste Create-Cmd sie als Default uebernimmt.
|
||||||
|
_DOSSIER_INHERIT = {
|
||||||
|
"wand_axis": [
|
||||||
|
("dossier_wand_dicke", "wand_dicke"),
|
||||||
|
("dossier_wand_referenz", "wand_referenz"),
|
||||||
|
("dossier_wand_modus", "wand_modus"),
|
||||||
|
],
|
||||||
|
"treppe_axis": [
|
||||||
|
("dossier_treppe_breite", "treppe_breite"),
|
||||||
|
("dossier_treppe_n", "treppe_n"),
|
||||||
|
("dossier_treppe_referenz", "treppe_referenz"),
|
||||||
|
("dossier_treppe_modus", "treppe_modus"),
|
||||||
|
("dossier_treppe_lauf_d", "treppe_lauf_d"),
|
||||||
|
("dossier_treppe_art", "treppe_art"),
|
||||||
|
],
|
||||||
|
"decke_outline": [
|
||||||
|
("dossier_decke_dicke", "decke_dicke"),
|
||||||
|
("dossier_decke_modus", "decke_modus"),
|
||||||
|
],
|
||||||
|
"dach_outline": [
|
||||||
|
("dossier_dach_dicke", "dach_dicke"),
|
||||||
|
("dossier_dach_neigung", "dach_neigung"),
|
||||||
|
],
|
||||||
|
"stuetze_point": [
|
||||||
|
("dossier_trag_profil", "stuetze_profil"),
|
||||||
|
("dossier_trag_b", "stuetze_b"),
|
||||||
|
("dossier_trag_h", "stuetze_h"),
|
||||||
|
],
|
||||||
|
"traeger_axis": [
|
||||||
|
("dossier_trag_profil", "traeger_profil"),
|
||||||
|
("dossier_trag_b", "traeger_b"),
|
||||||
|
("dossier_trag_h", "traeger_h"),
|
||||||
|
],
|
||||||
|
"oeffnung_point": [
|
||||||
|
("dossier_oeff_breite", "oeff_breite"),
|
||||||
|
("dossier_oeff_hoehe", "oeff_hoehe"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_sticky(key, value):
|
||||||
|
sc.sticky["elemente_last_" + key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def _find_curve_behind_hatch(doc, hatch_obj):
|
||||||
|
"""Hatches haben in DOSSIER oft eine zugeordnete Source-Curve (gestaltung
|
||||||
|
speichert die Curve-ID auf der Hatch via 'ebenen_fill_owner')."""
|
||||||
|
try:
|
||||||
|
owner = hatch_obj.Attributes.GetUserString("ebenen_fill_owner") or ""
|
||||||
|
if owner:
|
||||||
|
import System
|
||||||
|
cid = System.Guid(owner)
|
||||||
|
cobj = doc.Objects.FindId(cid)
|
||||||
|
if cobj is not None and not cobj.IsDeleted: return cobj
|
||||||
|
except Exception: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
|
||||||
|
go = ric.GetObject()
|
||||||
|
go.SetCommandPrompt("Pipette: Quell-Objekt picken (Attribute uebernehmen)")
|
||||||
|
go.GeometryFilter = (rdoc.ObjectType.Curve
|
||||||
|
| rdoc.ObjectType.Brep
|
||||||
|
| rdoc.ObjectType.Hatch
|
||||||
|
| rdoc.ObjectType.PointSet
|
||||||
|
| rdoc.ObjectType.Point
|
||||||
|
| rdoc.ObjectType.Annotation
|
||||||
|
| rdoc.ObjectType.TextDot)
|
||||||
|
go.SubObjectSelect = False
|
||||||
|
if go.Get() != GetResult.Object:
|
||||||
|
print("[PIPETTE] abgebrochen"); return
|
||||||
|
|
||||||
|
src = go.Object(0).Object()
|
||||||
|
if src is None: return
|
||||||
|
|
||||||
|
# Wenn Hatch gepickt, switch zur Source-Curve (gefuelltes Rechteck als
|
||||||
|
# Pipette-Quelle ist intuitiver als die Hatch selbst)
|
||||||
|
src_geom_type = type(src.Geometry).__name__
|
||||||
|
if src_geom_type == "Hatch":
|
||||||
|
cobj = _find_curve_behind_hatch(doc, src)
|
||||||
|
if cobj is not None:
|
||||||
|
src = cobj
|
||||||
|
print("[PIPETTE] Hatch → zugeordnete Curve verwendet")
|
||||||
|
|
||||||
|
sa = src.Attributes
|
||||||
|
msgs = []
|
||||||
|
|
||||||
|
# 1. Layer als Current setzen
|
||||||
|
try:
|
||||||
|
if doc.Layers.CurrentLayerIndex != sa.LayerIndex:
|
||||||
|
doc.Layers.SetCurrentLayerIndex(sa.LayerIndex, True)
|
||||||
|
try: lname = doc.Layers[sa.LayerIndex].FullPath
|
||||||
|
except Exception: lname = "idx=" + str(sa.LayerIndex)
|
||||||
|
msgs.append("Layer={}".format(lname))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] Layer-Set:", ex)
|
||||||
|
|
||||||
|
# 2. Color
|
||||||
|
try:
|
||||||
|
cs = Rhino.ApplicationSettings.AppearanceSettings
|
||||||
|
if sa.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
|
||||||
|
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromObject
|
||||||
|
cs.DefaultObjectColor = sa.ObjectColor
|
||||||
|
msgs.append("Color=obj")
|
||||||
|
else:
|
||||||
|
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromLayer
|
||||||
|
msgs.append("Color=byLayer")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] Color-Set:", ex)
|
||||||
|
|
||||||
|
# 3. Linetype + 4. PlotWeight — komplexer, weil Rhino keine direkten
|
||||||
|
# AppearanceSettings dafuer hat. Wir ueberspringen bewusst, weil der
|
||||||
|
# Layer-Wechsel die meisten Faelle abdeckt (Linetype + PlotWeight
|
||||||
|
# kommen typisch ByLayer).
|
||||||
|
|
||||||
|
# 5. DOSSIER-spezifische Attrs in sticky uebernehmen
|
||||||
|
try:
|
||||||
|
dtype = sa.GetUserString("dossier_element_type") or ""
|
||||||
|
if dtype in _DOSSIER_INHERIT:
|
||||||
|
inherited = []
|
||||||
|
for us_key, sticky_key in _DOSSIER_INHERIT[dtype]:
|
||||||
|
v = sa.GetUserString(us_key)
|
||||||
|
if v is None or v == "": continue
|
||||||
|
# Numerische Werte ggf. konvertieren
|
||||||
|
if any(k in sticky_key for k in ("dicke", "breite", "hoehe",
|
||||||
|
"neigung", "lauf_d", "_b", "_h")):
|
||||||
|
try: v = float(v)
|
||||||
|
except Exception: pass
|
||||||
|
elif "n" == sticky_key or sticky_key.endswith("_n"):
|
||||||
|
try: v = int(float(v))
|
||||||
|
except Exception: pass
|
||||||
|
_save_sticky(sticky_key, v)
|
||||||
|
inherited.append("{}={}".format(sticky_key, v))
|
||||||
|
if inherited:
|
||||||
|
msgs.append("DOSSIER " + dtype + ": " + ", ".join(inherited))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] DOSSIER-Inherit:", ex)
|
||||||
|
|
||||||
|
if msgs:
|
||||||
|
print("[PIPETTE] Uebernommen: " + " | ".join(msgs))
|
||||||
|
else:
|
||||||
|
print("[PIPETTE] Keine Aenderung (Source identisch zu Defaults)")
|
||||||
|
|
||||||
|
# 7. Per-Object Custom-Hatch / Custom-Attrs: speichern als "pending"
|
||||||
|
# + one-shot Listener auf AddRhinoObject — wenn naechster Curve
|
||||||
|
# gezeichnet ist, alle Custom-Attrs auf den uebertragen.
|
||||||
|
_setup_pending_apply(doc, src)
|
||||||
|
|
||||||
|
# 6. Auto-Chain: passendes Draw-Command starten basierend auf
|
||||||
|
# Source-Typ. So hat der User direkt "die richtige Tool in der Hand".
|
||||||
|
_auto_chain(doc, src)
|
||||||
|
|
||||||
|
|
||||||
|
def _capture_source_hatch_props(doc, src_obj):
|
||||||
|
"""Wenn Source einen per-Object Custom-Hatch hat, sample dessen
|
||||||
|
Properties (Pattern/Scale/Rotation/Color)."""
|
||||||
|
try:
|
||||||
|
sa = src_obj.Attributes
|
||||||
|
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
|
||||||
|
if not fill_hid: return None
|
||||||
|
import System
|
||||||
|
hid = System.Guid(fill_hid)
|
||||||
|
hobj = doc.Objects.FindId(hid)
|
||||||
|
if hobj is None or hobj.IsDeleted: return None
|
||||||
|
hg = hobj.Geometry
|
||||||
|
ha = hobj.Attributes
|
||||||
|
if not hasattr(hg, "PatternIndex"): return None
|
||||||
|
return {
|
||||||
|
"pattern_idx": int(hg.PatternIndex),
|
||||||
|
"scale": float(hg.PatternScale),
|
||||||
|
"rotation": float(hg.PatternRotation),
|
||||||
|
"layer_idx": int(ha.LayerIndex),
|
||||||
|
"color_source": int(ha.ColorSource),
|
||||||
|
"color_argb": int(ha.ObjectColor.ToArgb()),
|
||||||
|
"plot_color_source": int(ha.PlotColorSource),
|
||||||
|
"plot_color_argb": int(ha.PlotColor.ToArgb()),
|
||||||
|
"linetype_source": int(ha.LinetypeSource),
|
||||||
|
"linetype_idx": int(ha.LinetypeIndex),
|
||||||
|
}
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] capture-hatch:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_pending_apply(doc, src_obj):
|
||||||
|
"""Speichert Source-Custom-Attrs in sticky + registriert one-shot
|
||||||
|
AddRhinoObject-Listener der die Attrs (inkl. Hatch) auf den naechsten
|
||||||
|
neuen Curve uebertraegt. Nach Apply wird Listener wieder entfernt."""
|
||||||
|
sa = src_obj.Attributes
|
||||||
|
# Custom-User-Strings sammeln (DOSSIER-Element-Typen + andere). Skip
|
||||||
|
# die Fill-Tracking-Keys weil wir den Hatch neu erstellen mit neuer ID.
|
||||||
|
skip_keys = {
|
||||||
|
"ebenen_fill_hatch_id", # zeigt auf alte Source-Hatch-ID
|
||||||
|
"ebenen_fill_owner",
|
||||||
|
}
|
||||||
|
user_strings = {}
|
||||||
|
try:
|
||||||
|
for k in sa.GetUserStringKeys():
|
||||||
|
if k in skip_keys: continue
|
||||||
|
v = sa.GetUserString(k)
|
||||||
|
if v is not None: user_strings[k] = v
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] user-strings:", ex)
|
||||||
|
|
||||||
|
# Source-Geometrie Closed-State erfassen — wenn Source closed war,
|
||||||
|
# erzwingen wir nach dem Add auch auf der Kopie ein Close (Polyline
|
||||||
|
# bleibt sonst standardmaessig offen, hatten User-Feedback dazu).
|
||||||
|
src_closed = False
|
||||||
|
try:
|
||||||
|
import Rhino.Geometry as _rg
|
||||||
|
sg = src_obj.Geometry
|
||||||
|
if isinstance(sg, _rg.Curve) and sg.IsClosed:
|
||||||
|
src_closed = True
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
pending = {
|
||||||
|
"linetype_source": int(sa.LinetypeSource),
|
||||||
|
"linetype_idx": int(sa.LinetypeIndex),
|
||||||
|
"plot_weight_source": int(sa.PlotWeightSource),
|
||||||
|
"plot_weight": float(sa.PlotWeight),
|
||||||
|
"user_strings": user_strings,
|
||||||
|
"hatch_props": _capture_source_hatch_props(doc, src_obj),
|
||||||
|
"src_closed": src_closed,
|
||||||
|
}
|
||||||
|
sc.sticky["dossier_pipette_pending"] = pending
|
||||||
|
|
||||||
|
# One-shot handler — applied beim naechsten AddRhinoObject + entfernt sich
|
||||||
|
def _on_add(sender, e):
|
||||||
|
try:
|
||||||
|
obj = e.TheObject
|
||||||
|
if obj is None or obj.IsDeleted: return
|
||||||
|
import Rhino.Geometry as rg2
|
||||||
|
if not isinstance(obj.Geometry, rg2.Curve): return
|
||||||
|
_apply_pending(doc, obj, pending)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] one-shot apply:", ex)
|
||||||
|
finally:
|
||||||
|
try: Rhino.RhinoDoc.AddRhinoObject -= _on_add
|
||||||
|
except Exception: pass
|
||||||
|
sc.sticky.pop("dossier_pipette_pending", None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
Rhino.RhinoDoc.AddRhinoObject += _on_add
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] listener-install:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _force_close_curve(crv):
|
||||||
|
"""Schliesst eine offene Polyline durch Anhaengen des Startpunkts.
|
||||||
|
Generische Curves: MakeClosed (nur wenn Endpunkte nahe) oder Join mit
|
||||||
|
Lueckensegment. Returns geschlossene Curve oder None bei Fehler."""
|
||||||
|
import Rhino.Geometry as rg2
|
||||||
|
if crv is None or crv.IsClosed: return None
|
||||||
|
try:
|
||||||
|
if isinstance(crv, rg2.PolylineCurve):
|
||||||
|
ok, pl = crv.TryGetPolyline()
|
||||||
|
if ok and pl is not None and pl.Count >= 2:
|
||||||
|
if pl[0].DistanceTo(pl[pl.Count - 1]) > 1e-9:
|
||||||
|
pl.Add(pl[0])
|
||||||
|
return rg2.PolylineCurve(pl)
|
||||||
|
return None
|
||||||
|
# Generic: erst MakeClosed (closed wenn Endpunkte innerhalb tol)
|
||||||
|
try:
|
||||||
|
if crv.MakeClosed(1e-6): return crv
|
||||||
|
except Exception: pass
|
||||||
|
# Fallback: Lueckensegment einfuegen + joinen
|
||||||
|
line = rg2.LineCurve(crv.PointAtEnd, crv.PointAtStart)
|
||||||
|
joined = rg2.Curve.JoinCurves([crv, line], 1e-6)
|
||||||
|
if joined and len(joined) > 0 and joined[0].IsClosed:
|
||||||
|
return joined[0]
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] force-close:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_pending(doc, new_obj, pending):
|
||||||
|
"""Wendet pending state auf das neu erzeugte Objekt an."""
|
||||||
|
import Rhino.Geometry as rg2
|
||||||
|
import System
|
||||||
|
# Close-Erzwingen wenn Source geschlossen war — Polyline-Command erzeugt
|
||||||
|
# standardmaessig offene Curves; Pipette soll den Closed-State erhalten.
|
||||||
|
if pending.get("src_closed"):
|
||||||
|
try:
|
||||||
|
crv = new_obj.Geometry
|
||||||
|
if isinstance(crv, rg2.Curve) and not crv.IsClosed:
|
||||||
|
closed = _force_close_curve(crv)
|
||||||
|
if closed is not None:
|
||||||
|
if doc.Objects.Replace(new_obj.Id, closed):
|
||||||
|
ref = doc.Objects.FindId(new_obj.Id)
|
||||||
|
if ref is not None: new_obj = ref
|
||||||
|
print("[PIPETTE] Polyline auto-geschlossen (Source war closed)")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] close-replace:", ex)
|
||||||
|
# Linetype + PlotWeight overrides
|
||||||
|
try:
|
||||||
|
na = new_obj.Attributes.Duplicate()
|
||||||
|
if pending["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
|
||||||
|
na.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
|
||||||
|
na.LinetypeIndex = pending["linetype_idx"]
|
||||||
|
if pending["plot_weight_source"] == int(rdoc.ObjectPlotWeightSource.PlotWeightFromObject):
|
||||||
|
na.PlotWeightSource = rdoc.ObjectPlotWeightSource.PlotWeightFromObject
|
||||||
|
na.PlotWeight = pending["plot_weight"]
|
||||||
|
# UserStrings 1:1 kopieren
|
||||||
|
for k, v in pending["user_strings"].items():
|
||||||
|
try: na.SetUserString(k, v)
|
||||||
|
except Exception: pass
|
||||||
|
doc.Objects.ModifyAttributes(new_obj, na, True)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] apply-attrs:", ex)
|
||||||
|
|
||||||
|
# Per-Object Custom-Hatch: nachbauen wenn Source einen hatte UND
|
||||||
|
# der neue Curve closed ist
|
||||||
|
hp = pending.get("hatch_props")
|
||||||
|
if hp is None: return
|
||||||
|
try:
|
||||||
|
crv = new_obj.Geometry
|
||||||
|
if not isinstance(crv, rg2.Curve) or not crv.IsClosed: return
|
||||||
|
tol = doc.ModelAbsoluteTolerance
|
||||||
|
hatches = rg2.Hatch.Create(crv, hp["pattern_idx"],
|
||||||
|
hp["rotation"], hp["scale"], tol)
|
||||||
|
if not hatches or len(hatches) == 0: return
|
||||||
|
ha = rdoc.ObjectAttributes()
|
||||||
|
ha.LayerIndex = hp["layer_idx"]
|
||||||
|
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
|
||||||
|
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
|
||||||
|
try:
|
||||||
|
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
|
||||||
|
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
|
||||||
|
except Exception: pass
|
||||||
|
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
|
||||||
|
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
|
||||||
|
ha.LinetypeIndex = hp["linetype_idx"]
|
||||||
|
ha.SetUserString("ebenen_fill_source", "object")
|
||||||
|
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
|
||||||
|
new_hid = doc.Objects.AddHatch(hatches[0], ha)
|
||||||
|
if new_hid and new_hid != System.Guid.Empty:
|
||||||
|
# Cross-Link: Curve speichert Hatch-ID
|
||||||
|
ca = new_obj.Attributes.Duplicate()
|
||||||
|
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
|
||||||
|
ca.SetUserString("ebenen_fill_source", "object")
|
||||||
|
doc.Objects.ModifyAttributes(new_obj, ca, True)
|
||||||
|
print("[PIPETTE] Per-Object Hatch uebernommen (Pattern={}, Scale={})"
|
||||||
|
.format(hp["pattern_idx"], hp["scale"]))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[PIPETTE] hatch-replicate:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _auto_chain(doc, src):
|
||||||
|
"""Startet das passende Draw-Command basierend auf Source-Typ."""
|
||||||
|
sa = src.Attributes
|
||||||
|
dtype = sa.GetUserString("dossier_element_type") or ""
|
||||||
|
geom = src.Geometry
|
||||||
|
geom_type = type(geom).__name__
|
||||||
|
|
||||||
|
# DOSSIER-BIM: triggere den Dispatcher
|
||||||
|
_DOSSIER_DRAW = {
|
||||||
|
"wand_axis": "wand",
|
||||||
|
"treppe_axis": "treppe",
|
||||||
|
"decke_outline": "decke",
|
||||||
|
"dach_outline": "dach",
|
||||||
|
"stuetze_point": "stuetze",
|
||||||
|
"traeger_axis": "traeger",
|
||||||
|
"oeffnung_point": None, # braucht parent-Wand-Kontext → skip auto-chain
|
||||||
|
"raum_outline": "raum",
|
||||||
|
}
|
||||||
|
if dtype in _DOSSIER_DRAW:
|
||||||
|
action = _DOSSIER_DRAW[dtype]
|
||||||
|
if action:
|
||||||
|
import os
|
||||||
|
_here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
wrapper = os.path.join(_here, action + ".py")
|
||||||
|
if os.path.exists(wrapper):
|
||||||
|
Rhino.RhinoApp.RunScript(
|
||||||
|
'_-RunPythonScript "{}"'.format(wrapper), False)
|
||||||
|
print("[PIPETTE] → starte DOSSIER {}".format(action))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Standard-Rhino-Curves: detect Typ → entsprechendes Draw-Cmd
|
||||||
|
cmd = None
|
||||||
|
if geom_type == "LineCurve":
|
||||||
|
cmd = "_Line"
|
||||||
|
elif geom_type == "ArcCurve":
|
||||||
|
# ArcCurve mit voller Sweep = Kreis
|
||||||
|
try:
|
||||||
|
if geom.IsClosed: cmd = "_Circle"
|
||||||
|
else: cmd = "_Arc"
|
||||||
|
except Exception:
|
||||||
|
cmd = "_Arc"
|
||||||
|
elif geom_type == "PolylineCurve":
|
||||||
|
try:
|
||||||
|
ok, pl = geom.TryGetPolyline()
|
||||||
|
if ok and pl is not None and pl.IsClosed and pl.Count == 5:
|
||||||
|
# Geschlossen + 4 Segmente → vermutlich Rectangle
|
||||||
|
cmd = "_Rectangle"
|
||||||
|
else:
|
||||||
|
cmd = "_Polyline"
|
||||||
|
except Exception:
|
||||||
|
cmd = "_Polyline"
|
||||||
|
elif geom_type == "NurbsCurve":
|
||||||
|
cmd = "_Curve"
|
||||||
|
elif geom_type == "TextEntity":
|
||||||
|
cmd = "_Text"
|
||||||
|
|
||||||
|
if cmd:
|
||||||
|
Rhino.RhinoApp.RunScript(cmd, False)
|
||||||
|
print("[PIPETTE] → starte {}".format(cmd))
|
||||||
|
|
||||||
|
|
||||||
|
_run()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'raum'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("raum")
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Wrapper fuer dSection: interaktiver Schnitt-Pick (2 Punkte + Blickrichtung).
|
||||||
|
# Defaults kommen aus Project-Settings.defaults; nach erfolgreicher
|
||||||
|
# Erstellung wird der neue Schnitt als aktive Zeichnungs-Ebene set.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||||
|
import Rhino
|
||||||
|
import scriptcontext as sc
|
||||||
|
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None:
|
||||||
|
print("[SECTION] kein aktives Dokument")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
import schnitte
|
||||||
|
# Defaults aus Project-Settings; Fallback auf hartkodierte Werte.
|
||||||
|
defaults = {
|
||||||
|
"depthBack": 8.0, "heightMin": -1.0, "heightMax": 12.0,
|
||||||
|
"cutAtLine": True, "namePrefix": "S",
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
import layers_panel as rhinopanel
|
||||||
|
ps = rhinopanel.load_project_settings(doc)
|
||||||
|
d = (ps or {}).get("defaults", {})
|
||||||
|
defaults["depthBack"] = float(d.get("schnittDepthBack", 8.0))
|
||||||
|
defaults["heightMin"] = float(d.get("schnittHeightMin", -1.0))
|
||||||
|
defaults["heightMax"] = float(d.get("schnittHeightMax", 12.0))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SECTION] defaults from project-settings:", ex)
|
||||||
|
|
||||||
|
sid = schnitte.pick_schnitt_interactive(doc, defaults=defaults)
|
||||||
|
if not sid:
|
||||||
|
print("[SECTION] abgebrochen")
|
||||||
|
else:
|
||||||
|
# Broadcast neue Zeichnungs-Ebene an Panels + auto-aktivieren
|
||||||
|
try:
|
||||||
|
eb = sc.sticky.get("ebenen_bridge")
|
||||||
|
if eb is not None:
|
||||||
|
eb._send_state()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SECTION] broadcast:", ex)
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
|
||||||
|
z_list = json.loads(zraw)
|
||||||
|
new_z = next((x for x in z_list
|
||||||
|
if isinstance(x, dict) and x.get("id") == sid), None)
|
||||||
|
if new_z is not None:
|
||||||
|
eb = sc.sticky.get("ebenen_bridge")
|
||||||
|
if eb is not None:
|
||||||
|
eb._set_active_zeichnungsebene(new_z)
|
||||||
|
print("[SECTION] erstellt: {}".format(sid))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SECTION] auto-activate:", ex)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SECTION] error:", ex)
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Smart-Join: bei geschlossenen Curves → BooleanUnion (innere Linien weg),
|
||||||
|
# bei offenen Curves → normales _Join (Endpunkt-Verbindung).
|
||||||
|
# Sicherheits-Filter:
|
||||||
|
# A) Group by Layer + Object-Overrides (Color/Linetype/PlotWeight) + Fill —
|
||||||
|
# nur Curves mit IDENTISCHEN visuellen Attributen werden gemerged.
|
||||||
|
# C) Pre-Check Overlap — BooleanUnion liefert genauso viele Outputs wie
|
||||||
|
# Inputs wenn nichts overlapt → dann KEINE Aktion, Curves bleiben.
|
||||||
|
# Kombinierter Effekt: nur visuell zusammengehoerige UND tatsaechlich
|
||||||
|
# ueberlappende Curves werden zu einer Outline vereint.
|
||||||
|
import scriptcontext as sc
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
import Rhino.DocObjects as rdoc
|
||||||
|
|
||||||
|
|
||||||
|
def _attr_key(obj):
|
||||||
|
"""Tuple das definiert ob 2 Curves visuell identisch sind. Layer +
|
||||||
|
Per-Object-Overrides (alles was ByObject nicht ByLayer ist) + Fill-
|
||||||
|
State (Hatch-ID + No-Fill-Flag)."""
|
||||||
|
a = obj.Attributes
|
||||||
|
layer_idx = a.LayerIndex
|
||||||
|
|
||||||
|
# Color: nur Object-Override unterscheidend, ByLayer ist gleich.
|
||||||
|
col_key = ("layer",)
|
||||||
|
try:
|
||||||
|
if a.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
|
||||||
|
col_key = ("obj", a.ObjectColor.ToArgb())
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# Linetype
|
||||||
|
lt_key = ("layer",)
|
||||||
|
try:
|
||||||
|
if a.LinetypeSource == rdoc.ObjectLinetypeSource.LinetypeFromObject:
|
||||||
|
lt_key = ("obj", a.LinetypeIndex)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# PlotWeight
|
||||||
|
pw_key = ("layer",)
|
||||||
|
try:
|
||||||
|
if a.PlotWeightSource == rdoc.ObjectPlotWeightSource.PlotWeightFromObject:
|
||||||
|
pw_key = ("obj", float(a.PlotWeight))
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# Fill / Hatch via gestaltung-UserStrings
|
||||||
|
fill_hatch = ""
|
||||||
|
fill_source = ""
|
||||||
|
no_fill = ""
|
||||||
|
try:
|
||||||
|
fill_hatch = a.GetUserString("ebenen_fill_hatch_id") or ""
|
||||||
|
fill_source = a.GetUserString("ebenen_fill_source") or ""
|
||||||
|
no_fill = a.GetUserString("ebenen_no_fill") or ""
|
||||||
|
except Exception: pass
|
||||||
|
# Fuer Gruppierung zaehlt: "hatte Fill ja/nein" + Quelle + No-Fill-Flag.
|
||||||
|
fill_key = (bool(fill_hatch), fill_source, no_fill)
|
||||||
|
|
||||||
|
return (layer_idx, col_key, lt_key, pw_key, fill_key)
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_curve_endpoint(curve, which_end, new_pt):
|
||||||
|
"""Ersetze Start- (which_end=0) oder End-Punkt (which_end=1). Liefert
|
||||||
|
eine neue Curve oder None bei nicht-unterstuetztem Typ."""
|
||||||
|
if isinstance(curve, rg.LineCurve):
|
||||||
|
if which_end == 0:
|
||||||
|
return rg.LineCurve(new_pt, curve.PointAtEnd)
|
||||||
|
return rg.LineCurve(curve.PointAtStart, new_pt)
|
||||||
|
if isinstance(curve, rg.PolylineCurve):
|
||||||
|
n = curve.PointCount
|
||||||
|
pts = [curve.Point(i) for i in range(n)]
|
||||||
|
if which_end == 0: pts[0] = new_pt
|
||||||
|
else: pts[-1] = new_pt
|
||||||
|
return rg.PolylineCurve(pts)
|
||||||
|
# Fallback: generische Curve via Extend
|
||||||
|
cu = curve.DuplicateCurve()
|
||||||
|
if cu is None: return None
|
||||||
|
end_enum = rg.CurveEnd.Start if which_end == 0 else rg.CurveEnd.End
|
||||||
|
try:
|
||||||
|
return cu.Extend(end_enum,
|
||||||
|
rg.CurveExtensionStyle.Line,
|
||||||
|
[rg.Point3d(new_pt)])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _walls_and_curves_from_sel(doc, sel):
|
||||||
|
"""Liefert (axes, generic_curves). Axes = dedup Wand-Achsen (per wall_id),
|
||||||
|
generic_curves = offene Kurven die KEINE Wand sind. wand_volumes werden
|
||||||
|
auf ihre Achse via wall_id resolved (auto-group bringt axis+volume
|
||||||
|
automatisch beide in sel)."""
|
||||||
|
seen_walls = set()
|
||||||
|
axes = []
|
||||||
|
generic = []
|
||||||
|
# Pre-Index wand_axis by wall_id fuer schnelles Lookup
|
||||||
|
axis_by_id = {}
|
||||||
|
for o in doc.Objects:
|
||||||
|
if o.Attributes.GetUserString("dossier_element_type") == "wand_axis":
|
||||||
|
wid = o.Attributes.GetUserString("dossier_element_id") or ""
|
||||||
|
if wid: axis_by_id[wid] = o
|
||||||
|
for obj in sel:
|
||||||
|
t = obj.Attributes.GetUserString("dossier_element_type") or ""
|
||||||
|
wid = obj.Attributes.GetUserString("dossier_element_id") or ""
|
||||||
|
if t == "wand_axis" and wid and wid not in seen_walls:
|
||||||
|
axes.append(obj); seen_walls.add(wid)
|
||||||
|
elif t == "wand_volume" and wid:
|
||||||
|
wall_ids = {wid}
|
||||||
|
members_raw = obj.Attributes.GetUserString(
|
||||||
|
"dossier_wand_chain_members") or ""
|
||||||
|
if members_raw:
|
||||||
|
try:
|
||||||
|
import json as _j
|
||||||
|
for c in _j.loads(members_raw):
|
||||||
|
if c: wall_ids.add(c)
|
||||||
|
except Exception: pass
|
||||||
|
for w in wall_ids:
|
||||||
|
if w in seen_walls: continue
|
||||||
|
ax = axis_by_id.get(w)
|
||||||
|
if ax is not None:
|
||||||
|
axes.append(ax); seen_walls.add(w)
|
||||||
|
elif t == "":
|
||||||
|
g = obj.Geometry
|
||||||
|
if isinstance(g, rg.Curve) and not g.IsClosed:
|
||||||
|
generic.append(obj)
|
||||||
|
return axes, generic
|
||||||
|
|
||||||
|
|
||||||
|
def _find_nearest_other_wand_axis(doc, my_axis_obj, my_id, tol=1.0):
|
||||||
|
"""Findet die naechste andere wand_axis im Doc (innerhalb tol).
|
||||||
|
Return das axis Object oder None."""
|
||||||
|
if my_axis_obj is None: return None
|
||||||
|
g = my_axis_obj.Geometry
|
||||||
|
if not isinstance(g, rg.Curve): return None
|
||||||
|
bb = g.GetBoundingBox(True)
|
||||||
|
if not bb.IsValid: return None
|
||||||
|
best = None; best_d = tol
|
||||||
|
for obj in doc.Objects:
|
||||||
|
try:
|
||||||
|
if obj.Attributes.GetUserString("dossier_element_type") != "wand_axis":
|
||||||
|
continue
|
||||||
|
wid = obj.Attributes.GetUserString("dossier_element_id") or ""
|
||||||
|
if wid == my_id or not wid: continue
|
||||||
|
except Exception: continue
|
||||||
|
og = obj.Geometry
|
||||||
|
if not isinstance(og, rg.Curve): continue
|
||||||
|
try:
|
||||||
|
# Mindest-Distanz: Endpunkte gegeneinander UND ClosestPoint
|
||||||
|
d_min = float('inf')
|
||||||
|
for ep in (g.PointAtStart, g.PointAtEnd):
|
||||||
|
rc, t = og.ClosestPoint(ep)
|
||||||
|
if rc:
|
||||||
|
d = og.PointAt(t).DistanceTo(ep)
|
||||||
|
if d < d_min: d_min = d
|
||||||
|
for ep in (og.PointAtStart, og.PointAtEnd):
|
||||||
|
rc, t = g.ClosestPoint(ep)
|
||||||
|
if rc:
|
||||||
|
d = g.PointAt(t).DistanceTo(ep)
|
||||||
|
if d < d_min: d_min = d
|
||||||
|
if d_min < best_d:
|
||||||
|
best_d = d_min; best = obj
|
||||||
|
except Exception: continue
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def _t_join_attempt(doc, sel):
|
||||||
|
"""T-Join: 2 OFFENE Kurven wobei der EINE Endpunkt der einen Kurve
|
||||||
|
nahe (< 1m) auf der ANDEREN Kurve mitten landet (zwischen deren
|
||||||
|
Endpunkten). Schiebt diesen Endpunkt exakt auf die andere Kurve.
|
||||||
|
Die andere Kurve bleibt unchanged.
|
||||||
|
|
||||||
|
Auch 1-Wand-Modus: wenn nur 1 wand_axis selektiert, sucht automatisch
|
||||||
|
die naechste andere Wand und snappt diese eine.
|
||||||
|
|
||||||
|
Liefert True wenn ausgefuehrt."""
|
||||||
|
axes, generic = _walls_and_curves_from_sel(doc, sel)
|
||||||
|
if len(axes) == 2 and len(generic) == 0:
|
||||||
|
o1, o2 = axes[0], axes[1]
|
||||||
|
elif len(axes) == 1 and len(generic) == 0:
|
||||||
|
# 1-Wand-Modus: finde naechste andere wand_axis im Doc
|
||||||
|
my_id = axes[0].Attributes.GetUserString("dossier_element_id") or ""
|
||||||
|
other = _find_nearest_other_wand_axis(doc, axes[0], my_id, tol=1.0)
|
||||||
|
if other is None:
|
||||||
|
print("[SMART-JOIN] 1-Wand T-Join: keine Nachbar-Wand "
|
||||||
|
"innerhalb 1m gefunden")
|
||||||
|
return False
|
||||||
|
o1 = axes[0]; o2 = other
|
||||||
|
print("[SMART-JOIN] 1-Wand T-Join: snappe an Nachbar-Wand")
|
||||||
|
elif len(axes) == 0 and len(generic) == 2:
|
||||||
|
o1, o2 = generic[0], generic[1]
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
c1 = o1.Geometry; c2 = o2.Geometry
|
||||||
|
if not (isinstance(c1, rg.Curve) and isinstance(c2, rg.Curve)):
|
||||||
|
return False
|
||||||
|
if c1.IsClosed or c2.IsClosed: return False
|
||||||
|
tol_snap = 1.00 # 1 m Snap-Radius fuer T-Verbindung — generous damit
|
||||||
|
# auch grosse Drift (z.B. wenn User auf Outline statt
|
||||||
|
# Axis gesnappt hat = 30cm Wand-dicke) gefangen wird
|
||||||
|
end_tol = 0.05 # 5cm: wenn closest-point nahe Endpunkt → eigentlich L
|
||||||
|
candidates = []
|
||||||
|
debug_rows = []
|
||||||
|
# Pro Endpunkt der einen Kurve: ClosestPoint auf der ANDEREN Kurve
|
||||||
|
for (a_obj, ac, b_obj, bc) in ((o1, c1, o2, c2), (o2, c2, o1, c1)):
|
||||||
|
for end in (0, 1):
|
||||||
|
ep = ac.PointAtStart if end == 0 else ac.PointAtEnd
|
||||||
|
try:
|
||||||
|
rc, t = bc.ClosestPoint(ep)
|
||||||
|
if not rc:
|
||||||
|
debug_rows.append(("axis_end={}".format(end), "ClosestPoint failed"))
|
||||||
|
continue
|
||||||
|
cp = bc.PointAt(t)
|
||||||
|
d = cp.DistanceTo(ep)
|
||||||
|
ps = bc.PointAtStart; pe = bc.PointAtEnd
|
||||||
|
d_to_ps = cp.DistanceTo(ps)
|
||||||
|
d_to_pe = cp.DistanceTo(pe)
|
||||||
|
reason = None
|
||||||
|
if d < 1e-6: reason = "schon gesnappt"
|
||||||
|
elif d > tol_snap: reason = "zu weit ({:.3f} > {:.2f})".format(d, tol_snap)
|
||||||
|
elif d_to_ps < end_tol: reason = "cp nahe Endpunkt-Start ({:.3f}<{:.2f}) → L-Join Sache".format(d_to_ps, end_tol)
|
||||||
|
elif d_to_pe < end_tol: reason = "cp nahe Endpunkt-End ({:.3f}<{:.2f}) → L-Join Sache".format(d_to_pe, end_tol)
|
||||||
|
debug_rows.append(("axis_end={} d={:.3f}".format(end, d),
|
||||||
|
reason or "candidate"))
|
||||||
|
if reason is None:
|
||||||
|
candidates.append((d, a_obj, ac, end, cp))
|
||||||
|
except Exception as ex:
|
||||||
|
debug_rows.append(("axis_end={}".format(end), "exc: {}".format(ex)))
|
||||||
|
# Diagnostic alles ausgeben
|
||||||
|
for tag, msg in debug_rows:
|
||||||
|
print("[SMART-JOIN] T-Join check {}: {}".format(tag, msg))
|
||||||
|
if not candidates: return False
|
||||||
|
# Naechster Endpunkt → der wird gesnappt
|
||||||
|
candidates.sort(key=lambda x: x[0])
|
||||||
|
_d, a_obj, ac, end, cp = candidates[0]
|
||||||
|
new_c = _replace_curve_endpoint(ac, end, cp)
|
||||||
|
if new_c is None: return False
|
||||||
|
ur = doc.BeginUndoRecord("DOSSIER T-Join")
|
||||||
|
try:
|
||||||
|
ok = doc.Objects.Replace(a_obj.Id, new_c)
|
||||||
|
return bool(ok)
|
||||||
|
finally:
|
||||||
|
doc.EndUndoRecord(ur)
|
||||||
|
|
||||||
|
|
||||||
|
def _l_join_attempt(doc, sel):
|
||||||
|
"""Wenn genau 2 OFFENE Kurven (Wand-Achsen oder generische Lines)
|
||||||
|
selektiert sind, deren End-Tangenten sich in einem Punkt schneiden →
|
||||||
|
beide Kurven extend/shorten zu diesem Punkt (= L-Form). True wenn
|
||||||
|
ausgefuehrt."""
|
||||||
|
axes, generic = _walls_and_curves_from_sel(doc, sel)
|
||||||
|
# Erlaubte Konfigs: 2 Wand-Achsen ODER 2 generische Kurven (keine mix)
|
||||||
|
if len(axes) == 2 and len(generic) == 0:
|
||||||
|
o1, o2 = axes[0], axes[1]
|
||||||
|
elif len(axes) == 0 and len(generic) == 2:
|
||||||
|
o1, o2 = generic[0], generic[1]
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
c1 = o1.Geometry; c2 = o2.Geometry
|
||||||
|
if not (isinstance(c1, rg.Curve) and isinstance(c2, rg.Curve)):
|
||||||
|
return False
|
||||||
|
if c1.IsClosed or c2.IsClosed: return False
|
||||||
|
tol = max(doc.ModelAbsoluteTolerance, 1e-6)
|
||||||
|
# Closest endpoint pair (a_end, b_end ∈ {0=start, 1=end})
|
||||||
|
pairs = [
|
||||||
|
(c1.PointAtStart, c2.PointAtStart, 0, 0),
|
||||||
|
(c1.PointAtStart, c2.PointAtEnd, 0, 1),
|
||||||
|
(c1.PointAtEnd, c2.PointAtStart, 1, 0),
|
||||||
|
(c1.PointAtEnd, c2.PointAtEnd, 1, 1),
|
||||||
|
]
|
||||||
|
pairs.sort(key=lambda p: p[0].DistanceTo(p[1]))
|
||||||
|
p1, p2, e1, e2 = pairs[0]
|
||||||
|
if p1.DistanceTo(p2) < tol:
|
||||||
|
return False # bereits verbunden
|
||||||
|
def _out_dir(c, end):
|
||||||
|
return -c.TangentAtStart if end == 0 else c.TangentAtEnd
|
||||||
|
d1 = _out_dir(c1, e1)
|
||||||
|
d2 = _out_dir(c2, e2)
|
||||||
|
# Parallel-Check (Cross-Produkt-Laenge in XY)
|
||||||
|
cross_z = d1.X * d2.Y - d1.Y * d2.X
|
||||||
|
if abs(cross_z) < 1e-9: return False # parallel
|
||||||
|
# Unendliche Linien-Intersection
|
||||||
|
line1 = rg.Line(p1, p1 + d1)
|
||||||
|
line2 = rg.Line(p2, p2 + d2)
|
||||||
|
rc, t_a, t_b = rg.Intersect.Intersection.LineLine(line1, line2, tol, False)
|
||||||
|
if not rc: return False
|
||||||
|
ipt = line1.PointAt(t_a)
|
||||||
|
if line2.PointAt(t_b).DistanceTo(ipt) > 0.01:
|
||||||
|
return False # Schiefe Linien in 3D
|
||||||
|
nc1 = _replace_curve_endpoint(c1, e1, ipt)
|
||||||
|
nc2 = _replace_curve_endpoint(c2, e2, ipt)
|
||||||
|
if nc1 is None or nc2 is None: return False
|
||||||
|
ur = doc.BeginUndoRecord("DOSSIER L-Join")
|
||||||
|
try:
|
||||||
|
ok1 = doc.Objects.Replace(o1.Id, nc1)
|
||||||
|
ok2 = doc.Objects.Replace(o2.Id, nc2)
|
||||||
|
return bool(ok1 and ok2)
|
||||||
|
finally:
|
||||||
|
doc.EndUndoRecord(ur)
|
||||||
|
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
if not sel:
|
||||||
|
Rhino.RhinoApp.RunScript("_Join", False); return
|
||||||
|
# Info-Hint (T-Join unterstuetzt 1-Wand-Modus, L-Join braucht 2)
|
||||||
|
n_wand_axes = sum(1 for o in sel
|
||||||
|
if (o.Attributes.GetUserString("dossier_element_type")
|
||||||
|
or "") == "wand_axis")
|
||||||
|
if n_wand_axes == 0 and any(
|
||||||
|
(o.Attributes.GetUserString("dossier_element_type") or "")
|
||||||
|
.startswith("wand_") for o in sel):
|
||||||
|
print("[SMART-JOIN] keine Wand-Achse in Selection — selektiere die "
|
||||||
|
"Wand-Linie oder das Wand-Volumen.")
|
||||||
|
|
||||||
|
# T-Join: Endpunkt der einen Curve trifft mitten auf die andere → snap.
|
||||||
|
# L-Join: beide Endpunkte werden zum Schnittpunkt der verlaengerten Linien
|
||||||
|
# gezogen. T zuerst probieren (= spezifischer), dann L als Fallback.
|
||||||
|
if len(sel) >= 2:
|
||||||
|
# Diagnostic: was sieht smart_join in der Selection?
|
||||||
|
axes_dbg, generic_dbg = _walls_and_curves_from_sel(doc, sel)
|
||||||
|
type_counts = {}
|
||||||
|
for o in sel:
|
||||||
|
try:
|
||||||
|
t = o.Attributes.GetUserString("dossier_element_type") or "<none>"
|
||||||
|
wid_raw = o.Attributes.GetUserString("dossier_element_id") or ""
|
||||||
|
geom_kind = type(o.Geometry).__name__
|
||||||
|
key = "{}|{}|wid={}".format(t, geom_kind,
|
||||||
|
"yes" if wid_raw else "no")
|
||||||
|
type_counts[key] = type_counts.get(key, 0) + 1
|
||||||
|
except Exception: pass
|
||||||
|
print("[SMART-JOIN] sel-detect: {} Wand-Achsen, {} generische Curves "
|
||||||
|
"(sel total: {})".format(len(axes_dbg), len(generic_dbg), len(sel)))
|
||||||
|
for k, n in type_counts.items():
|
||||||
|
print("[SMART-JOIN] {} × {}".format(n, k))
|
||||||
|
try:
|
||||||
|
if _t_join_attempt(doc, sel):
|
||||||
|
doc.Views.Redraw()
|
||||||
|
print("[SMART-JOIN] T-Join: Endpunkt auf Achse gesnappt")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("[SMART-JOIN] T-Join: kein passender Kandidat (zu weit "
|
||||||
|
"weg oder am Endpunkt → L-Join Territory)")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SMART-JOIN] T-Join error:", ex)
|
||||||
|
try:
|
||||||
|
if _l_join_attempt(doc, sel):
|
||||||
|
doc.Views.Redraw()
|
||||||
|
print("[SMART-JOIN] L-Join: 2 Curves zu L verbunden")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("[SMART-JOIN] L-Join: konnte nicht ausfuehren (parallel, "
|
||||||
|
"schon verbunden, oder Geometrie ungueltig)")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SMART-JOIN] L-Join error:", ex)
|
||||||
|
|
||||||
|
# Safety: wenn Wand-Achsen selektiert sind, NIE auf Standard-_Join fallen
|
||||||
|
# — das wuerde mehrere Achsen zu einer Curve zusammenkleben und die Wand-
|
||||||
|
# Verknuepfung zerstoeren (Source-Duplikat-Listener kapert die alte ID).
|
||||||
|
has_wand_axis = any(
|
||||||
|
obj.Attributes.GetUserString("dossier_element_type") == "wand_axis"
|
||||||
|
for obj in sel)
|
||||||
|
if has_wand_axis:
|
||||||
|
print("[SMART-JOIN] Wand-Achsen selektiert: T-Join/L-Join hat nicht "
|
||||||
|
"gegriffen (zu viele Selektionen oder zu weit weg). Bitte "
|
||||||
|
"GENAU 2 Waende selektieren die sich verbinden sollen, dann "
|
||||||
|
"erneut Cmd+J.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Curves nach Closed/Open trennen
|
||||||
|
closed_objs = []
|
||||||
|
has_non_closed = False
|
||||||
|
for obj in sel:
|
||||||
|
g = obj.Geometry
|
||||||
|
if isinstance(g, rg.Curve) and g.IsClosed:
|
||||||
|
closed_objs.append(obj)
|
||||||
|
else:
|
||||||
|
has_non_closed = True
|
||||||
|
|
||||||
|
# Wenn nicht ALLE closed sind → einfach Standard-Join
|
||||||
|
if has_non_closed or len(closed_objs) < 2:
|
||||||
|
Rhino.RhinoApp.RunScript("_Join", False); return
|
||||||
|
|
||||||
|
# Gruppieren nach (Layer + Attrs + Fill)
|
||||||
|
groups = {} # key → [obj, obj, ...]
|
||||||
|
for obj in closed_objs:
|
||||||
|
try:
|
||||||
|
k = _attr_key(obj)
|
||||||
|
except Exception:
|
||||||
|
k = ("ungroup", id(obj))
|
||||||
|
groups.setdefault(k, []).append(obj)
|
||||||
|
|
||||||
|
# gestaltung fuer Fill-Re-Apply
|
||||||
|
_g = None
|
||||||
|
try:
|
||||||
|
import styles as _gmod; _g = _gmod
|
||||||
|
except Exception as iex:
|
||||||
|
print("[SMART-JOIN] gestaltung import:", iex)
|
||||||
|
|
||||||
|
tol = doc.ModelAbsoluteTolerance
|
||||||
|
ur = doc.BeginUndoRecord("DOSSIER Smart-Join (gruppiert)")
|
||||||
|
n_merged_total = 0
|
||||||
|
n_groups_ops = 0
|
||||||
|
try:
|
||||||
|
for key, objs in groups.items():
|
||||||
|
if len(objs) < 2: continue # einzelne Curve → nichts zu mergen
|
||||||
|
try:
|
||||||
|
curves = [o.Geometry for o in objs]
|
||||||
|
result = rg.Curve.CreateBooleanUnion(curves, tol)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SMART-JOIN] BooleanUnion in Gruppe failed:", ex)
|
||||||
|
continue
|
||||||
|
if not result: continue
|
||||||
|
# C) Pre-Check Overlap: wenn result-Anzahl gleich input-Anzahl
|
||||||
|
# ist, gab's keinen tatsaechlichen Overlap → Gruppe nicht
|
||||||
|
# anfassen.
|
||||||
|
if len(result) >= len(objs):
|
||||||
|
continue
|
||||||
|
# Tatsaechlich gemerged → replace
|
||||||
|
attrs_template = objs[0].Attributes.Duplicate()
|
||||||
|
# Fill-Key clearen damit _apply_ebene_fill nicht "schon gefuellt"
|
||||||
|
# zurueckgibt
|
||||||
|
try:
|
||||||
|
attrs_template.SetUserString("ebenen_fill_hatch_id", "")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
any_had_fill = bool(key[4][0]) # fill_key[0] = had-fill bool
|
||||||
|
|
||||||
|
new_ids = []
|
||||||
|
for crv in result:
|
||||||
|
nid = doc.Objects.AddCurve(crv, attrs_template)
|
||||||
|
if nid: new_ids.append(nid)
|
||||||
|
for o in objs:
|
||||||
|
try: doc.Objects.Delete(o.Id, True)
|
||||||
|
except Exception: pass
|
||||||
|
# Fill nachziehen wenn Inputs welche hatten
|
||||||
|
if any_had_fill and _g is not None:
|
||||||
|
for nid in new_ids:
|
||||||
|
try:
|
||||||
|
nobj = doc.Objects.FindId(nid)
|
||||||
|
if nobj is not None:
|
||||||
|
_g._apply_ebene_fill(doc, nobj)
|
||||||
|
except Exception as fex:
|
||||||
|
print("[SMART-JOIN] fill-apply:", fex)
|
||||||
|
n_merged_total += (len(objs) - len(result))
|
||||||
|
n_groups_ops += 1
|
||||||
|
finally:
|
||||||
|
doc.EndUndoRecord(ur)
|
||||||
|
|
||||||
|
if n_groups_ops == 0:
|
||||||
|
print("[SMART-JOIN] Nichts zu mergen — keine Curves overlappen "
|
||||||
|
"(oder verschiedene Attribute/Layer)")
|
||||||
|
else:
|
||||||
|
doc.Views.Redraw()
|
||||||
|
print("[SMART-JOIN] {} Gruppe(n) bearbeitet, {} Curve(s) zu Union vereint"
|
||||||
|
.format(n_groups_ops, n_merged_total))
|
||||||
|
|
||||||
|
|
||||||
|
_run()
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Smart-Split: User zeichnet eine Splitlinie/Polylinie waehrend des Befehls
|
||||||
|
# (mehrere Klicks, Enter beendet die Eingabe). Alle Curves die die Linie
|
||||||
|
# schneidet werden gesplittet.
|
||||||
|
# - Offene Curves: bei den Schnittpunkten in offene Segmente.
|
||||||
|
# - GESCHLOSSENE Curves: in mehrere CLOSED Sub-Regionen via
|
||||||
|
# Curve.CreateBooleanRegions (funktioniert auch bei multi-segment
|
||||||
|
# Polylinien-Cuttern). Per-Object-Hatch wird auf alle Regionen repliziert.
|
||||||
|
# DOSSIER-Source-Typen (Wand-Achse etc.) bleiben geschuetzt.
|
||||||
|
import scriptcontext as sc
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Input.Custom as ric
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
import Rhino.DocObjects as rdoc
|
||||||
|
from Rhino.Input import GetResult
|
||||||
|
|
||||||
|
|
||||||
|
# Was Smart-Split NIE anfasst:
|
||||||
|
# - oeffnung_point / stuetze_point: Punkte, nicht teilbar
|
||||||
|
# - schnitt_axis: Schnitt-Linien sollen bleiben, sonst kaputte Schnitte
|
||||||
|
# - treppe_axis: Treppen-State (Lauflinie, Schrittmass-Lock, Wendel-Sweep)
|
||||||
|
# waere bei einem Split inkonsistent
|
||||||
|
# Alles andere (wand/traeger/decke/dach/raum/aussparung) DARF gesplittet werden:
|
||||||
|
# der Add-Listener in elemente.py erkennt die Duplikat-IDs der neuen Stuecke
|
||||||
|
# und vergibt jedem Stueck ein frisches Element-ID + Regen → BIM-Volumen
|
||||||
|
# baut sich pro neuem Stueck neu auf.
|
||||||
|
_PROTECTED_TYPES = {
|
||||||
|
"treppe_axis",
|
||||||
|
"oeffnung_point", "stuetze_point", "schnitt_axis",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _capture_hatch_props(doc, src_obj):
|
||||||
|
try:
|
||||||
|
sa = src_obj.Attributes
|
||||||
|
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
|
||||||
|
if not fill_hid: return None
|
||||||
|
import System
|
||||||
|
hid = System.Guid(fill_hid)
|
||||||
|
hobj = doc.Objects.FindId(hid)
|
||||||
|
if hobj is None or hobj.IsDeleted: return None
|
||||||
|
hg = hobj.Geometry
|
||||||
|
ha = hobj.Attributes
|
||||||
|
if not hasattr(hg, "PatternIndex"): return None
|
||||||
|
return {
|
||||||
|
"pattern_idx": int(hg.PatternIndex),
|
||||||
|
"scale": float(hg.PatternScale),
|
||||||
|
"rotation": float(hg.PatternRotation),
|
||||||
|
"layer_idx": int(ha.LayerIndex),
|
||||||
|
"color_source": int(ha.ColorSource),
|
||||||
|
"color_argb": int(ha.ObjectColor.ToArgb()),
|
||||||
|
"plot_color_source": int(ha.PlotColorSource),
|
||||||
|
"plot_color_argb": int(ha.PlotColor.ToArgb()),
|
||||||
|
"linetype_source": int(ha.LinetypeSource),
|
||||||
|
"linetype_idx": int(ha.LinetypeIndex),
|
||||||
|
"fill_source": sa.GetUserString("ebenen_fill_source") or "object",
|
||||||
|
}
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SMART-SPLIT] capture-hatch:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _replicate_hatch(doc, new_obj, hp):
|
||||||
|
if hp is None: return
|
||||||
|
import System
|
||||||
|
try:
|
||||||
|
crv = new_obj.Geometry
|
||||||
|
if not isinstance(crv, rg.Curve) or not crv.IsClosed: return
|
||||||
|
tol = doc.ModelAbsoluteTolerance
|
||||||
|
hatches = rg.Hatch.Create(crv, hp["pattern_idx"], hp["rotation"],
|
||||||
|
hp["scale"], tol)
|
||||||
|
if not hatches or len(hatches) == 0: return
|
||||||
|
ha = rdoc.ObjectAttributes()
|
||||||
|
ha.LayerIndex = hp["layer_idx"]
|
||||||
|
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
|
||||||
|
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
|
||||||
|
try:
|
||||||
|
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
|
||||||
|
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
|
||||||
|
except Exception: pass
|
||||||
|
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
|
||||||
|
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
|
||||||
|
ha.LinetypeIndex = hp["linetype_idx"]
|
||||||
|
ha.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
|
||||||
|
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
|
||||||
|
new_hid = doc.Objects.AddHatch(hatches[0], ha)
|
||||||
|
if new_hid and new_hid != System.Guid.Empty:
|
||||||
|
ca = new_obj.Attributes.Duplicate()
|
||||||
|
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
|
||||||
|
ca.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
|
||||||
|
doc.Objects.ModifyAttributes(new_obj, ca, True)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SMART-SPLIT] hatch-replicate:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_polyline_cutter(prompt_first, prompt_more):
|
||||||
|
"""Sammelt n Punkte fuer den Cutter. Enter beendet (min. 2 Punkte).
|
||||||
|
ESC bricht ab. Returnt Polyline oder None."""
|
||||||
|
pts = []
|
||||||
|
while True:
|
||||||
|
gp = ric.GetPoint()
|
||||||
|
if not pts:
|
||||||
|
gp.SetCommandPrompt(prompt_first)
|
||||||
|
else:
|
||||||
|
gp.SetCommandPrompt(prompt_more + " (Enter zum Splitten, ESC = abbrechen)")
|
||||||
|
gp.SetBasePoint(pts[-1], True)
|
||||||
|
gp.DrawLineFromPoint(pts[-1], True)
|
||||||
|
gp.AcceptNothing(True)
|
||||||
|
res = gp.Get()
|
||||||
|
if res == GetResult.Nothing:
|
||||||
|
# Enter gedrueckt
|
||||||
|
if len(pts) >= 2: return rg.Polyline(pts)
|
||||||
|
print("[SMART-SPLIT] Mindestens 2 Punkte noetig"); return None
|
||||||
|
if res != GetResult.Point: return None
|
||||||
|
pts.append(gp.Point())
|
||||||
|
|
||||||
|
|
||||||
|
def _split_closed_with_cutter(closed_crv, cutter_crv, doc):
|
||||||
|
"""Splittet closed curve mit beliebigem cutter (Linie oder Polylinie) in
|
||||||
|
closed Sub-Regionen via Curve.CreateBooleanRegions."""
|
||||||
|
tol = doc.ModelAbsoluteTolerance
|
||||||
|
try:
|
||||||
|
# WorldXY-Plane als Default (DOSSIER ist 2D Plan-Workflow)
|
||||||
|
plane = rg.Plane.WorldXY
|
||||||
|
regions = rg.Curve.CreateBooleanRegions(
|
||||||
|
[closed_crv, cutter_crv], plane, False, tol)
|
||||||
|
if regions is None or regions.RegionCount == 0:
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for i in range(regions.RegionCount):
|
||||||
|
rcurves = list(regions.RegionCurves(i))
|
||||||
|
if not rcurves: continue
|
||||||
|
if len(rcurves) == 1:
|
||||||
|
if rcurves[0].IsClosed:
|
||||||
|
out.append(rcurves[0])
|
||||||
|
else:
|
||||||
|
# einzelne offene curve — sollte nicht passieren bei
|
||||||
|
# Boolean-Regions, aber defensiv
|
||||||
|
joined = rg.Curve.JoinCurves([rcurves[0]], tol)
|
||||||
|
if joined and len(joined) > 0 and joined[0].IsClosed:
|
||||||
|
out.append(joined[0])
|
||||||
|
else:
|
||||||
|
joined = rg.Curve.JoinCurves(rcurves, tol)
|
||||||
|
if joined:
|
||||||
|
for j in joined:
|
||||||
|
if j.IsClosed: out.append(j)
|
||||||
|
return out if out else None
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SMART-SPLIT] closed-split:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
|
||||||
|
# Polylinie als Cutter sammeln
|
||||||
|
poly = _collect_polyline_cutter(
|
||||||
|
"Splitlinie Startpunkt",
|
||||||
|
"Naechster Punkt")
|
||||||
|
if poly is None or poly.Count < 2:
|
||||||
|
return
|
||||||
|
cutter = rg.PolylineCurve(poly)
|
||||||
|
tol = doc.ModelAbsoluteTolerance
|
||||||
|
|
||||||
|
pre_sel = [o for o in doc.Objects.GetSelectedObjects(False, False)
|
||||||
|
if o is not None and not o.IsDeleted]
|
||||||
|
if pre_sel:
|
||||||
|
source = pre_sel
|
||||||
|
mode_label = "selektierte ({})".format(len(pre_sel))
|
||||||
|
else:
|
||||||
|
s = rdoc.ObjectEnumeratorSettings()
|
||||||
|
s.HiddenObjects = False; s.LockedObjects = False
|
||||||
|
source = list(doc.Objects.GetObjectList(s))
|
||||||
|
mode_label = "alle sichtbaren"
|
||||||
|
|
||||||
|
candidates_open = []
|
||||||
|
candidates_closed = []
|
||||||
|
for obj in source:
|
||||||
|
if obj is None or obj.IsDeleted: continue
|
||||||
|
try:
|
||||||
|
t = obj.Attributes.GetUserString("dossier_element_type") or ""
|
||||||
|
if t in _PROTECTED_TYPES: continue
|
||||||
|
except Exception: pass
|
||||||
|
g = obj.Geometry
|
||||||
|
if not isinstance(g, rg.Curve): continue
|
||||||
|
try:
|
||||||
|
ints = rg.Intersect.Intersection.CurveCurve(cutter, g, tol, tol)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not ints or ints.Count == 0: continue
|
||||||
|
|
||||||
|
if g.IsClosed:
|
||||||
|
candidates_closed.append((obj, g))
|
||||||
|
else:
|
||||||
|
params = []
|
||||||
|
for i in range(ints.Count):
|
||||||
|
ev = ints[i]
|
||||||
|
if ev.IsPoint:
|
||||||
|
params.append(ev.ParameterB)
|
||||||
|
else:
|
||||||
|
params.append(ev.ParameterB); params.append(ev.ParameterB2)
|
||||||
|
if params:
|
||||||
|
params = sorted(set(round(p, 6) for p in params))
|
||||||
|
candidates_open.append((obj, g, params))
|
||||||
|
|
||||||
|
if not candidates_open and not candidates_closed:
|
||||||
|
print("[SMART-SPLIT] Cutter schneidet nichts ({})".format(mode_label))
|
||||||
|
return
|
||||||
|
|
||||||
|
ur = doc.BeginUndoRecord("DOSSIER Smart-Split")
|
||||||
|
n_open = 0; n_closed = 0
|
||||||
|
try:
|
||||||
|
# Closed: Boolean-Regions → CLOSED Sub-Regionen + Fill replicate
|
||||||
|
for obj, crv in candidates_closed:
|
||||||
|
try:
|
||||||
|
regions = _split_closed_with_cutter(crv, cutter, doc)
|
||||||
|
if not regions or len(regions) <= 1: continue
|
||||||
|
hatch_props = _capture_hatch_props(doc, obj)
|
||||||
|
attrs = obj.Attributes.Duplicate()
|
||||||
|
try: attrs.SetUserString("ebenen_fill_hatch_id", "")
|
||||||
|
except Exception: pass
|
||||||
|
new_ids = []
|
||||||
|
for r in regions:
|
||||||
|
nid = doc.Objects.AddCurve(r, attrs)
|
||||||
|
if nid: new_ids.append(nid)
|
||||||
|
doc.Objects.Delete(obj.Id, True)
|
||||||
|
if hatch_props is not None:
|
||||||
|
for nid in new_ids:
|
||||||
|
nobj = doc.Objects.FindId(nid)
|
||||||
|
if nobj is not None:
|
||||||
|
_replicate_hatch(doc, nobj, hatch_props)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
import styles as _gmod
|
||||||
|
for nid in new_ids:
|
||||||
|
nobj = doc.Objects.FindId(nid)
|
||||||
|
if nobj is not None:
|
||||||
|
_gmod._apply_ebene_fill(doc, nobj)
|
||||||
|
except Exception: pass
|
||||||
|
n_closed += 1
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SMART-SPLIT] closed-fail:", ex)
|
||||||
|
|
||||||
|
# Open: split bei Params
|
||||||
|
for obj, crv, params in candidates_open:
|
||||||
|
try:
|
||||||
|
pieces = crv.Split(params)
|
||||||
|
if not pieces or len(pieces) <= 1: continue
|
||||||
|
attrs = obj.Attributes.Duplicate()
|
||||||
|
for p in pieces:
|
||||||
|
doc.Objects.AddCurve(p, attrs)
|
||||||
|
doc.Objects.Delete(obj.Id, True)
|
||||||
|
n_open += 1
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SMART-SPLIT] open-fail:", ex)
|
||||||
|
finally:
|
||||||
|
doc.EndUndoRecord(ur)
|
||||||
|
|
||||||
|
doc.Views.Redraw()
|
||||||
|
print("[SMART-SPLIT] {} closed-Regionen + {} offene Curves gesplittet "
|
||||||
|
"({} Cutter-Punkte, {})"
|
||||||
|
.format(n_closed, n_open, poly.Count, mode_label))
|
||||||
|
|
||||||
|
|
||||||
|
_run()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'stempel'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("stempel")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'stuetze'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("stuetze")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'symbol'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("symbol")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'traeger'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("traeger")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'treppe'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("treppe")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'tuer'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("tuer")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer Alias 'wand'. Importiert dossier_dispatch + ruft Action.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_dispatch
|
||||||
|
dossier_dispatch.dispatch("wand")
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""
|
||||||
|
dossier_dispatch.py
|
||||||
|
Universal-Wrapper fuer DOSSIER-Bridge-Commands via Rhino-Alias.
|
||||||
|
|
||||||
|
Aufruf vom Alias:
|
||||||
|
_-RunPythonScript "/.../dossier_dispatch.py" <action>
|
||||||
|
oder via Rhino.Input.RhinoGet — wir lesen den letzten String-Parameter
|
||||||
|
aus der Command-Line.
|
||||||
|
|
||||||
|
Aktionen mappen auf ElementeBridge._cmd_create_* via einer kleinen
|
||||||
|
Dispatch-Tabelle. Bridge-Referenz wird in sc.sticky vom panel_factory
|
||||||
|
abgelegt (siehe elemente.py _bridge_factory).
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import scriptcontext as sc
|
||||||
|
|
||||||
|
|
||||||
|
_ACTIONS = {
|
||||||
|
"wand": ("_cmd_create_wall", ()),
|
||||||
|
"tuer": ("_cmd_create_oeffnung", ("tuer",)),
|
||||||
|
"fenster": ("_cmd_create_oeffnung", ("fenster",)),
|
||||||
|
"decke": ("_cmd_create_decke", ()),
|
||||||
|
"aussparung":("_cmd_create_aussparung",()),
|
||||||
|
"dach": ("_cmd_create_dach", ()),
|
||||||
|
"treppe": ("_cmd_create_treppe", ()),
|
||||||
|
"stuetze": ("_cmd_create_stuetze", ()),
|
||||||
|
"traeger": ("_cmd_create_traeger", ()),
|
||||||
|
"raum": ("_cmd_create_raum", ()),
|
||||||
|
"stempel": ("_cmd_create_stempel", ()),
|
||||||
|
"symbol": ("_cmd_create_symbol", ()),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_PRETTY = {
|
||||||
|
"wand": "DOSSIER Wand",
|
||||||
|
"tuer": "DOSSIER Tuer",
|
||||||
|
"fenster": "DOSSIER Fenster",
|
||||||
|
"decke": "DOSSIER Decke",
|
||||||
|
"aussparung": "DOSSIER Aussparung",
|
||||||
|
"dach": "DOSSIER Dach",
|
||||||
|
"treppe": "DOSSIER Treppe",
|
||||||
|
"stuetze": "DOSSIER Stuetze",
|
||||||
|
"traeger": "DOSSIER Traeger",
|
||||||
|
"raum": "DOSSIER Raum",
|
||||||
|
"stempel": "DOSSIER Stempel",
|
||||||
|
"symbol": "DOSSIER Symbol",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def dispatch(action):
|
||||||
|
"""Public entry — von per-action Wrapper-Scripts aufgerufen."""
|
||||||
|
try:
|
||||||
|
import Rhino
|
||||||
|
Rhino.RhinoApp.SetCommandPrompt(_PRETTY.get(action, "DOSSIER " + action.capitalize()))
|
||||||
|
except Exception: pass
|
||||||
|
bridge = sc.sticky.get("dossier_bridge_elemente")
|
||||||
|
if bridge is None:
|
||||||
|
print("[DOSSIER-ALIAS] Elemente-Bridge nicht aktiv (Panel oeffnen)")
|
||||||
|
return
|
||||||
|
spec = _ACTIONS.get(action)
|
||||||
|
if spec is None:
|
||||||
|
print("[DOSSIER-ALIAS] Unbekannte Aktion:", action)
|
||||||
|
return
|
||||||
|
method_name, args = spec
|
||||||
|
method = getattr(bridge, method_name, None)
|
||||||
|
if method is None:
|
||||||
|
print("[DOSSIER-ALIAS] Bridge-Method fehlt:", method_name)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
method({}, *args)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[DOSSIER-ALIAS]", action, "->", method_name, ":", ex)
|
||||||
|
|
||||||
|
|
||||||
|
# Backwards-Compat (alter Name).
|
||||||
|
_dispatch = dispatch
|
||||||
|
|
||||||
|
|
||||||
|
def _read_action_from_argv():
|
||||||
|
# sys.argv enthaelt bei _-RunPythonScript "path" arg1 arg2 ... die
|
||||||
|
# Args nach dem Skript-Pfad. argv[0] = Skript-Pfad.
|
||||||
|
if len(sys.argv) >= 2:
|
||||||
|
return str(sys.argv[1]).strip().lower()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
a = _read_action_from_argv()
|
||||||
|
if a:
|
||||||
|
_dispatch(a)
|
||||||
|
else:
|
||||||
|
print("[DOSSIER-ALIAS] Keine Aktion uebergeben. Erwartet:",
|
||||||
|
", ".join(sorted(_ACTIONS.keys())))
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""
|
||||||
|
dossier_view_mode.py
|
||||||
|
Setzt Display-Mode (+ optional Standard-Ansicht) im aktiven Viewport.
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
_-RunPythonScript "/.../dossier_view_mode.py" <mode>
|
||||||
|
mode: plan | persp3d | material | raytracing
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import Rhino
|
||||||
|
|
||||||
|
|
||||||
|
_MODES = {
|
||||||
|
"plan": {"display": "Dossier Plan", "view": "Top", "label": "DOSSIER Plan-Mode"},
|
||||||
|
"persp3d": {"display": "Dossier 3D", "view": "Perspective","label": "DOSSIER 3D-Mode"},
|
||||||
|
"material": {"display": "Dossier Material", "view": None, "label": "DOSSIER Material-Mode"},
|
||||||
|
"raytracing": {"display": "Dossier Raytracing", "view": None, "label": "DOSSIER Raytracing"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply(mode_name):
|
||||||
|
spec = _MODES.get(mode_name)
|
||||||
|
if spec is None:
|
||||||
|
print("[VIEW-MODE] Unbekannt:", mode_name)
|
||||||
|
return
|
||||||
|
try: Rhino.RhinoApp.SetCommandPrompt(spec.get("label", "DOSSIER View"))
|
||||||
|
except Exception: pass
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None:
|
||||||
|
print("[VIEW-MODE] Kein aktives Doc")
|
||||||
|
return
|
||||||
|
view = doc.Views.ActiveView
|
||||||
|
if view is None:
|
||||||
|
print("[VIEW-MODE] Kein aktiver Viewport")
|
||||||
|
return
|
||||||
|
# Standard-View setzen (Top / Perspective) falls definiert
|
||||||
|
vw_name = spec["view"]
|
||||||
|
if vw_name:
|
||||||
|
try:
|
||||||
|
view.ActiveViewport.SetProjection(
|
||||||
|
Rhino.Display.DefinedViewportProjection.Top
|
||||||
|
if vw_name == "Top"
|
||||||
|
else Rhino.Display.DefinedViewportProjection.Perspective,
|
||||||
|
vw_name, True)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[VIEW-MODE] view-set:", ex)
|
||||||
|
# Display-Mode setzen via Description-Lookup
|
||||||
|
dm_name = spec["display"]
|
||||||
|
try:
|
||||||
|
all_dm = Rhino.Display.DisplayModeDescription.GetDisplayModes()
|
||||||
|
target = None
|
||||||
|
for d in all_dm:
|
||||||
|
if d.EnglishName == dm_name or d.LocalName == dm_name:
|
||||||
|
target = d; break
|
||||||
|
if target is None:
|
||||||
|
print("[VIEW-MODE] Display-Mode not found:", dm_name)
|
||||||
|
return
|
||||||
|
view.ActiveViewport.DisplayMode = target
|
||||||
|
view.Redraw()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[VIEW-MODE] display-mode:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) >= 2:
|
||||||
|
_apply(str(sys.argv[1]).strip().lower())
|
||||||
|
else:
|
||||||
|
print("[VIEW-MODE] Erwartet Mode-Name:", ", ".join(_MODES.keys()))
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""
|
||||||
|
aliases/loader.py
|
||||||
|
Liest shortcuts_default.json + User-Overrides aus dossier_settings.json,
|
||||||
|
merged und wendet via Rhino.ApplicationSettings.CommandAliasList /
|
||||||
|
ShortcutKeySettings an. Wird einmal beim Rhino-Start aus startup.py
|
||||||
|
aufgerufen (idempotent — SetMacro ueberschreibt).
|
||||||
|
|
||||||
|
User-Override-Format in dossier_settings.json:
|
||||||
|
"shortcuts_user": {
|
||||||
|
"<action_id>": "<trigger_string>" // leer = Default
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import Rhino
|
||||||
|
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_quit_xml_pairs = [] # gefuellt in apply_all(), genutzt vom Closing-Hook
|
||||||
|
_DEFAULTS_PATH = os.path.join(_HERE, "shortcuts_default.json")
|
||||||
|
_SETTINGS_PATHS = [
|
||||||
|
os.path.expanduser("~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json"),
|
||||||
|
os.path.expanduser("~/Library/Application Support/RhinoPanel/dossier_settings.json"), # legacy
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _read_defaults():
|
||||||
|
try:
|
||||||
|
with open(_DEFAULTS_PATH, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
out = {}
|
||||||
|
for k, v in data.items():
|
||||||
|
if k.startswith("_"): continue
|
||||||
|
if not isinstance(v, dict): continue
|
||||||
|
out[k] = v
|
||||||
|
return out
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ALIASES] Defaults lesen:", ex)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_user_overrides():
|
||||||
|
"""Liest 'shortcuts_user' aus dossier_settings.json. Format:
|
||||||
|
{ action_id: trigger_string }. Leerer String / None = Default."""
|
||||||
|
for path in _SETTINGS_PATHS:
|
||||||
|
if not os.path.exists(path): continue
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
so = data.get("shortcuts_user")
|
||||||
|
if isinstance(so, dict): return so
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ALIASES] Settings lesen:", ex)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_macro(macro):
|
||||||
|
"""Platzhalter {ALIASDIR} → absoluter Pfad zum aliases/-Ordner."""
|
||||||
|
return macro.replace("{ALIASDIR}", _HERE)
|
||||||
|
|
||||||
|
|
||||||
|
# Sonderzeichen → Rhino-Enum-Namen (Mac XML + ShortcutKey-API)
|
||||||
|
_SPECIAL_KEY_NAMES = {
|
||||||
|
"-": "Minus", "+": "Plus", "=": "Equals",
|
||||||
|
"/": "Slash", "\\": "Backslash",
|
||||||
|
".": "Period", ",": "Comma",
|
||||||
|
";": "Semicolon", "'": "Quote", "`": "Backquote",
|
||||||
|
"[": "OpenBracket", "]": "CloseBracket",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_key_part(key_part):
|
||||||
|
"""Mapped Sonderzeichen wie '-' auf Enum-Namen ('Minus'). Buchstaben/F-Keys
|
||||||
|
bleiben unchanged (Case-preserved)."""
|
||||||
|
if key_part in _SPECIAL_KEY_NAMES:
|
||||||
|
return _SPECIAL_KEY_NAMES[key_part]
|
||||||
|
return key_part
|
||||||
|
|
||||||
|
|
||||||
|
def _xml_key_from_trigger(trigger):
|
||||||
|
"""'Cmd+Shift+F3' → 'CommandShiftF3' (Mac Rhino XML-Schema).
|
||||||
|
Cmd/Ctrl → 'Command', Shift → 'Shift', Alt/Option → 'Option'.
|
||||||
|
Sonderzeichen ('-', '/', etc.) werden auf Enum-Namen gemapped."""
|
||||||
|
t = trigger.replace(" ", "")
|
||||||
|
parts = t.split("+") if "+" in t[1:] else [t]
|
||||||
|
# Edge-Case: trigger endet auf literal '+' oder '-' → letztes Element ist Key
|
||||||
|
# 'Cmd+-' → ['Cmd', '', '-'] via split. Fix: re-split last token wenn leer
|
||||||
|
parts = [p for p in parts if p != ""]
|
||||||
|
# Sonderfall trigger == 'Cmd+-' → split('+') = ['Cmd', '-'], OK
|
||||||
|
# Sonderfall trigger == 'Cmd++' → split('+') = ['Cmd', '', ''] → key = '+'
|
||||||
|
if "Cmd++" in trigger or "Ctrl++" in trigger or "Shift++" in trigger:
|
||||||
|
parts = trigger.replace(" ", "").rstrip("+").split("+") + ["+"]
|
||||||
|
if not parts: return None
|
||||||
|
key_part = _normalize_key_part(parts[-1])
|
||||||
|
mods = set(p.lower() for p in parts[:-1])
|
||||||
|
has_cmd = ("cmd" in mods) or ("ctrl" in mods) or ("command" in mods)
|
||||||
|
has_shift = "shift" in mods
|
||||||
|
has_alt = ("alt" in mods) or ("option" in mods) or ("opt" in mods)
|
||||||
|
prefix = ""
|
||||||
|
if has_cmd: prefix += "Command"
|
||||||
|
if has_shift: prefix += "Shift"
|
||||||
|
if has_alt: prefix += "Option"
|
||||||
|
return prefix + key_part
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_in_xml(xml_key, expected_macro):
|
||||||
|
"""True wenn <entry key='<xml_key>'>expected_macro</entry> bereits im
|
||||||
|
Mac Rhino settings-XML existiert."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
paths = [
|
||||||
|
os.path.expanduser("~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"),
|
||||||
|
]
|
||||||
|
_esc = lambda s: s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
pat = re.compile(
|
||||||
|
r'<entry\s+key="' + re.escape(xml_key) + r'"\s*>([^<]*)</entry>')
|
||||||
|
for path in paths:
|
||||||
|
if not os.path.exists(path): continue
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
m = pat.search(content)
|
||||||
|
if m and m.group(1) == _esc(expected_macro):
|
||||||
|
return True
|
||||||
|
except Exception: pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _xml_persist_shortcut(xml_key, macro, verbose=False):
|
||||||
|
"""Schreibt <entry key="<xml_key>"><macro></entry> direkt in Mac Rhino's
|
||||||
|
settings-Scheme__Default.xml unter <child key='ShortcutKeys'>. String-
|
||||||
|
basiert damit die Original-Formatierung 1:1 erhalten bleibt."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
paths = [
|
||||||
|
os.path.expanduser("~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/settings-Scheme__Default.xml"),
|
||||||
|
]
|
||||||
|
n_written = 0
|
||||||
|
_esc = lambda s: s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
if not os.path.exists(path): continue
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
new_entry = '<entry key="{}">{}</entry>'.format(xml_key, _esc(macro))
|
||||||
|
|
||||||
|
# Existing entry? Loeschen (mit umgebendem Whitespace+Newline)
|
||||||
|
# und neu hinzufuegen mit sauberem Format. Vermeidet
|
||||||
|
# kaputt-formatierte Entries.
|
||||||
|
pat = re.compile(
|
||||||
|
r'<entry\s+key="' + re.escape(xml_key) + r'"\s*(/>|>[^<]*</entry>)')
|
||||||
|
m = pat.search(content)
|
||||||
|
if m:
|
||||||
|
# Check Line-Kontext: nur diese Entry auf Zeile + unchanged?
|
||||||
|
line_start = content.rfind("\n", 0, m.start()) + 1
|
||||||
|
line_end = content.find("\n", m.end())
|
||||||
|
if line_end < 0: line_end = len(content)
|
||||||
|
line_trim = content[line_start:line_end].strip()
|
||||||
|
if line_trim == new_entry:
|
||||||
|
if verbose: print("[ALIASES] XML '{}' unchanged".format(xml_key))
|
||||||
|
continue
|
||||||
|
# Sonst: loeschen inkl. preceding-newline+whitespace damit
|
||||||
|
# keine orphan-line uebrig bleibt
|
||||||
|
del_start = m.start()
|
||||||
|
while del_start > 0 and content[del_start-1] in " \t":
|
||||||
|
del_start -= 1
|
||||||
|
if del_start > 0 and content[del_start-1] == "\n":
|
||||||
|
del_start -= 1
|
||||||
|
content = content[:del_start] + content[m.end():]
|
||||||
|
if True:
|
||||||
|
# ShortcutKeys-Section finden
|
||||||
|
sec_start = content.find('<child key="ShortcutKeys">')
|
||||||
|
if sec_start < 0:
|
||||||
|
if verbose: print("[ALIASES] ShortcutKeys-section fehlt")
|
||||||
|
continue
|
||||||
|
sec_end = content.find('</child>', sec_start)
|
||||||
|
if sec_end < 0:
|
||||||
|
if verbose: print("[ALIASES] ShortcutKeys-close fehlt")
|
||||||
|
continue
|
||||||
|
# Indent vom letzten <entry> in der Section uebernehmen
|
||||||
|
section = content[sec_start:sec_end]
|
||||||
|
ms = list(re.finditer(r'\n([ \t]*)<entry\s', section))
|
||||||
|
entry_indent = ms[-1].group(1) if ms else " "
|
||||||
|
# Indent vor </child> (typisch 6 spaces)
|
||||||
|
close_match = re.search(r'\n([ \t]*)$', content[:sec_end])
|
||||||
|
close_indent = close_match.group(1) if close_match else " "
|
||||||
|
# Section neu zusammensetzen: alles vor </child> bereinigt
|
||||||
|
# + sauberer Insert
|
||||||
|
before = content[:sec_end].rstrip(" \t") + "\n"
|
||||||
|
content = (before + entry_indent + new_entry + "\n"
|
||||||
|
+ close_indent + content[sec_end:])
|
||||||
|
action = "added"
|
||||||
|
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
n_written += 1
|
||||||
|
if verbose: print("[ALIASES] XML {} '{}'".format(action, xml_key))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ALIASES] XML-Write {}: {}".format(path, ex))
|
||||||
|
return n_written
|
||||||
|
|
||||||
|
|
||||||
|
def _install_quit_xml_save(pairs):
|
||||||
|
"""Rhino's Closing-Event fired auf Mac NICHT zuverlaessig. Wir
|
||||||
|
installieren MEHRERE Hooks parallel:
|
||||||
|
1. Rhino.RhinoApp.Closing (Mac: meist No-op, Windows: ok)
|
||||||
|
2. Python atexit (laeuft wenn Interpreter terminiert)
|
||||||
|
3. AppDomain.ProcessExit (.NET-Level Hook)
|
||||||
|
4. Idle-Watcher: schreibt XML alle 30s wenn Aenderung erkannt
|
||||||
|
(Fallback fuer Rhino's runtime-flush)
|
||||||
|
Marker-Logging zur Verifikation welcher Hook wirklich feuert."""
|
||||||
|
import os as _os
|
||||||
|
import datetime as _dt
|
||||||
|
_marker = _os.path.expanduser("~/Library/Logs/dossier_quit_hook.log")
|
||||||
|
try:
|
||||||
|
_os.makedirs(_os.path.dirname(_marker), exist_ok=True)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
def _log(msg):
|
||||||
|
try:
|
||||||
|
with open(_marker, "a") as f:
|
||||||
|
f.write("[{}] {}\n".format(_dt.datetime.now().isoformat(), msg))
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
def _write_all(source):
|
||||||
|
n_ok = 0
|
||||||
|
for xml_key, macro in pairs:
|
||||||
|
if _xml_persist_shortcut(xml_key, macro, verbose=False) > 0:
|
||||||
|
n_ok += 1
|
||||||
|
_log("{} FIRED — {}/{} ok".format(source, n_ok, len(pairs)))
|
||||||
|
return n_ok
|
||||||
|
|
||||||
|
n_hooks = 0
|
||||||
|
try:
|
||||||
|
import Rhino
|
||||||
|
def _on_closing(*_):
|
||||||
|
try: _write_all("RhinoClosing")
|
||||||
|
except Exception as ex: _log("RhinoClosing ERROR: {}".format(ex))
|
||||||
|
Rhino.RhinoApp.Closing += _on_closing
|
||||||
|
n_hooks += 1
|
||||||
|
except Exception as ex:
|
||||||
|
_log("RhinoClosing install err: {}".format(ex))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import atexit
|
||||||
|
def _on_atexit():
|
||||||
|
try: _write_all("atexit")
|
||||||
|
except Exception as ex: _log("atexit ERROR: {}".format(ex))
|
||||||
|
atexit.register(_on_atexit)
|
||||||
|
n_hooks += 1
|
||||||
|
except Exception as ex:
|
||||||
|
_log("atexit install err: {}".format(ex))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import System
|
||||||
|
def _on_process_exit(*_):
|
||||||
|
try: _write_all("ProcessExit")
|
||||||
|
except Exception as ex: _log("ProcessExit ERROR: {}".format(ex))
|
||||||
|
System.AppDomain.CurrentDomain.ProcessExit += _on_process_exit
|
||||||
|
n_hooks += 1
|
||||||
|
except Exception as ex:
|
||||||
|
_log("ProcessExit install err: {}".format(ex))
|
||||||
|
|
||||||
|
# Idle-Watcher: periodisch (alle ~30s) checken ob unsere XML-Entries
|
||||||
|
# noch da sind. Wenn nein → wieder reinschreiben. Ueberlebt Rhino-
|
||||||
|
# Runtime-Flushes auch ohne Close-Event.
|
||||||
|
try:
|
||||||
|
import Rhino
|
||||||
|
import time as _time
|
||||||
|
_state = {"last": 0.0}
|
||||||
|
def _idle_watcher(*_):
|
||||||
|
try:
|
||||||
|
now = _time.time()
|
||||||
|
if now - _state["last"] < 30.0: return
|
||||||
|
_state["last"] = now
|
||||||
|
# Pruefen ob entries fehlen — wenn ja, alle re-schreiben
|
||||||
|
_write_all("IdleWatch")
|
||||||
|
except Exception as ex:
|
||||||
|
_log("IdleWatch ERROR: {}".format(ex))
|
||||||
|
Rhino.RhinoApp.Idle += _idle_watcher
|
||||||
|
n_hooks += 1
|
||||||
|
_log("IdleWatch installed (30s interval)")
|
||||||
|
except Exception as ex:
|
||||||
|
_log("IdleWatch install err: {}".format(ex))
|
||||||
|
|
||||||
|
_log("Hooks INSTALLED ({} of 4) for {} shortcuts".format(n_hooks, len(pairs)))
|
||||||
|
# Initiale Schreibung im ersten Pass auch — falls Rhino sofort flusht
|
||||||
|
_write_all("InitialWrite")
|
||||||
|
return n_hooks > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_fkey(trigger):
|
||||||
|
"""'F3' / 'Shift+F3' / 'Cmd+F3' / 'Cmd+Alt+F3' → ShortcutKey-Enum-Wert.
|
||||||
|
Enum-Naming-Konvention von Rhino: Ctrl → Shift → Alt → KeyName
|
||||||
|
(z.B. CtrlAltF3, CtrlShiftAltF3). Cmd auf Mac mappt auf Ctrl,
|
||||||
|
Option/Opt auf Alt. Sonderzeichen via _SPECIAL_KEY_NAMES."""
|
||||||
|
SK = Rhino.ApplicationSettings.ShortcutKey
|
||||||
|
t = trigger.replace(" ", "")
|
||||||
|
parts = t.split("+")
|
||||||
|
parts = [p for p in parts if p != ""]
|
||||||
|
if not parts: return None
|
||||||
|
raw_last = parts[-1]
|
||||||
|
if raw_last in _SPECIAL_KEY_NAMES:
|
||||||
|
key_part = _SPECIAL_KEY_NAMES[raw_last]
|
||||||
|
else:
|
||||||
|
key_part = raw_last.upper()
|
||||||
|
mods = set(p.lower() for p in parts[:-1])
|
||||||
|
has_ctrl = ("ctrl" in mods) or ("cmd" in mods) or ("command" in mods)
|
||||||
|
has_shift = "shift" in mods
|
||||||
|
has_alt = ("alt" in mods) or ("option" in mods) or ("opt" in mods)
|
||||||
|
prefix = ""
|
||||||
|
if has_ctrl: prefix += "Ctrl"
|
||||||
|
if has_shift: prefix += "Shift"
|
||||||
|
if has_alt: prefix += "Alt"
|
||||||
|
return getattr(SK, prefix + key_part, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_cmd_letter(trigger):
|
||||||
|
"""'Cmd+W' / 'Cmd+Shift+W' → ShortcutKey-Enum (Ctrl* auf Rhino-Naming-
|
||||||
|
Konvention; Mac mappt Ctrl auf Cmd intern)."""
|
||||||
|
SK = Rhino.ApplicationSettings.ShortcutKey
|
||||||
|
t = trigger.replace(" ", "")
|
||||||
|
parts = t.split("+")
|
||||||
|
if len(parts) < 2: return None
|
||||||
|
letter = parts[-1].upper()
|
||||||
|
if not (len(letter) == 1 and letter.isalpha()): return None
|
||||||
|
mods = set(p.lower() for p in parts[:-1])
|
||||||
|
has_cmd = ("cmd" in mods) or ("ctrl" in mods)
|
||||||
|
if not has_cmd: return None
|
||||||
|
name = "Ctrl"
|
||||||
|
if "shift" in mods: name += "Shift"
|
||||||
|
if "alt" in mods: name += "Alt"
|
||||||
|
name += letter
|
||||||
|
return getattr(SK, name, None)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_all():
|
||||||
|
"""Liest Defaults + Overrides, wendet alle Aliases + Shortcuts an.
|
||||||
|
Returnt (n_alias, n_fkey, n_cmd, n_skipped)."""
|
||||||
|
global _quit_xml_pairs
|
||||||
|
_quit_xml_pairs = []
|
||||||
|
defaults = _read_defaults()
|
||||||
|
overrides = _read_user_overrides()
|
||||||
|
aliases = Rhino.ApplicationSettings.CommandAliasList
|
||||||
|
skset = Rhino.ApplicationSettings.ShortcutKeySettings
|
||||||
|
n_alias = n_fkey = n_cmd = n_skipped = 0
|
||||||
|
seen_triggers = {} # trigger_normalized -> action_id (Konflikt-Erkennung)
|
||||||
|
|
||||||
|
for action_id, spec in defaults.items():
|
||||||
|
# User-Override hat Vorrang. Leerer String = Default, None/missing = Default.
|
||||||
|
user_trig = overrides.get(action_id)
|
||||||
|
if user_trig is not None and str(user_trig).strip() == "":
|
||||||
|
user_trig = None
|
||||||
|
trigger = user_trig if user_trig else spec.get("trigger", "")
|
||||||
|
if not trigger:
|
||||||
|
n_skipped += 1
|
||||||
|
continue
|
||||||
|
spec_type = spec.get("type", "alias")
|
||||||
|
macro = _expand_macro(spec.get("macro", ""))
|
||||||
|
if not macro:
|
||||||
|
n_skipped += 1; continue
|
||||||
|
|
||||||
|
# Konflikt-Check (gleicher Trigger → letzter gewinnt, Warning)
|
||||||
|
norm = (spec_type, str(trigger).lower())
|
||||||
|
if norm in seen_triggers:
|
||||||
|
print("[ALIASES] Konflikt: '{}' fuer {} bereits von {} belegt"
|
||||||
|
.format(trigger, action_id, seen_triggers[norm]))
|
||||||
|
seen_triggers[norm] = action_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
if spec_type == "alias":
|
||||||
|
tname = str(trigger)
|
||||||
|
try:
|
||||||
|
if aliases.IsAlias(tname):
|
||||||
|
aliases.Delete(tname)
|
||||||
|
except Exception: pass
|
||||||
|
added = False
|
||||||
|
try:
|
||||||
|
added = aliases.Add(tname, macro)
|
||||||
|
except Exception as _addex:
|
||||||
|
print("[ALIASES] Add({}, ...) Exception: {}"
|
||||||
|
.format(tname, _addex))
|
||||||
|
if not added:
|
||||||
|
try: aliases.SetMacro(tname, macro)
|
||||||
|
except Exception: pass
|
||||||
|
# Verifizieren ob Alias wirklich registriert ist
|
||||||
|
try:
|
||||||
|
is_ok = aliases.IsAlias(tname)
|
||||||
|
if not is_ok:
|
||||||
|
print("[ALIASES] WARN: '{}' (action={}) NICHT registriert "
|
||||||
|
"— Rhino lehnt Namen wahrscheinlich ab (z.B. reine Zahl)"
|
||||||
|
.format(tname, action_id))
|
||||||
|
n_skipped += 1
|
||||||
|
continue
|
||||||
|
except Exception: pass
|
||||||
|
n_alias += 1
|
||||||
|
elif spec_type == "fkey":
|
||||||
|
sk = _resolve_fkey(str(trigger))
|
||||||
|
xml_key = _xml_key_from_trigger(str(trigger))
|
||||||
|
api_ok = False
|
||||||
|
if sk is not None:
|
||||||
|
try:
|
||||||
|
skset.SetMacro(sk, macro)
|
||||||
|
got = skset.GetMacro(sk)
|
||||||
|
api_ok = (got == macro)
|
||||||
|
except Exception as _sex:
|
||||||
|
print("[ALIASES] SetMacro({}): {}".format(trigger, _sex))
|
||||||
|
if not api_ok and xml_key:
|
||||||
|
# Enum-Wert fehlt → direkt ins XML (mit verbose-Log).
|
||||||
|
# n_xml=0 kann "schon korrekt" ODER "gescheitert" heissen
|
||||||
|
# — wir checken explizit ob Entry im XML existiert.
|
||||||
|
n_xml = _xml_persist_shortcut(xml_key, macro, verbose=True)
|
||||||
|
if n_xml > 0:
|
||||||
|
_quit_xml_pairs.append((xml_key, macro))
|
||||||
|
else:
|
||||||
|
# n_xml == 0 → entweder "unchanged" (= schon korrekt
|
||||||
|
# im XML) oder "missing path/section". Check via
|
||||||
|
# IsAliasInXml damit wir nicht falsch warnen.
|
||||||
|
if _entry_in_xml(xml_key, macro):
|
||||||
|
# Schon korrekt im XML → fuer Quit-Hook merken
|
||||||
|
# damit Rhino-Quit-Save sie nicht ueberschreibt
|
||||||
|
_quit_xml_pairs.append((xml_key, macro))
|
||||||
|
else:
|
||||||
|
print("[ALIASES] WARN F-Key {} ({}) konnte weder "
|
||||||
|
"API noch XML set werden".format(trigger, action_id))
|
||||||
|
n_skipped += 1; continue
|
||||||
|
n_fkey += 1
|
||||||
|
elif spec_type == "cmd":
|
||||||
|
sk = _resolve_cmd_letter(str(trigger))
|
||||||
|
if sk is None:
|
||||||
|
# Fallback: Cmd+Letter API u.U. nicht im Enum → als Alias mit dem
|
||||||
|
# Letter (single-char) registrieren. User tippt dann Letter+Enter.
|
||||||
|
letter_only = str(trigger).split("+")[-1].lower()
|
||||||
|
if len(letter_only) == 1 and letter_only.isalpha():
|
||||||
|
aliases.SetMacro(letter_only, macro)
|
||||||
|
n_alias += 1
|
||||||
|
print("[ALIASES] {} ({}): Cmd+Letter nicht im Enum, "
|
||||||
|
"fallback Alias '{}'".format(action_id, trigger, letter_only))
|
||||||
|
else:
|
||||||
|
n_skipped += 1
|
||||||
|
continue
|
||||||
|
skset.SetMacro(sk, macro)
|
||||||
|
n_cmd += 1
|
||||||
|
else:
|
||||||
|
print("[ALIASES] Unbekannter Type:", spec_type); n_skipped += 1
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ALIASES] Apply", action_id, "->", trigger, ":", ex)
|
||||||
|
n_skipped += 1
|
||||||
|
|
||||||
|
# Quit-Hook installieren falls XML-only Shortcuts set wurden — diese
|
||||||
|
# ueberlebt sonst Rhino's Auto-Save beim Quit nicht.
|
||||||
|
if _quit_xml_pairs:
|
||||||
|
_install_quit_xml_save(list(_quit_xml_pairs))
|
||||||
|
print("[ALIASES] {} XML-only Shortcuts werden bei Quit "
|
||||||
|
"re-persistiert (closing hook installed)"
|
||||||
|
.format(len(_quit_xml_pairs)))
|
||||||
|
|
||||||
|
return n_alias, n_fkey, n_cmd, n_skipped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
a, f, c, s = apply_all()
|
||||||
|
print("[ALIASES] OK: {} alias, {} fkey, {} cmd, {} skipped"
|
||||||
|
.format(a, f, c, s))
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"version": 2,
|
||||||
|
"description": "DOSSIER Default Shortcuts. Schema: F1-F12 = 2D-Werkzeuge (Single-Tastendruck). Shift+F* = Views/Panels. Cmd+F* = BIM-Objekte. F8/F9 bleiben Rhino-Default (Ortho/Snap). 2D-Tools auch als Alias n1-n0 (Fallback fuer typen). 2-Letter-Aliases (st/tg/ra/sy/sp/dh/au) fuer seltenere BIM. User-Overrides leben in dossier_settings.json unter 'shortcuts_user' = {action_id: trigger_string}. Macro-Platzhalter {ALIASDIR} wird zur Laufzeit ersetzt."
|
||||||
|
},
|
||||||
|
|
||||||
|
"wand": { "type": "fkey", "trigger": "Cmd+F1", "label": "DOSSIER Wand erstellen", "macro": "dWall" },
|
||||||
|
"tuer": { "type": "fkey", "trigger": "Cmd+F2", "label": "DOSSIER Tuer erstellen", "macro": "dDoor" },
|
||||||
|
"fenster": { "type": "fkey", "trigger": "Cmd+F3", "label": "DOSSIER Fenster erstellen", "macro": "dWindow" },
|
||||||
|
"decke": { "type": "fkey", "trigger": "Cmd+F4", "label": "DOSSIER Decke erstellen", "macro": "dSlab" },
|
||||||
|
"treppe": { "type": "fkey", "trigger": "Cmd+F5", "label": "DOSSIER Treppe erstellen", "macro": "dStair" },
|
||||||
|
"stuetze": { "type": "fkey", "trigger": "Cmd+F6", "label": "DOSSIER Stuetze erstellen", "macro": "dColumn" },
|
||||||
|
"traeger": { "type": "fkey", "trigger": "Cmd+F7", "label": "DOSSIER Traeger erstellen", "macro": "dBeam" },
|
||||||
|
"raum": { "type": "fkey", "trigger": "Cmd+F10", "label": "DOSSIER Raum erstellen", "macro": "dRoom" },
|
||||||
|
"symbol": { "type": "fkey", "trigger": "Cmd+F11", "label": "DOSSIER Symbol erstellen", "macro": "dSymbol" },
|
||||||
|
"stempel": { "type": "fkey", "trigger": "Cmd+F12", "label": "DOSSIER Stempel erstellen", "macro": "dTag" },
|
||||||
|
"dach": { "type": "alias", "trigger": "dh", "label": "DOSSIER Dach (Alias)", "macro": "dRoof" },
|
||||||
|
"aussparung": { "type": "alias", "trigger": "au", "label": "DOSSIER Aussparung (Alias)", "macro": "dVoid" },
|
||||||
|
|
||||||
|
"text": { "type": "fkey", "trigger": "F1", "label": "Text", "macro": "_Text" },
|
||||||
|
"line": { "type": "fkey", "trigger": "F2", "label": "Linie", "macro": "_Line" },
|
||||||
|
"arc": { "type": "fkey", "trigger": "F3", "label": "Kreisbogen", "macro": "_Arc" },
|
||||||
|
"rectangle": { "type": "fkey", "trigger": "F4", "label": "Rechteck", "macro": "_Rectangle" },
|
||||||
|
"polyline": { "type": "fkey", "trigger": "F5", "label": "Polylinie", "macro": "_Polyline" },
|
||||||
|
"curve": { "type": "fkey", "trigger": "F6", "label": "Spline / Kurve", "macro": "_Curve" },
|
||||||
|
"hatch": { "type": "fkey", "trigger": "F7", "label": "Schraffur", "macro": "_Hatch" },
|
||||||
|
"polygon": { "type": "fkey", "trigger": "F10", "label": "Polygon", "macro": "_Polygon" },
|
||||||
|
"ellipse": { "type": "fkey", "trigger": "F11", "label": "Ellipse", "macro": "_Ellipse" },
|
||||||
|
"circle": { "type": "fkey", "trigger": "F12", "label": "Kreis", "macro": "_Circle" },
|
||||||
|
|
||||||
|
"view_plan": { "type": "fkey", "trigger": "Cmd+K", "label": "Plan-Mode (Top + Dossier Plan)", "macro": "dPlan" },
|
||||||
|
"view_3d": { "type": "fkey", "trigger": "Cmd+L", "label": "3D-Mode (Perspective + Dossier 3D)", "macro": "d3D" },
|
||||||
|
"zoom_ext": { "type": "fkey", "trigger": "Cmd+U", "label": "Zoom Extents", "macro": "_Zoom _All _Extents" },
|
||||||
|
"zoom_sel": { "type": "fkey", "trigger": "Cmd+Shift+U", "label": "Zoom Selected", "macro": "_Zoom _Selected" },
|
||||||
|
"mod_group": { "type": "fkey", "trigger": "Cmd+G", "label": "Gruppieren (Group)", "macro": "_Group" },
|
||||||
|
"geschoss_up": { "type": "alias", "trigger": "gu", "label": "Geschoss hoch (Alias)", "macro": "dLevelUp" },
|
||||||
|
"geschoss_down": { "type": "fkey", "trigger": "Cmd+B", "label": "Geschoss tief", "macro": "dLevelDown" },
|
||||||
|
"view_material": { "type": "alias", "trigger": "ma", "label": "Material-Mode (Alias)", "macro": "dMaterial" },
|
||||||
|
"panel_layer": { "type": "alias", "trigger": "la", "label": "Layer-Panel (Alias)", "macro": "_Layer" },
|
||||||
|
"panel_elemente": { "type": "alias", "trigger": "el", "label": "DOSSIER Elemente-Panel (Alias)", "macro": "-_ShowPanel \"DOSSIER Elemente\"" },
|
||||||
|
|
||||||
|
"mod_mirror": { "type": "fkey", "trigger": "Cmd+I", "label": "Spiegeln (Mirror)", "macro": "_Mirror" },
|
||||||
|
"mod_copy": { "type": "fkey", "trigger": "Cmd+D", "label": "Kopieren (Copy = Duplicate)", "macro": "_Copy" },
|
||||||
|
"mod_rotate": { "type": "fkey", "trigger": "Cmd+R", "label": "Drehen (Rotate)", "macro": "_Rotate" },
|
||||||
|
"mod_trim": { "type": "fkey", "trigger": "Cmd+T", "label": "Trim (Schneiden)", "macro": "_Trim" },
|
||||||
|
"mod_join": { "type": "fkey", "trigger": "Cmd+J", "label": "Verbinden (Smart-Join: Regionen → Union, sonst Join)", "macro": "dJoin" },
|
||||||
|
"mod_explode": { "type": "fkey", "trigger": "Cmd+E", "label": "Trennen (Explode)", "macro": "_Explode" },
|
||||||
|
"mod_fillet": { "type": "fkey", "trigger": "Cmd+Shift+V", "label": "Verrunden (Fillet)", "macro": "_Fillet" },
|
||||||
|
"mod_move": { "type": "fkey", "trigger": "Cmd+M", "label": "Verschieben (Move)", "macro": "_Move" },
|
||||||
|
"mod_offset": { "type": "fkey", "trigger": "Cmd+Shift+P", "label": "Parallele (OffsetCrv)", "macro": "_OffsetCrv" },
|
||||||
|
"mod_split": { "type": "fkey", "trigger": "Cmd+X", "label": "Smart-Split (Splitlinie zeichnen — ueberschreibt Cut)", "macro": "dSplit" },
|
||||||
|
"mod_chamfer": { "type": "fkey", "trigger": "Cmd+Shift+C", "label": "Abfasen (Chamfer)", "macro": "_Chamfer" },
|
||||||
|
"mod_pipette": { "type": "fkey", "trigger": "Cmd+Y", "label": "Pipette (Einstellungen uebernehmen)", "macro": "dPipette" },
|
||||||
|
"cheatsheet": { "type": "fkey", "trigger": "Cmd+-", "label": "DOSSIER Shortcuts-Cheatsheet", "macro": "dKeys" },
|
||||||
|
|
||||||
|
"text_alias": { "type": "alias", "trigger": "n1", "label": "Text (Alias)", "macro": "_Text" },
|
||||||
|
"line_alias": { "type": "alias", "trigger": "n2", "label": "Linie (Alias)", "macro": "_Line" },
|
||||||
|
"arc_alias": { "type": "alias", "trigger": "n3", "label": "Kreisbogen (Alias)", "macro": "_Arc" },
|
||||||
|
"rectangle_alias": { "type": "alias", "trigger": "n4", "label": "Rechteck (Alias)", "macro": "_Rectangle" },
|
||||||
|
"polyline_alias": { "type": "alias", "trigger": "n5", "label": "Polylinie (Alias)", "macro": "_Polyline" },
|
||||||
|
"curve_alias": { "type": "alias", "trigger": "n6", "label": "Kurve (Alias)", "macro": "_Curve" },
|
||||||
|
"hatch_alias": { "type": "alias", "trigger": "n7", "label": "Schraffur (Alias)", "macro": "_Hatch" },
|
||||||
|
"polygon_alias": { "type": "alias", "trigger": "n8", "label": "Polygon (Alias)", "macro": "_Polygon" },
|
||||||
|
"ellipse_alias": { "type": "alias", "trigger": "n9", "label": "Ellipse (Alias)", "macro": "_Ellipse" },
|
||||||
|
"circle_alias": { "type": "alias", "trigger": "n0", "label": "Kreis (Alias)", "macro": "_Circle" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Geschoss runter (zum naechsttieferen Eintrag in der Zeichnungsebenen-Liste)
|
||||||
|
import json
|
||||||
|
import scriptcontext as sc
|
||||||
|
import Rhino
|
||||||
|
|
||||||
|
|
||||||
|
def _go(delta):
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None:
|
||||||
|
print("[GESCHOSS-NAV] kein Doc"); return
|
||||||
|
bridge = sc.sticky.get("ebenen_bridge_ref")
|
||||||
|
if bridge is None:
|
||||||
|
print("[GESCHOSS-NAV] Ebenen-Bridge nicht aktiv (Panel oeffnen)"); return
|
||||||
|
try:
|
||||||
|
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or ""
|
||||||
|
zs = json.loads(zraw) if zraw else []
|
||||||
|
if not isinstance(zs, list) or not zs:
|
||||||
|
print("[GESCHOSS-NAV] keine Zeichnungsebenen"); return
|
||||||
|
cur_id = doc.Strings.GetValue("dossier_active_id") or ""
|
||||||
|
idx = -1
|
||||||
|
for i, z in enumerate(zs):
|
||||||
|
if isinstance(z, dict) and z.get("id") == cur_id:
|
||||||
|
idx = i; break
|
||||||
|
if idx < 0:
|
||||||
|
idx = len(zs) # nichts aktiv → starten unten
|
||||||
|
new_idx = max(0, min(len(zs) - 1, idx + delta))
|
||||||
|
if new_idx == idx:
|
||||||
|
print("[GESCHOSS-NAV] schon am {}".format(
|
||||||
|
"untersten" if delta > 0 else "obersten")); return
|
||||||
|
target = zs[new_idx]
|
||||||
|
if not isinstance(target, dict) or not target.get("id"):
|
||||||
|
print("[GESCHOSS-NAV] Zielebene ungueltig"); return
|
||||||
|
print("[GESCHOSS-NAV] wechsle zu '{}'".format(target.get("name") or target["id"]))
|
||||||
|
bridge._set_active_zeichnungsebene(target)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[GESCHOSS-NAV]", ex)
|
||||||
|
|
||||||
|
|
||||||
|
# delta=+1 = nach unten (naechster Eintrag in der Liste)
|
||||||
|
_go(+1)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Geschoss hoch (zum naechstoberen Eintrag in der Zeichnungsebenen-Liste)
|
||||||
|
import json
|
||||||
|
import scriptcontext as sc
|
||||||
|
import Rhino
|
||||||
|
|
||||||
|
|
||||||
|
def _go(delta):
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None:
|
||||||
|
print("[GESCHOSS-NAV] kein Doc"); return
|
||||||
|
bridge = sc.sticky.get("ebenen_bridge_ref")
|
||||||
|
if bridge is None:
|
||||||
|
print("[GESCHOSS-NAV] Ebenen-Bridge nicht aktiv (Panel oeffnen)"); return
|
||||||
|
try:
|
||||||
|
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or ""
|
||||||
|
zs = json.loads(zraw) if zraw else []
|
||||||
|
if not isinstance(zs, list) or not zs:
|
||||||
|
print("[GESCHOSS-NAV] keine Zeichnungsebenen"); return
|
||||||
|
cur_id = doc.Strings.GetValue("dossier_active_id") or ""
|
||||||
|
idx = -1
|
||||||
|
for i, z in enumerate(zs):
|
||||||
|
if isinstance(z, dict) and z.get("id") == cur_id:
|
||||||
|
idx = i; break
|
||||||
|
if idx < 0:
|
||||||
|
idx = 0 # nichts aktiv → starten oben
|
||||||
|
new_idx = max(0, min(len(zs) - 1, idx + delta))
|
||||||
|
if new_idx == idx:
|
||||||
|
print("[GESCHOSS-NAV] schon am {}".format(
|
||||||
|
"obersten" if delta < 0 else "untersten")); return
|
||||||
|
target = zs[new_idx]
|
||||||
|
if not isinstance(target, dict) or not target.get("id"):
|
||||||
|
print("[GESCHOSS-NAV] Zielebene ungueltig"); return
|
||||||
|
print("[GESCHOSS-NAV] wechsle zu '{}'".format(target.get("name") or target["id"]))
|
||||||
|
bridge._set_active_zeichnungsebene(target)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[GESCHOSS-NAV]", ex)
|
||||||
|
|
||||||
|
|
||||||
|
# delta=-1 = nach oben (vorheriger Eintrag in der Liste, weil Listen
|
||||||
|
# typischerweise oberste Ebene oben sind)
|
||||||
|
_go(-1)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer View-Mode 'material'.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_view_mode
|
||||||
|
dossier_view_mode._apply("material")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer View-Mode 'persp3d'.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_view_mode
|
||||||
|
dossier_view_mode._apply("persp3d")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Auto-Wrapper fuer View-Mode 'plan'.
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import dossier_view_mode
|
||||||
|
dossier_view_mode._apply("plan")
|
||||||
+41
-39
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
ausschnitte.py
|
ausschnitte.py
|
||||||
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
|
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
|
||||||
@@ -147,9 +149,9 @@ def _apply_camera(vp, cam):
|
|||||||
Rhino.RhinoApp.RunScript(
|
Rhino.RhinoApp.RunScript(
|
||||||
"_-Zoom _Factor {:.6f} _Enter".format(factor), False)
|
"_-Zoom _Factor {:.6f} _Enter".format(factor), False)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] Frustum-Apply:", ex)
|
print("[VIEWPORTS] Frustum-Apply:", ex)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] Camera-Apply:", ex)
|
print("[VIEWPORTS] Camera-Apply:", ex)
|
||||||
|
|
||||||
|
|
||||||
def _capture_layers(doc):
|
def _capture_layers(doc):
|
||||||
@@ -241,7 +243,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
|
|||||||
Liefert True bei Erfolg."""
|
Liefert True bei Erfolg."""
|
||||||
snap = next((s for s in _load_snapshots(doc) if s.get("id") == snap_id), None)
|
snap = next((s for s in _load_snapshots(doc) if s.get("id") == snap_id), None)
|
||||||
if not snap:
|
if not snap:
|
||||||
print("[AUSSCHNITTE] apply_to_detail: snap nicht gefunden", snap_id)
|
print("[VIEWPORTS] apply_to_detail: snap not found", snap_id)
|
||||||
return False
|
return False
|
||||||
# Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive)
|
# Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive)
|
||||||
page_view = None
|
page_view = None
|
||||||
@@ -255,14 +257,14 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] page-view-suche:", ex)
|
print("[VIEWPORTS] page-view-suche:", ex)
|
||||||
# Detail muss aktiv sein, damit Kamera-Aenderungen anschlagen
|
# Detail muss aktiv sein, damit Kamera-Aenderungen anschlagen
|
||||||
was_active = False
|
was_active = False
|
||||||
try: was_active = detail.IsActive
|
try: was_active = detail.IsActive
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
if page_view is not None and not was_active:
|
if page_view is not None and not was_active:
|
||||||
try: page_view.SetActiveDetail(detail.Id)
|
try: page_view.SetActiveDetail(detail.Id)
|
||||||
except Exception as ex: print("[AUSSCHNITTE] SetActiveDetail:", ex)
|
except Exception as ex: print("[VIEWPORTS] SetActiveDetail:", ex)
|
||||||
# Kamera + Layer + Name
|
# Kamera + Layer + Name
|
||||||
vp = detail.Viewport
|
vp = detail.Viewport
|
||||||
_apply_camera(vp, snap.get("camera"))
|
_apply_camera(vp, snap.get("camera"))
|
||||||
@@ -272,7 +274,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
|
|||||||
if new_name and vp.Name != new_name:
|
if new_name and vp.Name != new_name:
|
||||||
vp.Name = new_name
|
vp.Name = new_name
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] Detail-Rename:", ex)
|
print("[VIEWPORTS] Detail-Rename:", ex)
|
||||||
# Massstab
|
# Massstab
|
||||||
ratio = _parse_scale(snap.get("scale", ""))
|
ratio = _parse_scale(snap.get("scale", ""))
|
||||||
if ratio is not None:
|
if ratio is not None:
|
||||||
@@ -298,7 +300,7 @@ def apply_snapshot_to_detail(doc, detail, snap_id):
|
|||||||
(page_view or doc.Views).Redraw()
|
(page_view or doc.Views).Redraw()
|
||||||
except Exception:
|
except Exception:
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
print("[AUSSCHNITTE] '{}' auf Detail {} angewendet".format(snap.get("name"), detail.Id))
|
print("[VIEWPORTS] '{}' auf Detail {} applied".format(snap.get("name"), detail.Id))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -448,7 +450,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
snaps.insert(idx + 1, copy)
|
snaps.insert(idx + 1, copy)
|
||||||
self._store(doc, snaps)
|
self._store(doc, snaps)
|
||||||
self._send_list()
|
self._send_list()
|
||||||
print("[AUSSCHNITTE] '{}' dupliziert".format(src.get("name")))
|
print("[VIEWPORTS] '{}' dupliziert".format(src.get("name")))
|
||||||
|
|
||||||
def _set_field(self, snap_id, field, value):
|
def _set_field(self, snap_id, field, value):
|
||||||
if not snap_id: return
|
if not snap_id: return
|
||||||
@@ -464,7 +466,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
def _capture(self, doc, name, existing_id=None, prior_scale=""):
|
def _capture(self, doc, name, existing_id=None, prior_scale=""):
|
||||||
view = doc.Views.ActiveView
|
view = doc.Views.ActiveView
|
||||||
if view is None:
|
if view is None:
|
||||||
print("[AUSSCHNITTE] Keine aktive View")
|
print("[VIEWPORTS] Keine aktive View")
|
||||||
return None
|
return None
|
||||||
vp = view.ActiveViewport
|
vp = view.ActiveViewport
|
||||||
# Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel-
|
# Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel-
|
||||||
@@ -481,7 +483,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
else:
|
else:
|
||||||
scale_str = "1:{:.1f}".format(ratio)
|
scale_str = "1:{:.1f}".format(ratio)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] Live-Skala lesen:", ex)
|
print("[VIEWPORTS] Live-Skala lesen:", ex)
|
||||||
# Fallback: wenn kein Massstab gepinnt war, die aus dem Frustum
|
# Fallback: wenn kein Massstab gepinnt war, die aus dem Frustum
|
||||||
# berechnete Live-Skala speichern. So bleibt das Massstab-Dropdown
|
# berechnete Live-Skala speichern. So bleibt das Massstab-Dropdown
|
||||||
# nach Restore konsistent (auch wenn der eigentliche Zoom-Restore
|
# nach Restore konsistent (auch wenn der eigentliche Zoom-Restore
|
||||||
@@ -493,7 +495,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
if live is not None and live > 0:
|
if live is not None and live > 0:
|
||||||
scale_str = "1:{:.0f}".format(live) if live >= 10 else "1:{:.1f}".format(live)
|
scale_str = "1:{:.0f}".format(live) if live >= 10 else "1:{:.1f}".format(live)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] Live-Skala (Fallback):", ex)
|
print("[VIEWPORTS] Live-Skala (Fallback):", ex)
|
||||||
if not scale_str and prior_scale:
|
if not scale_str and prior_scale:
|
||||||
scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben
|
scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben
|
||||||
# Darstellungs-Override aus dem aktuellen Doc-Setting uebernehmen.
|
# Darstellungs-Override aus dem aktuellen Doc-Setting uebernehmen.
|
||||||
@@ -521,7 +523,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
snaps.append(snap)
|
snaps.append(snap)
|
||||||
self._store(doc, snaps)
|
self._store(doc, snaps)
|
||||||
self._send_list()
|
self._send_list()
|
||||||
print("[AUSSCHNITTE] '{}' gespeichert".format(name))
|
print("[VIEWPORTS] '{}' gespeichert".format(name))
|
||||||
|
|
||||||
def _update(self, snap_id):
|
def _update(self, snap_id):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
@@ -538,7 +540,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
break
|
break
|
||||||
self._store(doc, snaps)
|
self._store(doc, snaps)
|
||||||
self._send_list()
|
self._send_list()
|
||||||
print("[AUSSCHNITTE] '{}' aktualisiert".format(target.get("name")))
|
print("[VIEWPORTS] '{}' aktualisiert".format(target.get("name")))
|
||||||
|
|
||||||
def _restore(self, snap_id):
|
def _restore(self, snap_id):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
@@ -554,16 +556,16 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
kombi = (snap.get("layerCombination") or "").strip()
|
kombi = (snap.get("layerCombination") or "").strip()
|
||||||
if kombi:
|
if kombi:
|
||||||
try:
|
try:
|
||||||
import rhinopanel
|
import layers_panel as rhinopanel
|
||||||
rhinopanel.apply_layer_preset_by_name(doc, kombi)
|
rhinopanel.apply_layer_preset_by_name(doc, kombi)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] kombi-apply '{}':".format(kombi), ex)
|
print("[VIEWPORTS] kombi-apply '{}':".format(kombi), ex)
|
||||||
_apply_layers_global(doc, snap.get("layers", []))
|
_apply_layers_global(doc, snap.get("layers", []))
|
||||||
else:
|
else:
|
||||||
_apply_layers_global(doc, snap.get("layers", []))
|
_apply_layers_global(doc, snap.get("layers", []))
|
||||||
# Eigene Sichtbarkeit → active_comb_name clearen
|
# Eigene Sichtbarkeit → active_comb_name clearen
|
||||||
try:
|
try:
|
||||||
import rhinopanel
|
import layers_panel as rhinopanel
|
||||||
rhinopanel.set_active_comb_name(doc, None)
|
rhinopanel.set_active_comb_name(doc, None)
|
||||||
rhinopanel._notify_oberleiste_combs()
|
rhinopanel._notify_oberleiste_combs()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
@@ -582,8 +584,8 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
if b is not None: b._send_state(force=True)
|
if b is not None: b._send_state(force=True)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] darstellung apply:", ex)
|
print("[VIEWPORTS] darstellung apply:", ex)
|
||||||
# Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat.
|
# Overrides: nur anwenden wenn das Snap "applyOverrides" set hat.
|
||||||
# Sonst bleibt der aktuelle User-Override-State unangetastet.
|
# Sonst bleibt der aktuelle User-Override-State unangetastet.
|
||||||
if snap.get("applyOverrides"):
|
if snap.get("applyOverrides"):
|
||||||
try:
|
try:
|
||||||
@@ -598,7 +600,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
b._send_state(force=True)
|
b._send_state(force=True)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] overrides-apply:", ex)
|
print("[VIEWPORTS] overrides-apply:", ex)
|
||||||
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
|
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
|
||||||
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
|
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
|
||||||
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
|
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
|
||||||
@@ -608,8 +610,8 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
if new_name and vp.Name != new_name:
|
if new_name and vp.Name != new_name:
|
||||||
vp.Name = new_name
|
vp.Name = new_name
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] Rename:", ex)
|
print("[VIEWPORTS] Rename:", ex)
|
||||||
# Gespeicherten Massstab anwenden (z.B. "1:50") — falls vorhanden und
|
# Gespeicherten Massstab anwenden (z.B. "1:50") — falls present und
|
||||||
# Viewport parallel ist (in Perspective ignoriert massstab._apply_scale).
|
# Viewport parallel ist (in Perspective ignoriert massstab._apply_scale).
|
||||||
try:
|
try:
|
||||||
scale_str = (snap.get("scale") or "").strip()
|
scale_str = (snap.get("scale") or "").strip()
|
||||||
@@ -619,7 +621,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
_, model_v = ratio # (page=1, model=N) -> N
|
_, model_v = ratio # (page=1, model=N) -> N
|
||||||
import massstab
|
import massstab
|
||||||
massstab._apply_scale(doc, vp, float(model_v))
|
massstab._apply_scale(doc, vp, float(model_v))
|
||||||
print("[AUSSCHNITTE] Massstab gesetzt auf 1:{} (applied={})".format(
|
print("[VIEWPORTS] Massstab set auf 1:{} (applied={})".format(
|
||||||
model_v, massstab.get_applied_scale_ratio()))
|
model_v, massstab.get_applied_scale_ratio()))
|
||||||
# Andere Panels (Massstab, Oberleiste) sofort ueber den
|
# Andere Panels (Massstab, Oberleiste) sofort ueber den
|
||||||
# neuen appliedScale informieren — sonst zeigt das Dropdown
|
# neuen appliedScale informieren — sonst zeigt das Dropdown
|
||||||
@@ -628,15 +630,15 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
for key in ("massstab_bridge", "oberleiste_bridge"):
|
for key in ("massstab_bridge", "oberleiste_bridge"):
|
||||||
try:
|
try:
|
||||||
b = sc.sticky.get(key)
|
b = sc.sticky.get(key)
|
||||||
print("[AUSSCHNITTE] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING"))
|
print("[VIEWPORTS] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING"))
|
||||||
if b is not None:
|
if b is not None:
|
||||||
b._send_state(force=True)
|
b._send_state(force=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[AUSSCHNITTE] force-send {} failed: {}".format(key, e))
|
print("[VIEWPORTS] force-send {} failed: {}".format(key, e))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] Massstab-Restore:", ex)
|
print("[VIEWPORTS] Massstab-Restore:", ex)
|
||||||
view.Redraw()
|
view.Redraw()
|
||||||
print("[AUSSCHNITTE] '{}' wiederhergestellt".format(snap.get("name")))
|
print("[VIEWPORTS] '{}' wiederhergestellt".format(snap.get("name")))
|
||||||
|
|
||||||
def _apply_to_detail(self, snap_id):
|
def _apply_to_detail(self, snap_id):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
@@ -651,9 +653,9 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
detail = d
|
detail = d
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] Active-Detail-Suche:", ex)
|
print("[VIEWPORTS] Active-Detail-Suche:", ex)
|
||||||
if detail is None:
|
if detail is None:
|
||||||
print("[AUSSCHNITTE] Kein Detail ausgewaehlt — bitte:")
|
print("[VIEWPORTS] Kein Detail ausgewaehlt — bitte:")
|
||||||
print(" 1) ins Layout wechseln")
|
print(" 1) ins Layout wechseln")
|
||||||
print(" 2) Detail-Rahmen einmal anklicken (so dass er hervorgehoben ist)")
|
print(" 2) Detail-Rahmen einmal anklicken (so dass er hervorgehoben ist)")
|
||||||
print(" 3) erneut 'Auf Detail anwenden' waehlen")
|
print(" 3) erneut 'Auf Detail anwenden' waehlen")
|
||||||
@@ -666,7 +668,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
|
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
|
||||||
if not snap:
|
if not snap:
|
||||||
print("[AUSSCHNITTE] Snap nicht gefunden:", snap_id)
|
print("[VIEWPORTS] Snap not found:", snap_id)
|
||||||
return
|
return
|
||||||
snap_by_id = {}
|
snap_by_id = {}
|
||||||
for ls in (snap.get("layers") or []):
|
for ls in (snap.get("layers") or []):
|
||||||
@@ -712,7 +714,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
target["layers"] = new_list
|
target["layers"] = new_list
|
||||||
self._store(doc, snaps)
|
self._store(doc, snaps)
|
||||||
self._send_list()
|
self._send_list()
|
||||||
print("[AUSSCHNITTE] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
|
print("[VIEWPORTS] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
|
||||||
|
|
||||||
def _save_preset(self, name, layers):
|
def _save_preset(self, name, layers):
|
||||||
if not name: return
|
if not name: return
|
||||||
@@ -736,7 +738,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
presets.append({"name": name, "layers": clean})
|
presets.append({"name": name, "layers": clean})
|
||||||
self._store_presets(doc, presets)
|
self._store_presets(doc, presets)
|
||||||
self._send_list()
|
self._send_list()
|
||||||
print("[AUSSCHNITTE] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
|
print("[VIEWPORTS] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
|
||||||
|
|
||||||
def _delete_preset(self, name):
|
def _delete_preset(self, name):
|
||||||
if not name: return
|
if not name: return
|
||||||
@@ -771,7 +773,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
|
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
|
||||||
if not snap:
|
if not snap:
|
||||||
print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id)
|
print("[VIEWPORTS] open_settings: snap not found", snap_id)
|
||||||
return
|
return
|
||||||
outer = self
|
outer = self
|
||||||
bridge_holder = {"form": None, "id": snap_id}
|
bridge_holder = {"form": None, "id": snap_id}
|
||||||
@@ -783,22 +785,22 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
# Listen fuer Dropdowns
|
# Listen fuer Dropdowns
|
||||||
display_modes = []
|
display_modes = []
|
||||||
try:
|
try:
|
||||||
import oberleiste
|
import toolbar as oberleiste
|
||||||
display_modes = oberleiste._list_display_modes()
|
display_modes = oberleiste._list_display_modes()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] display_modes:", ex)
|
print("[VIEWPORTS] display_modes:", ex)
|
||||||
overrides_presets = []
|
overrides_presets = []
|
||||||
try:
|
try:
|
||||||
import overrides
|
import overrides
|
||||||
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
|
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] overrides_presets:", ex)
|
print("[VIEWPORTS] overrides_presets:", ex)
|
||||||
layer_kombis = []
|
layer_kombis = []
|
||||||
try:
|
try:
|
||||||
import rhinopanel
|
import layers_panel as rhinopanel
|
||||||
layer_kombis = rhinopanel.list_layer_preset_names(d)
|
layer_kombis = rhinopanel.list_layer_preset_names(d)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[AUSSCHNITTE] layer_kombis:", ex)
|
print("[VIEWPORTS] layer_kombis:", ex)
|
||||||
cam = sn.get("camera") or {}
|
cam = sn.get("camera") or {}
|
||||||
return {
|
return {
|
||||||
"snap": {
|
"snap": {
|
||||||
@@ -824,7 +826,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
sid = bridge_holder["id"]
|
sid = bridge_holder["id"]
|
||||||
target = next((s for s in snaps if s.get("id") == sid), None)
|
target = next((s for s in snaps if s.get("id") == sid), None)
|
||||||
if target is None:
|
if target is None:
|
||||||
print("[AUSSCHNITTE] persist settings: snap weg"); return
|
print("[VIEWPORTS] persist settings: snap weg"); return
|
||||||
# Massstab
|
# Massstab
|
||||||
sc_val = (settings.get("scale") or "").strip()
|
sc_val = (settings.get("scale") or "").strip()
|
||||||
target["scale"] = sc_val
|
target["scale"] = sc_val
|
||||||
@@ -846,7 +848,7 @@ class AusschnittBridge(panel_base.BaseBridge):
|
|||||||
target["darstellung"] = darst if darst in ("einfach", "standard", "detail") else ""
|
target["darstellung"] = darst if darst in ("einfach", "standard", "detail") else ""
|
||||||
outer._store(d, snaps)
|
outer._store(d, snaps)
|
||||||
outer._send_list()
|
outer._send_list()
|
||||||
print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name")))
|
print("[VIEWPORTS] Settings fuer '{}' aktualisiert".format(target.get("name")))
|
||||||
|
|
||||||
class _AusschnittSettingsBridge(panel_base.BaseBridge):
|
class _AusschnittSettingsBridge(panel_base.BaseBridge):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""
|
||||||
|
begin_cmd_hook.py
|
||||||
|
Hook auf Rhino.Commands.Command.BeginCommand. Wenn der User ein Drawing-
|
||||||
|
Command startet (Line, Polyline, Rectangle, Circle etc.), oeffnen wir
|
||||||
|
automatisch das DOSSIER-Gestaltung-Panel und bringen es in den Vordergrund.
|
||||||
|
|
||||||
|
Idempotent — Re-Install nach _reset_panels deregistriert alten Handler.
|
||||||
|
"""
|
||||||
|
import Rhino
|
||||||
|
import scriptcontext as sc
|
||||||
|
import System
|
||||||
|
|
||||||
|
|
||||||
|
# Commands bei denen wir Gestaltung-Panel fokussieren.
|
||||||
|
# CommandEnglishName ohne Underscore-Prefix.
|
||||||
|
_DRAWING_COMMANDS = {
|
||||||
|
"Line", "Polyline", "Curve", "InterpCrv",
|
||||||
|
"Arc", "Circle", "Ellipse",
|
||||||
|
"Rectangle", "Polygon",
|
||||||
|
"Hatch", "Text",
|
||||||
|
"Point", "Points",
|
||||||
|
"InfiniteLine",
|
||||||
|
}
|
||||||
|
|
||||||
|
_GESTALTUNG_PANEL_GUID = "4b8d3f2e-5c9d-4e0f-b2c3-d4e5f6071829"
|
||||||
|
_HANDLER_KEY = "_dossier_begin_cmd_handler"
|
||||||
|
_VERBOSE_KEY = "_dossier_begin_cmd_verbose"
|
||||||
|
|
||||||
|
|
||||||
|
def _on_begin_command(sender, e):
|
||||||
|
try:
|
||||||
|
cmd = getattr(e, "CommandEnglishName", "") or ""
|
||||||
|
if sc.sticky.get(_VERBOSE_KEY):
|
||||||
|
print("[CMD-HOOK] cmd='{}'".format(cmd))
|
||||||
|
if cmd not in _DRAWING_COMMANDS: return
|
||||||
|
try:
|
||||||
|
guid = System.Guid(_GESTALTUNG_PANEL_GUID)
|
||||||
|
Rhino.UI.Panels.OpenPanel(guid)
|
||||||
|
try:
|
||||||
|
Rhino.UI.Panels.FocusPanel(guid)
|
||||||
|
except Exception: pass
|
||||||
|
if sc.sticky.get(_VERBOSE_KEY):
|
||||||
|
print("[CMD-HOOK] Gestaltung-Panel opened/focused")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[CMD-HOOK] OpenPanel:", ex)
|
||||||
|
try:
|
||||||
|
Rhino.RhinoApp.RunScript(
|
||||||
|
'-_ShowPanel "DOSSIER Gestaltung"', False)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[CMD-HOOK] handler:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def install(verbose=False):
|
||||||
|
"""Einmalige Registrierung. Bei Re-Install (z.B. nach _reset_panels)
|
||||||
|
wird der alte Handler-Ref aus sc.sticky deregistriert."""
|
||||||
|
old = sc.sticky.get(_HANDLER_KEY)
|
||||||
|
if old is not None:
|
||||||
|
try: Rhino.Commands.Command.BeginCommand -= old
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
Rhino.Commands.Command.BeginCommand += _on_begin_command
|
||||||
|
sc.sticky[_HANDLER_KEY] = _on_begin_command
|
||||||
|
sc.sticky[_VERBOSE_KEY] = bool(verbose)
|
||||||
|
print("[CMD-HOOK] Hook installed (verbose={})".format(bool(verbose)))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[CMD-HOOK] install:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def set_verbose(flag):
|
||||||
|
sc.sticky[_VERBOSE_KEY] = bool(flag)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
install(verbose=True)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
clean.py
|
clean.py
|
||||||
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
|
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
clean_layers.py
|
clean_layers.py
|
||||||
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
|
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
|
||||||
@@ -48,4 +50,4 @@ else:
|
|||||||
print("[clean_layers] Nichts geloescht (schon sauber?)")
|
print("[clean_layers] Nichts geloescht (schon sauber?)")
|
||||||
if skip:
|
if skip:
|
||||||
print("[clean_layers] Uebersprungen (Objekte drauf): {}".format(", ".join(skip)))
|
print("[clean_layers] Uebersprungen (Objekte drauf): {}".format(", ".join(skip)))
|
||||||
print("[clean_layers] Panel-Sticky zurueckgesetzt")
|
print("[clean_layers] Panel-Sticky zurueckset")
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""
|
||||||
|
curve_vertex_dots.py
|
||||||
|
Display-only Vertex-Dots fuer GENERISCHE Curves (Polylinen, Linien,
|
||||||
|
Rectangles, NurbsCurves etc). Zeigt gruene Punkte an allen Vertices
|
||||||
|
selektierter Curves — hilft beim Visuell-Finden von Grip-Positionen
|
||||||
|
wenn die Curve eine Fuellung (Hatch) hat und schwer per Klick auf
|
||||||
|
einen einzelnen Vertex zu treffen ist.
|
||||||
|
|
||||||
|
Display-only — kein eigener Drag-Handler. User editiert Vertices via
|
||||||
|
Rhino's native _Grips (Punkte sichtbar machen + Standard-Drag) oder
|
||||||
|
direktes Object-Snapping waehrend Drag.
|
||||||
|
|
||||||
|
Skipt dossier-managed Curves (wand_axis, treppe_axis, schnitt_axis,
|
||||||
|
wand_outline, wand_centerline, raum_polylinie etc) — die haben ihre
|
||||||
|
eigenen Conduits oder duerfen nicht via Vertex editiert werden.
|
||||||
|
"""
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Display as rd
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
import scriptcontext as sc
|
||||||
|
import System.Drawing as SD
|
||||||
|
|
||||||
|
|
||||||
|
# --- Konstanten ------------------------------------------------------------
|
||||||
|
|
||||||
|
_MARKER_RADIUS_PX = 6
|
||||||
|
_MARKER_FILL = SD.Color.FromArgb(200, 95, 168, 150) # accent-gruen
|
||||||
|
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
|
||||||
|
|
||||||
|
# Dossier-managed Element-Types die NICHT mit generic dots versehen werden
|
||||||
|
# (= haben eigene Conduits oder sind nicht editierbar via Vertex-Click).
|
||||||
|
_SKIP_TYPES = {
|
||||||
|
"wand_axis", "wand_centerline", "wand_outline", "wand_volume",
|
||||||
|
"treppe_axis", "treppe_outline", "treppe_volume",
|
||||||
|
"schnitt_axis", "schnitt_outline",
|
||||||
|
"raum_polylinie", "raum_stempel",
|
||||||
|
"ausschnitt_polylinie",
|
||||||
|
"decke_polylinie", "decke_volume",
|
||||||
|
"dach_polylinie", "dach_volume",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Helpers --------------------------------------------------------------
|
||||||
|
|
||||||
|
def _is_dossier_managed(obj):
|
||||||
|
"""True wenn obj ein dossier-managed Element ist (= Skip)."""
|
||||||
|
if obj is None or obj.IsDeleted: return True
|
||||||
|
try:
|
||||||
|
t = obj.Attributes.GetUserString("dossier_element_type") or ""
|
||||||
|
return t in _SKIP_TYPES
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _curve_vertices(curve):
|
||||||
|
"""Liefert Liste von rg.Point3d fuer alle relevanten Vertices der
|
||||||
|
Curve. Verschiedene Curve-Types haben verschiedene Vertices:
|
||||||
|
- LineCurve: 2 Endpunkte
|
||||||
|
- PolylineCurve: alle Polyline-Punkte (deduplizert wenn closed)
|
||||||
|
- PolyCurve: rekursiv Segmente
|
||||||
|
- NurbsCurve/sonst: Start + End (control points nicht — zu viele)"""
|
||||||
|
pts = []
|
||||||
|
if curve is None: return pts
|
||||||
|
try:
|
||||||
|
if isinstance(curve, rg.PolylineCurve):
|
||||||
|
ok, pline = curve.TryGetPolyline()
|
||||||
|
if ok and pline is not None:
|
||||||
|
n = pline.Count
|
||||||
|
# Deduplizieren wenn closed (letzter Punkt = erster)
|
||||||
|
last = n
|
||||||
|
try:
|
||||||
|
if (n >= 2
|
||||||
|
and pline[0].DistanceTo(pline[n - 1]) < 1e-6):
|
||||||
|
last = n - 1
|
||||||
|
except Exception: pass
|
||||||
|
for i in range(last):
|
||||||
|
pts.append(rg.Point3d(pline[i]))
|
||||||
|
return pts
|
||||||
|
if isinstance(curve, rg.LineCurve):
|
||||||
|
pts.append(curve.PointAtStart)
|
||||||
|
pts.append(curve.PointAtEnd)
|
||||||
|
return pts
|
||||||
|
if isinstance(curve, rg.PolyCurve):
|
||||||
|
for i in range(curve.SegmentCount):
|
||||||
|
seg = curve.SegmentCurve(i)
|
||||||
|
if seg is None: continue
|
||||||
|
# Nur Start jedes Segments (End ist Start des naechsten)
|
||||||
|
pts.append(seg.PointAtStart)
|
||||||
|
# Letztes Segment-End anhaengen
|
||||||
|
try:
|
||||||
|
pts.append(curve.PointAtEnd)
|
||||||
|
except Exception: pass
|
||||||
|
return pts
|
||||||
|
# Generic Curve: nur Start + End
|
||||||
|
try:
|
||||||
|
pts.append(curve.PointAtStart)
|
||||||
|
pts.append(curve.PointAtEnd)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
# --- Conduit -------------------------------------------------------------
|
||||||
|
|
||||||
|
class _VertexDotConduit(rd.DisplayConduit):
|
||||||
|
"""Zeichnet bei jeder selektierten generischen Curve gruene Punkte
|
||||||
|
an allen Vertices."""
|
||||||
|
|
||||||
|
def DrawForeground(self, e):
|
||||||
|
try:
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
try:
|
||||||
|
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
except Exception: return
|
||||||
|
seen_curve_ids = set()
|
||||||
|
for obj in sel:
|
||||||
|
if _is_dossier_managed(obj): continue
|
||||||
|
try:
|
||||||
|
cid = str(obj.Id)
|
||||||
|
except Exception: continue
|
||||||
|
if cid in seen_curve_ids: continue
|
||||||
|
seen_curve_ids.add(cid)
|
||||||
|
geom = obj.Geometry
|
||||||
|
if not isinstance(geom, rg.Curve): continue
|
||||||
|
for pt in _curve_vertices(geom):
|
||||||
|
try:
|
||||||
|
e.Display.DrawPoint(
|
||||||
|
pt, rd.PointStyle.RoundControlPoint,
|
||||||
|
_MARKER_RADIUS_PX, _MARKER_FILL)
|
||||||
|
except Exception:
|
||||||
|
try: e.Display.DrawDot(
|
||||||
|
pt, "·", _MARKER_FILL, _MARKER_BORDER)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[CURVE-DOTS] DrawForeground:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Install -------------------------------------------------------------
|
||||||
|
|
||||||
|
_STICKY_CONDUIT = "_dossier_curve_vertex_dots_conduit"
|
||||||
|
|
||||||
|
|
||||||
|
def install_curve_vertex_dots():
|
||||||
|
"""Idempotent: alten Conduit disable, neuen installieren."""
|
||||||
|
try:
|
||||||
|
old = sc.sticky.get(_STICKY_CONDUIT)
|
||||||
|
if old is not None:
|
||||||
|
try: old.Enabled = False
|
||||||
|
except Exception: pass
|
||||||
|
conduit = _VertexDotConduit()
|
||||||
|
conduit.Enabled = True
|
||||||
|
sc.sticky[_STICKY_CONDUIT] = conduit
|
||||||
|
print("[CURVE-DOTS] Vertex dot conduit active")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[CURVE-DOTS] install:", ex)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
dimensionen.py
|
dimensionen.py
|
||||||
DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild.
|
DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild.
|
||||||
@@ -181,7 +183,7 @@ def _apply_xform(doc, objs, xform):
|
|||||||
if doc.Objects.Transform(obj.Id, xform, True):
|
if doc.Objects.Transform(obj.Id, xform, True):
|
||||||
n += 1
|
n += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[DIMENSIONEN] Transform-Fehler:", ex)
|
print("[DIMENSIONS] Transform-Fehler:", ex)
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
@@ -199,14 +201,14 @@ class _UndoRecord(object):
|
|||||||
try:
|
try:
|
||||||
self.serial = self.doc.BeginUndoRecord(self.label)
|
self.serial = self.doc.BeginUndoRecord(self.label)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[DIMENSIONEN] BeginUndoRecord:", ex)
|
print("[DIMENSIONS] BeginUndoRecord:", ex)
|
||||||
self.serial = 0
|
self.serial = 0
|
||||||
return self
|
return self
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
if self.serial:
|
if self.serial:
|
||||||
try: self.doc.EndUndoRecord(self.serial)
|
try: self.doc.EndUndoRecord(self.serial)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[DIMENSIONEN] EndUndoRecord:", ex)
|
print("[DIMENSIONS] EndUndoRecord:", ex)
|
||||||
return False # exceptions propagieren
|
return False # exceptions propagieren
|
||||||
|
|
||||||
|
|
||||||
@@ -225,7 +227,7 @@ def _scale_around_point(doc, objs, plane, ref_world, sx, sy, sz):
|
|||||||
ausgerichtet an plane."""
|
ausgerichtet an plane."""
|
||||||
if sx == 1 and sy == 1 and sz == 1: return
|
if sx == 1 and sy == 1 and sz == 1: return
|
||||||
if sx <= 0 or sy <= 0 or sz <= 0:
|
if sx <= 0 or sy <= 0 or sz <= 0:
|
||||||
print("[DIMENSIONEN] Ungueltige Skalierungsfaktoren:", sx, sy, sz)
|
print("[DIMENSIONS] Ungueltige Skalierungsfaktoren:", sx, sy, sz)
|
||||||
return
|
return
|
||||||
p = rg.Plane(plane)
|
p = rg.Plane(plane)
|
||||||
p.Origin = ref_world
|
p.Origin = ref_world
|
||||||
@@ -584,15 +586,28 @@ def _install_listeners(bridge):
|
|||||||
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
|
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
|
||||||
# Tick zwischen den einzelnen Deletes. CommandEnd refresht.
|
# Tick zwischen den einzelnen Deletes. CommandEnd refresht.
|
||||||
if sc.sticky.get("_dossier_bulk_op_active"): return
|
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||||
|
# Waehrend Gumball/Move/Rotate: nicht pollen. Geometrie ist gerade
|
||||||
|
# in Transit (Live-Replace pro Frame), Werte wuerden mit ~5/s
|
||||||
|
# zwischen Frames flickern. CommandEnd triggert finalen _send_state.
|
||||||
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
b = sc.sticky.get("dimensionen_bridge")
|
b = sc.sticky.get("dimensionen_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b.tick_idle()
|
try: b.tick_idle()
|
||||||
except Exception as ex: print("[DIMENSIONEN] idle:", ex)
|
except Exception as ex: print("[DIMENSIONS] idle:", ex)
|
||||||
|
|
||||||
def on_select(s, e):
|
def on_select(s, e):
|
||||||
# Swisstopo-Import feuert tausende Selection-Events → bail.
|
# Swisstopo-Import feuert tausende Selection-Events → bail.
|
||||||
if sc.sticky.get("dossier_swisstopo_busy"): return
|
if sc.sticky.get("dossier_swisstopo_busy"): return
|
||||||
if sc.sticky.get("_dossier_bulk_op_active"): return
|
if sc.sticky.get("_dossier_bulk_op_active"): return
|
||||||
|
# Waehrend elemente.py's Partnership-Cascade (Klick auf Wand/Treppe
|
||||||
|
# → 30+ Partner selektiert in einem Rutsch): NICHT pro Event ein
|
||||||
|
# _send_state feuern. Sonst rauscht das Dimensionen-Panel mit 30+
|
||||||
|
# Re-Renders durch und die Werte/Auswahl-Anzeige flickert wild.
|
||||||
|
# Der Idle-Tick holt die finale Selektion eh ~5/s nach.
|
||||||
|
if sc.sticky.get("_elemente_select_busy"): return
|
||||||
|
# Waehrend User-Transform (Gumball/Move/Rotate): kein Re-Send, sonst
|
||||||
|
# rauscht Replace-Storm durch und der Frontend-State zappelt.
|
||||||
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
b = sc.sticky.get("dimensionen_bridge")
|
b = sc.sticky.get("dimensionen_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b._send_state(force=True)
|
try: b._send_state(force=True)
|
||||||
@@ -604,9 +619,9 @@ def _install_listeners(bridge):
|
|||||||
Rhino.RhinoDoc.DeselectObjects += on_select
|
Rhino.RhinoDoc.DeselectObjects += on_select
|
||||||
Rhino.RhinoDoc.DeselectAllObjects += on_select
|
Rhino.RhinoDoc.DeselectAllObjects += on_select
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[DIMENSIONEN] select-events:", ex)
|
print("[DIMENSIONS] select-events:", ex)
|
||||||
sc.sticky[flag] = True
|
sc.sticky[flag] = True
|
||||||
print("[DIMENSIONEN] Listener aktiv (Idle + SelectObjects)")
|
print("[DIMENSIONS] Listener active (Idle + SelectObjects)")
|
||||||
|
|
||||||
|
|
||||||
def _bridge_factory():
|
def _bridge_factory():
|
||||||
+6675
-467
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
elemente_properties.py
|
elemente_properties.py
|
||||||
Properties-Satellite-Window. Zeigt die Property-Forms (WallProperties,
|
Properties-Satellite-Window. Zeigt die Property-Forms (WallProperties,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
elemente_uebersicht.py
|
elemente_uebersicht.py
|
||||||
BIM-artiger Project Browser: alle Smart-Elemente in einem Tree
|
BIM-artiger Project Browser: alle Smart-Elemente in einem Tree
|
||||||
@@ -29,6 +31,7 @@ _KIND_MAP = {
|
|||||||
"stuetze_point": "stuetze",
|
"stuetze_point": "stuetze",
|
||||||
"traeger_axis": "traeger",
|
"traeger_axis": "traeger",
|
||||||
"raum_outline": "raum",
|
"raum_outline": "raum",
|
||||||
|
"stempel": "stempel",
|
||||||
"decke_aussparung_outline": "aussparung",
|
"decke_aussparung_outline": "aussparung",
|
||||||
"oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest
|
"oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest
|
||||||
}
|
}
|
||||||
@@ -114,7 +117,39 @@ def _build_overview(doc):
|
|||||||
out_geschosse.append({
|
out_geschosse.append({
|
||||||
"id": "__keingeschoss__", "name": "(kein Geschoss)", "okff": None,
|
"id": "__keingeschoss__", "name": "(kein Geschoss)", "okff": None,
|
||||||
})
|
})
|
||||||
return {"geschosse": out_geschosse, "items": items}
|
|
||||||
|
# SIA-416 Bilanz pro Geschoss: aggregiert alle raum_outline-Flaechen
|
||||||
|
# nach raum_sia-Klassifikation. Räume ohne SIA-Tag landen in "ohne".
|
||||||
|
# NF = HNF + NNF (Nutzflaeche). Wird im Frontend als Tabelle gerendert.
|
||||||
|
sia_bilanz = {} # {geschossId: {hnf, nnf, vf, ff, ohne, nf, total, count}}
|
||||||
|
for obj in doc.Objects:
|
||||||
|
meta = _elm._read_meta(obj)
|
||||||
|
if meta is None: continue
|
||||||
|
if meta.get("type") != "raum_outline": continue
|
||||||
|
try:
|
||||||
|
area, _, _ = _elm._raum_amp(obj.Geometry)
|
||||||
|
except Exception: continue
|
||||||
|
if not area or area <= 0: continue
|
||||||
|
g_id = meta.get("geschoss") or "__keingeschoss__"
|
||||||
|
sia = (meta.get("raum_sia") or "").lower()
|
||||||
|
if sia not in ("hnf", "nnf", "vf", "ff", "gf", "agf"):
|
||||||
|
sia = "ohne"
|
||||||
|
b = sia_bilanz.setdefault(g_id, {
|
||||||
|
"hnf": 0.0, "nnf": 0.0, "vf": 0.0, "ff": 0.0,
|
||||||
|
"gf": 0.0, "agf": 0.0,
|
||||||
|
"ohne": 0.0, "count": 0,
|
||||||
|
})
|
||||||
|
b[sia] += float(area)
|
||||||
|
b["count"] += 1
|
||||||
|
# NF/NGF/Total ableiten
|
||||||
|
for b in sia_bilanz.values():
|
||||||
|
b["nf"] = b["hnf"] + b["nnf"]
|
||||||
|
b["ngf"] = b["nf"] + b["vf"] + b["ff"]
|
||||||
|
b["total"] = (b["hnf"] + b["nnf"] + b["vf"] + b["ff"]
|
||||||
|
+ b["gf"] + b["agf"] + b["ohne"])
|
||||||
|
|
||||||
|
return {"geschosse": out_geschosse, "items": items,
|
||||||
|
"siaBilanz": sia_bilanz}
|
||||||
|
|
||||||
|
|
||||||
class ElementeUebersichtBridge(panel_base.BaseBridge):
|
class ElementeUebersichtBridge(panel_base.BaseBridge):
|
||||||
@@ -173,6 +208,88 @@ class ElementeUebersichtBridge(panel_base.BaseBridge):
|
|||||||
print("[UEBERSICHT] zoom:", ex)
|
print("[UEBERSICHT] zoom:", ex)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[UEBERSICHT] zoom find:", ex)
|
print("[UEBERSICHT] zoom find:", ex)
|
||||||
|
elif t == "EXPORT_BILANZ":
|
||||||
|
self._export_bilanz()
|
||||||
|
|
||||||
|
def _export_bilanz(self):
|
||||||
|
"""Exportiert SIA-416 Bilanz als CSV (Excel-kompatibel: Semikolon-
|
||||||
|
Separator + UTF-8 BOM + Komma als Dezimaltrenner). Wide-Format:
|
||||||
|
eine Spalte pro Geschoss + Total-Spalte, Zeilen pro Kategorie.
|
||||||
|
"""
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
# Geschoss-Liste (geordnet) + Total am Ende
|
||||||
|
geschosse = _elm._load_geschosse(doc) or []
|
||||||
|
gs_list = [g for g in geschosse
|
||||||
|
if isinstance(g, dict) and g.get("isGeschoss")]
|
||||||
|
# Bilanz pro Geschoss + Total via compute_sia_bilanz
|
||||||
|
per_gid = {} # gid → bilanz dict
|
||||||
|
for g in gs_list:
|
||||||
|
per_gid[g["id"]] = _elm.compute_sia_bilanz(
|
||||||
|
doc, "geschoss:" + g["id"])
|
||||||
|
total = _elm.compute_sia_bilanz(doc, "total")
|
||||||
|
# SaveFileDialog
|
||||||
|
try:
|
||||||
|
from Rhino.UI import SaveFileDialog
|
||||||
|
sfd = SaveFileDialog()
|
||||||
|
sfd.DefaultExt = "csv"
|
||||||
|
sfd.Filter = "CSV (*.csv)|*.csv"
|
||||||
|
sfd.FileName = "sia_bilanz.csv"
|
||||||
|
ok = False
|
||||||
|
try: ok = sfd.ShowSaveDialog()
|
||||||
|
except Exception:
|
||||||
|
try: ok = sfd.ShowDialog()
|
||||||
|
except Exception: ok = False
|
||||||
|
if not ok:
|
||||||
|
print("[UEBERSICHT] Bilanz-Export abgebrochen"); return
|
||||||
|
path = sfd.FileName
|
||||||
|
except Exception as ex:
|
||||||
|
print("[UEBERSICHT] SaveFileDialog:", ex); return
|
||||||
|
|
||||||
|
# Zeilen-Definition: (Label, Bilanz-Key, ist_personen?)
|
||||||
|
rows = [
|
||||||
|
("HNF (m²)", "hnf", False),
|
||||||
|
("NNF (m²)", "nnf", False),
|
||||||
|
("NF (m²)", "nf", False),
|
||||||
|
("VF (m²)", "vf", False),
|
||||||
|
("FF (m²)", "ff", False),
|
||||||
|
("NGF (m²)", "ngf", False),
|
||||||
|
("GF (m²)", "gf", False),
|
||||||
|
("AGF (m²)", "agf", False),
|
||||||
|
("Räume", "count", True),
|
||||||
|
("Personen", "personen", True),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _fmt(val, is_count):
|
||||||
|
if val is None: return ""
|
||||||
|
if is_count: return str(int(val))
|
||||||
|
return "{:.2f}".format(float(val)).replace(".", ",")
|
||||||
|
|
||||||
|
def _esc(s):
|
||||||
|
s = str(s)
|
||||||
|
if ";" in s or '"' in s or "\n" in s:
|
||||||
|
return '"' + s.replace('"', '""') + '"'
|
||||||
|
return s
|
||||||
|
|
||||||
|
try:
|
||||||
|
import io
|
||||||
|
with io.open(path, "w", encoding="utf-8-sig", newline="") as f:
|
||||||
|
# Header — Kategorie + Geschoss-Namen + Total
|
||||||
|
header = ["Kategorie"]
|
||||||
|
for g in gs_list: header.append(_esc(g.get("name") or "?"))
|
||||||
|
header.append("Total")
|
||||||
|
f.write(";".join(header) + "\n")
|
||||||
|
for label, key, is_count in rows:
|
||||||
|
line = [_esc(label)]
|
||||||
|
for g in gs_list:
|
||||||
|
b = per_gid.get(g["id"], {})
|
||||||
|
line.append(_fmt(b.get(key, 0), is_count))
|
||||||
|
line.append(_fmt(total.get(key, 0), is_count))
|
||||||
|
f.write(";".join(line) + "\n")
|
||||||
|
print("[UEBERSICHT] SIA-Bilanz exportiert: {} ({} Geschosse + Total)".format(
|
||||||
|
path, len(gs_list)))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[UEBERSICHT] CSV schreiben:", ex)
|
||||||
|
|
||||||
|
|
||||||
def open_as_window():
|
def open_as_window():
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
inspect_section.py
|
inspect_section.py
|
||||||
Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log,
|
Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log,
|
||||||
@@ -54,7 +56,7 @@ for n in dir(layer):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(" layer.{} -> err: {}".format(n, ex))
|
print(" layer.{} -> err: {}".format(n, ex))
|
||||||
|
|
||||||
# layer.SectionStyle dumpen wenn vorhanden
|
# layer.SectionStyle dumpen wenn present
|
||||||
try:
|
try:
|
||||||
if hasattr(layer, "SectionStyle"):
|
if hasattr(layer, "SectionStyle"):
|
||||||
dump("layer.SectionStyle", layer.SectionStyle)
|
dump("layer.SectionStyle", layer.SectionStyle)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
kamera.py
|
kamera.py
|
||||||
Kamera-Panel: liest/setzt Viewport-Kamera (Position, Target, Projektion,
|
Kamera-Panel: liest/setzt Viewport-Kamera (Position, Target, Projektion,
|
||||||
|
|||||||
+41
-35
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
layer_builder.py
|
layer_builder.py
|
||||||
Layer-Struktur:
|
Layer-Struktur:
|
||||||
@@ -80,7 +82,7 @@ def _find_hatch_pattern_index(doc, name):
|
|||||||
if hp.Name and hp.Name.strip().lower() == target:
|
if hp.Name and hp.Name.strip().lower() == target:
|
||||||
return i
|
return i
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] hatch lookup:", ex)
|
print("[LAYERS] hatch lookup:", ex)
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ def _find_linetype_index(doc, name):
|
|||||||
|
|
||||||
|
|
||||||
def _try_set(obj, prop_names, value):
|
def _try_set(obj, prop_names, value):
|
||||||
"""Versucht den Wert auf das erste vorhandene Property zu setzen.
|
"""Versucht den Wert auf das erste presente Property zu setzen.
|
||||||
Liefert den Property-Namen bei Erfolg, sonst None."""
|
Liefert den Property-Namen bei Erfolg, sonst None."""
|
||||||
if isinstance(prop_names, str):
|
if isinstance(prop_names, str):
|
||||||
prop_names = (prop_names,)
|
prop_names = (prop_names,)
|
||||||
@@ -152,11 +154,14 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
|
|||||||
try:
|
try:
|
||||||
SS = Rhino.DocObjects.SectionStyle
|
SS = Rhino.DocObjects.SectionStyle
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] SectionStyle-Klasse nicht da:", ex); return
|
print("[LAYERS] SectionStyle-Klasse nicht da:", ex); return
|
||||||
|
|
||||||
pat = (section_cfg.get("hatchPattern") or "None").strip()
|
pat = (section_cfg.get("hatchPattern") or "None").strip()
|
||||||
show = bool(section_cfg.get("boundaryShow", True))
|
show = bool(section_cfg.get("boundaryShow", True))
|
||||||
diag = "[SS:{}]".format(layer.Name if layer else "?")
|
diag = "[SS:{}]".format(layer.Name if layer else "?")
|
||||||
|
# DEBUG: zeigt was an section_cfg ankommt (zur Diagnose des Hatch-Bugs)
|
||||||
|
print(diag, "section_cfg.hatchPattern='{}' scale={} rot={}".format(
|
||||||
|
pat, section_cfg.get("hatchScale"), section_cfg.get("hatchRotation")))
|
||||||
|
|
||||||
# Wenn weder Hatch noch Boundary → Custom-Style entfernen
|
# Wenn weder Hatch noch Boundary → Custom-Style entfernen
|
||||||
if pat == "None" and not show:
|
if pat == "None" and not show:
|
||||||
@@ -165,7 +170,7 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
|
|||||||
layer.RemoveCustomSectionStyle()
|
layer.RemoveCustomSectionStyle()
|
||||||
print(diag, "removed (kein Hatch + kein Boundary)")
|
print(diag, "removed (kein Hatch + kein Boundary)")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(diag, "remove fehlgeschlagen:", ex)
|
print(diag, "remove failed:", ex)
|
||||||
return
|
return
|
||||||
|
|
||||||
style = SS()
|
style = SS()
|
||||||
@@ -194,17 +199,20 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
|
|||||||
rot_deg = float(section_cfg.get("hatchRotation") or 0)
|
rot_deg = float(section_cfg.get("hatchRotation") or 0)
|
||||||
_try_set(style, ("HatchRotation", "HatchAngle"), math.radians(rot_deg))
|
_try_set(style, ("HatchRotation", "HatchAngle"), math.radians(rot_deg))
|
||||||
|
|
||||||
# Hatch-Color: explizit ColorFromObject setzen damit der eigene Wert greift
|
# Hatch-Color: explizit setzen — wenn User keine Override-Farbe angegeben
|
||||||
|
# hat, nehmen wir die Layer-Farbe als Default (sonst rendert Rhino sonst
|
||||||
|
# schwarz). Section-Style hat keine ByLayer-Option, also Farbwert
|
||||||
|
# explizit reinkopieren.
|
||||||
hatch_color = section_cfg.get("hatchColor")
|
hatch_color = section_cfg.get("hatchColor")
|
||||||
if hatch_color:
|
if hatch_color:
|
||||||
col = _color(hatch_color)
|
col = _color(hatch_color)
|
||||||
set_color = _try_set(style, ("HatchColor", "FillColor"), col)
|
elif layer_color is not None:
|
||||||
# Source auf "FromObject" — sonst nutzt Rhino den Layer-Color
|
col = _color(layer_color) if isinstance(layer_color, str) else layer_color
|
||||||
src_from_object = _enum_int(
|
else:
|
||||||
(("DocObjects", "ObjectColorSource"), "ColorFromObject"))
|
col = None
|
||||||
if src_from_object is not None:
|
if col is not None:
|
||||||
_try_set(style, ("HatchColorSource", "FillColorSource"), src_from_object)
|
set_color = _try_set(style, ("HatchPatternColor", "HatchColor", "FillColor"), col)
|
||||||
print(diag, "HatchColor via {}".format(set_color))
|
print(diag, "HatchColor via {} (default=layer)".format(set_color))
|
||||||
|
|
||||||
# Background (viewport=0/transparent vs object=1)
|
# Background (viewport=0/transparent vs object=1)
|
||||||
bg = section_cfg.get("background")
|
bg = section_cfg.get("background")
|
||||||
@@ -226,20 +234,18 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
|
|||||||
print(diag, "BoundaryVisible={} via {}".format(show, set_show))
|
print(diag, "BoundaryVisible={} via {}".format(show, set_show))
|
||||||
|
|
||||||
if show:
|
if show:
|
||||||
# Boundary-Color: setze Color + Source auf FromObject
|
# Boundary-Color: User-Override oder Layer-Farbe als Default
|
||||||
bc = section_cfg.get("boundaryColor")
|
bc = section_cfg.get("boundaryColor")
|
||||||
if bc:
|
if bc:
|
||||||
col = _color(bc)
|
bcol = _color(bc)
|
||||||
|
elif layer_color is not None:
|
||||||
|
bcol = _color(layer_color) if isinstance(layer_color, str) else layer_color
|
||||||
|
else:
|
||||||
|
bcol = None
|
||||||
|
if bcol is not None:
|
||||||
set_to = _try_set(style,
|
set_to = _try_set(style,
|
||||||
("BoundaryColor", "OutlineColor", "EdgeColor"), col)
|
("BoundaryColor", "OutlineColor", "EdgeColor"), bcol)
|
||||||
src_from_object = _enum_int(
|
print(diag, "BoundaryColor via {} (default=layer)".format(set_to))
|
||||||
(("DocObjects", "ObjectColorSource"), "ColorFromObject"))
|
|
||||||
if src_from_object is not None:
|
|
||||||
_try_set(style,
|
|
||||||
("BoundaryColorSource", "OutlineColorSource",
|
|
||||||
"EdgeColorSource"),
|
|
||||||
src_from_object)
|
|
||||||
print(diag, "BoundaryColor={} via {}".format(bc, set_to))
|
|
||||||
|
|
||||||
# Width-Scale auf PlotWeight uebertragen (RW8 hat keine WidthScale direkt;
|
# Width-Scale auf PlotWeight uebertragen (RW8 hat keine WidthScale direkt;
|
||||||
# alternative Property-Namen probieren)
|
# alternative Property-Namen probieren)
|
||||||
@@ -335,7 +341,7 @@ def _build_ebene_layer(doc, parent_id, e, diag_prefix=""):
|
|||||||
_apply_section_style(doc, doc.Layers[sub_idx],
|
_apply_section_style(doc, doc.Layers[sub_idx],
|
||||||
e.get("section"), e.get("color"))
|
e.get("section"), e.get("color"))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] section-style apply ({}{}): {}".format(
|
print("[LAYERS] section-style apply ({}{}): {}".format(
|
||||||
diag_prefix, sub_name, ex))
|
diag_prefix, sub_name, ex))
|
||||||
return sub_idx
|
return sub_idx
|
||||||
|
|
||||||
@@ -378,7 +384,7 @@ def build_layers(doc, zeichnungsebenen, ebenen):
|
|||||||
diag_prefix=z_name + "/")
|
diag_prefix=z_name + "/")
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
n_total = len(walk_ebenen(ebenen))
|
n_total = len(walk_ebenen(ebenen))
|
||||||
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert (inkl. {} Sub)".format(
|
print("[LAYERS] {} drawing levels x {} layers updated (incl. {} sub)".format(
|
||||||
len(zeichnungsebenen), len(ebenen), max(0, n_total - len(ebenen))))
|
len(zeichnungsebenen), len(ebenen), max(0, n_total - len(ebenen))))
|
||||||
|
|
||||||
|
|
||||||
@@ -427,7 +433,7 @@ def set_ebene_locked(doc, code, locked):
|
|||||||
def delete_ebene(doc, code, move_to=None):
|
def delete_ebene(doc, code, move_to=None):
|
||||||
"""
|
"""
|
||||||
Loescht alle Sublayer mit dem gegebenen Code in allen Zeichnungsebenen.
|
Loescht alle Sublayer mit dem gegebenen Code in allen Zeichnungsebenen.
|
||||||
Falls move_to gesetzt: verschiebt vorher alle Objekte zum Sublayer
|
Falls move_to set: verschiebt vorher alle Objekte zum Sublayer
|
||||||
mit move_to-Code unter dem selben Parent. Sonst: loescht Objekte mit.
|
mit move_to-Code unter dem selben Parent. Sonst: loescht Objekte mit.
|
||||||
"""
|
"""
|
||||||
if not code:
|
if not code:
|
||||||
@@ -481,10 +487,10 @@ def delete_ebene(doc, code, move_to=None):
|
|||||||
if doc.Layers.Delete(from_idx, True):
|
if doc.Layers.Delete(from_idx, True):
|
||||||
deleted_layers += 1
|
deleted_layers += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[EBENEN] Layer-Delete:", ex)
|
print("[LAYERS] Layer-Delete:", ex)
|
||||||
|
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
print("[EBENEN] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format(
|
print("[LAYERS] Ebene {} entfernt: {} Sublayer, {} Objekte verschoben, {} Objekte geloescht".format(
|
||||||
code, deleted_layers, moved, deleted_objs))
|
code, deleted_layers, moved, deleted_objs))
|
||||||
|
|
||||||
|
|
||||||
@@ -526,7 +532,7 @@ def update_clipping_plane(doc, active_z, enabled):
|
|||||||
|
|
||||||
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
|
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
|
||||||
|
|
||||||
# IMMER vorhandene Plane loeschen — bei Re-Enable wollen wir frische
|
# IMMER presente Plane loeschen — bei Re-Enable wollen wir frische
|
||||||
# vp_ids (alte koennten leer/falsch sein, dann clippt das Replace zwar
|
# vp_ids (alte koennten leer/falsch sein, dann clippt das Replace zwar
|
||||||
# die Geometrie aber keinen Viewport).
|
# die Geometrie aber keinen Viewport).
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
@@ -534,15 +540,15 @@ def update_clipping_plane(doc, active_z, enabled):
|
|||||||
doc.Objects.Delete(existing.Id, True)
|
doc.Objects.Delete(existing.Id, True)
|
||||||
print("[CLIP] alte Plane geloescht")
|
print("[CLIP] alte Plane geloescht")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[CLIP] Delete fehlgeschlagen:", ex)
|
print("[CLIP] Delete failed:", ex)
|
||||||
|
|
||||||
if (not enabled) or (not is_geschoss):
|
if (not enabled) or (not is_geschoss):
|
||||||
print("[CLIP] disabled — fertig (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
|
print("[CLIP] disabled — done (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
return
|
return
|
||||||
|
|
||||||
# dict.get(k, default) liefert default NUR wenn Key fehlt — bei
|
# dict.get(k, default) liefert default NUR wenn Key fehlt — bei
|
||||||
# Key-vorhanden-aber-None gibt's None zurueck. float(None) crasht.
|
# Key-present-aber-None gibt's None zurueck. float(None) crasht.
|
||||||
# Daher explizit None-faangen:
|
# Daher explizit None-faangen:
|
||||||
okff_raw = active_z.get("okff")
|
okff_raw = active_z.get("okff")
|
||||||
sh_raw = active_z.get("schnitthoehe")
|
sh_raw = active_z.get("schnitthoehe")
|
||||||
@@ -650,7 +656,7 @@ def cleanup_default_layers(doc):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if deleted:
|
if deleted:
|
||||||
print("[EBENEN] Default-Layer entfernt: {}".format(", ".join(deleted)))
|
print("[LAYERS] Default layer removed: {}".format(", ".join(deleted)))
|
||||||
|
|
||||||
|
|
||||||
def _find_sublayer_by_code_recursive(doc, parent_id, code):
|
def _find_sublayer_by_code_recursive(doc, parent_id, code):
|
||||||
@@ -676,14 +682,14 @@ def set_active_sublayer(doc, zeichnungsebene_id, code):
|
|||||||
7101_Strassen liegt zwei Ebenen tief)."""
|
7101_Strassen liegt zwei Ebenen tief)."""
|
||||||
parent_idx = _find_top_by_id(doc, zeichnungsebene_id)
|
parent_idx = _find_top_by_id(doc, zeichnungsebene_id)
|
||||||
if parent_idx < 0:
|
if parent_idx < 0:
|
||||||
print("[EBENEN] Parent-Layer fuer Zeichnungsebene {} nicht gefunden".format(zeichnungsebene_id))
|
print("[LAYERS] Parent-Layer fuer Zeichnungsebene {} not found".format(zeichnungsebene_id))
|
||||||
return
|
return
|
||||||
parent_id = doc.Layers[parent_idx].Id
|
parent_id = doc.Layers[parent_idx].Id
|
||||||
sub_idx = _find_sublayer_by_code_recursive(doc, parent_id, code)
|
sub_idx = _find_sublayer_by_code_recursive(doc, parent_id, code)
|
||||||
if sub_idx >= 0:
|
if sub_idx >= 0:
|
||||||
doc.Layers.SetCurrentLayerIndex(sub_idx, True)
|
doc.Layers.SetCurrentLayerIndex(sub_idx, True)
|
||||||
else:
|
else:
|
||||||
print("[EBENEN] Sublayer mit Code {} unter Parent {} nicht gefunden".format(code, doc.Layers[parent_idx].Name))
|
print("[LAYERS] Sublayer with code {} under parent {} not found".format(code, doc.Layers[parent_idx].Name))
|
||||||
|
|
||||||
|
|
||||||
def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_mode, e_mode):
|
def apply_visibility(doc, zeichnungsebenen, ebenen, active_z_id, active_code, z_mode, e_mode):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+8
-6
@@ -1,9 +1,11 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
layouts.py
|
layouts.py
|
||||||
LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken.
|
LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken.
|
||||||
Phase 1 — Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail angewendet,
|
Phase 1 — Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail applied,
|
||||||
Re-Sync per Knopf. Live-Link und Masterlayouts kommen spaeter.
|
Re-Sync per Knopf. Live-Link und Masterlayouts kommen spaeter.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
@@ -387,7 +389,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
|||||||
try:
|
try:
|
||||||
page = doc.Views.AddPageView(name, w, h)
|
page = doc.Views.AddPageView(name, w, h)
|
||||||
if page is None:
|
if page is None:
|
||||||
print("[LAYOUTS] AddPageView fehlgeschlagen"); return
|
print("[LAYOUTS] AddPageView failed"); return
|
||||||
print("[LAYOUTS] '{}' angelegt ({}x{})".format(name, w, h))
|
print("[LAYOUTS] '{}' angelegt ({}x{})".format(name, w, h))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[LAYOUTS] AddPageView Fehler:", ex)
|
print("[LAYOUTS] AddPageView Fehler:", ex)
|
||||||
@@ -412,7 +414,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
|||||||
done = True
|
done = True
|
||||||
print("[LAYOUTS] SetPageSize -> {}x{}".format(w, h))
|
print("[LAYOUTS] SetPageSize -> {}x{}".format(w, h))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[LAYOUTS] SetPageSize fehlgeschlagen:", ex)
|
print("[LAYOUTS] SetPageSize failed:", ex)
|
||||||
# 2) Fallback: Properties (haengt von Rhino-Version ab)
|
# 2) Fallback: Properties (haengt von Rhino-Version ab)
|
||||||
if not done:
|
if not done:
|
||||||
try:
|
try:
|
||||||
@@ -421,7 +423,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
|||||||
done = True
|
done = True
|
||||||
print("[LAYOUTS] PageWidth/Height-Properties -> {}x{}".format(w, h))
|
print("[LAYOUTS] PageWidth/Height-Properties -> {}x{}".format(w, h))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[LAYOUTS] Property-Setter fehlgeschlagen:", ex)
|
print("[LAYOUTS] Property-Setter failed:", ex)
|
||||||
if not done:
|
if not done:
|
||||||
print("[LAYOUTS] Konnte Seiten-Groesse nicht setzen — bitte ueber Rhinos Layout-Dialog aendern")
|
print("[LAYOUTS] Konnte Seiten-Groesse nicht setzen — bitte ueber Rhinos Layout-Dialog aendern")
|
||||||
try: page.Redraw()
|
try: page.Redraw()
|
||||||
@@ -513,7 +515,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
|||||||
pdf.Write(path)
|
pdf.Write(path)
|
||||||
print("[LAYOUTS] PDF geschrieben: {} ({} Seite(n))".format(path, n_added))
|
print("[LAYOUTS] PDF geschrieben: {} ({} Seite(n))".format(path, n_added))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[LAYOUTS] PDF-Export fehlgeschlagen:", ex)
|
print("[LAYOUTS] PDF-Export failed:", ex)
|
||||||
finally:
|
finally:
|
||||||
# Vorherige View wieder aktivieren
|
# Vorherige View wieder aktivieren
|
||||||
if prev_view is not None:
|
if prev_view is not None:
|
||||||
@@ -536,7 +538,7 @@ class LayoutsBridge(panel_base.BaseBridge):
|
|||||||
if doc.Path:
|
if doc.Path:
|
||||||
base = os.path.splitext(os.path.basename(doc.Path))[0] + "_Layouts"
|
base = os.path.splitext(os.path.basename(doc.Path))[0] + "_Layouts"
|
||||||
dlg.FileName = "{}.pdf".format(base)
|
dlg.FileName = "{}.pdf".format(base)
|
||||||
# Default-Folder — neben der .3dm wenn vorhanden
|
# Default-Folder — neben der .3dm wenn present
|
||||||
if doc.Path:
|
if doc.Path:
|
||||||
try: dlg.Directory = System.Uri(os.path.dirname(doc.Path))
|
try: dlg.Directory = System.Uri(os.path.dirname(doc.Path))
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|||||||
+557
-58
@@ -1,5 +1,7 @@
|
|||||||
#! python3
|
#! python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
library.py — Dossier-Library (Phase A: lokal, read-only)
|
library.py — Dossier-Library (Phase A: lokal, read-only)
|
||||||
|
|
||||||
@@ -148,6 +150,459 @@ def load_manifest():
|
|||||||
return _empty_manifest()
|
return _empty_manifest()
|
||||||
|
|
||||||
|
|
||||||
|
def save_manifest(manifest):
|
||||||
|
"""Schreibt das Manifest zurueck zur library.json. Items werden
|
||||||
|
normalisiert. Returns True/False."""
|
||||||
|
ensure_library()
|
||||||
|
path = os.path.join(library_root(), _MANIFEST_FN)
|
||||||
|
try:
|
||||||
|
if not isinstance(manifest, dict): manifest = _empty_manifest()
|
||||||
|
manifest.setdefault("schemaVersion", _SCHEMA_VERSION)
|
||||||
|
manifest.setdefault("name", "Dossier-Library")
|
||||||
|
items = manifest.get("items") or []
|
||||||
|
manifest["items"] = [_normalize_item(x) for x in items
|
||||||
|
if _normalize_item(x) is not None]
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] save_manifest:", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_item(item_id, patch):
|
||||||
|
"""Patcht ein Item im Manifest. Returns (ok, new_manifest)."""
|
||||||
|
m = load_manifest()
|
||||||
|
items = m.get("items", [])
|
||||||
|
found = False
|
||||||
|
for it in items:
|
||||||
|
if it.get("id") == item_id:
|
||||||
|
for k, v in (patch or {}).items():
|
||||||
|
it[k] = v
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found: return False, m
|
||||||
|
ok = save_manifest(m)
|
||||||
|
return ok, load_manifest()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_item(item_id):
|
||||||
|
"""Loescht ein Item aus dem Manifest. Asset-Files bleiben auf Disk
|
||||||
|
(User koennte sie noch wollen)."""
|
||||||
|
m = load_manifest()
|
||||||
|
items = m.get("items", [])
|
||||||
|
new_items = [it for it in items if it.get("id") != item_id]
|
||||||
|
if len(new_items) == len(items): return False, m
|
||||||
|
m["items"] = new_items
|
||||||
|
ok = save_manifest(m)
|
||||||
|
return ok, load_manifest()
|
||||||
|
|
||||||
|
|
||||||
|
def add_item(item):
|
||||||
|
"""Fuegt ein neues Item zum Manifest hinzu. Returns (ok, new_manifest)."""
|
||||||
|
norm = _normalize_item(item)
|
||||||
|
if norm is None: return False, load_manifest()
|
||||||
|
m = load_manifest()
|
||||||
|
items = m.get("items", [])
|
||||||
|
# Dedupe per id
|
||||||
|
items = [it for it in items if it.get("id") != norm["id"]]
|
||||||
|
items.append(norm)
|
||||||
|
m["items"] = items
|
||||||
|
ok = save_manifest(m)
|
||||||
|
return ok, load_manifest()
|
||||||
|
|
||||||
|
|
||||||
|
def _previews_dir():
|
||||||
|
"""Pfad zum previews/-Subfolder. Wird angelegt falls fehlt."""
|
||||||
|
p = os.path.join(library_root(), "previews")
|
||||||
|
if not os.path.isdir(p):
|
||||||
|
try: os.makedirs(p)
|
||||||
|
except Exception: pass
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _preview_rel_for(asset_rel_or_id):
|
||||||
|
"""Erzeugt einen relativen Preview-Pfad fuer ein Asset (oder Item-ID).
|
||||||
|
Liefert z.B. 'previews/<name>.png'. Wir benutzen den Stamm des
|
||||||
|
.3dm-Files damit Asset + Preview gleichen Namen haben (debuggbar)."""
|
||||||
|
stem = asset_rel_or_id
|
||||||
|
if "/" in stem: stem = stem.split("/")[-1]
|
||||||
|
if "\\" in stem: stem = stem.split("\\")[-1]
|
||||||
|
if stem.lower().endswith(".3dm"): stem = stem[:-4]
|
||||||
|
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in stem)
|
||||||
|
return "previews/" + safe + ".png"
|
||||||
|
|
||||||
|
|
||||||
|
def _capture_thumbnail_of_objects(target_objects, png_abs_path, size=128):
|
||||||
|
"""Captured einen Top-View der gegebenen Objekte als PNG. Hided
|
||||||
|
temporaer alle anderen Objekte damit der Background sauber ist.
|
||||||
|
Returns True/False."""
|
||||||
|
if Rhino is None: return False
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None or not target_objects: return False
|
||||||
|
try:
|
||||||
|
from System.Drawing import Size
|
||||||
|
import scriptcontext as sc
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
# IDs der Ziel-Objekte
|
||||||
|
target_ids = set()
|
||||||
|
for o in target_objects:
|
||||||
|
try:
|
||||||
|
if not o.IsDeleted: target_ids.add(str(o.Id))
|
||||||
|
except Exception: pass
|
||||||
|
if not target_ids: return False
|
||||||
|
# Andere Objekte temporaer ausblenden
|
||||||
|
hidden_by_us = []
|
||||||
|
try:
|
||||||
|
for o in list(doc.Objects):
|
||||||
|
try:
|
||||||
|
if o.IsDeleted: continue
|
||||||
|
if str(o.Id) in target_ids: continue
|
||||||
|
if o.IsHidden: continue
|
||||||
|
if not o.IsNormal: continue # bereits hidden/locked → skip
|
||||||
|
doc.Objects.Hide(o.Id, True)
|
||||||
|
hidden_by_us.append(o.Id)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
capture_ok = False
|
||||||
|
try:
|
||||||
|
view = doc.Views.ActiveView
|
||||||
|
if view is None:
|
||||||
|
return False
|
||||||
|
vp = view.ActiveViewport
|
||||||
|
# Viewport-State sichern damit User nichts verliert
|
||||||
|
saved_target = vp.CameraTarget
|
||||||
|
saved_loc = vp.CameraLocation
|
||||||
|
saved_proj = vp.IsParallelProjection
|
||||||
|
try:
|
||||||
|
# Auf Top-Parallel wechseln + Zoom auf Ziel
|
||||||
|
vp.SetProjection(Rhino.Display.DefinedViewportProjection.Top, "Top", True)
|
||||||
|
try:
|
||||||
|
bbox = Rhino.Geometry.BoundingBox.Empty
|
||||||
|
for o in target_objects:
|
||||||
|
g = o.Geometry
|
||||||
|
if g is None: continue
|
||||||
|
try:
|
||||||
|
bb = g.GetBoundingBox(True)
|
||||||
|
if bb.IsValid: bbox.Union(bb)
|
||||||
|
except Exception: pass
|
||||||
|
if bbox.IsValid:
|
||||||
|
# Etwas Padding
|
||||||
|
bbox.Inflate(bbox.Diagonal.Length * 0.1)
|
||||||
|
vp.ZoomBoundingBox(bbox)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] thumbnail zoom:", ex)
|
||||||
|
view.Redraw()
|
||||||
|
# Capture
|
||||||
|
try:
|
||||||
|
bmp = view.CaptureToBitmap(Size(int(size), int(size)))
|
||||||
|
if bmp is not None:
|
||||||
|
# Sicherstellen dass Verzeichnis da ist
|
||||||
|
try:
|
||||||
|
d = os.path.dirname(png_abs_path)
|
||||||
|
if d and not os.path.isdir(d): os.makedirs(d)
|
||||||
|
except Exception: pass
|
||||||
|
bmp.Save(png_abs_path)
|
||||||
|
capture_ok = True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] thumbnail capture:", ex)
|
||||||
|
finally:
|
||||||
|
# Viewport wiederherstellen
|
||||||
|
try:
|
||||||
|
vp.SetCameraLocation(saved_loc, False)
|
||||||
|
vp.SetCameraTarget(saved_target, True)
|
||||||
|
if saved_proj:
|
||||||
|
vp.IsParallelProjection = True
|
||||||
|
else:
|
||||||
|
vp.IsParallelProjection = False
|
||||||
|
except Exception: pass
|
||||||
|
finally:
|
||||||
|
# Hidden Objekte wieder einblenden
|
||||||
|
for gid in hidden_by_us:
|
||||||
|
try: doc.Objects.Show(gid, True)
|
||||||
|
except Exception: pass
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
return capture_ok
|
||||||
|
|
||||||
|
|
||||||
|
def read_preview_data_uri(rel_path):
|
||||||
|
"""Liest die PNG-Vorschau als data:image/png;base64-URI fuer
|
||||||
|
direktes Einsetzen in <img src='...'>. Liefert None wenn Datei fehlt."""
|
||||||
|
if not rel_path: return None
|
||||||
|
abs_p = os.path.join(library_root(), rel_path)
|
||||||
|
if not os.path.isfile(abs_p): return None
|
||||||
|
try:
|
||||||
|
import base64
|
||||||
|
with open(abs_p, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
return "data:image/png;base64," + base64.b64encode(data).decode("ascii")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] read_preview:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_3dm_via_import(src_path, target_name):
|
||||||
|
"""Konvertiert eine beliebige CAD-Datei (.dwg/.obj/.fbx/.dae/.stl/...)
|
||||||
|
nach .3dm. Strategie: Rhinos _-Import in den aktiven Doc, dann die
|
||||||
|
NEU hinzugekommenen Objekte als File3dm in library/assets/<name>.3dm
|
||||||
|
speichern + aus dem Doc wieder loeschen. Returns relativer Pfad oder
|
||||||
|
None.
|
||||||
|
|
||||||
|
WARNUNG: User-Doc wird kurz veraendert (Objects in/out) — wir
|
||||||
|
delete'n alles wieder am Ende. Setzt einen sticky-Flag damit unsere
|
||||||
|
eigenen Listener nichts cascaden."""
|
||||||
|
if not src_path or not os.path.isfile(src_path): return None
|
||||||
|
if Rhino is None: return None
|
||||||
|
import scriptcontext as sc
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return None
|
||||||
|
ensure_library()
|
||||||
|
assets_dir = os.path.join(library_root(), "assets")
|
||||||
|
if not os.path.isdir(assets_dir):
|
||||||
|
try: os.makedirs(assets_dir)
|
||||||
|
except Exception: pass
|
||||||
|
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in target_name)
|
||||||
|
if not safe.endswith(".3dm"): safe += ".3dm"
|
||||||
|
target = os.path.join(assets_dir, safe)
|
||||||
|
if os.path.isfile(target):
|
||||||
|
stem, ext = os.path.splitext(safe)
|
||||||
|
n = 2
|
||||||
|
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
|
||||||
|
n += 1
|
||||||
|
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
|
||||||
|
# Listener stilllegen: unsere Add-/Delete-Cascade soll bei diesem
|
||||||
|
# temporaeren Import nicht greifen (Objekte haben keine DOSSIER-
|
||||||
|
# UserStrings, kommen aber trotzdem durch unsere Schnellfilter).
|
||||||
|
sc.sticky["dossier_library_import_busy"] = True
|
||||||
|
sc.sticky["dossier_swisstopo_busy"] = True # blockt schon viele Listener
|
||||||
|
# Snapshot der existierenden Object-IDs
|
||||||
|
before_ids = set()
|
||||||
|
try:
|
||||||
|
for o in doc.Objects:
|
||||||
|
try:
|
||||||
|
if not o.IsDeleted: before_ids.add(str(o.Id))
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
# User-Selection sichern damit wir sie am Ende restoren
|
||||||
|
sel_before_ids = []
|
||||||
|
try:
|
||||||
|
for o in doc.Objects.GetSelectedObjects(False, False):
|
||||||
|
sel_before_ids.append(o.Id)
|
||||||
|
except Exception: pass
|
||||||
|
new_objs = []
|
||||||
|
try:
|
||||||
|
try: doc.Objects.UnselectAll()
|
||||||
|
except Exception: pass
|
||||||
|
# _-Import dash-prefix = scripted, kein UI-Dialog. Pfad in Quotes
|
||||||
|
# damit Spaces nicht splitten. _Enter beendet die Optionen.
|
||||||
|
cmd = '_-Import "' + src_path + '" _Enter _Enter'
|
||||||
|
try:
|
||||||
|
Rhino.RhinoApp.RunScript(cmd, False)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] convert_to_3dm RunScript:", ex)
|
||||||
|
# Sammle die NEU hinzugekommenen Objekte
|
||||||
|
try:
|
||||||
|
for o in doc.Objects:
|
||||||
|
try:
|
||||||
|
if o.IsDeleted: continue
|
||||||
|
if str(o.Id) not in before_ids:
|
||||||
|
new_objs.append(o)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
if not new_objs:
|
||||||
|
print("[LIBRARY] convert_to_3dm: Import lieferte keine Objekte")
|
||||||
|
return None
|
||||||
|
# In File3dm packen
|
||||||
|
try:
|
||||||
|
from Rhino.FileIO import File3dm
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
bbox = rg.BoundingBox.Empty
|
||||||
|
geoms_attrs = []
|
||||||
|
for o in new_objs:
|
||||||
|
g = o.Geometry
|
||||||
|
if g is None: continue
|
||||||
|
try:
|
||||||
|
bb = g.GetBoundingBox(True)
|
||||||
|
if bb.IsValid: bbox.Union(bb)
|
||||||
|
except Exception: pass
|
||||||
|
geoms_attrs.append((g, o.Attributes))
|
||||||
|
if bbox.IsValid:
|
||||||
|
offset = rg.Transform.Translation(
|
||||||
|
-bbox.Min.X, -bbox.Min.Y, -bbox.Min.Z)
|
||||||
|
else:
|
||||||
|
offset = rg.Transform.Identity
|
||||||
|
f3 = File3dm()
|
||||||
|
for g, a in geoms_attrs:
|
||||||
|
try:
|
||||||
|
g2 = g.Duplicate()
|
||||||
|
try: g2.Transform(offset)
|
||||||
|
except Exception: pass
|
||||||
|
try: f3.Objects.Add(g2, a)
|
||||||
|
except Exception:
|
||||||
|
if isinstance(g2, rg.Brep): f3.Objects.AddBrep(g2)
|
||||||
|
elif isinstance(g2, rg.Curve): f3.Objects.AddCurve(g2)
|
||||||
|
elif isinstance(g2, rg.Mesh): f3.Objects.AddMesh(g2)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] convert add geom:", ex)
|
||||||
|
try: f3.Write(target, 8)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] convert_to_3dm Write:", ex)
|
||||||
|
return None
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] convert_to_3dm File3dm:", ex)
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
# Thumbnail-Capture BEVOR die Objekte geloescht werden.
|
||||||
|
try:
|
||||||
|
rel_for_preview = os.path.relpath(target, library_root())
|
||||||
|
preview_rel = _preview_rel_for(rel_for_preview)
|
||||||
|
preview_abs = os.path.join(library_root(), preview_rel)
|
||||||
|
_capture_thumbnail_of_objects(new_objs, preview_abs)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] convert thumbnail:", ex)
|
||||||
|
# Cleanup: importierte Objekte wieder loeschen
|
||||||
|
for o in new_objs:
|
||||||
|
try: doc.Objects.Delete(o.Id, True)
|
||||||
|
except Exception: pass
|
||||||
|
# Restore Selection
|
||||||
|
try:
|
||||||
|
for gid in sel_before_ids:
|
||||||
|
try: doc.Objects.Select(gid, True)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
rel = os.path.relpath(target, library_root())
|
||||||
|
print("[LIBRARY] convert_to_3dm OK: {} → {}".format(src_path, rel))
|
||||||
|
return rel
|
||||||
|
finally:
|
||||||
|
sc.sticky["dossier_library_import_busy"] = False
|
||||||
|
sc.sticky["dossier_swisstopo_busy"] = False
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
|
def save_selection_to_asset(doc, target_name):
|
||||||
|
"""Speichert die aktuelle Selection aus dem Doc als eigene .3dm-Datei
|
||||||
|
in library/assets/<target_name>.3dm. Returns relativer Pfad oder None.
|
||||||
|
Geometry wird relativ zum BoundingBox-Min platziert damit der Block-
|
||||||
|
Origin am Ursprung sitzt — sinnvoll fuer Symbol-Insert."""
|
||||||
|
if doc is None or not target_name: return None
|
||||||
|
try:
|
||||||
|
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
except Exception:
|
||||||
|
sel = []
|
||||||
|
if not sel:
|
||||||
|
print("[LIBRARY] save_selection: keine Auswahl")
|
||||||
|
return None
|
||||||
|
if Rhino is None: return None
|
||||||
|
ensure_library()
|
||||||
|
assets_dir = os.path.join(library_root(), "assets")
|
||||||
|
if not os.path.isdir(assets_dir):
|
||||||
|
try: os.makedirs(assets_dir)
|
||||||
|
except Exception: pass
|
||||||
|
# Safe filename
|
||||||
|
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in target_name)
|
||||||
|
if not safe.endswith(".3dm"): safe += ".3dm"
|
||||||
|
target = os.path.join(assets_dir, safe)
|
||||||
|
if os.path.isfile(target):
|
||||||
|
stem, ext = os.path.splitext(safe)
|
||||||
|
n = 2
|
||||||
|
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
|
||||||
|
n += 1
|
||||||
|
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
|
||||||
|
# File3dm aufbauen + Selection rein
|
||||||
|
try:
|
||||||
|
from Rhino.FileIO import File3dm, File3dmWriteOptions
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
# BoundingBox sammeln um geometrie an Ursprung zu verschieben
|
||||||
|
bbox = rg.BoundingBox.Empty
|
||||||
|
geoms = []
|
||||||
|
for o in sel:
|
||||||
|
g = o.Geometry
|
||||||
|
if g is None: continue
|
||||||
|
try:
|
||||||
|
bb = g.GetBoundingBox(True)
|
||||||
|
if bb.IsValid: bbox.Union(bb)
|
||||||
|
except Exception: pass
|
||||||
|
geoms.append((g, o.Attributes))
|
||||||
|
if not geoms:
|
||||||
|
return None
|
||||||
|
if not bbox.IsValid:
|
||||||
|
origin = rg.Point3d(0, 0, 0)
|
||||||
|
else:
|
||||||
|
origin = bbox.Min
|
||||||
|
offset = rg.Transform.Translation(-origin.X, -origin.Y, -origin.Z)
|
||||||
|
f3 = File3dm()
|
||||||
|
for g, a in geoms:
|
||||||
|
try:
|
||||||
|
g2 = g.Duplicate()
|
||||||
|
try: g2.Transform(offset)
|
||||||
|
except Exception: pass
|
||||||
|
# Generischer Add fuer alle GeometryBase
|
||||||
|
try: f3.Objects.Add(g2, a)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: typ-spezifisch
|
||||||
|
if isinstance(g2, rg.Brep): f3.Objects.AddBrep(g2)
|
||||||
|
elif isinstance(g2, rg.Curve): f3.Objects.AddCurve(g2)
|
||||||
|
elif isinstance(g2, rg.Mesh): f3.Objects.AddMesh(g2)
|
||||||
|
elif isinstance(g2, rg.Point): f3.Objects.AddPoint(g2.Location)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] save_selection add:", ex)
|
||||||
|
# Write
|
||||||
|
try:
|
||||||
|
opts = File3dmWriteOptions()
|
||||||
|
opts.Version = 8
|
||||||
|
f3.Write(target, opts)
|
||||||
|
except Exception:
|
||||||
|
try: f3.Write(target, 8)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] save_selection write:", ex)
|
||||||
|
return None
|
||||||
|
rel = os.path.relpath(target, library_root())
|
||||||
|
# Thumbnail aus den (noch selektierten) Objekten capturen
|
||||||
|
try:
|
||||||
|
preview_rel = _preview_rel_for(rel)
|
||||||
|
preview_abs = os.path.join(library_root(), preview_rel)
|
||||||
|
_capture_thumbnail_of_objects(sel, preview_abs)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] save_selection thumbnail:", ex)
|
||||||
|
print("[LIBRARY] save_selection: {} objs → {}".format(len(geoms), rel))
|
||||||
|
return rel
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] save_selection:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def copy_to_assets(src_path, target_name=None):
|
||||||
|
"""Kopiert eine Datei in library/assets/. target_name optional (sonst
|
||||||
|
Original-Name). Returns relativer Pfad ('assets/foo.3dm') oder None."""
|
||||||
|
if not src_path or not os.path.isfile(src_path):
|
||||||
|
return None
|
||||||
|
ensure_library()
|
||||||
|
assets_dir = os.path.join(library_root(), "assets")
|
||||||
|
if not os.path.isdir(assets_dir):
|
||||||
|
try: os.makedirs(assets_dir)
|
||||||
|
except Exception: pass
|
||||||
|
base = target_name or os.path.basename(src_path)
|
||||||
|
target = os.path.join(assets_dir, base)
|
||||||
|
# Konflikt-Resolution: bei doppeltem Namen Nummer dran
|
||||||
|
if os.path.isfile(target):
|
||||||
|
stem, ext = os.path.splitext(base)
|
||||||
|
n = 2
|
||||||
|
while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))):
|
||||||
|
n += 1
|
||||||
|
target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))
|
||||||
|
try:
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(src_path, target)
|
||||||
|
rel = os.path.relpath(target, library_root())
|
||||||
|
return rel
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] copy_to_assets:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _empty_manifest():
|
def _empty_manifest():
|
||||||
return {"schemaVersion": _SCHEMA_VERSION,
|
return {"schemaVersion": _SCHEMA_VERSION,
|
||||||
"name": "Dossier-Library", "items": []}
|
"name": "Dossier-Library", "items": []}
|
||||||
@@ -156,6 +611,14 @@ def _empty_manifest():
|
|||||||
def _normalize_item(it):
|
def _normalize_item(it):
|
||||||
if not isinstance(it, dict): return None
|
if not isinstance(it, dict): return None
|
||||||
if not it.get("id") or not it.get("type"): return None
|
if not it.get("id") or not it.get("type"): return None
|
||||||
|
# 2D/3D-Files: ein Item kann beide haben (Objekt mit Plan-Darstellung 2D
|
||||||
|
# + perspektivischem 3D-Modell) oder nur eines. Legacy 'files'-Feld wird
|
||||||
|
# als files2d interpretiert (Symbole = nur 2D historisch).
|
||||||
|
files_legacy = list(it.get("files") or [])
|
||||||
|
files2d = list(it.get("files2d") or [])
|
||||||
|
files3d = list(it.get("files3d") or [])
|
||||||
|
if not files2d and not files3d and files_legacy:
|
||||||
|
files2d = files_legacy
|
||||||
out = {
|
out = {
|
||||||
"id": str(it["id"]),
|
"id": str(it["id"]),
|
||||||
"type": str(it["type"]),
|
"type": str(it["type"]),
|
||||||
@@ -164,9 +627,10 @@ def _normalize_item(it):
|
|||||||
"tags": list(it.get("tags") or []),
|
"tags": list(it.get("tags") or []),
|
||||||
"preview": it.get("preview"),
|
"preview": it.get("preview"),
|
||||||
"data": it.get("data") or {},
|
"data": it.get("data") or {},
|
||||||
# files: relative Pfade (zur library_root()) auf .3dm-Fragmente.
|
"files2d": files2d,
|
||||||
# Symbol/Object-Import liest die ueber File3dm.Read.
|
"files3d": files3d,
|
||||||
"files": list(it.get("files") or []),
|
# legacy "files" mitgeben fuer Backwards-Kompatibilitaet
|
||||||
|
"files": files_legacy or files2d,
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -200,7 +664,7 @@ def import_material(doc, item):
|
|||||||
"uvScaleM", "textures"):
|
"uvScaleM", "textures"):
|
||||||
if k in data: new_mat[k] = data[k]
|
if k in data: new_mat[k] = data[k]
|
||||||
# Lazy-Import um Zyklen zu vermeiden
|
# Lazy-Import um Zyklen zu vermeiden
|
||||||
import rhinopanel
|
import layers_panel as rhinopanel
|
||||||
settings = rhinopanel.load_project_settings(doc)
|
settings = rhinopanel.load_project_settings(doc)
|
||||||
mats = list(settings.get("materials", []))
|
mats = list(settings.get("materials", []))
|
||||||
for m in mats:
|
for m in mats:
|
||||||
@@ -220,20 +684,21 @@ def _lib_asset_path(rel_path):
|
|||||||
return os.path.join(library_root(), rel_path)
|
return os.path.join(library_root(), rel_path)
|
||||||
|
|
||||||
|
|
||||||
def _block_name_for(item):
|
def _block_name_for(item, variant=""):
|
||||||
"""Stabiler Block-Name fuer InstanceDefinition. Format:
|
"""Stabiler Block-Name fuer InstanceDefinition. Format:
|
||||||
'dossier_lib_<libraryId>' — Dedup ueber Lib-ID, nicht Item-Name (sonst
|
'dossier_lib_<libraryId>[_<variant>]'. variant '2d'/'3d' fuer Pair-Items."""
|
||||||
Konflikt bei Umbenennen)."""
|
|
||||||
lid = item.get("id") or ""
|
lid = item.get("id") or ""
|
||||||
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in lid)
|
safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in lid)
|
||||||
return "dossier_lib_" + safe
|
name = "dossier_lib_" + safe
|
||||||
|
if variant: name += "_" + variant
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
def _read_3dm_geometry(abs_path):
|
def _read_3dm_geometry(abs_path):
|
||||||
"""Liest alle Top-Level-Objekte aus einer .3dm-Datei. Returns Liste von
|
"""Liest alle Top-Level-Objekte aus einer .3dm-Datei. Returns Liste von
|
||||||
(GeometryBase, ObjectAttributes). Bei Fehler leere Liste."""
|
(GeometryBase, ObjectAttributes). Bei Fehler leere Liste."""
|
||||||
if not abs_path or not os.path.isfile(abs_path):
|
if not abs_path or not os.path.isfile(abs_path):
|
||||||
print("[LIBRARY] _read_3dm: Datei nicht gefunden:", abs_path)
|
print("[LIBRARY] _read_3dm: Datei not found:", abs_path)
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
from Rhino.FileIO import File3dm
|
from Rhino.FileIO import File3dm
|
||||||
@@ -256,24 +721,24 @@ def _read_3dm_geometry(abs_path):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _ensure_block_definition(doc, item, geometry_attrs):
|
def _ensure_block_definition(doc, item, geometry_attrs, variant=""):
|
||||||
"""Erstellt InstanceDefinition fuer dieses Library-Item wenn noch nicht
|
"""Erstellt InstanceDefinition fuer dieses Library-Item wenn noch nicht
|
||||||
da. Returns (idx, was_created). idx<0 bei Fehler."""
|
da. variant unterscheidet '2d'/'3d'. Returns (idx, was_created)."""
|
||||||
if Rhino is None: return -1, False
|
if Rhino is None: return -1, False
|
||||||
name = _block_name_for(item)
|
name = _block_name_for(item, variant=variant)
|
||||||
try:
|
try:
|
||||||
existing = doc.InstanceDefinitions.Find(name)
|
existing = doc.InstanceDefinitions.Find(name)
|
||||||
except Exception:
|
except Exception:
|
||||||
existing = None
|
existing = None
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
return existing.Index, False
|
return existing.Index, False
|
||||||
# Geometry + Attributes separat sammeln
|
|
||||||
geoms = [g for g, _ in geometry_attrs if g is not None]
|
geoms = [g for g, _ in geometry_attrs if g is not None]
|
||||||
attrs = [a for _, a in geometry_attrs]
|
attrs = [a for _, a in geometry_attrs]
|
||||||
if not geoms:
|
if not geoms:
|
||||||
return -1, False
|
return -1, False
|
||||||
base_pt = Rhino.Geometry.Point3d(0, 0, 0)
|
base_pt = Rhino.Geometry.Point3d(0, 0, 0)
|
||||||
desc = "Dossier-Library: " + (item.get("name") or "")
|
desc_suffix = " ({})".format(variant.upper()) if variant else ""
|
||||||
|
desc = "Dossier-Library: " + (item.get("name") or "") + desc_suffix
|
||||||
try:
|
try:
|
||||||
idx = doc.InstanceDefinitions.Add(name, desc, base_pt, geoms, attrs)
|
idx = doc.InstanceDefinitions.Add(name, desc, base_pt, geoms, attrs)
|
||||||
return idx, True
|
return idx, True
|
||||||
@@ -282,52 +747,83 @@ def _ensure_block_definition(doc, item, geometry_attrs):
|
|||||||
return -1, False
|
return -1, False
|
||||||
|
|
||||||
|
|
||||||
def import_symbol(doc, item):
|
def _build_variant_block(doc, item, variant_files, variant_label):
|
||||||
"""Importiert ein Symbol-Item (= 2D-Block) in das Doc. Liest die
|
"""Liest .3dm-Files und legt eine InstanceDefinition (variant) an.
|
||||||
.3dm-Datei(en) aus item.files, erstellt eine InstanceDefinition mit
|
Returns Block-Index oder -1."""
|
||||||
stabilem Namen, fuegt eine Instanz am Ursprung ein.
|
if not variant_files: return -1
|
||||||
Returns (ok, message)."""
|
|
||||||
return _import_block_like(doc, item, kind="symbol")
|
|
||||||
|
|
||||||
|
|
||||||
def import_object(doc, item):
|
|
||||||
"""Importiert ein Object-Item (= 3D-Block, BIM-Element) in das Doc.
|
|
||||||
Gleiche Pipeline wie import_symbol — Symbol/Object unterscheiden sich
|
|
||||||
nur in der UI-Kategorisierung."""
|
|
||||||
return _import_block_like(doc, item, kind="object")
|
|
||||||
|
|
||||||
|
|
||||||
def _import_block_like(doc, item, kind):
|
|
||||||
if doc is None: return False, "Kein aktives Dokument"
|
|
||||||
files = item.get("files") or []
|
|
||||||
if not files:
|
|
||||||
return False, "Item hat keine .3dm-Files: " + str(item.get("id"))
|
|
||||||
# Alle Files zusammen in eine Block-Definition packen.
|
|
||||||
all_geom = []
|
all_geom = []
|
||||||
for f in files:
|
for f in variant_files:
|
||||||
abs_p = _lib_asset_path(f)
|
abs_p = _lib_asset_path(f)
|
||||||
all_geom.extend(_read_3dm_geometry(abs_p))
|
all_geom.extend(_read_3dm_geometry(abs_p))
|
||||||
if not all_geom:
|
if not all_geom:
|
||||||
return False, "Keine importierbare Geometrie in {}".format(files)
|
return -1
|
||||||
idx, created = _ensure_block_definition(doc, item, all_geom)
|
idx, _ = _ensure_block_definition(doc, item, all_geom, variant=variant_label)
|
||||||
if idx < 0:
|
return idx
|
||||||
return False, "InstanceDefinition konnte nicht erstellt werden"
|
|
||||||
# Instanz am Ursprung einfuegen — User kann danach verschieben.
|
|
||||||
|
def _place_instance(doc, block_idx, point, layer_idx=-1):
|
||||||
|
"""Platziert eine InstanceObject am gegebenen Punkt, optional auf
|
||||||
|
spezifischem Layer. Returns Guid oder None."""
|
||||||
|
if block_idx < 0: return None
|
||||||
try:
|
try:
|
||||||
xform = Rhino.Geometry.Transform.Identity
|
xform = Rhino.Geometry.Transform.Translation(
|
||||||
inst_id = doc.Objects.AddInstanceObject(idx, xform)
|
point.X, point.Y, point.Z)
|
||||||
if inst_id == System_Guid_Empty():
|
attrs = Rhino.DocObjects.ObjectAttributes()
|
||||||
return False, "AddInstanceObject fehlgeschlagen"
|
if layer_idx >= 0:
|
||||||
|
attrs.LayerIndex = layer_idx
|
||||||
|
gid = doc.Objects.AddInstanceObject(block_idx, xform, attrs)
|
||||||
|
return gid
|
||||||
|
except Exception as ex:
|
||||||
|
print("[LIBRARY] _place_instance:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def import_symbol(doc, item, at_point=None, layer2d=-1, layer3d=-1):
|
||||||
|
return _import_block_like(doc, item, at_point=at_point,
|
||||||
|
layer2d=layer2d, layer3d=layer3d)
|
||||||
|
|
||||||
|
|
||||||
|
def import_object(doc, item, at_point=None, layer2d=-1, layer3d=-1):
|
||||||
|
return _import_block_like(doc, item, at_point=at_point,
|
||||||
|
layer2d=layer2d, layer3d=layer3d)
|
||||||
|
|
||||||
|
|
||||||
|
def _import_block_like(doc, item, at_point=None, layer2d=-1, layer3d=-1):
|
||||||
|
"""Platziert ein Library-Item im Doc. Item kann files2d und/oder files3d
|
||||||
|
haben → beide Varianten werden geladen + an gleichem Punkt platziert auf
|
||||||
|
ihren respektiven Layern.
|
||||||
|
at_point: Point3d (default = Origin).
|
||||||
|
layer2d/3d: optionale Layer-Indizes (default = aktiver Layer)."""
|
||||||
|
if doc is None: return False, "Kein aktives Dokument"
|
||||||
|
files2d = item.get("files2d") or []
|
||||||
|
files3d = item.get("files3d") or []
|
||||||
|
if not files2d and not files3d:
|
||||||
|
# Legacy fallback
|
||||||
|
legacy = item.get("files") or []
|
||||||
|
if legacy: files2d = legacy
|
||||||
|
if not files2d and not files3d:
|
||||||
|
return False, "Item hat keine .3dm-Files: " + str(item.get("id"))
|
||||||
|
if at_point is None:
|
||||||
|
at_point = Rhino.Geometry.Point3d(0, 0, 0)
|
||||||
|
placed = []
|
||||||
|
if files2d:
|
||||||
|
idx2 = _build_variant_block(doc, item, files2d, "2d")
|
||||||
|
if idx2 >= 0:
|
||||||
|
gid = _place_instance(doc, idx2, at_point, layer_idx=layer2d)
|
||||||
|
if gid is not None and gid != System_Guid_Empty():
|
||||||
|
placed.append("2D")
|
||||||
|
if files3d:
|
||||||
|
idx3 = _build_variant_block(doc, item, files3d, "3d")
|
||||||
|
if idx3 >= 0:
|
||||||
|
gid = _place_instance(doc, idx3, at_point, layer_idx=layer3d)
|
||||||
|
if gid is not None and gid != System_Guid_Empty():
|
||||||
|
placed.append("3D")
|
||||||
|
if not placed:
|
||||||
|
return False, "Konnte keinen Block platzieren (Files fehlen?)"
|
||||||
try: doc.Views.Redraw()
|
try: doc.Views.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
msg = ("Block '{}' importiert + am Ursprung eingefuegt".format(
|
return True, "{} eingefuegt ({})".format(
|
||||||
item.get("name") or "")
|
item.get("name") or "", "+".join(placed))
|
||||||
if created else
|
|
||||||
"Block bereits vorhanden — neue Instanz eingefuegt")
|
|
||||||
return True, msg
|
|
||||||
except Exception as ex:
|
|
||||||
print("[LIBRARY] AddInstanceObject:", ex)
|
|
||||||
return False, str(ex)
|
|
||||||
|
|
||||||
|
|
||||||
def System_Guid_Empty():
|
def System_Guid_Empty():
|
||||||
@@ -339,16 +835,19 @@ def System_Guid_Empty():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def import_item(doc, item_id):
|
def import_item(doc, item_id, at_point=None, layer2d=-1, layer3d=-1):
|
||||||
"""Type-dispatching Import. material → Project-Settings-Liste.
|
"""Type-dispatching Import. material → Project-Settings-Liste.
|
||||||
symbol/object → InstanceDefinition im Doc via File3dm.Read."""
|
symbol/object → InstanceDefinition im Doc via File3dm.Read.
|
||||||
|
at_point + layer2d/3d nur fuer symbol/object."""
|
||||||
item = find_item(item_id)
|
item = find_item(item_id)
|
||||||
if item is None: return False, "Item nicht gefunden: " + str(item_id)
|
if item is None: return False, "Item not found: " + str(item_id)
|
||||||
t = item.get("type")
|
t = item.get("type")
|
||||||
if t == "material":
|
if t == "material":
|
||||||
return import_material(doc, item)
|
return import_material(doc, item)
|
||||||
if t == "symbol":
|
if t == "symbol":
|
||||||
return import_symbol(doc, item)
|
return import_symbol(doc, item, at_point=at_point,
|
||||||
|
layer2d=layer2d, layer3d=layer3d)
|
||||||
if t == "object":
|
if t == "object":
|
||||||
return import_object(doc, item)
|
return import_object(doc, item, at_point=at_point,
|
||||||
|
layer2d=layer2d, layer3d=layer3d)
|
||||||
return False, "Unbekannter Typ: '{}'".format(t)
|
return False, "Unbekannter Typ: '{}'".format(t)
|
||||||
|
|||||||
+5
-3
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
mass_style.py
|
mass_style.py
|
||||||
Globale Mass-Stil-Presets fuer Dossier — speichert pro Dokument benannte
|
Globale Mass-Stil-Presets fuer Dossier — speichert pro Dokument benannte
|
||||||
@@ -12,7 +14,7 @@ Persistiert als JSON in doc.Strings["dossier_mass_styles"] (Liste) und
|
|||||||
doc.Strings["dossier_mass_style_active"] (aktive ID).
|
doc.Strings["dossier_mass_style_active"] (aktive ID).
|
||||||
|
|
||||||
Ein Mass-Style wird als globale Vorgabe gelesen. Per-Element-Override
|
Ein Mass-Style wird als globale Vorgabe gelesen. Per-Element-Override
|
||||||
(z.B. raum_rundung UserString am einzelnen Raum) hat Vorrang wenn gesetzt.
|
(z.B. raum_rundung UserString am einzelnen Raum) hat Vorrang wenn set.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
@@ -86,7 +88,7 @@ def list_presets(doc):
|
|||||||
# Erst-Initialisierung: Default-Liste schreiben
|
# Erst-Initialisierung: Default-Liste schreiben
|
||||||
items = [_normalize(p) for p in _DEFAULT_PRESETS]
|
items = [_normalize(p) for p in _DEFAULT_PRESETS]
|
||||||
_save_all(doc, items)
|
_save_all(doc, items)
|
||||||
# Default-Aktiv setzen falls noch nichts gesetzt
|
# Default-Aktiv setzen falls noch nichts set
|
||||||
if not doc.Strings.GetValue(_KEY_ACTIVE):
|
if not doc.Strings.GetValue(_KEY_ACTIVE):
|
||||||
doc.Strings.SetString(_KEY_ACTIVE, items[0]["id"])
|
doc.Strings.SetString(_KEY_ACTIVE, items[0]["id"])
|
||||||
return items
|
return items
|
||||||
@@ -173,7 +175,7 @@ def delete_preset(doc, preset_id):
|
|||||||
|
|
||||||
def raum_rundung_default(doc):
|
def raum_rundung_default(doc):
|
||||||
"""Default-Rundung fuer Raum-Stempel wenn keine per-Raum-Override
|
"""Default-Rundung fuer Raum-Stempel wenn keine per-Raum-Override
|
||||||
gesetzt ist."""
|
set ist."""
|
||||||
p = get_active(doc)
|
p = get_active(doc)
|
||||||
return p["raumRundung"] if p else "0.1"
|
return p["raumRundung"] if p else "0.1"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
masse_settings.py
|
masse_settings.py
|
||||||
Satellite-Fenster fuer das Bearbeiten der Masse-Presets
|
Satellite-Fenster fuer das Bearbeiten der Masse-Presets
|
||||||
|
|||||||
+63
-52
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
massstab.py
|
massstab.py
|
||||||
MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports.
|
MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports.
|
||||||
@@ -32,11 +34,11 @@ PANEL_GUID_STR = "5c8e4f3f-6d0e-4f1a-a3d4-e5f607182941"
|
|||||||
# Wir legen sie in einer Config-Datei im Home des Users ab.
|
# Wir legen sie in einer Config-Datei im Home des Users ab.
|
||||||
_DOC_DPI_KEY = "dossier_dpi" # Legacy, fuer Migration
|
_DOC_DPI_KEY = "dossier_dpi" # Legacy, fuer Migration
|
||||||
|
|
||||||
# Pro Viewport-Name der zuletzt vom User explizit gesetzte Massstab
|
# Pro Viewport-Name der zuletzt vom User explizit sete Massstab
|
||||||
# (Dropdown/Input/100%-Button/Ausschnitt-Restore). NICHT der Live-Zoom — der
|
# (Dropdown/Input/100%-Button/Ausschnitt-Restore). NICHT der Live-Zoom — der
|
||||||
# drifted bei Pan/Zoom. Wird von Ausschnitten beim Speichern als "der
|
# drifted bei Pan/Zoom. Wird von Ausschnitten beim Speichern als "der
|
||||||
# eingestellte Massstab" gelesen. Per-doc persistiert in doc.Strings als
|
# eingestellte Massstab" gelesen. Per-doc persistiert in doc.Strings als
|
||||||
# JSON-Dict, damit ein Wechsel zurueck auf einen frueher gesetzten Viewport
|
# JSON-Dict, damit ein Wechsel zurueck auf einen frueher seten Viewport
|
||||||
# den korrekten Wert wieder rausgibt — auch nach Restart.
|
# den korrekten Wert wieder rausgibt — auch nach Restart.
|
||||||
_user_set_scales = {} # {viewport_name: float}
|
_user_set_scales = {} # {viewport_name: float}
|
||||||
_user_set_scales_loaded = False # lazy load aus doc.Strings beim ersten Zugriff
|
_user_set_scales_loaded = False # lazy load aus doc.Strings beim ersten Zugriff
|
||||||
@@ -102,7 +104,7 @@ def _detect_dpi():
|
|||||||
try:
|
try:
|
||||||
from System.Diagnostics import Process, ProcessStartInfo
|
from System.Diagnostics import Process, ProcessStartInfo
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] auto-detect: .NET Process nicht verfuegbar:", ex)
|
print("[SCALE] auto-detect: .NET Process not available:", ex)
|
||||||
return None
|
return None
|
||||||
if not os.path.isfile("/usr/bin/osascript"):
|
if not os.path.isfile("/usr/bin/osascript"):
|
||||||
# Vermutlich nicht macOS -> nichts zu detecten
|
# Vermutlich nicht macOS -> nichts zu detecten
|
||||||
@@ -135,10 +137,10 @@ def _detect_dpi():
|
|||||||
if not finished:
|
if not finished:
|
||||||
try: p.Kill()
|
try: p.Kill()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
print("[MASSSTAB] auto-detect: osascript timeout")
|
print("[SCALE] auto-detect: osascript timeout")
|
||||||
return None
|
return None
|
||||||
if p.ExitCode != 0:
|
if p.ExitCode != 0:
|
||||||
print("[MASSSTAB] auto-detect osascript ExitCode={}:".format(p.ExitCode), err)
|
print("[SCALE] auto-detect osascript ExitCode={}:".format(p.ExitCode), err)
|
||||||
return None
|
return None
|
||||||
import json as _json
|
import json as _json
|
||||||
data = _json.loads((out or "").strip())
|
data = _json.loads((out or "").strip())
|
||||||
@@ -152,15 +154,15 @@ def _detect_dpi():
|
|||||||
return None
|
return None
|
||||||
dpi = px * 25.4 / mm
|
dpi = px * 25.4 / mm
|
||||||
if dpi < 30.0 or dpi > 600.0:
|
if dpi < 30.0 or dpi > 600.0:
|
||||||
print("[MASSSTAB] auto-detect: DPI {:.1f} ausserhalb 30..600 -> ignoriert".format(dpi))
|
print("[SCALE] auto-detect: DPI {:.1f} ausserhalb 30..600 -> ignoriert".format(dpi))
|
||||||
return None
|
return None
|
||||||
print("[MASSSTAB] DPI auto-detected: {:.1f} physisch (Bildschirm {:.0f}x{:.0f}px / {:.1f}x{:.1f}mm, logisch {:.0f}x{:.0f})".format(
|
print("[SCALE] DPI auto-detected: {:.1f} physical (screen {:.0f}x{:.0f}px / ... logical {:.0f}x{:.0f})".format(
|
||||||
dpi, px, float(data.get("py") or 0),
|
dpi, px, float(data.get("py") or 0),
|
||||||
mm, float(data.get("mh") or 0),
|
mm, float(data.get("mh") or 0),
|
||||||
lpx, float(data.get("lpy") or 0)))
|
lpx, float(data.get("lpy") or 0)))
|
||||||
return dpi
|
return dpi
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] auto-detect fehlgeschlagen:", ex)
|
print("[SCALE] auto-detect failed:", ex)
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
if script_path:
|
if script_path:
|
||||||
@@ -186,7 +188,7 @@ def _read_config():
|
|||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
cfg = data
|
cfg = data
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] config lesen:", ex)
|
print("[SCALE] config lesen:", ex)
|
||||||
_config_cache = cfg
|
_config_cache = cfg
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
@@ -202,7 +204,7 @@ def _write_config(cfg):
|
|||||||
_config_cache = cfg # Cache mit dem geschriebenen Stand aktualisieren
|
_config_cache = cfg # Cache mit dem geschriebenen Stand aktualisieren
|
||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] config schreiben:", ex)
|
print("[SCALE] config schreiben:", ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -264,7 +266,7 @@ def _set_dpi(doc, value, source="manual"):
|
|||||||
cfg["dpi_source"] = source
|
cfg["dpi_source"] = source
|
||||||
if not _write_config(cfg):
|
if not _write_config(cfg):
|
||||||
return False
|
return False
|
||||||
print("[MASSSTAB] DPI={:.1f} ({}) -> {}".format(v, source, _CONFIG_PATH))
|
print("[SCALE] DPI={:.1f} ({}) -> {}".format(v, source, _CONFIG_PATH))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -346,7 +348,7 @@ def _compute_scale(doc, vp):
|
|||||||
pass
|
pass
|
||||||
# appliedScale pro Viewport. Map ist gefuettert durch _apply_scale und
|
# appliedScale pro Viewport. Map ist gefuettert durch _apply_scale und
|
||||||
# Ausschnitt-Restore — wenn ein anderer Viewport aktiv ist als beim letzten
|
# Ausschnitt-Restore — wenn ein anderer Viewport aktiv ist als beim letzten
|
||||||
# Setzen, kommt entweder dessen frueher gesetzter Wert oder None zurueck.
|
# Setzen, kommt entweder dessen frueher seter Wert oder None zurueck.
|
||||||
# Niemals auf die Live-Skala mappen — das Dropdown soll STATISCH sein.
|
# Niemals auf die Live-Skala mappen — das Dropdown soll STATISCH sein.
|
||||||
# Wichtig: nur bei Parallelprojektion zurueckgeben. In Perspective ist ein
|
# Wichtig: nur bei Parallelprojektion zurueckgeben. In Perspective ist ein
|
||||||
# Massstab konzeptionell unsinnig — selbst wenn der gleiche Viewport vorher
|
# Massstab konzeptionell unsinnig — selbst wenn der gleiche Viewport vorher
|
||||||
@@ -419,9 +421,9 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
|
|||||||
layer.PlotWeight = new
|
layer.PlotWeight = new
|
||||||
n_layer += 1
|
n_layer += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] LW scale layer '{}': {}".format(layer.Name, ex))
|
print("[SCALE] LW scale layer '{}': {}".format(layer.Name, ex))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] LW scale layers:", ex)
|
print("[SCALE] LW scale layers:", ex)
|
||||||
|
|
||||||
# -- Objekte -------------------------------------------------------------
|
# -- Objekte -------------------------------------------------------------
|
||||||
try:
|
try:
|
||||||
@@ -449,11 +451,11 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] LW scale objects:", ex)
|
print("[SCALE] LW scale objects:", ex)
|
||||||
|
|
||||||
try: doc.Views.Redraw()
|
try: doc.Views.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
print("[MASSSTAB] PlotWeight-Skalierung x{:.1f}: {} Layer, {} Objekte angepasst".format(
|
print("[SCALE] PlotWeight-Skalierung x{:.1f}: {} Layer, {} Objekte angepasst".format(
|
||||||
factor, n_layer, n_obj))
|
factor, n_layer, n_obj))
|
||||||
# Diagnose: zeige die ersten paar Layer mit ihren echten PlotWeights
|
# Diagnose: zeige die ersten paar Layer mit ihren echten PlotWeights
|
||||||
try:
|
try:
|
||||||
@@ -461,7 +463,7 @@ def _apply_scaled_lineweights(doc, enabled, scale_n):
|
|||||||
for layer in doc.Layers:
|
for layer in doc.Layers:
|
||||||
if layer.IsDeleted or not layer.PlotWeight: continue
|
if layer.IsDeleted or not layer.PlotWeight: continue
|
||||||
stored = layer.GetUserString(_LW_ORIG_KEY) or "-"
|
stored = layer.GetUserString(_LW_ORIG_KEY) or "-"
|
||||||
print("[MASSSTAB] Layer '{}' PlotWeight={:.3f}mm (orig={})".format(
|
print("[SCALE] Layer '{}' PlotWeight={:.3f}mm (orig={})".format(
|
||||||
layer.Name, float(layer.PlotWeight), stored))
|
layer.Name, float(layer.PlotWeight), stored))
|
||||||
shown += 1
|
shown += 1
|
||||||
if shown >= 5: break
|
if shown >= 5: break
|
||||||
@@ -477,7 +479,7 @@ def write_plotweight(doc, target, value):
|
|||||||
Print-Mode-aware. value = "echter" Wert in mm wie er auf Papier landet.
|
Print-Mode-aware. value = "echter" Wert in mm wie er auf Papier landet.
|
||||||
|
|
||||||
Speichert value als Original-UserString. Wenn Print-Mode aktiv ist wird
|
Speichert value als Original-UserString. Wenn Print-Mode aktiv ist wird
|
||||||
PlotWeight = value * scale gesetzt damit die Anzeige direkt skaliert.
|
PlotWeight = value * scale set damit die Anzeige direkt skaliert.
|
||||||
|
|
||||||
Aufrufer ist verantwortlich fuer ModifyAttributes / Doc-Refresh."""
|
Aufrufer ist verantwortlich fuer ModifyAttributes / Doc-Refresh."""
|
||||||
if target is None: return
|
if target is None: return
|
||||||
@@ -497,7 +499,7 @@ def write_plotweight(doc, target, value):
|
|||||||
try:
|
try:
|
||||||
target.PlotWeight = v * factor
|
target.PlotWeight = v * factor
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] write_plotweight set:", ex)
|
print("[SCALE] write_plotweight set:", ex)
|
||||||
|
|
||||||
|
|
||||||
def apply_scaled_hatches(doc, scale_n):
|
def apply_scaled_hatches(doc, scale_n):
|
||||||
@@ -570,11 +572,11 @@ def apply_scaled_hatches(doc, scale_n):
|
|||||||
if doc.Objects.Replace(hid, new_g):
|
if doc.Objects.Replace(hid, new_g):
|
||||||
n_scaled += 1
|
n_scaled += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] hatch set PatternScale:", ex)
|
print("[SCALE] hatch set PatternScale:", ex)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] hatch iter:", ex)
|
print("[SCALE] hatch iter:", ex)
|
||||||
if n_scaled or hatch_ids:
|
if n_scaled or hatch_ids:
|
||||||
print("[MASSSTAB] Hatch-Skalierung: {} gefunden, {} mit Faktor x{:.2f} angepasst".format(
|
print("[SCALE] Hatch-Skalierung: {} gefunden, {} mit Faktor x{:.2f} angepasst".format(
|
||||||
len(hatch_ids), n_scaled, factor))
|
len(hatch_ids), n_scaled, factor))
|
||||||
try: doc.Views.Redraw()
|
try: doc.Views.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
@@ -597,7 +599,7 @@ def post_create_hatch_scale(doc, hatch_obj, user_scale):
|
|||||||
a.SetUserString(_HATCH_ORIG_KEY, "{:.6f}".format(u))
|
a.SetUserString(_HATCH_ORIG_KEY, "{:.6f}".format(u))
|
||||||
doc.Objects.ModifyAttributes(hatch_obj, a, True)
|
doc.Objects.ModifyAttributes(hatch_obj, a, True)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] post_create_hatch_scale orig:", ex)
|
print("[SCALE] post_create_hatch_scale orig:", ex)
|
||||||
# Mit aktuellem Massstab skalieren (sqrt-Formel /10, siehe apply_scaled_hatches)
|
# Mit aktuellem Massstab skalieren (sqrt-Formel /10, siehe apply_scaled_hatches)
|
||||||
scale_n = _read_user_scale(doc, default=1.0)
|
scale_n = _read_user_scale(doc, default=1.0)
|
||||||
if not scale_n or scale_n <= 0: scale_n = 1.0
|
if not scale_n or scale_n <= 0: scale_n = 1.0
|
||||||
@@ -610,7 +612,7 @@ def post_create_hatch_scale(doc, hatch_obj, user_scale):
|
|||||||
new_g.PatternScale = u * factor
|
new_g.PatternScale = u * factor
|
||||||
doc.Objects.Replace(h2.Id, new_g)
|
doc.Objects.Replace(h2.Id, new_g)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] post_create_hatch_scale rescale:", ex)
|
print("[SCALE] post_create_hatch_scale rescale:", ex)
|
||||||
|
|
||||||
|
|
||||||
def read_plotweight(target):
|
def read_plotweight(target):
|
||||||
@@ -654,7 +656,7 @@ def _set_lineweights_enabled(doc, enabled):
|
|||||||
try:
|
try:
|
||||||
doc.Strings.SetString(_LW_KEY, flag)
|
doc.Strings.SetString(_LW_KEY, flag)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] _set_lineweights_enabled persist:", ex)
|
print("[SCALE] _set_lineweights_enabled persist:", ex)
|
||||||
# Print-Display togglen — primaerer Befehl auf Mac Rhino
|
# Print-Display togglen — primaerer Befehl auf Mac Rhino
|
||||||
on_off = "_On" if enabled else "_Off"
|
on_off = "_On" if enabled else "_Off"
|
||||||
yes_no = "_Yes" if enabled else "_No"
|
yes_no = "_Yes" if enabled else "_No"
|
||||||
@@ -673,17 +675,17 @@ def _set_lineweights_enabled(doc, enabled):
|
|||||||
scale_n = _read_user_scale(doc, default=1.0)
|
scale_n = _read_user_scale(doc, default=1.0)
|
||||||
_apply_scaled_lineweights(doc, enabled, scale_n)
|
_apply_scaled_lineweights(doc, enabled, scale_n)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] PlotWeight-Scale:", ex)
|
print("[SCALE] PlotWeight-Scale:", ex)
|
||||||
try:
|
try:
|
||||||
for v in doc.Views: v.Redraw()
|
for v in doc.Views: v.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
print("[MASSSTAB] Print-Display:", "AN (Strichstaerken sichtbar)" if enabled else "AUS")
|
print("[SCALE] Print-Display:", "AN (Strichstaerken sichtbar)" if enabled else "AUS")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _read_user_scale(doc, default=1.0):
|
def _read_user_scale(doc, default=1.0):
|
||||||
"""Persistierter eingestellter Massstab oder default. Setze default=None
|
"""Persistierter eingestellter Massstab oder default. Setze default=None
|
||||||
um "nie gesetzt" zu erkennen."""
|
um "nie set" zu erkennen."""
|
||||||
if doc is None: return default
|
if doc is None: return default
|
||||||
try:
|
try:
|
||||||
raw = doc.Strings.GetValue(_DOC_USER_SCALE_KEY)
|
raw = doc.Strings.GetValue(_DOC_USER_SCALE_KEY)
|
||||||
@@ -700,7 +702,7 @@ def _write_user_scale(doc, ratio):
|
|||||||
try:
|
try:
|
||||||
doc.Strings.SetString(_DOC_USER_SCALE_KEY, "{:.6f}".format(float(ratio)))
|
doc.Strings.SetString(_DOC_USER_SCALE_KEY, "{:.6f}".format(float(ratio)))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] _write_user_scale:", ex)
|
print("[SCALE] _write_user_scale:", ex)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_user_scales_loaded(doc):
|
def _ensure_user_scales_loaded(doc):
|
||||||
@@ -721,7 +723,7 @@ def _ensure_user_scales_loaded(doc):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] _ensure_user_scales_loaded:", ex)
|
print("[SCALE] _ensure_user_scales_loaded:", ex)
|
||||||
_user_set_scales_loaded = True
|
_user_set_scales_loaded = True
|
||||||
|
|
||||||
|
|
||||||
@@ -731,7 +733,7 @@ def _write_user_scales(doc):
|
|||||||
doc.Strings.SetString(_DOC_USER_SCALES_KEY,
|
doc.Strings.SetString(_DOC_USER_SCALES_KEY,
|
||||||
json.dumps(_user_set_scales, ensure_ascii=False))
|
json.dumps(_user_set_scales, ensure_ascii=False))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] _write_user_scales:", ex)
|
print("[SCALE] _write_user_scales:", ex)
|
||||||
|
|
||||||
|
|
||||||
def _get_applied_scale_for_vp(doc, vp_name):
|
def _get_applied_scale_for_vp(doc, vp_name):
|
||||||
@@ -776,7 +778,7 @@ def _rescale_doc_patterns(doc, factor):
|
|||||||
doc.Objects.Replace(obj.Id, g2)
|
doc.Objects.Replace(obj.Id, g2)
|
||||||
n_h += 1
|
n_h += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] hatch rescale:", ex)
|
print("[SCALE] hatch rescale:", ex)
|
||||||
# Per-Objekt Linetype-Scale (Rhino 8 Attribut)
|
# Per-Objekt Linetype-Scale (Rhino 8 Attribut)
|
||||||
try:
|
try:
|
||||||
a = obj.Attributes
|
a = obj.Attributes
|
||||||
@@ -784,7 +786,7 @@ def _rescale_doc_patterns(doc, factor):
|
|||||||
if hasattr(a, prop):
|
if hasattr(a, prop):
|
||||||
cur = getattr(a, prop)
|
cur = getattr(a, prop)
|
||||||
if cur and cur > 0 and abs(cur - 1.0) > 1e-9:
|
if cur and cur > 0 and abs(cur - 1.0) > 1e-9:
|
||||||
# Nur Objekte mit explizit gesetzter Skala anfassen
|
# Nur Objekte mit explizit seter Skala anfassen
|
||||||
# (Default=1.0 ueberlassen wir dem globalen Multiplikator).
|
# (Default=1.0 ueberlassen wir dem globalen Multiplikator).
|
||||||
new_a = a.Duplicate()
|
new_a = a.Duplicate()
|
||||||
setattr(new_a, prop, cur * factor)
|
setattr(new_a, prop, cur * factor)
|
||||||
@@ -794,7 +796,7 @@ def _rescale_doc_patterns(doc, factor):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] _rescale_doc_patterns:", ex)
|
print("[SCALE] _rescale_doc_patterns:", ex)
|
||||||
|
|
||||||
# Globale Linetype-Pattern-Length-Skala (Rhino-doc-Setting) versuchen.
|
# Globale Linetype-Pattern-Length-Skala (Rhino-doc-Setting) versuchen.
|
||||||
# Property-Namen variieren je nach Version — wir probieren.
|
# Property-Namen variieren je nach Version — wir probieren.
|
||||||
@@ -813,7 +815,7 @@ def _rescale_doc_patterns(doc, factor):
|
|||||||
pass
|
pass
|
||||||
try: doc.Views.Redraw()
|
try: doc.Views.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
print("[MASSSTAB] Rescale x{:.4f}: {} Hatches, {} per-obj Linetypes{}".format(
|
print("[SCALE] Rescale x{:.4f}: {} Hatches, {} per-obj Linetypes{}".format(
|
||||||
factor, n_h, n_l, ", global Linetype-Scale" if set_global else ""))
|
factor, n_h, n_l, ", global Linetype-Scale" if set_global else ""))
|
||||||
|
|
||||||
|
|
||||||
@@ -831,7 +833,7 @@ def _apply_scale(doc, vp, ratio):
|
|||||||
if vp is None or doc is None: return False
|
if vp is None or doc is None: return False
|
||||||
try:
|
try:
|
||||||
if not vp.IsParallelProjection:
|
if not vp.IsParallelProjection:
|
||||||
print("[MASSSTAB] Viewport ist nicht parallel — Skala nicht setzbar")
|
print("[SCALE] Viewport ist nicht parallel — Skala nicht setzbar")
|
||||||
return False
|
return False
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
@@ -854,7 +856,7 @@ def _apply_scale(doc, vp, ratio):
|
|||||||
# factor > 1 zoomt rein (kleineres Frustum). factor = cur_w / new_w.
|
# factor > 1 zoomt rein (kleineres Frustum). factor = cur_w / new_w.
|
||||||
factor = cur_w / new_frustum_u
|
factor = cur_w / new_frustum_u
|
||||||
if factor <= 0 or not (factor < 1e9 and factor > 1e-9):
|
if factor <= 0 or not (factor < 1e9 and factor > 1e-9):
|
||||||
print("[MASSSTAB] _apply_scale: ungueltiger Faktor", factor)
|
print("[SCALE] _apply_scale: ungueltiger Faktor", factor)
|
||||||
return False
|
return False
|
||||||
applied = False
|
applied = False
|
||||||
# Verschiedene API-Signaturen je nach Rhino-Version durchprobieren.
|
# Verschiedene API-Signaturen je nach Rhino-Version durchprobieren.
|
||||||
@@ -873,7 +875,7 @@ def _apply_scale(doc, vp, ratio):
|
|||||||
Rhino.RhinoApp.RunScript("_-Zoom _Factor {:.6f} _Enter".format(factor), False)
|
Rhino.RhinoApp.RunScript("_-Zoom _Factor {:.6f} _Enter".format(factor), False)
|
||||||
applied = True
|
applied = True
|
||||||
except Exception as ex3:
|
except Exception as ex3:
|
||||||
print("[MASSSTAB] _apply_scale alle Varianten fehlgeschlagen:",
|
print("[SCALE] _apply_scale alle Varianten failed:",
|
||||||
ex1, ex2, ex3)
|
ex1, ex2, ex3)
|
||||||
if not applied:
|
if not applied:
|
||||||
return False
|
return False
|
||||||
@@ -882,28 +884,37 @@ def _apply_scale(doc, vp, ratio):
|
|||||||
if _get_lineweights_enabled(doc):
|
if _get_lineweights_enabled(doc):
|
||||||
_apply_scaled_lineweights(doc, True, float(ratio))
|
_apply_scaled_lineweights(doc, True, float(ratio))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] LW-Rescale:", ex)
|
print("[SCALE] LW-Rescale:", ex)
|
||||||
# Hatches mit sqrt(N) skalieren — moderate Anpassung.
|
# Hatches mit sqrt(N) skalieren — moderate Anpassung.
|
||||||
try:
|
try:
|
||||||
apply_scaled_hatches(doc, float(ratio))
|
apply_scaled_hatches(doc, float(ratio))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] Hatch-Rescale:", ex)
|
print("[SCALE] Hatch-Rescale:", ex)
|
||||||
# Neuen Wert persistieren — sowohl per-Viewport (fuer das Dropdown,
|
# Neuen Wert ZUERST persistieren — sowohl per-Viewport (fuer das
|
||||||
# damit jeder Viewport seinen eigenen Massstab behaelt) als auch als
|
# Dropdown, damit jeder Viewport seinen eigenen Massstab behaelt) als
|
||||||
# globaler "letzter Wert" (Legacy-Key; wird von Plotweight/Hatch-Rescale
|
# auch als globaler "letzter Wert". WICHTIG: vor dem Raumstempel-
|
||||||
# doc-weit benutzt — dort ist nur EIN Faktor sinnvoll).
|
# Regen weil _resolve_raum_text_height_m get_applied_scale_ratio()
|
||||||
|
# liest — sonst regennt mit ALTER Skala.
|
||||||
_write_user_scale(doc, ratio)
|
_write_user_scale(doc, ratio)
|
||||||
try:
|
try:
|
||||||
_set_applied_scale_for_vp(doc, vp.Name, float(ratio))
|
_set_applied_scale_for_vp(doc, vp.Name, float(ratio))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] per-vp scale write:", ex)
|
print("[SCALE] per-vp scale write:", ex)
|
||||||
|
# Raumstempel im masstab-Modus regennen mit der NEUEN Skala.
|
||||||
|
try:
|
||||||
|
import elemente as _el
|
||||||
|
n_regen = _el.regen_masstab_raeume(doc)
|
||||||
|
if n_regen > 0:
|
||||||
|
print("[SCALE] {} masstab-Raum/Raeume regenned".format(n_regen))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCALE] Raumstempel-Regen:", ex)
|
||||||
try: doc.Views.Redraw()
|
try: doc.Views.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
print("[MASSSTAB] Skala 1:{:.2f} gesetzt (Faktor {:.4f}, soll-frustum {:.4f} {})".format(
|
print("[SCALE] Skala 1:{:.2f} set (Faktor {:.4f}, soll-frustum {:.4f} {})".format(
|
||||||
ratio, factor, new_frustum_u, str(doc.ModelUnitSystem)))
|
ratio, factor, new_frustum_u, str(doc.ModelUnitSystem)))
|
||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] _apply_scale:", ex)
|
print("[SCALE] _apply_scale:", ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -913,7 +924,7 @@ def _zoom_extents(doc, vp, selected_only=False):
|
|||||||
if selected_only:
|
if selected_only:
|
||||||
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
if not objs:
|
if not objs:
|
||||||
print("[MASSSTAB] Keine Selektion fuer Zoom-Selection")
|
print("[SCALE] Keine Selektion fuer Zoom-Selection")
|
||||||
return False
|
return False
|
||||||
bbox = Rhino.Geometry.BoundingBox.Empty
|
bbox = Rhino.Geometry.BoundingBox.Empty
|
||||||
for o in objs:
|
for o in objs:
|
||||||
@@ -946,7 +957,7 @@ def _zoom_extents(doc, vp, selected_only=False):
|
|||||||
except Exception: pass
|
except Exception: pass
|
||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[MASSSTAB] _zoom_extents:", ex)
|
print("[SCALE] _zoom_extents:", ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -961,7 +972,7 @@ class MassstabBridge(panel_base.BaseBridge):
|
|||||||
def _on_ready(self):
|
def _on_ready(self):
|
||||||
# Einmalige Bootstrap-Detection falls noch keine DPI in der Config.
|
# Einmalige Bootstrap-Detection falls noch keine DPI in der Config.
|
||||||
try: _bootstrap_dpi()
|
try: _bootstrap_dpi()
|
||||||
except Exception as ex: print("[MASSSTAB] bootstrap:", ex)
|
except Exception as ex: print("[SCALE] bootstrap:", ex)
|
||||||
self._send_state(force=True)
|
self._send_state(force=True)
|
||||||
|
|
||||||
def handle(self, data):
|
def handle(self, data):
|
||||||
@@ -1003,7 +1014,7 @@ class MassstabBridge(panel_base.BaseBridge):
|
|||||||
elif t == "DETECT_DPI":
|
elif t == "DETECT_DPI":
|
||||||
v = _force_redetect_dpi()
|
v = _force_redetect_dpi()
|
||||||
if v is None:
|
if v is None:
|
||||||
print("[MASSSTAB] Auto-Detect: keine Bildschirminfo verfuegbar")
|
print("[SCALE] Auto-Detect: keine Bildschirminfo verfuegbar")
|
||||||
self._send_state(force=True)
|
self._send_state(force=True)
|
||||||
elif t == "SET_LINEWEIGHTS":
|
elif t == "SET_LINEWEIGHTS":
|
||||||
doc, _ = _active_vp()
|
doc, _ = _active_vp()
|
||||||
@@ -1050,7 +1061,7 @@ def _install_listeners(bridge):
|
|||||||
Rhino.RhinoApp.Idle += on_idle
|
Rhino.RhinoApp.Idle += on_idle
|
||||||
Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change
|
Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change
|
||||||
sc.sticky[flag] = True
|
sc.sticky[flag] = True
|
||||||
print("[MASSSTAB] Listener aktiv (Idle-Poll + Doc-Change)")
|
print("[SCALE] Listener active (Idle-Poll + Doc-Change)")
|
||||||
|
|
||||||
|
|
||||||
def get_current_scale_ratio():
|
def get_current_scale_ratio():
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python3
|
#! python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
OSM-Importer fuer Dossier — holt OpenStreetMap-Daten via Overpass-API als
|
OSM-Importer fuer Dossier — holt OpenStreetMap-Daten via Overpass-API als
|
||||||
Polylinien (Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege).
|
Polylinien (Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege).
|
||||||
|
|||||||
+6
-4
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
overrides.py
|
overrides.py
|
||||||
Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides /
|
Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides /
|
||||||
@@ -255,8 +257,8 @@ def delete_rule_template(name):
|
|||||||
def set_active_preset(doc, name):
|
def set_active_preset(doc, name):
|
||||||
"""Aktiviert ein gespeichertes Preset: kopiert dessen Rules ins Doc-Config
|
"""Aktiviert ein gespeichertes Preset: kopiert dessen Rules ins Doc-Config
|
||||||
und markiert es als activePreset. Wenn name leer/None: aktives Preset
|
und markiert es als activePreset. Wenn name leer/None: aktives Preset
|
||||||
geclear-t, Rules bleiben unveraendert (User waehlt "kein Preset"). Bei
|
geclear-t, Rules bleiben unchanged (User waehlt "kein Preset"). Bei
|
||||||
aktivem enabled-Flag wird sofort neu angewendet. True bei Erfolg."""
|
aktivem enabled-Flag wird sofort neu applied. True bei Erfolg."""
|
||||||
if doc is None: return False
|
if doc is None: return False
|
||||||
cfg = load_config(doc)
|
cfg = load_config(doc)
|
||||||
if name:
|
if name:
|
||||||
@@ -674,7 +676,7 @@ def _apply_to_object(doc, obj, overrides):
|
|||||||
|
|
||||||
def apply_all(doc):
|
def apply_all(doc):
|
||||||
"""Wendet alle aktiven Regeln auf alle Objekte im Doc an.
|
"""Wendet alle aktiven Regeln auf alle Objekte im Doc an.
|
||||||
Objekte die NICHT (mehr) matchen werden auf Originale zurueckgesetzt."""
|
Objekte die NICHT (mehr) matchen werden auf Originale zurueckset."""
|
||||||
if doc is None: return 0, 0
|
if doc is None: return 0, 0
|
||||||
cfg = load_config(doc)
|
cfg = load_config(doc)
|
||||||
if not cfg.get("enabled"): return 0, 0
|
if not cfg.get("enabled"): return 0, 0
|
||||||
@@ -863,4 +865,4 @@ def install_listeners():
|
|||||||
return
|
return
|
||||||
|
|
||||||
sc.sticky["overrides_listeners"] = True
|
sc.sticky["overrides_listeners"] = True
|
||||||
print("[OVERRIDES] Live-Update Listener aktiv (Add/Replace/LayerTable)")
|
print("[OVERRIDES] Live-Update Listener active (Add/Replace/LayerTable)")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
overrides_panel.py
|
overrides_panel.py
|
||||||
OVERRIDES-Panel: Rule-Editor fuer grafische Overrides.
|
OVERRIDES-Panel: Rule-Editor fuer grafische Overrides.
|
||||||
@@ -197,7 +199,7 @@ class OverridesBridge(panel_base.BaseBridge):
|
|||||||
overrides.set_active_preset(doc, name)
|
overrides.set_active_preset(doc, name)
|
||||||
else:
|
else:
|
||||||
# Append-Mode: bestehende + Preset-Rules. activePreset wird
|
# Append-Mode: bestehende + Preset-Rules. activePreset wird
|
||||||
# in update_rules auf None gesetzt — passt, weil's eine
|
# in update_rules auf None set — passt, weil's eine
|
||||||
# Mischung ist, kein einzelnes Preset mehr.
|
# Mischung ist, kein einzelnes Preset mehr.
|
||||||
rules = overrides.load_preset(name)
|
rules = overrides.load_preset(name)
|
||||||
if rules is not None:
|
if rules is not None:
|
||||||
|
|||||||
+120
-60
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
panel_base.py
|
panel_base.py
|
||||||
Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView.
|
Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView.
|
||||||
@@ -18,6 +20,43 @@ import scriptcontext as sc
|
|||||||
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
_DIST = os.path.join(_HERE, "..", "dist", "index.html")
|
_DIST = os.path.join(_HERE, "..", "dist", "index.html")
|
||||||
|
_SETTINGS_PATH = os.path.expanduser(
|
||||||
|
"~/Library/Application Support/ch.gabrielevarano.Dossier/dossier_settings.json")
|
||||||
|
|
||||||
|
|
||||||
|
_MODE_LOG_TAG = {
|
||||||
|
"ebenen": "LAYERS",
|
||||||
|
"zeichnungsebenen": "DRAWING-LEVELS",
|
||||||
|
"oberleiste": "TOOLBAR",
|
||||||
|
"gestaltung": "STYLES",
|
||||||
|
"werkzeuge": "TOOLS",
|
||||||
|
"dimensionen": "DIMENSIONS",
|
||||||
|
"ausschnitte": "VIEWPORTS",
|
||||||
|
"massstab": "SCALE",
|
||||||
|
"overrides": "OVERRIDES",
|
||||||
|
"layouts": "LAYOUTS",
|
||||||
|
"elemente": "ELEMENTS",
|
||||||
|
"kamera": "CAMERA",
|
||||||
|
"layer_combinations": "LAYER-COMBINATIONS",
|
||||||
|
"dossier_settings": "DOSSIER-SETTINGS",
|
||||||
|
"project_settings": "PROJECT-SETTINGS",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _tag(mode):
|
||||||
|
return _MODE_LOG_TAG.get(mode, mode.upper())
|
||||||
|
|
||||||
|
|
||||||
|
def _read_lang():
|
||||||
|
"""Liest die UI-Sprache aus dossier_settings.json. Default: 'de'."""
|
||||||
|
try:
|
||||||
|
if os.path.isfile(_SETTINGS_PATH):
|
||||||
|
with open(_SETTINGS_PATH, "rb") as f:
|
||||||
|
d = json.loads(f.read().decode("utf-8"))
|
||||||
|
lang = d.get("lang", "de")
|
||||||
|
return lang if lang in ("de", "en") else "de"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "de"
|
||||||
|
|
||||||
|
|
||||||
# --- Timing-Instrumentierung ------------------------------------------------
|
# --- Timing-Instrumentierung ------------------------------------------------
|
||||||
@@ -45,16 +84,16 @@ def print_startup_summary():
|
|||||||
print("[STARTUP] ===== SUMMARY =====")
|
print("[STARTUP] ===== SUMMARY =====")
|
||||||
print("[STARTUP] Wall-time (first to last mark): {:.1f} ms".format(total_wall))
|
print("[STARTUP] Wall-time (first to last mark): {:.1f} ms".format(total_wall))
|
||||||
print("[STARTUP] Sum of measured work: {:.1f} ms".format(total_work))
|
print("[STARTUP] Sum of measured work: {:.1f} ms".format(total_work))
|
||||||
# Top-10 nach Dauer
|
# Top-10 by duration
|
||||||
top = sorted(_TIMINGS, key=lambda x: -x[2])[:10]
|
top = sorted(_TIMINGS, key=lambda x: -x[2])[:10]
|
||||||
print("[STARTUP] --- Top-10 nach Dauer ---")
|
print("[STARTUP] --- Top-10 by duration ---")
|
||||||
for phase, label, ms in top:
|
for phase, label, ms in top:
|
||||||
print("[STARTUP] {:7.1f} ms {} / {}".format(ms, phase, label))
|
print("[STARTUP] {:7.1f} ms {} / {}".format(ms, phase, label))
|
||||||
# Aggregat nach Phase
|
# Aggregate by phase
|
||||||
by_phase = {}
|
by_phase = {}
|
||||||
for phase, _, ms in _TIMINGS:
|
for phase, _, ms in _TIMINGS:
|
||||||
by_phase[phase] = by_phase.get(phase, 0.0) + ms
|
by_phase[phase] = by_phase.get(phase, 0.0) + ms
|
||||||
print("[STARTUP] --- Aggregat nach Phase ---")
|
print("[STARTUP] --- Aggregate by phase ---")
|
||||||
for phase, ms in sorted(by_phase.items(), key=lambda x: -x[1]):
|
for phase, ms in sorted(by_phase.items(), key=lambda x: -x[1]):
|
||||||
print("[STARTUP] {:7.1f} ms {}".format(ms, phase))
|
print("[STARTUP] {:7.1f} ms {}".format(ms, phase))
|
||||||
|
|
||||||
@@ -96,7 +135,7 @@ def migrate_to_dossier(doc):
|
|||||||
new = "dossier_" + suffix
|
new = "dossier_" + suffix
|
||||||
try:
|
try:
|
||||||
if doc.Strings.GetValue(new):
|
if doc.Strings.GetValue(new):
|
||||||
continue # Dossier-Variante vorhanden -> nicht ueberschreiben
|
continue # Dossier-Variante present -> nicht ueberschreiben
|
||||||
for prefix in _LEGACY_PREFIXES:
|
for prefix in _LEGACY_PREFIXES:
|
||||||
old_v = doc.Strings.GetValue(prefix + suffix)
|
old_v = doc.Strings.GetValue(prefix + suffix)
|
||||||
if old_v:
|
if old_v:
|
||||||
@@ -190,7 +229,7 @@ class BaseBridge(object):
|
|||||||
try:
|
try:
|
||||||
data = json.loads(raw_str)
|
data = json.loads(raw_str)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] JSON-Fehler: {}".format(self._mode.upper(), ex))
|
print("[{}] JSON-Fehler: {}".format(_tag(self._mode), ex))
|
||||||
return
|
return
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
return
|
return
|
||||||
@@ -206,8 +245,8 @@ class BaseBridge(object):
|
|||||||
self.handle(json.loads(full))
|
self.handle(json.loads(full))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
import traceback
|
import traceback
|
||||||
print("[{}] Chunk-Reassembly: {}".format(self._mode.upper(), ex))
|
print("[{}] Chunk-Reassembly: {}".format(_tag(self._mode), ex))
|
||||||
print("[{}] Traceback:\n{}".format(self._mode.upper(), traceback.format_exc()))
|
print("[{}] Traceback:\n{}".format(_tag(self._mode), traceback.format_exc()))
|
||||||
else:
|
else:
|
||||||
self.handle(data)
|
self.handle(data)
|
||||||
|
|
||||||
@@ -265,10 +304,12 @@ def _build_inline_template():
|
|||||||
html = f.read().decode("utf-8")
|
html = f.read().decode("utf-8")
|
||||||
|
|
||||||
placeholder_script = '<script>' + _MODE_SCRIPT_PLACEHOLDER + '</script>'
|
placeholder_script = '<script>' + _MODE_SCRIPT_PLACEHOLDER + '</script>'
|
||||||
|
_no_select = '<style>*{-webkit-user-select:none!important;user-select:none!important;}</style>'
|
||||||
|
_no_ctx = '<script>document.addEventListener("contextmenu",function(e){e.preventDefault();},true);</script>'
|
||||||
if "</head>" in html:
|
if "</head>" in html:
|
||||||
html = html.replace("</head>", placeholder_script + "</head>")
|
html = html.replace("</head>", placeholder_script + _no_select + _no_ctx + "</head>")
|
||||||
else:
|
else:
|
||||||
html = placeholder_script + html
|
html = placeholder_script + _no_select + _no_ctx + html
|
||||||
|
|
||||||
def inline_css(m):
|
def inline_css(m):
|
||||||
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
|
p = os.path.join(dist_dir, m.group(1).lstrip("./").replace("/", os.sep))
|
||||||
@@ -308,17 +349,18 @@ def load_inline(wv, mode, params=None):
|
|||||||
if _INLINE_TEMPLATE is None or _INLINE_TEMPLATE[0] != cur_mtime:
|
if _INLINE_TEMPLATE is None or _INLINE_TEMPLATE[0] != cur_mtime:
|
||||||
sig, tmpl = _build_inline_template()
|
sig, tmpl = _build_inline_template()
|
||||||
if tmpl is None:
|
if tmpl is None:
|
||||||
print("[{}] dist nicht gefunden".format(mode.upper()))
|
print("[{}] dist not found".format(_tag(mode)))
|
||||||
return
|
return
|
||||||
_INLINE_TEMPLATE = (sig, tmpl)
|
_INLINE_TEMPLATE = (sig, tmpl)
|
||||||
|
|
||||||
# Per-Mount: nur das Mode-Script-Snippet bauen
|
# Per-Mount: nur das Mode-Script-Snippet bauen
|
||||||
parts = ['window.PANEL_MODE="{}";'.format(mode)]
|
parts = ['window.PANEL_MODE="{}";'.format(mode),
|
||||||
|
'window.DOSSIER_LANG="{}";'.format(_read_lang())]
|
||||||
if params is not None:
|
if params is not None:
|
||||||
try:
|
try:
|
||||||
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
|
parts.append('window.PANEL_PARAMS=' + json.dumps(params, ensure_ascii=False) + ';')
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] PANEL_PARAMS serialize: {}".format(mode.upper(), ex))
|
print("[{}] PANEL_PARAMS serialize: {}".format(_tag(mode), ex))
|
||||||
mode_script = ''.join(parts)
|
mode_script = ''.join(parts)
|
||||||
html = _INLINE_TEMPLATE[1].replace(_MODE_SCRIPT_PLACEHOLDER, mode_script)
|
html = _INLINE_TEMPLATE[1].replace(_MODE_SCRIPT_PLACEHOLDER, mode_script)
|
||||||
t_loadhtml = time.time()
|
t_loadhtml = time.time()
|
||||||
@@ -339,10 +381,10 @@ def attach_webview(panel, bridge, mode):
|
|||||||
try:
|
try:
|
||||||
bridge.handle_raw(title[10:])
|
bridge.handle_raw(title[10:])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
|
print("[{}] Message-Fehler: {}".format(_tag(mode), ex))
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
|
wv.ExecuteScript("document.title='{}';".format(_tag(mode)))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -351,13 +393,22 @@ def attach_webview(panel, bridge, mode):
|
|||||||
wv.ExecuteScript("window.RHINO_MODE=true;")
|
wv.ExecuteScript("window.RHINO_MODE=true;")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
wv.ExecuteScript(
|
||||||
|
"var _ds=document.createElement('style');"
|
||||||
|
"_ds.textContent='*{-webkit-user-select:none!important;user-select:none!important;}';"
|
||||||
|
"document.head.appendChild(_ds);"
|
||||||
|
"document.addEventListener('contextmenu',function(e){e.preventDefault();},true);"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def on_idle(s, e):
|
def on_idle(s, e):
|
||||||
Rhino.RhinoApp.Idle -= on_idle
|
Rhino.RhinoApp.Idle -= on_idle
|
||||||
try:
|
try:
|
||||||
load_inline(wv, mode)
|
load_inline(wv, mode)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] Inline-Fehler: {}".format(mode.upper(), ex))
|
print("[{}] Inline-Fehler: {}".format(_tag(mode), ex))
|
||||||
|
|
||||||
wv.DocumentTitleChanged += on_title
|
wv.DocumentTitleChanged += on_title
|
||||||
wv.DocumentLoaded += on_loaded
|
wv.DocumentLoaded += on_loaded
|
||||||
@@ -409,7 +460,7 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560),
|
|||||||
if on_save is not None:
|
if on_save is not None:
|
||||||
try: on_save(p)
|
try: on_save(p)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] on_save: {}".format(mode.upper(), ex))
|
print("[{}] on_save: {}".format(_tag(mode), ex))
|
||||||
try: form.Close()
|
try: form.Close()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
elif t == "CANCEL":
|
elif t == "CANCEL":
|
||||||
@@ -428,16 +479,24 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560),
|
|||||||
try:
|
try:
|
||||||
bridge.handle_raw(title_str[10:])
|
bridge.handle_raw(title_str[10:])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] Message-Fehler: {}".format(mode.upper(), ex))
|
print("[{}] Message-Fehler: {}".format(_tag(mode), ex))
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
wv.ExecuteScript("document.title='{}';".format(mode.upper()))
|
wv.ExecuteScript("document.title='{}';".format(_tag(mode)))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_loaded(s, e):
|
def on_loaded(s, e):
|
||||||
try: wv.ExecuteScript("window.RHINO_MODE=true;")
|
try: wv.ExecuteScript("window.RHINO_MODE=true;")
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
wv.ExecuteScript(
|
||||||
|
"var _ds=document.createElement('style');"
|
||||||
|
"_ds.textContent='*{-webkit-user-select:none!important;user-select:none!important;}';"
|
||||||
|
"document.head.appendChild(_ds);"
|
||||||
|
"document.addEventListener('contextmenu',function(e){e.preventDefault();},true);"
|
||||||
|
)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
wv.DocumentTitleChanged += on_title_
|
wv.DocumentTitleChanged += on_title_
|
||||||
wv.DocumentLoaded += on_loaded
|
wv.DocumentLoaded += on_loaded
|
||||||
@@ -449,7 +508,7 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560),
|
|||||||
try:
|
try:
|
||||||
load_inline(wv, mode, params=params)
|
load_inline(wv, mode, params=params)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] Inline-Fehler: {}".format(mode.upper(), ex))
|
print("[{}] Inline-Fehler: {}".format(_tag(mode), ex))
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
|
||||||
@@ -545,7 +604,7 @@ def _try_load_png_white(png_path, size):
|
|||||||
g.Dispose()
|
g.Dispose()
|
||||||
return target
|
return target
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] PNG-load failed:", ex)
|
print("[CORE] PNG-load failed:", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -591,7 +650,7 @@ def _try_load_svg_white(svg_path, size):
|
|||||||
g.Dispose()
|
g.Dispose()
|
||||||
return target
|
return target
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] SVG-load failed:", ex)
|
print("[CORE] SVG-load failed:", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -680,10 +739,10 @@ def make_panel_icon(name_or_letter, bg_hex):
|
|||||||
if os.path.isfile(png_path):
|
if os.path.isfile(png_path):
|
||||||
icon_bmp = _try_load_png_white(png_path, size - 8)
|
icon_bmp = _try_load_png_white(png_path, size - 8)
|
||||||
if icon_bmp is not None: chosen_path = png_path
|
if icon_bmp is not None: chosen_path = png_path
|
||||||
else: print("[panel_base] PNG geladen aber Bitmap None:",
|
else: print("[CORE] PNG loaded but Bitmap is None:",
|
||||||
png_path)
|
png_path)
|
||||||
else:
|
# PNG-not-found ist normal: Fallback auf SVG dann Material-Font.
|
||||||
print("[panel_base] PNG nicht gefunden:", png_path)
|
# Nur loggen wenn final ALLES failt (s.u.).
|
||||||
if icon_bmp is None:
|
if icon_bmp is None:
|
||||||
svg_path = os.path.join(_PANEL_ICONS_SVG_DIR,
|
svg_path = os.path.join(_PANEL_ICONS_SVG_DIR,
|
||||||
name_or_letter + ".svg")
|
name_or_letter + ".svg")
|
||||||
@@ -697,31 +756,32 @@ def make_panel_icon(name_or_letter, bg_hex):
|
|||||||
size - 2*pad, size - 2*pad)
|
size - 2*pad, size - 2*pad)
|
||||||
used_svg = True
|
used_svg = True
|
||||||
used_material = True # → kein Letter-Fallback
|
used_material = True # → kein Letter-Fallback
|
||||||
print("[panel_base] Icon-Pfad: {} ← {}".format(
|
print("[CORE] Icon path: {} ← {}".format(
|
||||||
name_or_letter, chosen_path))
|
name_or_letter, chosen_path))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] Icon-Composite Fehler:", ex)
|
print("[CORE] Icon composite error:", ex)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] Icon-Pfad-Check:", ex)
|
print("[CORE] Icon path check error:", ex)
|
||||||
|
|
||||||
# 1) Material-Icon-Font (wenn keine SVG vorhanden)
|
# 1) Material-Icon-Font (wenn keine SVG present)
|
||||||
mat_cp = _MATERIAL_CODEPOINTS.get(name_or_letter)
|
mat_cp = _MATERIAL_CODEPOINTS.get(name_or_letter)
|
||||||
if not used_svg and mat_cp is not None:
|
if not used_svg and mat_cp is not None:
|
||||||
font_family_name = _try_material_font()
|
font_family_name = _try_material_font()
|
||||||
if font_family_name:
|
if font_family_name:
|
||||||
try:
|
try:
|
||||||
ff = drawing.FontFamily(font_family_name)
|
ff = drawing.FontFamily(font_family_name)
|
||||||
# FontStyle.None: in Python3 nicht direkt zugreifbar
|
# FontStyle.None: in Python3 ist None ein Keyword, deshalb
|
||||||
# (None ist Keyword) → getattr-Workaround, sonst 0
|
# via System.Enum.ToObject explizit konstruieren — Python.NET 3
|
||||||
try: fs = getattr(drawing.FontStyle, "None")
|
# konvertiert int → Enum nicht mehr implizit.
|
||||||
except Exception: fs = 0
|
import System
|
||||||
|
fs = System.Enum.ToObject(drawing.FontStyle, 0)
|
||||||
font = drawing.Font(ff, 20, fs)
|
font = drawing.Font(ff, 20, fs)
|
||||||
glyph = chr(mat_cp)
|
glyph = chr(mat_cp)
|
||||||
_draw_glyph(g, size, font, glyph,
|
_draw_glyph(g, size, font, glyph,
|
||||||
drawing.Colors.White)
|
drawing.Colors.White)
|
||||||
used_material = True
|
used_material = True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] Material-Render Fehler:", ex)
|
print("[CORE] Material render error:", ex)
|
||||||
used_material = False
|
used_material = False
|
||||||
|
|
||||||
# 2) Fallback: Buchstabe (erstes Zeichen bzw. eingegebener Buchstabe)
|
# 2) Fallback: Buchstabe (erstes Zeichen bzw. eingegebener Buchstabe)
|
||||||
@@ -749,7 +809,7 @@ def make_panel_icon(name_or_letter, bg_hex):
|
|||||||
tag, safe, bg_hex.lstrip("#")))
|
tag, safe, bg_hex.lstrip("#")))
|
||||||
bmp.Save(path, drawing.ImageFormat.Png)
|
bmp.Save(path, drawing.ImageFormat.Png)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] Icon-Save:", ex)
|
print("[CORE] Icon save error:", ex)
|
||||||
path = None
|
path = None
|
||||||
# WICHTIG: Mac Rhinos RegisterPanel meldet "expected Icon, got Icon"
|
# WICHTIG: Mac Rhinos RegisterPanel meldet "expected Icon, got Icon"
|
||||||
# wenn wir Eto.Drawing.Icon uebergeben — die API erwartet
|
# wenn wir Eto.Drawing.Icon uebergeben — die API erwartet
|
||||||
@@ -759,35 +819,35 @@ def make_panel_icon(name_or_letter, bg_hex):
|
|||||||
try:
|
try:
|
||||||
import System.Drawing as _sd
|
import System.Drawing as _sd
|
||||||
ic = _sd.Icon(path)
|
ic = _sd.Icon(path)
|
||||||
print("[panel_base] Icon erzeugt via System.Drawing.Icon(path) [{}]".format(tag))
|
print("[CORE] Icon created via System.Drawing.Icon(path) [{}]".format(tag))
|
||||||
return ic
|
return ic
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] System.Drawing.Icon(path) fehlgeschlagen:", ex)
|
print("[CORE] System.Drawing.Icon(path) failed:", ex)
|
||||||
# System.Drawing.Bitmap als Fallback (manche RegisterPanel-Overloads akzeptieren Bitmap)
|
# System.Drawing.Bitmap als Fallback (manche RegisterPanel-Overloads akzeptieren Bitmap)
|
||||||
try:
|
try:
|
||||||
import System.Drawing as _sd
|
import System.Drawing as _sd
|
||||||
bmp_sd = _sd.Bitmap(path)
|
bmp_sd = _sd.Bitmap(path)
|
||||||
print("[panel_base] Icon erzeugt via System.Drawing.Bitmap(path) [{}]".format(tag))
|
print("[CORE] Icon created via System.Drawing.Bitmap(path) [{}]".format(tag))
|
||||||
return bmp_sd
|
return bmp_sd
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] System.Drawing.Bitmap(path) fehlgeschlagen:", ex)
|
print("[CORE] System.Drawing.Bitmap(path) failed:", ex)
|
||||||
# Eto.Drawing.Icon als letzter Versuch — falls Rhino-Version anders ist
|
# Eto.Drawing.Icon als letzter Versuch — falls Rhino-Version anders ist
|
||||||
try:
|
try:
|
||||||
ic = drawing.Icon(path)
|
ic = drawing.Icon(path)
|
||||||
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(path) [{}]".format(tag))
|
print("[CORE] Icon erzeugt via Eto.Drawing.Icon(path) [{}]".format(tag))
|
||||||
return ic
|
return ic
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] Eto.Drawing.Icon(path) fehlgeschlagen:", ex)
|
print("[CORE] Eto.Drawing.Icon(path) failed:", ex)
|
||||||
# Bitmap-Fallback (in-memory) — wenn alles vorherige fehlschlaegt
|
# Bitmap-Fallback (in-memory) — wenn alles vorherige fehlschlaegt
|
||||||
try:
|
try:
|
||||||
ic = drawing.Icon(1.0, bmp)
|
ic = drawing.Icon(1.0, bmp)
|
||||||
print("[panel_base] Icon erzeugt via Eto.Drawing.Icon(scale, bmp) [{}]".format(tag))
|
print("[CORE] Icon erzeugt via Eto.Drawing.Icon(scale, bmp) [{}]".format(tag))
|
||||||
return ic
|
return ic
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
print("[panel_base] Icon Fallback: Eto.Bitmap zurueck ({})".format(tag))
|
print("[CORE] Icon Fallback: Eto.Bitmap zurueck ({})".format(tag))
|
||||||
return bmp
|
return bmp
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] Icon-Erstellung fehlgeschlagen:", ex)
|
print("[CORE] Icon-Erstellung failed:", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -801,7 +861,7 @@ def find_plugin():
|
|||||||
if p is not None:
|
if p is not None:
|
||||||
return p
|
return p
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[panel_base] Plugin-Suche:", ex)
|
print("[CORE] Plugin-Suche:", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -822,7 +882,7 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
|||||||
t_reg = time.time()
|
t_reg = time.time()
|
||||||
plugin = find_plugin()
|
plugin = find_plugin()
|
||||||
if plugin is None:
|
if plugin is None:
|
||||||
print("[{}] Plugin nicht gefunden".format(mode.upper()))
|
print("[{}] Plugin not found".format(_tag(mode)))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
type_name = "DynPanel_" + mode
|
type_name = "DynPanel_" + mode
|
||||||
@@ -836,7 +896,7 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
|||||||
try:
|
try:
|
||||||
panel.MinimumSize = drawing.Size(int(min_size[0]), int(min_size[1]))
|
panel.MinimumSize = drawing.Size(int(min_size[0]), int(min_size[1]))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] MinimumSize konnte nicht gesetzt werden: {}".format(mode.upper(), ex))
|
print("[{}] MinimumSize konnte nicht set werden: {}".format(_tag(mode), ex))
|
||||||
# Auf einigen Eto-Versionen gibt es zusaetzlich Size/ClientSize
|
# Auf einigen Eto-Versionen gibt es zusaetzlich Size/ClientSize
|
||||||
for attr in ("Size", "ClientSize"):
|
for attr in ("Size", "ClientSize"):
|
||||||
try:
|
try:
|
||||||
@@ -855,14 +915,14 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
|||||||
try:
|
try:
|
||||||
icon = make_panel_icon(icon_spec[0], icon_spec[1])
|
icon = make_panel_icon(icon_spec[0], icon_spec[1])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] Icon-Erstellung uebersprungen: {}".format(mode.upper(), ex))
|
print("[{}] Icon-Erstellung uebersprungen: {}".format(_tag(mode), ex))
|
||||||
icon = None
|
icon = None
|
||||||
_t_mark("icon", mode, t_icon)
|
_t_mark("icon", mode, t_icon)
|
||||||
registered = False
|
registered = False
|
||||||
registered_with_icon = False
|
registered_with_icon = False
|
||||||
# Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels
|
# Erst mit Icon versuchen, dann stillschweigend ohne (Mac Rhino-Panels
|
||||||
# akzeptieren auf manchen Versionen nur System.Drawing.Icon, das auf
|
# akzeptieren auf manchen Versionen nur System.Drawing.Icon, das auf
|
||||||
# Mac nicht verfuegbar ist - die Registrierung ohne Icon ist OK).
|
# Mac not available ist - die Registrierung ohne Icon ist OK).
|
||||||
attempts = [(icon, True)] if icon is not None else []
|
attempts = [(icon, True)] if icon is not None else []
|
||||||
attempts.append((None, False))
|
attempts.append((None, False))
|
||||||
for arg, with_icon in attempts:
|
for arg, with_icon in attempts:
|
||||||
@@ -871,25 +931,25 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
|||||||
registered = True
|
registered = True
|
||||||
registered_with_icon = with_icon
|
registered_with_icon = with_icon
|
||||||
if with_icon:
|
if with_icon:
|
||||||
print("[{}] Panel mit Icon registriert ({})".format(
|
print("[{}] Panel registered with icon ({})".format(
|
||||||
mode.upper(), type(arg).__name__))
|
_tag(mode), type(arg).__name__))
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if with_icon:
|
if with_icon:
|
||||||
print("[{}] RegisterPanel mit Icon fehlgeschlagen: {}".format(
|
print("[{}] RegisterPanel mit Icon failed: {}".format(
|
||||||
mode.upper(), ex))
|
_tag(mode), ex))
|
||||||
else:
|
else:
|
||||||
print("[{}] RegisterPanel fehlgeschlagen: {}".format(
|
print("[{}] RegisterPanel failed: {}".format(
|
||||||
mode.upper(), ex))
|
_tag(mode), ex))
|
||||||
if registered and not registered_with_icon and icon is not None:
|
if registered and not registered_with_icon and icon is not None:
|
||||||
print("[{}] Panel ohne Icon registriert (Fallback)".format(mode.upper()))
|
print("[{}] Panel ohne Icon registriert (Fallback)".format(_tag(mode)))
|
||||||
if not registered:
|
if not registered:
|
||||||
return
|
return
|
||||||
sc.sticky[sticky_reg] = True
|
sc.sticky[sticky_reg] = True
|
||||||
sc.sticky[sticky_guid] = System.Guid(guid_str)
|
sc.sticky[sticky_guid] = System.Guid(guid_str)
|
||||||
print("[{}] Panel registriert".format(mode.upper()))
|
print("[{}] Panel registered".format(_tag(mode)))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] Registrierung fehlgeschlagen: {}".format(mode.upper(), ex))
|
print("[{}] Registrierung failed: {}".format(_tag(mode), ex))
|
||||||
return
|
return
|
||||||
_t_mark("register", mode, t_reg)
|
_t_mark("register", mode, t_reg)
|
||||||
|
|
||||||
@@ -898,7 +958,7 @@ def register_and_open(mode, caption, guid_str, bridge_factory, icon_spec=None, m
|
|||||||
guid = sc.sticky.get(sticky_guid, System.Guid(guid_str))
|
guid = sc.sticky.get(sticky_guid, System.Guid(guid_str))
|
||||||
RhinoUI.Panels.OpenPanel(guid)
|
RhinoUI.Panels.OpenPanel(guid)
|
||||||
_t_mark("OpenPanel", mode, t_open)
|
_t_mark("OpenPanel", mode, t_open)
|
||||||
print("[{}] Panel geoeffnet".format(mode.upper()))
|
print("[{}] Panel opened".format(_tag(mode)))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[{}] OpenPanel fehlgeschlagen: {}".format(mode.upper(), ex))
|
print("[{}] OpenPanel failed: {}".format(_tag(mode), ex))
|
||||||
_t_mark("register_and_open", mode, t_outer)
|
_t_mark("register_and_open", mode, t_outer)
|
||||||
|
|||||||
+12
-10
@@ -1,5 +1,7 @@
|
|||||||
#! python3
|
#! python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
schnitt_grips.py
|
schnitt_grips.py
|
||||||
Endpoint-Grips fuer Schnitt/Ansicht-Symbole im Plan.
|
Endpoint-Grips fuer Schnitt/Ansicht-Symbole im Plan.
|
||||||
@@ -98,7 +100,7 @@ def _update_linePts(doc, schnitt_id, new_p1, new_p2):
|
|||||||
doc.Strings.SetString("dossier_zeichnungsebenen",
|
doc.Strings.SetString("dossier_zeichnungsebenen",
|
||||||
json.dumps(z_list, ensure_ascii=False))
|
json.dumps(z_list, ensure_ascii=False))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT_GRIPS] persist linePts:", ex)
|
print("[SECTION-GRIPS] persist linePts:", ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Symbol regenerieren — Layer aus altem Symbol uebernehmen
|
# Symbol regenerieren — Layer aus altem Symbol uebernehmen
|
||||||
@@ -131,7 +133,7 @@ def _update_linePts(doc, schnitt_id, new_p1, new_p2):
|
|||||||
if i == 0 and gid and gid != System.Guid.Empty:
|
if i == 0 and gid and gid != System.Guid.Empty:
|
||||||
first_new_id = gid
|
first_new_id = gid
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT_GRIPS] add new symbol curve:", ex)
|
print("[SECTION-GRIPS] add new symbol curve:", ex)
|
||||||
|
|
||||||
# Neue Hauptlinie selektieren — damit der Conduit die Marker
|
# Neue Hauptlinie selektieren — damit der Conduit die Marker
|
||||||
# gleich wieder zeigt (sonst muesste der User nochmal klicken).
|
# gleich wieder zeigt (sonst muesste der User nochmal klicken).
|
||||||
@@ -148,19 +150,19 @@ def _update_linePts(doc, schnitt_id, new_p1, new_p2):
|
|||||||
if active_id == schnitt_id:
|
if active_id == schnitt_id:
|
||||||
schnitte.activate_schnitt(doc, target, skip_view=True)
|
schnitte.activate_schnitt(doc, target, skip_view=True)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT_GRIPS] re-activate:", ex)
|
print("[SECTION-GRIPS] re-activate:", ex)
|
||||||
|
|
||||||
# Panel-Broadcast (linePts haben sich geaendert, Ebenen-Panel will
|
# Panel-Broadcast (linePts haben sich geaendert, Ebenen-Panel will
|
||||||
# ggf. mit-rendern)
|
# ggf. mit-rendern)
|
||||||
try:
|
try:
|
||||||
import rhinopanel
|
import layers_panel as rhinopanel
|
||||||
rhinopanel._broadcast_state(doc)
|
rhinopanel._broadcast_state(doc)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
try: doc.Views.Redraw()
|
try: doc.Views.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT_GRIPS] update endpoint:", ex)
|
print("[SECTION-GRIPS] update endpoint:", ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +236,7 @@ class _SchnittEndpointConduit(rd.DisplayConduit):
|
|||||||
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
|
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT_GRIPS] DrawForeground:", ex)
|
print("[SECTION-GRIPS] DrawForeground:", ex)
|
||||||
|
|
||||||
|
|
||||||
# --- MouseCallback --------------------------------------------------------
|
# --- MouseCallback --------------------------------------------------------
|
||||||
@@ -289,7 +291,7 @@ class _SchnittMouseHandler(Rhino.UI.MouseCallback):
|
|||||||
sid, z, kind, anchor_pt = hit
|
sid, z, kind, anchor_pt = hit
|
||||||
self._start_drag(view.Document, sid, z, kind, anchor_pt)
|
self._start_drag(view.Document, sid, z, kind, anchor_pt)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT_GRIPS] OnMouseDown:", ex)
|
print("[SECTION-GRIPS] OnMouseDown:", ex)
|
||||||
|
|
||||||
def _start_drag(self, doc, schnitt_id, z, kind, anchor_pt):
|
def _start_drag(self, doc, schnitt_id, z, kind, anchor_pt):
|
||||||
if doc is None: return
|
if doc is None: return
|
||||||
@@ -368,7 +370,7 @@ class _SchnittMouseHandler(Rhino.UI.MouseCallback):
|
|||||||
confirmed = bool(_update_linePts(
|
confirmed = bool(_update_linePts(
|
||||||
doc, schnitt_id, new_p1, new_p2))
|
doc, schnitt_id, new_p1, new_p2))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT_GRIPS] _start_drag:", ex)
|
print("[SECTION-GRIPS] _start_drag:", ex)
|
||||||
finally:
|
finally:
|
||||||
if not confirmed:
|
if not confirmed:
|
||||||
for pid in hidden_clip_ids:
|
for pid in hidden_clip_ids:
|
||||||
@@ -404,6 +406,6 @@ def install_handlers():
|
|||||||
handler.Enabled = True
|
handler.Enabled = True
|
||||||
sc.sticky[_STICKY_CONDUIT] = conduit
|
sc.sticky[_STICKY_CONDUIT] = conduit
|
||||||
sc.sticky[_STICKY_HANDLER] = handler
|
sc.sticky[_STICKY_HANDLER] = handler
|
||||||
print("[SCHNITT_GRIPS] Endpoint-Conduit + Mouse-Handler aktiv")
|
print("[SECTION-GRIPS] Endpoint conduit + mouse handler active")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT_GRIPS] install:", ex)
|
print("[SECTION-GRIPS] install:", ex)
|
||||||
|
|||||||
+41
-28
@@ -1,5 +1,7 @@
|
|||||||
#! python3
|
#! python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
schnitte.py
|
schnitte.py
|
||||||
Schnitte + Ansichten als Zeichnungsebenen-Typ.
|
Schnitte + Ansichten als Zeichnungsebenen-Typ.
|
||||||
@@ -94,7 +96,7 @@ def _collect_viewport_ids(doc):
|
|||||||
|
|
||||||
def find_schnitt_clip_objects(doc):
|
def find_schnitt_clip_objects(doc):
|
||||||
"""Findet alle Clipping-Plane-Objekte die zu einem aktiven Schnitt
|
"""Findet alle Clipping-Plane-Objekte die zu einem aktiven Schnitt
|
||||||
gehoeren (UserString _KEY_SCHNITT_CLIP gesetzt)."""
|
gehoeren (UserString _KEY_SCHNITT_CLIP set)."""
|
||||||
out = []
|
out = []
|
||||||
try:
|
try:
|
||||||
for obj in doc.Objects:
|
for obj in doc.Objects:
|
||||||
@@ -125,9 +127,9 @@ def clear_schnitt_clipping(doc):
|
|||||||
if doc.Objects.Delete(obj.Id, True):
|
if doc.Objects.Delete(obj.Id, True):
|
||||||
n += 1
|
n += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] clear: {}".format(ex))
|
print("[SECTION] clear: {}".format(ex))
|
||||||
if n:
|
if n:
|
||||||
print("[SCHNITT] {} Clipping-Plane(s) entfernt".format(n))
|
print("[SECTION] {} Clipping-Plane(s) entfernt".format(n))
|
||||||
|
|
||||||
|
|
||||||
def _add_clipping_plane(doc, plane, du, dv, vp_ids, role):
|
def _add_clipping_plane(doc, plane, du, dv, vp_ids, role):
|
||||||
@@ -135,7 +137,7 @@ def _add_clipping_plane(doc, plane, du, dv, vp_ids, role):
|
|||||||
try:
|
try:
|
||||||
gid = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
|
gid = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
|
||||||
if gid is None or gid == System.Guid.Empty:
|
if gid is None or gid == System.Guid.Empty:
|
||||||
print("[SCHNITT] AddClippingPlane lieferte Empty Guid")
|
print("[SECTION] AddClippingPlane lieferte Empty Guid")
|
||||||
return None
|
return None
|
||||||
obj = doc.Objects.FindId(gid)
|
obj = doc.Objects.FindId(gid)
|
||||||
if obj is None: return None
|
if obj is None: return None
|
||||||
@@ -149,7 +151,7 @@ def _add_clipping_plane(doc, plane, du, dv, vp_ids, role):
|
|||||||
doc.Objects.ModifyAttributes(obj, attrs, True)
|
doc.Objects.ModifyAttributes(obj, attrs, True)
|
||||||
return obj
|
return obj
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] AddClippingPlane Fehler ({}):".format(role), ex)
|
print("[SECTION] AddClippingPlane Fehler ({}):".format(role), ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -179,13 +181,13 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
if z is None: return
|
if z is None: return
|
||||||
pts = z.get("linePts") or []
|
pts = z.get("linePts") or []
|
||||||
if len(pts) < 2:
|
if len(pts) < 2:
|
||||||
print("[SCHNITT] '{}' hat keine linePts".format(z.get("name")))
|
print("[SECTION] '{}' hat keine linePts".format(z.get("name")))
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
p1 = rg.Point3d(float(pts[0][0]), float(pts[0][1]), 0)
|
p1 = rg.Point3d(float(pts[0][0]), float(pts[0][1]), 0)
|
||||||
p2 = rg.Point3d(float(pts[1][0]), float(pts[1][1]), 0)
|
p2 = rg.Point3d(float(pts[1][0]), float(pts[1][1]), 0)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] linePts ungueltig:", ex)
|
print("[SECTION] linePts ungueltig:", ex)
|
||||||
return
|
return
|
||||||
dir_sign = 1 if int(z.get("dirSign", 1) or 1) >= 0 else -1
|
dir_sign = 1 if int(z.get("dirSign", 1) or 1) >= 0 else -1
|
||||||
depth_back = max(0.5, float(z.get("depthBack", 8.0) or 8.0))
|
depth_back = max(0.5, float(z.get("depthBack", 8.0) or 8.0))
|
||||||
@@ -197,7 +199,7 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
|
|
||||||
line_dir, view_dir, mid = _line_vectors(p1, p2, dir_sign)
|
line_dir, view_dir, mid = _line_vectors(p1, p2, dir_sign)
|
||||||
if line_dir is None:
|
if line_dir is None:
|
||||||
print("[SCHNITT] '{}' hat zu kurze Linie".format(z.get("name")))
|
print("[SECTION] '{}' hat zu kurze Linie".format(z.get("name")))
|
||||||
return
|
return
|
||||||
line_len = p1.DistanceTo(p2)
|
line_len = p1.DistanceTo(p2)
|
||||||
|
|
||||||
@@ -212,7 +214,7 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
|
|
||||||
vp_ids = _collect_viewport_ids(doc)
|
vp_ids = _collect_viewport_ids(doc)
|
||||||
if not vp_ids:
|
if not vp_ids:
|
||||||
print("[SCHNITT] keine Viewports — Plane wuerde nichts schneiden")
|
print("[SECTION] keine Viewports — Plane wuerde nichts schneiden")
|
||||||
return
|
return
|
||||||
|
|
||||||
n_planes = 0
|
n_planes = 0
|
||||||
@@ -250,7 +252,7 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
# Projektion: 'parallel' (klassischer Schnitt) oder 'perspective'
|
# Projektion: 'parallel' (klassischer Schnitt) oder 'perspective'
|
||||||
# (Schnittperspektive — perspektivische Section mit gleicher Cut-
|
# (Schnittperspektive — perspektivische Section mit gleicher Cut-
|
||||||
# Logik). Bei perspective wird Kamera leicht naeher geholt + FOV
|
# Logik). Bei perspective wird Kamera leicht naeher geholt + FOV
|
||||||
# gesetzt; Cut-Planes sind identisch.
|
# set; Cut-Planes sind identisch.
|
||||||
projection = (z.get("projection") or "parallel").strip().lower()
|
projection = (z.get("projection") or "parallel").strip().lower()
|
||||||
if projection not in ("parallel", "perspective"): projection = "parallel"
|
if projection not in ("parallel", "perspective"): projection = "parallel"
|
||||||
|
|
||||||
@@ -274,7 +276,7 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
cam_dist = max(50.0, depth_back * 3 + line_len)
|
cam_dist = max(50.0, depth_back * 3 + line_len)
|
||||||
# Bei Perspektive: Kamera + Target auf cam_z. Bei Parallel:
|
# Bei Perspektive: Kamera + Target auf cam_z. Bei Parallel:
|
||||||
# plane_z (Mitte Hoehenrange) — Z spielt eh keine Rolle
|
# plane_z (Mitte Hoehenrange) — Z spielt eh keine Rolle
|
||||||
# fuers Bild, aber sauber gesetzt fuer konsistente
|
# fuers Bild, aber sauber set fuer konsistente
|
||||||
# Kamera-Ausrichtung.
|
# Kamera-Ausrichtung.
|
||||||
view_z = cam_z if projection == "perspective" else plane_z
|
view_z = cam_z if projection == "perspective" else plane_z
|
||||||
cam_pos = rg.Point3d(
|
cam_pos = rg.Point3d(
|
||||||
@@ -291,6 +293,17 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
vp.ChangeToParallelProjection(True)
|
vp.ChangeToParallelProjection(True)
|
||||||
vp.SetCameraLocations(target, cam_pos)
|
vp.SetCameraLocations(target, cam_pos)
|
||||||
vp.CameraUp = rg.Vector3d(0, 0, 1)
|
vp.CameraUp = rg.Vector3d(0, 0, 1)
|
||||||
|
# Display-Mode auf 'Dossier Plan' — auch bei Perspektive, damit
|
||||||
|
# die Section-Hatches sichtbar sind. User kann manuell wechseln.
|
||||||
|
try:
|
||||||
|
from Rhino.Display import DisplayModeDescription
|
||||||
|
for dm in DisplayModeDescription.GetDisplayModes():
|
||||||
|
try:
|
||||||
|
if dm.EnglishName == "Dossier Plan":
|
||||||
|
vp.DisplayMode = dm
|
||||||
|
break
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
# Zoom auf Schnitt-BoundingBox + etwas Rand. Bei Perspektive
|
# Zoom auf Schnitt-BoundingBox + etwas Rand. Bei Perspektive
|
||||||
# macht ZoomBoundingBox auch Sinn — Rhino passt das FOV-Frame
|
# macht ZoomBoundingBox auch Sinn — Rhino passt das FOV-Frame
|
||||||
# entsprechend an.
|
# entsprechend an.
|
||||||
@@ -303,10 +316,10 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
vp.ZoomBoundingBox(bb)
|
vp.ZoomBoundingBox(bb)
|
||||||
view.Redraw()
|
view.Redraw()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] view setup:", ex)
|
print("[SECTION] view setup:", ex)
|
||||||
|
|
||||||
kind = "Schnitt" if cut_at_line else "Ansicht"
|
kind = "Schnitt" if cut_at_line else "Ansicht"
|
||||||
print("[SCHNITT] {} '{}' aktiviert: {} Plane(s), depthBack={:.1f}m".format(
|
print("[SECTION] {} '{}' aktiviert: {} Plane(s), depthBack={:.1f}m".format(
|
||||||
kind, z.get("name"), n_planes, depth_back))
|
kind, z.get("name"), n_planes, depth_back))
|
||||||
|
|
||||||
|
|
||||||
@@ -352,7 +365,7 @@ def save_pre_schnitt_view(doc):
|
|||||||
except Exception: key = _STICKY_PRE_VIEW + "_default"
|
except Exception: key = _STICKY_PRE_VIEW + "_default"
|
||||||
sc.sticky[key] = snap
|
sc.sticky[key] = snap
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] save view:", ex)
|
print("[SECTION] save view:", ex)
|
||||||
|
|
||||||
|
|
||||||
def restore_pre_schnitt_view(doc):
|
def restore_pre_schnitt_view(doc):
|
||||||
@@ -380,10 +393,10 @@ def restore_pre_schnitt_view(doc):
|
|||||||
view.Redraw()
|
view.Redraw()
|
||||||
try: del sc.sticky[key]
|
try: del sc.sticky[key]
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
print("[SCHNITT] Pre-Schnitt-View restored")
|
print("[SECTION] Pre-Schnitt-View restored")
|
||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] restore view:", ex)
|
print("[SECTION] restore view:", ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -436,7 +449,7 @@ def delete_schnitt_entry(doc, schnitt_id):
|
|||||||
doc.Strings.SetString("dossier_zeichnungsebenen",
|
doc.Strings.SetString("dossier_zeichnungsebenen",
|
||||||
json.dumps(new_lst, ensure_ascii=False))
|
json.dumps(new_lst, ensure_ascii=False))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] delete entry:", ex)
|
print("[SECTION] delete entry:", ex)
|
||||||
return False
|
return False
|
||||||
cleanup_schnitt_artifacts(doc, schnitt_id, active_id=active_id)
|
cleanup_schnitt_artifacts(doc, schnitt_id, active_id=active_id)
|
||||||
return True
|
return True
|
||||||
@@ -486,7 +499,7 @@ def create_schnitt_entry(doc, name, p1, p2, dir_sign=1, depth_back=8.0,
|
|||||||
doc.Strings.SetString("dossier_zeichnungsebenen",
|
doc.Strings.SetString("dossier_zeichnungsebenen",
|
||||||
json.dumps(lst, ensure_ascii=False))
|
json.dumps(lst, ensure_ascii=False))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] persist entry:", ex)
|
print("[SECTION] persist entry:", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 2D-Symbol auf Plan
|
# 2D-Symbol auf Plan
|
||||||
@@ -500,7 +513,7 @@ def create_schnitt_entry(doc, name, p1, p2, dir_sign=1, depth_back=8.0,
|
|||||||
attrs.SetUserString(_KEY_SCHNITT_ID, schnitt_id)
|
attrs.SetUserString(_KEY_SCHNITT_ID, schnitt_id)
|
||||||
doc.Objects.AddCurve(crv, attrs)
|
doc.Objects.AddCurve(crv, attrs)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] add symbol curve:", ex)
|
print("[SECTION] add symbol curve:", ex)
|
||||||
return schnitt_id
|
return schnitt_id
|
||||||
|
|
||||||
|
|
||||||
@@ -526,13 +539,13 @@ def activate_schnitt_by_id(doc, schnitt_id):
|
|||||||
or sc.sticky.get("zeichnungsebenen_bridge_ref")
|
or sc.sticky.get("zeichnungsebenen_bridge_ref")
|
||||||
if eb is None:
|
if eb is None:
|
||||||
# Fallback: direkt aktivieren ohne broadcast
|
# Fallback: direkt aktivieren ohne broadcast
|
||||||
print("[SCHNITT] keine EbenenBridge — direkt aktivieren")
|
print("[SECTION] keine EbenenBridge — direkt aktivieren")
|
||||||
activate_schnitt(doc, z)
|
activate_schnitt(doc, z)
|
||||||
return True
|
return True
|
||||||
eb._set_active_zeichnungsebene(z)
|
eb._set_active_zeichnungsebene(z)
|
||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] activate_by_id:", ex)
|
print("[SECTION] activate_by_id:", ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -543,7 +556,7 @@ class _SchnittDoubleClickHandler(Rhino.UI.MouseCallback):
|
|||||||
|
|
||||||
Wichtig: die Klicks selektieren das Curve vorab (Rhino-Default), wir
|
Wichtig: die Klicks selektieren das Curve vorab (Rhino-Default), wir
|
||||||
pruefen also einfach die aktuelle Selection. Bei Treffer wird der
|
pruefen also einfach die aktuelle Selection. Bei Treffer wird der
|
||||||
Schnitt aktiviert + e.Cancel=True gesetzt damit Rhinos default
|
Schnitt aktiviert + e.Cancel=True set damit Rhinos default
|
||||||
Edit-Modus nicht zusaetzlich aufpoppt."""
|
Edit-Modus nicht zusaetzlich aufpoppt."""
|
||||||
def OnMouseDoubleClick(self, e):
|
def OnMouseDoubleClick(self, e):
|
||||||
try:
|
try:
|
||||||
@@ -564,7 +577,7 @@ class _SchnittDoubleClickHandler(Rhino.UI.MouseCallback):
|
|||||||
return
|
return
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] OnMouseDoubleClick:", ex)
|
print("[SECTION] OnMouseDoubleClick:", ex)
|
||||||
|
|
||||||
|
|
||||||
def install_double_click_handler():
|
def install_double_click_handler():
|
||||||
@@ -580,9 +593,9 @@ def install_double_click_handler():
|
|||||||
h = _SchnittDoubleClickHandler()
|
h = _SchnittDoubleClickHandler()
|
||||||
h.Enabled = True
|
h.Enabled = True
|
||||||
sc.sticky["_dossier_schnitt_dblclick_handler"] = h
|
sc.sticky["_dossier_schnitt_dblclick_handler"] = h
|
||||||
print("[SCHNITT] Doppelklick-Handler aktiv")
|
print("[SECTION] Double-click handler active")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] install_double_click_handler:", ex)
|
print("[SECTION] install_double_click_handler:", ex)
|
||||||
|
|
||||||
|
|
||||||
def pick_schnitt_interactive(doc, defaults=None):
|
def pick_schnitt_interactive(doc, defaults=None):
|
||||||
@@ -596,7 +609,7 @@ def pick_schnitt_interactive(doc, defaults=None):
|
|||||||
# Project-Defaults als 2-stufiges Fallback (defaults > project > hardcoded)
|
# Project-Defaults als 2-stufiges Fallback (defaults > project > hardcoded)
|
||||||
proj_d = {}
|
proj_d = {}
|
||||||
try:
|
try:
|
||||||
import rhinopanel
|
import layers_panel as rhinopanel
|
||||||
ps = rhinopanel.load_project_settings(doc) or {}
|
ps = rhinopanel.load_project_settings(doc) or {}
|
||||||
proj_d = ps.get("defaults", {}) or {}
|
proj_d = ps.get("defaults", {}) or {}
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
@@ -629,7 +642,7 @@ def pick_schnitt_interactive(doc, defaults=None):
|
|||||||
p2 = rg.Point3d(p2.X, p2.Y, 0)
|
p2 = rg.Point3d(p2.X, p2.Y, 0)
|
||||||
|
|
||||||
if p1.DistanceTo(p2) < 0.01:
|
if p1.DistanceTo(p2) < 0.01:
|
||||||
print("[SCHNITT] Linie zu kurz")
|
print("[SECTION] Linie zu kurz")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Pick Blickrichtung (welche Seite ist "hinten")
|
# Pick Blickrichtung (welche Seite ist "hinten")
|
||||||
@@ -674,7 +687,7 @@ def pick_schnitt_interactive(doc, defaults=None):
|
|||||||
sym_path = _el._layer_path_schnittlinie(doc, geschoss["name"])
|
sym_path = _el._layer_path_schnittlinie(doc, geschoss["name"])
|
||||||
symbol_layer_idx = _el._ensure_layer(doc, sym_path)
|
symbol_layer_idx = _el._ensure_layer(doc, sym_path)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] symbol-layer resolve:", ex)
|
print("[SECTION] symbol-layer resolve:", ex)
|
||||||
|
|
||||||
sid = create_schnitt_entry(doc, auto_name, p1, p2,
|
sid = create_schnitt_entry(doc, auto_name, p1, p2,
|
||||||
dir_sign=dir_sign, depth_back=depth_back,
|
dir_sign=dir_sign, depth_back=depth_back,
|
||||||
|
|||||||
+277
-22
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
startup.py
|
startup.py
|
||||||
Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine
|
Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine
|
||||||
@@ -13,16 +15,28 @@ import json
|
|||||||
import Rhino
|
import Rhino
|
||||||
import scriptcontext as sc
|
import scriptcontext as sc
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if _HERE not in sys.path:
|
||||||
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
|
# Splash SOFORT als allererstes — bevor irgendwas anderes passiert, damit der
|
||||||
|
# Nutzer waehrend Python-Imports + Panel-Registrierung nicht in eine schwarze
|
||||||
|
# Rhino-Oberflaeche schaut. Skipt automatisch wenn Launcher seinen eigenen
|
||||||
|
# Splash zeigt (Owner-Marker-Check).
|
||||||
|
# Skipt auch wenn Plugin bereits in dieser Session geladen ist (z.B. Cmd+N).
|
||||||
|
if not sc.sticky.get("_dossier_startup_scheduled"):
|
||||||
|
try:
|
||||||
|
import _startup_splash as _splash_first
|
||||||
|
_splash_first.show()
|
||||||
|
except Exception as _ex_splash:
|
||||||
|
print("[STARTUP] splash error:", _ex_splash)
|
||||||
|
|
||||||
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
|
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
|
||||||
print("[STARTUP] Python: {}".format(sys.version))
|
print("[STARTUP] Python: {}".format(sys.version))
|
||||||
print("[STARTUP] Implementation: {}".format(
|
print("[STARTUP] Implementation: {}".format(
|
||||||
sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)"))
|
sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)"))
|
||||||
print("[STARTUP] Platform: {}".format(sys.platform))
|
print("[STARTUP] Platform: {}".format(sys.platform))
|
||||||
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
if _HERE not in sys.path:
|
|
||||||
sys.path.insert(0, _HERE)
|
|
||||||
|
|
||||||
# Pfad zur Custom-UI (Toolbars/Sidebar) — wird einmal pro Session geladen
|
# Pfad zur Custom-UI (Toolbars/Sidebar) — wird einmal pro Session geladen
|
||||||
_UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
|
_UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
|
||||||
|
|
||||||
@@ -30,13 +44,13 @@ _UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
|
|||||||
# Muss synchron sein mit launcher/modules.json. Wenn neue Module dazukommen,
|
# Muss synchron sein mit launcher/modules.json. Wenn neue Module dazukommen,
|
||||||
# beide Stellen pflegen.
|
# beide Stellen pflegen.
|
||||||
_MODULE_TO_PY = {
|
_MODULE_TO_PY = {
|
||||||
"ebenen": "rhinopanel",
|
"ebenen": "layers_panel",
|
||||||
"oberleiste": "oberleiste",
|
"oberleiste": "toolbar",
|
||||||
"ausschnitte": "ausschnitte",
|
"ausschnitte": "ausschnitte",
|
||||||
"gestaltung": "gestaltung",
|
"gestaltung": "styles",
|
||||||
"werkzeuge": "werkzeuge",
|
"werkzeuge": "tools",
|
||||||
"overrides": "overrides_panel",
|
"overrides": "overrides_panel",
|
||||||
"dimensionen": "dimensionen",
|
"dimensionen": "dimensions",
|
||||||
"layouts": "layouts",
|
"layouts": "layouts",
|
||||||
"elemente": "elemente",
|
"elemente": "elemente",
|
||||||
}
|
}
|
||||||
@@ -74,6 +88,178 @@ def _migrate_active_doc(*_):
|
|||||||
print("[STARTUP] Migration:", ex)
|
print("[STARTUP] Migration:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
_DOC_FLAG_VIEW_MODES = "dossier_view_modes_initialized"
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_default_display_modes(doc):
|
||||||
|
"""Setzt einmalig pro Doc die Display-Modes auf die Dossier-Defaults:
|
||||||
|
- Parallel-Projektionen (Top/Front/Right/Schnitt-parallel) -> 'Dossier Plan'
|
||||||
|
- Perspektive (Perspective/Schnittperspektive) -> 'Dossier 3D'
|
||||||
|
Persistiert einen Flag in doc.Strings → laeuft nur EINMAL pro Doc.
|
||||||
|
User-Overrides (manuelles Wechseln) bleiben damit erhalten.
|
||||||
|
"""
|
||||||
|
if doc is None: return
|
||||||
|
try:
|
||||||
|
if doc.Strings.GetValue(_DOC_FLAG_VIEW_MODES) == "1":
|
||||||
|
return # schon initialisiert
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from Rhino.Display import DisplayModeDescription
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] view-modes: DMD not available:", ex); return
|
||||||
|
|
||||||
|
# Mode-Lookup per Name
|
||||||
|
mode_plan = mode_3d = None
|
||||||
|
try:
|
||||||
|
for dm in DisplayModeDescription.GetDisplayModes():
|
||||||
|
try:
|
||||||
|
n = dm.EnglishName
|
||||||
|
if n == "Dossier Plan": mode_plan = dm
|
||||||
|
elif n == "Dossier 3D": mode_3d = dm
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] view-modes: mode list error:", ex); return
|
||||||
|
|
||||||
|
if mode_plan is None and mode_3d is None:
|
||||||
|
print("[STARTUP] view-modes: no Dossier display modes found — skip")
|
||||||
|
return
|
||||||
|
|
||||||
|
n_set = 0
|
||||||
|
try:
|
||||||
|
for view in doc.Views:
|
||||||
|
try:
|
||||||
|
vp = view.ActiveViewport
|
||||||
|
if vp is None: continue
|
||||||
|
is_par = bool(vp.IsParallelProjection)
|
||||||
|
target = mode_plan if is_par else mode_3d
|
||||||
|
if target is None: continue
|
||||||
|
try:
|
||||||
|
vp.DisplayMode = target
|
||||||
|
n_set += 1
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] view-modes set ({}): {}".format(
|
||||||
|
vp.Name, ex))
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] view-modes iterate:", ex)
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString(_DOC_FLAG_VIEW_MODES, "1")
|
||||||
|
except Exception: pass
|
||||||
|
print("[STARTUP] view-modes: {} viewport(s) set".format(n_set))
|
||||||
|
|
||||||
|
|
||||||
|
_DOC_FLAG_VIEW_MAXIMIZED = "dossier_top_view_maximized"
|
||||||
|
|
||||||
|
|
||||||
|
def _maximize_top_view(doc):
|
||||||
|
"""Maximiert den Top-Viewport (= einzige aktive View statt 4-Viewport-
|
||||||
|
Default). Persistiert Flag in doc.Strings → laeuft nur EINMAL pro Doc.
|
||||||
|
User-Overrides (manuelles Wechseln zu 4-View etc) bleiben erhalten."""
|
||||||
|
if doc is None: return
|
||||||
|
try:
|
||||||
|
if doc.Strings.GetValue(_DOC_FLAG_VIEW_MAXIMIZED) == "1":
|
||||||
|
return # schon initialisiert
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
top_view = None
|
||||||
|
for view in doc.Views:
|
||||||
|
try:
|
||||||
|
vp = view.ActiveViewport
|
||||||
|
if vp is None: continue
|
||||||
|
if vp.Name == "Top":
|
||||||
|
top_view = view
|
||||||
|
break
|
||||||
|
except Exception: pass
|
||||||
|
if top_view is None:
|
||||||
|
print("[STARTUP] view-max: no Top viewport found")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
top_view.Maximized = True
|
||||||
|
doc.Views.ActiveView = top_view
|
||||||
|
doc.Views.Redraw()
|
||||||
|
print("[STARTUP] view-max: Top viewport maximized")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] view-max set error:", ex); return
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString(_DOC_FLAG_VIEW_MAXIMIZED, "1")
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] view-max:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
_DOC_FLAG_UNIT_CHECKED = "dossier_unit_checked"
|
||||||
|
|
||||||
|
|
||||||
|
def _check_doc_unit(doc):
|
||||||
|
"""Prueft ob doc.ModelUnitSystem der DOSSIER-Project-Setting-Arbeitseinheit
|
||||||
|
entspricht. Bei Mismatch: Modal-Dialog mit "Umstellen" / "Spaeter"-Option.
|
||||||
|
|
||||||
|
Idempotent pro Doc via doc.Strings-Flag — wird nur EINMAL pro Doc gefragt.
|
||||||
|
Wenn User "Spaeter" waehlt, fragt DOSSIER beim selben Doc nicht mehr (Flag
|
||||||
|
bleibt set). Fuer erneute Frage: doc.Strings-Key loeschen.
|
||||||
|
"""
|
||||||
|
if doc is None: return
|
||||||
|
try:
|
||||||
|
if doc.Strings.GetValue(_DOC_FLAG_UNIT_CHECKED) == "1":
|
||||||
|
return
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
import layers_panel as rhinopanel
|
||||||
|
target_unit_str = rhinopanel.get_project_unit(doc)
|
||||||
|
target_unit_enum = rhinopanel.get_project_unit_enum(doc)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] unit-check: project setting read error:", ex)
|
||||||
|
return
|
||||||
|
if target_unit_enum is None: return
|
||||||
|
try:
|
||||||
|
current = doc.ModelUnitSystem
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if current == target_unit_enum:
|
||||||
|
# Schon passend → einmalig Flag setzen, beim naechsten Open kein Check
|
||||||
|
try: doc.Strings.SetString(_DOC_FLAG_UNIT_CHECKED, "1")
|
||||||
|
except Exception: pass
|
||||||
|
return
|
||||||
|
# Mismatch — Dialog zeigen
|
||||||
|
try:
|
||||||
|
import Eto.Forms as ef
|
||||||
|
msg = ("Dieses Doc ist in '{}'.\n"
|
||||||
|
"DOSSIER-Projekteinstellung: '{}'.\n\n"
|
||||||
|
"Doc auf '{}' umstellen?\n"
|
||||||
|
"(Bestehende Geometrie wird skaliert)").format(
|
||||||
|
str(current), target_unit_str, target_unit_str)
|
||||||
|
result = ef.MessageBox.Show(
|
||||||
|
msg, "DOSSIER — Arbeitseinheit",
|
||||||
|
ef.MessageBoxButtons.YesNo,
|
||||||
|
ef.MessageBoxType.Question)
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString(_DOC_FLAG_UNIT_CHECKED, "1")
|
||||||
|
except Exception: pass
|
||||||
|
if str(result).lower().endswith("yes"):
|
||||||
|
# _-Units _<unit> _Yes konvertiert Geometrie automatisch mit
|
||||||
|
unit_cmd = {"meters": "_Meters",
|
||||||
|
"millimeters": "_Millimeters",
|
||||||
|
"centimeters": "_Centimeters"}.get(target_unit_str)
|
||||||
|
if unit_cmd:
|
||||||
|
try:
|
||||||
|
Rhino.RhinoApp.RunScript(
|
||||||
|
"_-Units _Model {} _Yes _EnterEnd".format(unit_cmd),
|
||||||
|
False)
|
||||||
|
print("[STARTUP] Doc auf {} umgestellt (Geometrie skaliert)".format(
|
||||||
|
target_unit_str))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] unit-convert RunScript:", ex)
|
||||||
|
else:
|
||||||
|
print("[STARTUP] User declined unit change — doc stays {}".format(current))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] unit-check dialog error:", ex)
|
||||||
|
|
||||||
|
|
||||||
def _on_doc_opened(sender, e):
|
def _on_doc_opened(sender, e):
|
||||||
"""Greift bei jedem geoeffneten Doc nach Rhino-Start. Migration ist
|
"""Greift bei jedem geoeffneten Doc nach Rhino-Start. Migration ist
|
||||||
idempotent (Flag in doc.Strings)."""
|
idempotent (Flag in doc.Strings)."""
|
||||||
@@ -81,6 +267,9 @@ def _on_doc_opened(sender, e):
|
|||||||
doc = e.Document if hasattr(e, "Document") else Rhino.RhinoDoc.ActiveDoc
|
doc = e.Document if hasattr(e, "Document") else Rhino.RhinoDoc.ActiveDoc
|
||||||
import panel_base
|
import panel_base
|
||||||
panel_base.migrate_to_dossier(doc)
|
panel_base.migrate_to_dossier(doc)
|
||||||
|
_assign_default_display_modes(doc)
|
||||||
|
_maximize_top_view(doc)
|
||||||
|
_check_doc_unit(doc)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[STARTUP] _on_doc_opened:", ex)
|
print("[STARTUP] _on_doc_opened:", ex)
|
||||||
|
|
||||||
@@ -92,9 +281,9 @@ def _hint_dossier_ui():
|
|||||||
manuell laden kann. Rhino merkt sich die Anordnung dann persistent."""
|
manuell laden kann. Rhino merkt sich die Anordnung dann persistent."""
|
||||||
if not os.path.isfile(_UI_FILE):
|
if not os.path.isfile(_UI_FILE):
|
||||||
return
|
return
|
||||||
print("[STARTUP] DOSSIERUI gefunden: {}".format(_UI_FILE))
|
print("[STARTUP] DOSSIERUI found: {}".format(_UI_FILE))
|
||||||
print("[STARTUP] Einmalig laden: Window -> Window Layout -> Open -> obige Datei")
|
print("[STARTUP] Load once: Window -> Window Layout -> Open -> file above")
|
||||||
print("[STARTUP] Anordnung bleibt danach ueber Rhino-Restarts erhalten.")
|
print("[STARTUP] Layout persists across Rhino restarts.")
|
||||||
|
|
||||||
|
|
||||||
def _load_all(sender, e):
|
def _load_all(sender, e):
|
||||||
@@ -103,7 +292,8 @@ def _load_all(sender, e):
|
|||||||
Rhino.RhinoApp.Idle -= _load_all
|
Rhino.RhinoApp.Idle -= _load_all
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
print("[STARTUP] Lade DOSSIER-Panels...")
|
# Splash wird ganz oben in startup.py (vor diesem Idle) gezeigt.
|
||||||
|
print("[STARTUP] Loading DOSSIER panels...")
|
||||||
# Migration einmal fuer das beim Start aktive Doc
|
# Migration einmal fuer das beim Start aktive Doc
|
||||||
_migrate_active_doc()
|
_migrate_active_doc()
|
||||||
# Und Listener fuer spaeter geoeffnete Docs registrieren
|
# Und Listener fuer spaeter geoeffnete Docs registrieren
|
||||||
@@ -111,34 +301,92 @@ def _load_all(sender, e):
|
|||||||
Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened
|
Rhino.RhinoDoc.EndOpenDocument += _on_doc_opened
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[STARTUP] EndOpenDocument-Hook:", ex)
|
print("[STARTUP] EndOpenDocument-Hook:", ex)
|
||||||
|
try:
|
||||||
|
Rhino.RhinoDoc.NewDocument += _on_doc_opened
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] NewDocument-Hook:", ex)
|
||||||
# Projekt-Config bestimmt, welche Module geladen werden. Ohne Config
|
# Projekt-Config bestimmt, welche Module geladen werden. Ohne Config
|
||||||
# (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles.
|
# (kein Launcher benutzt, oder Datei nicht da) laedt der Host alles.
|
||||||
config = _read_project_config()
|
config = _read_project_config()
|
||||||
if config and isinstance(config.get("modules"), list):
|
if config and isinstance(config.get("modules"), list):
|
||||||
enabled_ids = [m for m in config["modules"] if m in _MODULE_TO_PY]
|
enabled_ids = [m for m in config["modules"] if m in _MODULE_TO_PY]
|
||||||
unknown = [m for m in config["modules"] if m not in _MODULE_TO_PY]
|
unknown = [m for m in config["modules"] if m not in _MODULE_TO_PY]
|
||||||
print("[STARTUP] Projekt: '{}'".format(config.get("name") or "?"))
|
print("[STARTUP] Project: '{}'".format(config.get("name") or "?"))
|
||||||
print("[STARTUP] Aktivierte Module: {}".format(", ".join(enabled_ids) or "(keine)"))
|
print("[STARTUP] Active modules: {}".format(", ".join(enabled_ids) or "(keine)"))
|
||||||
if unknown:
|
if unknown:
|
||||||
print("[STARTUP] Unbekannte Modul-IDs in Config: {}".format(unknown))
|
print("[STARTUP] Unknown module IDs in config: {}".format(unknown))
|
||||||
else:
|
else:
|
||||||
enabled_ids = _ALL_MODULES
|
enabled_ids = _ALL_MODULES
|
||||||
print("[STARTUP] Keine dossier.project.json — alle Module laden")
|
print("[STARTUP] No dossier.project.json — loading all modules")
|
||||||
# massstab.py wird als Library mitgeladen (von oberleiste/ausschnitte/...)
|
# massstab.py wird als Library mitgeladen (von oberleiste/ausschnitte/...)
|
||||||
# und braucht hier nicht mehr als eigenstaendiges Panel zu erscheinen.
|
# und braucht hier nicht mehr als eigenstaendiges Panel zu erscheinen.
|
||||||
|
# Imports messen — das ist der grosse Block der bisher unmeasured war
|
||||||
|
import time as _t
|
||||||
|
import panel_base as _pb
|
||||||
for mod_id in enabled_ids:
|
for mod_id in enabled_ids:
|
||||||
py_name = _MODULE_TO_PY[mod_id]
|
py_name = _MODULE_TO_PY[mod_id]
|
||||||
|
_t_imp = _t.time()
|
||||||
try:
|
try:
|
||||||
__import__(py_name)
|
__import__(py_name)
|
||||||
|
_pb._t_mark("import", mod_id, _t_imp)
|
||||||
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
|
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
|
print("[STARTUP] {} ({}) ERROR: {}".format(mod_id, py_name, ex))
|
||||||
# Text-Editor Doppelklick-Hook fuer DOSSIER-Texte
|
# Text-Editor Doppelklick-Hook fuer DOSSIER-Texte
|
||||||
|
_t_te = _t.time()
|
||||||
try:
|
try:
|
||||||
import text_editor
|
import text_editor
|
||||||
text_editor._ensure_double_click_hook()
|
text_editor._ensure_double_click_hook()
|
||||||
|
_pb._t_mark("hook", "text_editor", _t_te)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[STARTUP] text_editor hook:", ex)
|
print("[STARTUP] text_editor hook:", ex)
|
||||||
|
# Display-Modes auf Default fuer aktives Doc setzen (einmalig)
|
||||||
|
_t_vm = _t.time()
|
||||||
|
try:
|
||||||
|
_assign_default_display_modes(Rhino.RhinoDoc.ActiveDoc)
|
||||||
|
_pb._t_mark("post_init", "view_modes", _t_vm)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] view-modes assign error:", ex)
|
||||||
|
# Top-View maximieren (= einzige aktive View statt 4-View Default)
|
||||||
|
try:
|
||||||
|
_maximize_top_view(Rhino.RhinoDoc.ActiveDoc)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] view-max:", ex)
|
||||||
|
# Unit-Check fuer das beim Start aktive Doc — fragt einmal pro Doc
|
||||||
|
# wenn doc.ModelUnitSystem != Project-Setting
|
||||||
|
_t_uc = _t.time()
|
||||||
|
try:
|
||||||
|
_check_doc_unit(Rhino.RhinoDoc.ActiveDoc)
|
||||||
|
_pb._t_mark("post_init", "unit_check", _t_uc)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] unit-check active doc error:", ex)
|
||||||
|
# Aliases + Shortcuts (Defaults aus rhino/aliases/shortcuts_default.json
|
||||||
|
# + User-Overrides aus dossier_settings.json) registrieren. Idempotent —
|
||||||
|
# SetMacro/SetMacro ueberschreibt presente Eintraege. Wenn Bridge noch
|
||||||
|
# nicht in sticky liegt (elemente-Panel noch nicht geladen) ist das ok,
|
||||||
|
# die Aliases zeigen auf das Dispatch-Skript das die Bridge lazy aus
|
||||||
|
# sticky liest.
|
||||||
|
_t_al = _t.time()
|
||||||
|
try:
|
||||||
|
from aliases import loader as _alias_loader
|
||||||
|
_na, _nf, _nc, _ns = _alias_loader.apply_all()
|
||||||
|
print("[STARTUP] Aliases: {} alias, {} fkey, {} cmd, {} skipped"
|
||||||
|
.format(_na, _nf, _nc, _ns))
|
||||||
|
_pb._t_mark("post_init", "aliases", _t_al)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] alias-loader error:", ex)
|
||||||
|
# BeginCommand-Hook: Gestaltung-Panel oeffnen bei Drawing-Commands
|
||||||
|
try:
|
||||||
|
import begin_cmd_hook
|
||||||
|
begin_cmd_hook.install(verbose=True)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] begin_cmd_hook:", ex)
|
||||||
|
# Welcome-Screen einmalig pro Version (markiert sich selbst)
|
||||||
|
try:
|
||||||
|
import welcome
|
||||||
|
welcome.show_welcome(force=False)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] welcome:", ex)
|
||||||
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
|
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
|
||||||
_hint_dossier_ui()
|
_hint_dossier_ui()
|
||||||
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
|
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
|
||||||
@@ -165,11 +413,18 @@ def _load_all(sender, e):
|
|||||||
with open(os.path.join(marker_dir, "plugin_loaded.flag"), "w") as f:
|
with open(os.path.join(marker_dir, "plugin_loaded.flag"), "w") as f:
|
||||||
f.write("ok")
|
f.write("ok")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[STARTUP] marker schreiben:", ex)
|
print("[STARTUP] marker write error:", ex)
|
||||||
import threading
|
import threading
|
||||||
threading.Timer(2.0, _write_marker).start()
|
threading.Timer(2.0, _write_marker).start()
|
||||||
print("[STARTUP] Fertig")
|
print("[STARTUP] Done")
|
||||||
|
|
||||||
|
|
||||||
Rhino.RhinoApp.Idle += _load_all
|
# Idempotency-Guard: wenn beide Pfade gleichzeitig feuern (C#-Plugin OnLoad
|
||||||
print("[STARTUP] geplant - laedt sobald Rhino idle ist")
|
# UND legacy StartupCommands-XML), nur das erste registriert den Idle-Loader.
|
||||||
|
# Verhindert doppelte Panel-Registrierung + doppelte Listener.
|
||||||
|
if sc.sticky.get("_dossier_startup_scheduled"):
|
||||||
|
print("[STARTUP] already scheduled — skip (parallel call)")
|
||||||
|
else:
|
||||||
|
sc.sticky["_dossier_startup_scheduled"] = True
|
||||||
|
Rhino.RhinoApp.Idle += _load_all
|
||||||
|
print("[STARTUP] scheduled — loads when Rhino is idle")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
gestaltung.py
|
gestaltung.py
|
||||||
GESTALTUNG-Panel: Attribute der Selektion (Farbe, Stiftdicke, Linientyp,
|
GESTALTUNG-Panel: Attribute der Selektion (Farbe, Stiftdicke, Linientyp,
|
||||||
@@ -48,11 +50,11 @@ def _sync_plot_color_to_display(attrs):
|
|||||||
else:
|
else:
|
||||||
attrs.PlotColorSource = _PLOT_FROM_LAYER
|
attrs.PlotColorSource = _PLOT_FROM_LAYER
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] sync plot-color:", ex)
|
print("[STYLES] sync plot-color:", ex)
|
||||||
|
|
||||||
_FILL_KEY = "ebenen_fill_hatch_id"
|
_FILL_KEY = "ebenen_fill_hatch_id"
|
||||||
_FILL_SOURCE_KEY = "ebenen_fill_source" # "layer" oder "object"
|
_FILL_SOURCE_KEY = "ebenen_fill_source" # "layer" oder "object"
|
||||||
_FILL_OWNER_KEY = "ebenen_fill_owner" # Curve-ID, auf Hatch gesetzt
|
_FILL_OWNER_KEY = "ebenen_fill_owner" # Curve-ID, auf Hatch set
|
||||||
_NO_FILL_KEY = "ebenen_no_fill" # "1" wenn User Fuellung explizit aus hat
|
_NO_FILL_KEY = "ebenen_no_fill" # "1" wenn User Fuellung explizit aus hat
|
||||||
|
|
||||||
# Loop-Guard fuer Live-Update
|
# Loop-Guard fuer Live-Update
|
||||||
@@ -99,7 +101,7 @@ def _save_pending_hatch(curve_id, hatch_obj):
|
|||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
}
|
}
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] save pending-hatch err:", ex)
|
print("[STYLES] save pending-hatch err:", ex)
|
||||||
return
|
return
|
||||||
m = sc.sticky.get("gestaltung_pending_hatch")
|
m = sc.sticky.get("gestaltung_pending_hatch")
|
||||||
if not isinstance(m, dict):
|
if not isinstance(m, dict):
|
||||||
@@ -128,7 +130,7 @@ def _restore_hatch_from_pending(doc, obj, meta):
|
|||||||
new_hatches = rg.Hatch.Create(geom,
|
new_hatches = rg.Hatch.Create(geom,
|
||||||
meta["pattern_idx"], meta["rotation"], meta["scale"], 0.0)
|
meta["pattern_idx"], meta["rotation"], meta["scale"], 0.0)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] restore Hatch.Create:", ex)
|
print("[STYLES] restore Hatch.Create:", ex)
|
||||||
return False
|
return False
|
||||||
if not new_hatches or len(new_hatches) == 0: return False
|
if not new_hatches or len(new_hatches) == 0: return False
|
||||||
new_attrs = Rhino.DocObjects.ObjectAttributes()
|
new_attrs = Rhino.DocObjects.ObjectAttributes()
|
||||||
@@ -148,7 +150,7 @@ def _restore_hatch_from_pending(doc, obj, meta):
|
|||||||
try:
|
try:
|
||||||
hatch_id = doc.Objects.AddHatch(new_hatches[0], new_attrs)
|
hatch_id = doc.Objects.AddHatch(new_hatches[0], new_attrs)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] restore AddHatch:", ex)
|
print("[STYLES] restore AddHatch:", ex)
|
||||||
return False
|
return False
|
||||||
if hatch_id == System.Guid.Empty: return False
|
if hatch_id == System.Guid.Empty: return False
|
||||||
try:
|
try:
|
||||||
@@ -171,7 +173,7 @@ def _color_to_hex(c):
|
|||||||
try:
|
try:
|
||||||
return "#{:02x}{:02x}{:02x}".format(int(c.R), int(c.G), int(c.B))
|
return "#{:02x}{:02x}{:02x}".format(int(c.R), int(c.G), int(c.B))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] color-hex Fehler:", ex)
|
print("[STYLES] color-hex Fehler:", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -187,9 +189,97 @@ def _hex_to_color(h):
|
|||||||
return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
||||||
|
|
||||||
|
|
||||||
|
# Default-Pen ("Stift fuer neue Objekte"): wird in on_add auf frisch
|
||||||
|
# gezeichnete Kurven gestempelt, solange aktiv. Persistiert in Sticky und
|
||||||
|
# gilt bis der User wieder auf "Nach Ebene" zurueckstellt. Wird vom selben
|
||||||
|
# UI-Block gesetzt wie der Selektions-Pen — nur ohne Auswahl (siehe Setter).
|
||||||
|
_DEFAULT_PEN_KEY = "_dossier_default_pen"
|
||||||
|
|
||||||
|
|
||||||
|
def _default_pen():
|
||||||
|
p = sc.sticky.get(_DEFAULT_PEN_KEY)
|
||||||
|
if not isinstance(p, dict):
|
||||||
|
p = {}
|
||||||
|
return {
|
||||||
|
"colorSource": p.get("colorSource", "layer"),
|
||||||
|
"color": p.get("color"),
|
||||||
|
"lwSource": p.get("lwSource", "layer"),
|
||||||
|
"lw": p.get("lw"),
|
||||||
|
"linetypeSource": p.get("linetypeSource", "layer"),
|
||||||
|
"linetype": p.get("linetype"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _set_default_pen(**changes):
|
||||||
|
p = _default_pen()
|
||||||
|
p.update(changes)
|
||||||
|
sc.sticky[_DEFAULT_PEN_KEY] = p
|
||||||
|
|
||||||
|
|
||||||
|
def _default_pen_active(pen=None):
|
||||||
|
p = pen or _default_pen()
|
||||||
|
return (p["colorSource"] == "object" or p["lwSource"] == "object"
|
||||||
|
or p["linetypeSource"] == "object")
|
||||||
|
|
||||||
|
|
||||||
|
def _has_selection(doc):
|
||||||
|
return bool(list(doc.Objects.GetSelectedObjects(False, False)))
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_pen_to_attrs(doc, a, pen):
|
||||||
|
"""Stempelt den aktiven Default-Pen auf eine Attribut-Kopie (in-place)."""
|
||||||
|
if pen["colorSource"] == "object":
|
||||||
|
a.ColorSource = _FROM_OBJECT
|
||||||
|
if pen["color"]:
|
||||||
|
a.ObjectColor = _hex_to_color(pen["color"])
|
||||||
|
_sync_plot_color_to_display(a)
|
||||||
|
if pen["lwSource"] == "object" and pen["lw"] is not None:
|
||||||
|
a.PlotWeightSource = _LW_FROM_OBJECT
|
||||||
|
try:
|
||||||
|
import massstab
|
||||||
|
massstab.write_plotweight(doc, a, float(pen["lw"]))
|
||||||
|
except Exception:
|
||||||
|
a.PlotWeight = float(pen["lw"])
|
||||||
|
if pen["linetypeSource"] == "object" and pen["linetype"]:
|
||||||
|
idx = -1
|
||||||
|
try: idx = doc.Linetypes.Find(pen["linetype"], True)
|
||||||
|
except Exception: idx = -1
|
||||||
|
if idx >= 0:
|
||||||
|
a.LinetypeSource = _LT_FROM_OBJECT
|
||||||
|
a.LinetypeIndex = idx
|
||||||
|
|
||||||
|
|
||||||
|
def _new_object_pen_summary(doc, pen):
|
||||||
|
"""PenBlock-kompatibles Objekt fuer den leeren Panel-Zustand: zeigt den
|
||||||
|
aktiven Default-Pen, "Nach Ebene"-Vorschau aus der aktuellen Ebene."""
|
||||||
|
cur = doc.Layers.CurrentLayer
|
||||||
|
layer_color = _color_to_hex(cur.Color)
|
||||||
|
try:
|
||||||
|
import massstab as _ms
|
||||||
|
layer_lw = round(_ms.read_plotweight(cur), 4)
|
||||||
|
except Exception:
|
||||||
|
layer_lw = round(cur.PlotWeight, 4)
|
||||||
|
layer_lt = _linetype_name(doc, cur.LinetypeIndex)
|
||||||
|
return {
|
||||||
|
"colorSource": pen["colorSource"],
|
||||||
|
"color": pen["color"] or layer_color,
|
||||||
|
"layerColor": layer_color,
|
||||||
|
"lwSource": pen["lwSource"],
|
||||||
|
"lw": pen["lw"] if pen["lw"] is not None else layer_lw,
|
||||||
|
"layerLw": layer_lw,
|
||||||
|
"linetypeSource": pen["linetypeSource"],
|
||||||
|
"linetype": pen["linetype"] or layer_lt,
|
||||||
|
"layerLinetype": layer_lt,
|
||||||
|
"linetypes": _all_linetypes(doc),
|
||||||
|
"layerName": _safe_layer_label(doc, cur, doc.Layers.CurrentLayerIndex),
|
||||||
|
"geometryKind": "curveOpen",
|
||||||
|
"active": _default_pen_active(pen),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _force_load_linetypes(doc):
|
def _force_load_linetypes(doc):
|
||||||
"""Rhinos Linetype-Tabelle wird lazy initialisiert — wir triggern es."""
|
"""Rhinos Linetype-Tabelle wird lazy initialisiert — wir triggern es."""
|
||||||
# 1) Eingebaute Methode (falls vorhanden)
|
# 1) Eingebaute Methode (falls present)
|
||||||
for method_name in ("LoadDefaultLinetypes", "LoadDefaults", "LoadStandardLinetypes"):
|
for method_name in ("LoadDefaultLinetypes", "LoadDefaults", "LoadStandardLinetypes"):
|
||||||
try:
|
try:
|
||||||
getattr(doc.Linetypes, method_name)()
|
getattr(doc.Linetypes, method_name)()
|
||||||
@@ -289,17 +379,17 @@ def _ebene_fill_for_layer(doc, layer):
|
|||||||
except Exception:
|
except Exception:
|
||||||
code = None
|
code = None
|
||||||
if not code:
|
if not code:
|
||||||
print("[GESTALTUNG] _ebene_fill_for_layer: kein dossier_code auf Layer idx={}".format(
|
print("[STYLES] _ebene_fill_for_layer: kein dossier_code auf Layer idx={}".format(
|
||||||
getattr(layer, "LayerIndex", "?")))
|
getattr(layer, "LayerIndex", "?")))
|
||||||
return None
|
return None
|
||||||
raw = doc.Strings.GetValue("dossier_ebenen")
|
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
if not raw:
|
if not raw:
|
||||||
print("[GESTALTUNG] _ebene_fill_for_layer: dossier_ebenen leer in doc.Strings")
|
print("[STYLES] _ebene_fill_for_layer: dossier_ebenen leer in doc.Strings")
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
ebenen = json.loads(raw)
|
ebenen = json.loads(raw)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] _ebene_fill_for_layer: json-Fehler:", ex)
|
print("[STYLES] _ebene_fill_for_layer: json-Fehler:", ex)
|
||||||
return None
|
return None
|
||||||
if not isinstance(ebenen, list): return None
|
if not isinstance(ebenen, list): return None
|
||||||
# Rekursiv durch Tree — Sub-Ebenen sind in children verschachtelt
|
# Rekursiv durch Tree — Sub-Ebenen sind in children verschachtelt
|
||||||
@@ -318,7 +408,7 @@ def _ebene_fill_for_layer(doc, layer):
|
|||||||
if True:
|
if True:
|
||||||
f = e.get("fill")
|
f = e.get("fill")
|
||||||
if not isinstance(f, dict):
|
if not isinstance(f, dict):
|
||||||
print("[GESTALTUNG] _ebene_fill_for_layer: Ebene code={} hat KEIN fill-Feld".format(code))
|
print("[STYLES] _ebene_fill_for_layer: Ebene code={} has NO fill field".format(code))
|
||||||
return None
|
return None
|
||||||
# lw: Strichstaerke der Hatch-Linien in mm. None = "wie Stift der Ebene"
|
# lw: Strichstaerke der Hatch-Linien in mm. None = "wie Stift der Ebene"
|
||||||
# (ColorSource/PlotWeightSource bleibt auf FromLayer).
|
# (ColorSource/PlotWeightSource bleibt auf FromLayer).
|
||||||
@@ -338,9 +428,9 @@ def _ebene_fill_for_layer(doc, layer):
|
|||||||
"rotation": float(f.get("rotation", 0)) if f.get("rotation") is not None else 0.0,
|
"rotation": float(f.get("rotation", 0)) if f.get("rotation") is not None else 0.0,
|
||||||
"lw": lw_val,
|
"lw": lw_val,
|
||||||
}
|
}
|
||||||
print("[GESTALTUNG] _ebene_fill_for_layer code={} -> {}".format(code, result))
|
print("[STYLES] _ebene_fill_for_layer code={} -> {}".format(code, result))
|
||||||
return result
|
return result
|
||||||
print("[GESTALTUNG] _ebene_fill_for_layer: code={} nicht in dossier_ebenen gefunden".format(code))
|
print("[STYLES] _ebene_fill_for_layer: code={} nicht in dossier_ebenen gefunden".format(code))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -398,7 +488,7 @@ def _apply_ebene_fill(doc, obj):
|
|||||||
try:
|
try:
|
||||||
hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0)
|
hatches = rg.Hatch.Create(geom, pattern_idx, rot_rad, scale_v, 0.0)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Auto-Fill Hatch.Create:", ex)
|
print("[STYLES] Auto-Fill Hatch.Create:", ex)
|
||||||
return False
|
return False
|
||||||
if not hatches or len(hatches) == 0: return False
|
if not hatches or len(hatches) == 0: return False
|
||||||
|
|
||||||
@@ -427,7 +517,7 @@ def _apply_ebene_fill(doc, obj):
|
|||||||
try:
|
try:
|
||||||
hatch_id = doc.Objects.AddHatch(hatches[0], new_attrs)
|
hatch_id = doc.Objects.AddHatch(hatches[0], new_attrs)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Auto-Fill AddHatch:", ex)
|
print("[STYLES] Auto-Fill AddHatch:", ex)
|
||||||
return False
|
return False
|
||||||
if hatch_id == System.Guid.Empty: return False
|
if hatch_id == System.Guid.Empty: return False
|
||||||
|
|
||||||
@@ -438,7 +528,7 @@ def _apply_ebene_fill(doc, obj):
|
|||||||
if h_obj is not None:
|
if h_obj is not None:
|
||||||
massstab.post_create_hatch_scale(doc, h_obj, float(fill["scale"]) or 1.0)
|
massstab.post_create_hatch_scale(doc, h_obj, float(fill["scale"]) or 1.0)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] post_create_hatch_scale (auto-fill):", ex)
|
print("[STYLES] post_create_hatch_scale (auto-fill):", ex)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ca = obj.Attributes.Duplicate()
|
ca = obj.Attributes.Duplicate()
|
||||||
@@ -447,7 +537,7 @@ def _apply_ebene_fill(doc, obj):
|
|||||||
try: doc.Objects.ModifyAttributes(obj, ca, True)
|
try: doc.Objects.ModifyAttributes(obj, ca, True)
|
||||||
finally: _processing.discard(obj.Id)
|
finally: _processing.discard(obj.Id)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Auto-Fill UserString:", ex)
|
print("[STYLES] Auto-Fill UserString:", ex)
|
||||||
|
|
||||||
_link_curve_hatch(obj.Id, hatch_id)
|
_link_curve_hatch(obj.Id, hatch_id)
|
||||||
return True
|
return True
|
||||||
@@ -531,7 +621,7 @@ def refresh_layer_fills(doc):
|
|||||||
targets.append((obj, owner))
|
targets.append((obj, owner))
|
||||||
owner_ids.add(str(owner.Id))
|
owner_ids.add(str(owner.Id))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] refresh_layer_fills scan:", ex)
|
print("[STYLES] refresh_layer_fills scan:", ex)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
updated = 0
|
updated = 0
|
||||||
@@ -600,9 +690,9 @@ def refresh_layer_fills(doc):
|
|||||||
if h_obj is not None:
|
if h_obj is not None:
|
||||||
_ms.post_create_hatch_scale(doc, h_obj, scale_v)
|
_ms.post_create_hatch_scale(doc, h_obj, scale_v)
|
||||||
except Exception as _ex:
|
except Exception as _ex:
|
||||||
print("[GESTALTUNG] post_create_hatch_scale (refresh):", _ex)
|
print("[STYLES] post_create_hatch_scale (refresh):", _ex)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] refresh rebuild:", ex)
|
print("[STYLES] refresh rebuild:", ex)
|
||||||
|
|
||||||
# (2) Farb-Sync — Hatch mit source=='layer' folgt der Ebenen-Definition
|
# (2) Farb-Sync — Hatch mit source=='layer' folgt der Ebenen-Definition
|
||||||
try:
|
try:
|
||||||
@@ -639,7 +729,7 @@ def refresh_layer_fills(doc):
|
|||||||
_processing.discard(refreshed.Id)
|
_processing.discard(refreshed.Id)
|
||||||
color_updated += 1
|
color_updated += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] refresh color-sync:", ex)
|
print("[STYLES] refresh color-sync:", ex)
|
||||||
|
|
||||||
# (3) Hatch-PlotWeight an fill.lw anpassen (None = wieder ByLayer)
|
# (3) Hatch-PlotWeight an fill.lw anpassen (None = wieder ByLayer)
|
||||||
try:
|
try:
|
||||||
@@ -681,7 +771,7 @@ def refresh_layer_fills(doc):
|
|||||||
finally:
|
finally:
|
||||||
_processing.discard(refreshed.Id)
|
_processing.discard(refreshed.Id)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] refresh lw-sync:", ex)
|
print("[STYLES] refresh lw-sync:", ex)
|
||||||
|
|
||||||
# --- 3) Auto-Fill nachziehen fuer Kurven ohne Hatch ---
|
# --- 3) Auto-Fill nachziehen fuer Kurven ohne Hatch ---
|
||||||
added = 0
|
added = 0
|
||||||
@@ -729,13 +819,13 @@ def refresh_layer_fills(doc):
|
|||||||
if _apply_ebene_fill(doc, obj):
|
if _apply_ebene_fill(doc, obj):
|
||||||
added += 1
|
added += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] refresh auto-fill:", ex)
|
print("[STYLES] refresh auto-fill:", ex)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] refresh auto-fill scan:", ex)
|
print("[STYLES] refresh auto-fill scan:", ex)
|
||||||
|
|
||||||
if updated or color_updated or added:
|
if updated or color_updated or added:
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
print("[GESTALTUNG] refresh_layer_fills: pattern={}, farbe={}, neu={}, unveraendert={}".format(
|
print("[STYLES] refresh_layer_fills: pattern={}, farbe={}, neu={}, unchanged={}".format(
|
||||||
updated, color_updated, added, skipped))
|
updated, color_updated, added, skipped))
|
||||||
return updated + color_updated + added
|
return updated + color_updated + added
|
||||||
|
|
||||||
@@ -789,13 +879,13 @@ def repair_plot_colors(doc):
|
|||||||
_processing.discard(obj.Id)
|
_processing.discard(obj.Id)
|
||||||
fixed += 1
|
fixed += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] repair_plot_colors entry:", ex)
|
print("[STYLES] repair_plot_colors entry:", ex)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] repair_plot_colors scan:", ex)
|
print("[STYLES] repair_plot_colors scan:", ex)
|
||||||
return 0
|
return 0
|
||||||
if fixed:
|
if fixed:
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
print("[GESTALTUNG] repair_plot_colors: {} Objekte repariert (von {} mit Eigenfarbe gescannt)".format(fixed, scanned))
|
print("[STYLES] repair_plot_colors: {} Objekte repariert (von {} mit Eigenfarbe gescannt)".format(fixed, scanned))
|
||||||
return fixed
|
return fixed
|
||||||
|
|
||||||
|
|
||||||
@@ -838,6 +928,7 @@ def _selection_summary(doc):
|
|||||||
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
base = {"count": 0, "linetypes": _all_linetypes(doc), "hatchPatterns": _all_hatch_patterns(doc)}
|
base = {"count": 0, "linetypes": _all_linetypes(doc), "hatchPatterns": _all_hatch_patterns(doc)}
|
||||||
if not objs:
|
if not objs:
|
||||||
|
base["newObjectPen"] = _new_object_pen_summary(doc, _default_pen())
|
||||||
return base
|
return base
|
||||||
|
|
||||||
color_sources, colors = set(), set()
|
color_sources, colors = set(), set()
|
||||||
@@ -860,6 +951,11 @@ def _selection_summary(doc):
|
|||||||
sec_patterns = set()
|
sec_patterns = set()
|
||||||
sec_scales = set()
|
sec_scales = set()
|
||||||
sec_rots = set()
|
sec_rots = set()
|
||||||
|
# Boundary-Subsettings (Schnittkante)
|
||||||
|
sec_bdy_visible = set()
|
||||||
|
sec_bdy_colors = set()
|
||||||
|
sec_bdy_widths = set()
|
||||||
|
sec_bg_colors = set() # Background-Fill (None wenn FillMode != SolidColor)
|
||||||
# Geometry-Kind-Klassifikation: 'curve' (closed planar 2D), 'curveOpen'
|
# Geometry-Kind-Klassifikation: 'curve' (closed planar 2D), 'curveOpen'
|
||||||
# (offene Kurve), '3d' (Brep/Extrusion/Mesh — Volumen mit Schnittflaeche),
|
# (offene Kurve), '3d' (Brep/Extrusion/Mesh — Volumen mit Schnittflaeche),
|
||||||
# 'other'. Aggregiert ueber alle Selektions-Objekte zu kind=
|
# 'other'. Aggregiert ueber alle Selektions-Objekte zu kind=
|
||||||
@@ -903,7 +999,7 @@ def _selection_summary(doc):
|
|||||||
# WICHTIG: layer.FullPath/Name liefert auf Mac mit Umlauten (Ä in WAENDE etc.)
|
# WICHTIG: layer.FullPath/Name liefert auf Mac mit Umlauten (Ä in WAENDE etc.)
|
||||||
# eine UnicodeDecodeError ueber die IronPython<->.NET-Bruecke. Wir benutzen
|
# eine UnicodeDecodeError ueber die IronPython<->.NET-Bruecke. Wir benutzen
|
||||||
# stattdessen unsere ASCII-only UserStrings (dossier_id + dossier_code) die wir
|
# stattdessen unsere ASCII-only UserStrings (dossier_id + dossier_code) die wir
|
||||||
# beim Layer-Bau gesetzt haben.
|
# beim Layer-Bau set haben.
|
||||||
nm = _safe_layer_label(doc, layer, a.LayerIndex)
|
nm = _safe_layer_label(doc, layer, a.LayerIndex)
|
||||||
layer_names.add(nm)
|
layer_names.add(nm)
|
||||||
|
|
||||||
@@ -926,25 +1022,47 @@ def _selection_summary(doc):
|
|||||||
else:
|
else:
|
||||||
geometry_kinds.add('other')
|
geometry_kinds.add('other')
|
||||||
|
|
||||||
# Section-Style aus Object-Attributes lesen (Rhino 8, mit Fallbacks
|
# Section-Style aus Object-Attributes lesen — Rhino 8 Mac packt die
|
||||||
# fuer Property-Namen die je nach Build variieren).
|
# Settings in ein SectionStyle-Objekt (via GetCustomSectionStyle),
|
||||||
|
# NICHT in direkte Attribute-Properties wie das alte API.
|
||||||
if is_3d:
|
if is_3d:
|
||||||
src_attr = None
|
src_attr = None
|
||||||
try:
|
try:
|
||||||
src_attr = getattr(a, "SectionAttributesSource", None)
|
src_attr = getattr(a, "SectionAttributesSource", None)
|
||||||
except Exception: src_attr = None
|
except Exception: src_attr = None
|
||||||
|
src_is_object = False
|
||||||
if src_attr is not None:
|
if src_attr is not None:
|
||||||
try:
|
try:
|
||||||
src_name = str(src_attr).lower()
|
src_name = str(src_attr).lower()
|
||||||
if "layer" in src_name: sec_sources.add("layer")
|
if "object" in src_name:
|
||||||
elif "object" in src_name: sec_sources.add("object")
|
sec_sources.add("object"); src_is_object = True
|
||||||
|
elif "layer" in src_name:
|
||||||
|
sec_sources.add("layer")
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
# Hatch-Index/Scale/Rotation
|
|
||||||
hidx = None
|
# Wenn Source=FromObject: aus dem Custom-SectionStyle lesen.
|
||||||
for n in ("SectionHatchIndex", "HatchPatternIndex"):
|
# Sonst (FromLayer): vom Layer.GetCustomSectionStyle() lesen damit
|
||||||
if hasattr(a, n):
|
# die UI auch im Layer-Modus den effektiven Hatch zeigt.
|
||||||
|
css = None
|
||||||
try:
|
try:
|
||||||
v = getattr(a, n)
|
if src_is_object and hasattr(a, "GetCustomSectionStyle"):
|
||||||
|
css = a.GetCustomSectionStyle()
|
||||||
|
if css is None:
|
||||||
|
# Fallback: Layer-SectionStyle
|
||||||
|
try:
|
||||||
|
lyr = doc.Layers[obj.Attributes.LayerIndex]
|
||||||
|
if hasattr(lyr, "GetCustomSectionStyle"):
|
||||||
|
css = lyr.GetCustomSectionStyle()
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
if css is not None:
|
||||||
|
# HatchIndex
|
||||||
|
hidx = None
|
||||||
|
for n in ("HatchIndex", "HatchPatternIndex"):
|
||||||
|
if hasattr(css, n):
|
||||||
|
try:
|
||||||
|
v = getattr(css, n)
|
||||||
if v is not None: hidx = int(v); break
|
if v is not None: hidx = int(v); break
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count:
|
if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count:
|
||||||
@@ -953,25 +1071,44 @@ def _selection_summary(doc):
|
|||||||
except Exception: pass
|
except Exception: pass
|
||||||
elif hidx == -1:
|
elif hidx == -1:
|
||||||
sec_enabled.add(False)
|
sec_enabled.add(False)
|
||||||
for n, target in (
|
# Scale
|
||||||
(("SectionHatchScale", "HatchPatternScale"), sec_scales),
|
for n in ("HatchScale", "HatchPatternScale"):
|
||||||
(("SectionHatchRotation", "HatchPatternRotation"), sec_rots),
|
if hasattr(css, n):
|
||||||
):
|
try: sec_scales.add(round(float(getattr(css, n)), 4)); break
|
||||||
for nn in n:
|
|
||||||
if hasattr(a, nn):
|
|
||||||
try:
|
|
||||||
v = float(getattr(a, nn))
|
|
||||||
target.add(round(v, 4)
|
|
||||||
if target is sec_scales
|
|
||||||
else round(math.degrees(v), 2))
|
|
||||||
break
|
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
for n in ("SectionFillColor", "SectionHatchColor", "HatchColor"):
|
# Rotation (rad → deg)
|
||||||
if hasattr(a, n):
|
for n in ("HatchRotationRadians", "HatchRotation", "HatchAngle"):
|
||||||
|
if hasattr(css, n):
|
||||||
|
try: sec_rots.add(round(math.degrees(float(getattr(css, n))), 2)); break
|
||||||
|
except Exception: pass
|
||||||
|
# Color
|
||||||
|
for n in ("HatchPatternColor", "HatchColor", "FillColor"):
|
||||||
|
if hasattr(css, n):
|
||||||
try:
|
try:
|
||||||
c = _color_to_hex(getattr(a, n))
|
c = _color_to_hex(getattr(css, n))
|
||||||
if c: sec_colors.add(c); break
|
if c: sec_colors.add(c); break
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
# Boundary-Settings auslesen
|
||||||
|
if hasattr(css, "BoundaryVisible"):
|
||||||
|
try: sec_bdy_visible.add(bool(css.BoundaryVisible))
|
||||||
|
except Exception: pass
|
||||||
|
if hasattr(css, "BoundaryColor"):
|
||||||
|
try:
|
||||||
|
c = _color_to_hex(css.BoundaryColor)
|
||||||
|
if c: sec_bdy_colors.add(c)
|
||||||
|
except Exception: pass
|
||||||
|
if hasattr(css, "BoundaryWidthScale"):
|
||||||
|
try: sec_bdy_widths.add(round(float(css.BoundaryWidthScale), 2))
|
||||||
|
except Exception: pass
|
||||||
|
# Background nur lesen wenn FillMode != Viewport (sonst transparent)
|
||||||
|
try:
|
||||||
|
mode = getattr(css, "BackgroundFillMode", None)
|
||||||
|
if mode is not None and "viewport" not in str(mode).lower():
|
||||||
|
c = _color_to_hex(css.BackgroundFillColor)
|
||||||
|
if c: sec_bg_colors.add(c)
|
||||||
|
except Exception: pass
|
||||||
|
else:
|
||||||
|
sec_enabled.add(False)
|
||||||
|
|
||||||
# Fuellung
|
# Fuellung
|
||||||
if _is_closed_planar_curve(obj.Geometry):
|
if _is_closed_planar_curve(obj.Geometry):
|
||||||
@@ -1072,6 +1209,10 @@ def _selection_summary(doc):
|
|||||||
"sectionPattern": single(sec_patterns),
|
"sectionPattern": single(sec_patterns),
|
||||||
"sectionScale": single(sec_scales),
|
"sectionScale": single(sec_scales),
|
||||||
"sectionRotation": single(sec_rots),
|
"sectionRotation": single(sec_rots),
|
||||||
|
"sectionBoundaryVisible": single(sec_bdy_visible),
|
||||||
|
"sectionBoundaryColor": single(sec_bdy_colors),
|
||||||
|
"sectionBoundaryWidthScale": single(sec_bdy_widths),
|
||||||
|
"sectionBackgroundColor": single(sec_bg_colors),
|
||||||
# geometryKind: 'curve' | 'curveOpen' | '3d' | 'mixed' | 'other'
|
# geometryKind: 'curve' | 'curveOpen' | '3d' | 'mixed' | 'other'
|
||||||
"geometryKind": (
|
"geometryKind": (
|
||||||
'mixed' if len(geometry_kinds & {'curve', 'curveOpen', '3d'}) > 1
|
'mixed' if len(geometry_kinds & {'curve', 'curveOpen', '3d'}) > 1
|
||||||
@@ -1085,7 +1226,7 @@ def _selection_summary(doc):
|
|||||||
"fillRotation": single(fill_rots),
|
"fillRotation": single(fill_rots),
|
||||||
"hatchPatterns": _all_hatch_patterns(doc),
|
"hatchPatterns": _all_hatch_patterns(doc),
|
||||||
})
|
})
|
||||||
print("[GESTALTUNG] sel: n={} colorSrc={} color={} layerColor={}".format(
|
print("[STYLES] sel: n={} colorSrc={} color={} layerColor={}".format(
|
||||||
result.get("count"), result.get("colorSource"),
|
result.get("count"), result.get("colorSource"),
|
||||||
result.get("color"), result.get("layerColor")))
|
result.get("color"), result.get("layerColor")))
|
||||||
return result
|
return result
|
||||||
@@ -1101,7 +1242,7 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
before = doc.Linetypes.Count
|
before = doc.Linetypes.Count
|
||||||
ok = _force_load_linetypes(doc)
|
ok = _force_load_linetypes(doc)
|
||||||
after = doc.Linetypes.Count
|
after = doc.Linetypes.Count
|
||||||
print("[GESTALTUNG] Linetypes vor: {}, nach LoadDefaults({}): {}".format(before, ok, after))
|
print("[STYLES] Linetypes before: {}, nach LoadDefaults({}): {}".format(before, ok, after))
|
||||||
entries = []
|
entries = []
|
||||||
for i in range(after):
|
for i in range(after):
|
||||||
lt = doc.Linetypes[i]
|
lt = doc.Linetypes[i]
|
||||||
@@ -1111,15 +1252,15 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
try: nm = lt.Name
|
try: nm = lt.Name
|
||||||
except Exception: nm = "?"
|
except Exception: nm = "?"
|
||||||
entries.append("[{}] {} ({})".format(i, nm, flags))
|
entries.append("[{}] {} ({})".format(i, nm, flags))
|
||||||
print("[GESTALTUNG] {}".format(" | ".join(entries)))
|
print("[STYLES] {}".format(" | ".join(entries)))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Linetype-Diagnose:", ex)
|
print("[STYLES] Linetype-Diagnose:", ex)
|
||||||
# One-Shot Repair: aeltere Hatches (vor dem PlotColor-Fix angelegt)
|
# One-Shot Repair: aeltere Hatches (vor dem PlotColor-Fix angelegt)
|
||||||
# bekommen ihre Print-Attribute mit Display synchronisiert.
|
# bekommen ihre Print-Attribute mit Display synchronisiert.
|
||||||
try:
|
try:
|
||||||
repair_plot_colors(doc)
|
repair_plot_colors(doc)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] repair on ready:", ex)
|
print("[STYLES] repair on ready:", ex)
|
||||||
self._send_selection()
|
self._send_selection()
|
||||||
|
|
||||||
def handle(self, data):
|
def handle(self, data):
|
||||||
@@ -1159,6 +1300,10 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
p.get("pattern"),
|
p.get("pattern"),
|
||||||
p.get("scale"),
|
p.get("scale"),
|
||||||
p.get("rotation"),
|
p.get("rotation"),
|
||||||
|
boundary_visible=p.get("boundaryVisible", True),
|
||||||
|
boundary_width_scale=p.get("boundaryWidthScale", 1.0),
|
||||||
|
boundary_color_hex=p.get("boundaryColor"),
|
||||||
|
background_color_hex=p.get("backgroundColor"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _send_selection(self):
|
def _send_selection(self):
|
||||||
@@ -1166,7 +1311,7 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
try:
|
try:
|
||||||
self.send("SELECTION", _selection_summary(doc))
|
self.send("SELECTION", _selection_summary(doc))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Selection:", ex)
|
print("[STYLES] Selection:", ex)
|
||||||
|
|
||||||
# ---- Attribute-Setter ------------------------------------------------
|
# ---- Attribute-Setter ------------------------------------------------
|
||||||
|
|
||||||
@@ -1182,6 +1327,12 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
self._send_selection()
|
self._send_selection()
|
||||||
|
|
||||||
def _set_color_source(self, source, color_hex):
|
def _set_color_source(self, source, color_hex):
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if not _has_selection(doc):
|
||||||
|
_set_default_pen(colorSource=source,
|
||||||
|
color=(color_hex if source == "object" else None))
|
||||||
|
self._send_selection()
|
||||||
|
return
|
||||||
col = _hex_to_color(color_hex) if (source == "object" and color_hex) else None
|
col = _hex_to_color(color_hex) if (source == "object" and color_hex) else None
|
||||||
def m(a, _obj):
|
def m(a, _obj):
|
||||||
if source == "layer":
|
if source == "layer":
|
||||||
@@ -1199,11 +1350,16 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
# Print-Mode-aware: bei aktivem Print-View werden PlotWeights skaliert.
|
# Print-Mode-aware: bei aktivem Print-View werden PlotWeights skaliert.
|
||||||
# write_plotweight() kuemmert sich um beides (Original-Speicherung +
|
# write_plotweight() kuemmert sich um beides (Original-Speicherung +
|
||||||
# Skalierungs-Multiplier).
|
# Skalierungs-Multiplier).
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if not _has_selection(doc):
|
||||||
|
_set_default_pen(lwSource=source,
|
||||||
|
lw=(lw if source == "object" else None))
|
||||||
|
self._send_selection()
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
import massstab
|
import massstab
|
||||||
except Exception:
|
except Exception:
|
||||||
massstab = None
|
massstab = None
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
|
||||||
def m(a, _obj):
|
def m(a, _obj):
|
||||||
if source == "layer":
|
if source == "layer":
|
||||||
a.PlotWeightSource = _LW_FROM_LAYER
|
a.PlotWeightSource = _LW_FROM_LAYER
|
||||||
@@ -1238,7 +1394,7 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
applied = True
|
applied = True
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] attr {} fehler: {}".format(prop, ex))
|
print("[STYLES] attr {} fehler: {}".format(prop, ex))
|
||||||
# Versuch 2: direkt auf RhinoObject
|
# Versuch 2: direkt auf RhinoObject
|
||||||
if not applied:
|
if not applied:
|
||||||
for prop in ("LinetypePatternLengthScale", "LinetypeScale"):
|
for prop in ("LinetypePatternLengthScale", "LinetypeScale"):
|
||||||
@@ -1248,18 +1404,23 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
applied = True
|
applied = True
|
||||||
break
|
break
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] obj {} fehler: {}".format(prop, ex))
|
print("[STYLES] obj {} fehler: {}".format(prop, ex))
|
||||||
if applied:
|
if applied:
|
||||||
ok += 1
|
ok += 1
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
if ok == 0:
|
if ok == 0:
|
||||||
print("[GESTALTUNG] Linetype-Scale nicht unterstuetzt (Rhino-Version?)")
|
print("[STYLES] Linetype-Scale nicht unterstuetzt (Rhino-Version?)")
|
||||||
else:
|
else:
|
||||||
print("[GESTALTUNG] Linetype-Scale auf {} Objekt(e) angewendet".format(ok))
|
print("[STYLES] Linetype-Scale auf {} Objekt(e) applied".format(ok))
|
||||||
self._send_selection()
|
self._send_selection()
|
||||||
|
|
||||||
def _set_linetype_source(self, source, name):
|
def _set_linetype_source(self, source, name):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if not _has_selection(doc):
|
||||||
|
_set_default_pen(linetypeSource=source,
|
||||||
|
linetype=(name if source == "object" else None))
|
||||||
|
self._send_selection()
|
||||||
|
return
|
||||||
idx = -1
|
idx = -1
|
||||||
if source == "object" and name:
|
if source == "object" and name:
|
||||||
try:
|
try:
|
||||||
@@ -1331,7 +1492,7 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
try: doc.Objects.ModifyAttributes(obj, ca, True)
|
try: doc.Objects.ModifyAttributes(obj, ca, True)
|
||||||
finally: _processing.discard(obj.Id)
|
finally: _processing.discard(obj.Id)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] _set_fill follow-layer empty:", ex)
|
print("[STYLES] _set_fill follow-layer empty:", ex)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True)
|
pattern_idx = doc.HatchPatterns.Find(fill["pattern"], True)
|
||||||
@@ -1404,7 +1565,7 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
if h_obj is not None:
|
if h_obj is not None:
|
||||||
_ms2.post_create_hatch_scale(doc, h_obj, scale_v)
|
_ms2.post_create_hatch_scale(doc, h_obj, scale_v)
|
||||||
except Exception as _ex:
|
except Exception as _ex:
|
||||||
print("[GESTALTUNG] post_create_hatch_scale (replace):", _ex)
|
print("[STYLES] post_create_hatch_scale (replace):", _ex)
|
||||||
# Farbe / Source / FILL_SOURCE-Marker aktualisieren
|
# Farbe / Source / FILL_SOURCE-Marker aktualisieren
|
||||||
refreshed = doc.Objects.FindId(existing_hatch.Id) or existing_hatch
|
refreshed = doc.Objects.FindId(existing_hatch.Id) or existing_hatch
|
||||||
ha = refreshed.Attributes.Duplicate()
|
ha = refreshed.Attributes.Duplicate()
|
||||||
@@ -1454,7 +1615,7 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
if h_obj is not None:
|
if h_obj is not None:
|
||||||
_ms.post_create_hatch_scale(doc, h_obj, scale_v)
|
_ms.post_create_hatch_scale(doc, h_obj, scale_v)
|
||||||
except Exception as _ex:
|
except Exception as _ex:
|
||||||
print("[GESTALTUNG] post_create_hatch_scale (set_fill):", _ex)
|
print("[STYLES] post_create_hatch_scale (set_fill):", _ex)
|
||||||
else:
|
else:
|
||||||
if existing_hatch is not None and not existing_hatch.IsDeleted:
|
if existing_hatch is not None and not existing_hatch.IsDeleted:
|
||||||
_processing.add(existing_hatch.Id)
|
_processing.add(existing_hatch.Id)
|
||||||
@@ -1478,73 +1639,223 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
# ---- SectionStyle (per-Object, Rhino 8) -------------------------------
|
# ---- SectionStyle (per-Object, Rhino 8) -------------------------------
|
||||||
|
|
||||||
def _set_section_style(self, enabled, source, color_hex,
|
def _set_section_style(self, enabled, source, color_hex,
|
||||||
pattern_name=None, scale=None, rotation_deg=None):
|
pattern_name=None, scale=None, rotation_deg=None,
|
||||||
"""Setzt Per-Object SectionStyle-Properties auf die selektierten
|
boundary_visible=True, boundary_width_scale=1.0,
|
||||||
3D-Objekte. Rhino 8 expone diese Properties auf ObjectAttributes
|
boundary_color_hex=None,
|
||||||
unter teils variierenden Namen — wir versuchen die bekannten."""
|
background_color_hex=None):
|
||||||
|
"""Setzt einen Per-Object SectionStyle ueber die Rhino-8 API
|
||||||
|
(analog zu Layer.SetCustomSectionStyle). source='layer' entfernt
|
||||||
|
den Custom-Style → Layer-Default greift. source='object' setzt
|
||||||
|
einen frischen SectionStyle pro Objekt."""
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
is_layer_source = (source == "layer")
|
is_layer_source = (source == "layer")
|
||||||
|
print("[STYLES] _set_section_style: source={} enabled={} pattern={}".format(
|
||||||
|
source, enabled, pattern_name))
|
||||||
|
|
||||||
|
# SectionStyle-Klasse + Source-Enum holen.
|
||||||
|
# Mac Rhino 8: Enum heisst ObjectSectionAttributesSource (mit
|
||||||
|
# "Object"-Prefix) — per Inspektion verifiziert. Ohne explizites
|
||||||
|
# Setzen von Attributes.SectionAttributesSource = FromObject wird
|
||||||
|
# der Custom-SectionStyle zwar persistiert, aber visuell ignoriert
|
||||||
|
# weil der Default-Wert FromLayer bleibt.
|
||||||
|
try:
|
||||||
|
SS = Rhino.DocObjects.SectionStyle
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STYLES] SectionStyle-Klasse fehlt:", ex)
|
||||||
|
return
|
||||||
|
SAS = None
|
||||||
|
for cls_name in ("ObjectSectionAttributesSource", "SectionAttributesSource"):
|
||||||
|
try:
|
||||||
|
SAS = getattr(Rhino.DocObjects, cls_name)
|
||||||
|
if SAS is not None:
|
||||||
|
print("[STYLES] Source-Enum: Rhino.DocObjects.{}".format(cls_name))
|
||||||
|
break
|
||||||
|
except Exception: pass
|
||||||
|
if SAS is None:
|
||||||
|
print("[STYLES] WARNUNG: kein Source-Enum gefunden")
|
||||||
|
if objs and not getattr(self, "_ss_api_logged", False):
|
||||||
|
o = objs[0]
|
||||||
|
for meth in ("SetCustomSectionStyle", "RemoveCustomSectionStyle",
|
||||||
|
"HasCustomSectionStyle", "GetCustomSectionStyle"):
|
||||||
|
print("[STYLES] RhinoObject.{}: {}".format(
|
||||||
|
meth, hasattr(o, meth)))
|
||||||
|
try:
|
||||||
|
a = o.Attributes
|
||||||
|
for meth in ("SetCustomSectionStyle", "RemoveCustomSectionStyle"):
|
||||||
|
print("[STYLES] Attributes.{}: {}".format(
|
||||||
|
meth, hasattr(a, meth)))
|
||||||
|
except Exception: pass
|
||||||
|
self._ss_api_logged = True
|
||||||
|
|
||||||
# Hatch-Pattern-Index ermitteln
|
# Hatch-Pattern-Index ermitteln
|
||||||
pat_idx = -1
|
pat_idx = -1
|
||||||
if pattern_name and pattern_name not in ("None", ""):
|
if pattern_name and pattern_name not in ("None", ""):
|
||||||
try: pat_idx = doc.HatchPatterns.Find(pattern_name, True)
|
try: pat_idx = doc.HatchPatterns.Find(pattern_name, True)
|
||||||
except Exception: pat_idx = -1
|
except Exception: pat_idx = -1
|
||||||
if pat_idx < 0 and pattern_name not in ("None", ""):
|
|
||||||
try: pat_idx = doc.HatchPatterns.Find("Solid", True)
|
|
||||||
except Exception: pat_idx = -1
|
|
||||||
|
|
||||||
col = _hex_to_color(color_hex) if color_hex else None
|
col = _hex_to_color(color_hex) if color_hex else None
|
||||||
scale_v = float(scale) if scale is not None else 1.0
|
scale_v = float(scale) if scale is not None else 1.0
|
||||||
rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0
|
rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0
|
||||||
|
|
||||||
def _try_set_attr(a, names, value):
|
def _try_set(target, names, value):
|
||||||
for n in names:
|
for n in names:
|
||||||
if hasattr(a, n):
|
if hasattr(target, n):
|
||||||
try:
|
try:
|
||||||
setattr(a, n, value)
|
setattr(target, n, value)
|
||||||
return n
|
return n
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _apply_custom(obj, style):
|
||||||
|
"""Setzt Custom-SectionStyle + schaltet SectionAttributesSource
|
||||||
|
auf FromObject. Beides muss persistiert sein damit Rhino den
|
||||||
|
Custom-Style auch tatsaechlich rendert."""
|
||||||
|
try:
|
||||||
|
a = obj.Attributes.Duplicate()
|
||||||
|
if hasattr(a, "SetCustomSectionStyle"):
|
||||||
|
a.SetCustomSectionStyle(style)
|
||||||
|
# KRITISCH: Source auf FromObject — ohne das ignoriert Rhino
|
||||||
|
# den Custom-Style und nutzt weiter den Layer-Style.
|
||||||
|
if SAS is not None and hasattr(a, "SectionAttributesSource"):
|
||||||
|
try:
|
||||||
|
a.SectionAttributesSource = SAS.FromObject
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STYLES] set Source.FromObject fail:", ex)
|
||||||
|
ok_modify = doc.Objects.ModifyAttributes(obj, a, True)
|
||||||
|
_log_post(obj, "Attributes.SetCustomSectionStyle+FromObject",
|
||||||
|
ok_modify)
|
||||||
|
return "Attributes.SetCustomSectionStyle"
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STYLES] attr.SetCustomSectionStyle fail:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _log_post(obj, via, ok_modify=None):
|
||||||
|
"""Nach SetCustom: pruefen ob Rhino den Style auch behalten hat."""
|
||||||
|
try:
|
||||||
|
ob = doc.Objects.FindId(obj.Id) if hasattr(obj, "Id") else obj
|
||||||
|
if ob is None: ob = obj
|
||||||
|
a = ob.Attributes
|
||||||
|
src = "n/a"
|
||||||
|
if hasattr(a, "SectionAttributesSource"):
|
||||||
|
try: src = str(a.SectionAttributesSource)
|
||||||
|
except Exception: pass
|
||||||
|
got = None
|
||||||
|
if hasattr(a, "GetCustomSectionStyle"):
|
||||||
|
try:
|
||||||
|
css = a.GetCustomSectionStyle()
|
||||||
|
if css is not None:
|
||||||
|
got = "HatchIndex={}".format(getattr(css, "HatchIndex", "?"))
|
||||||
|
except Exception as ex:
|
||||||
|
got = "get-err: {}".format(ex)
|
||||||
|
print("[STYLES] post via {} (modify_ok={}): Source={} Got={}".format(
|
||||||
|
via, ok_modify, src, got))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STYLES] post-check:", ex)
|
||||||
|
|
||||||
|
def _remove_custom(obj):
|
||||||
|
"""Entfernt Custom-SectionStyle + schaltet Source auf FromLayer
|
||||||
|
zurueck. Damit greift wieder der Layer-Default-SectionStyle."""
|
||||||
|
try:
|
||||||
|
a = obj.Attributes.Duplicate()
|
||||||
|
if hasattr(a, "RemoveCustomSectionStyle"):
|
||||||
|
a.RemoveCustomSectionStyle()
|
||||||
|
if SAS is not None and hasattr(a, "SectionAttributesSource"):
|
||||||
|
try:
|
||||||
|
a.SectionAttributesSource = SAS.FromLayer
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STYLES] set Source.FromLayer fail:", ex)
|
||||||
|
doc.Objects.ModifyAttributes(obj, a, True)
|
||||||
|
return "Attributes.RemoveCustomSectionStyle+FromLayer"
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STYLES] attr.RemoveCustomSectionStyle fail:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
n_ok = 0
|
n_ok = 0
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
geom = obj.Geometry
|
geom = obj.Geometry
|
||||||
if not isinstance(geom, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD)):
|
if not isinstance(geom, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD)):
|
||||||
continue
|
continue
|
||||||
a = obj.Attributes.Duplicate()
|
|
||||||
|
|
||||||
# Source: FromLayer vs FromObject — verschiedene Enum-Namen
|
|
||||||
if is_layer_source:
|
if is_layer_source:
|
||||||
# Versuche SectionAttributesSource auf FromLayer
|
# Custom entfernen → Layer-SectionStyle wird wirksam
|
||||||
_try_set_attr(a, ("SectionAttributesSource",),
|
via = _remove_custom(obj)
|
||||||
Rhino.DocObjects.SectionAttributesSource.FromLayer
|
print("[STYLES] obj {}: remove custom via {}".format(
|
||||||
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 0)
|
str(obj.Id)[:8], via))
|
||||||
else:
|
if via: n_ok += 1
|
||||||
_try_set_attr(a, ("SectionAttributesSource",),
|
continue
|
||||||
Rhino.DocObjects.SectionAttributesSource.FromObject
|
# Default-Farbe = Layer-Farbe wenn der User keine Override-Farbe
|
||||||
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 1)
|
# gewaehlt hat. Section-Style hat keine "ByLayer"-Source-Option,
|
||||||
|
# also setzen wir die echte Layer-Farbe explizit auf den Style.
|
||||||
if not enabled or pattern_name == "None":
|
obj_col = col
|
||||||
# Hatch-Index auf -1 = keine Fuellung
|
obj_col_src = "user-override" if col is not None else "n/a"
|
||||||
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), -1)
|
if obj_col is None:
|
||||||
|
try:
|
||||||
|
lyr = doc.Layers[obj.Attributes.LayerIndex]
|
||||||
|
obj_col = lyr.Color
|
||||||
|
obj_col_src = "layer({})".format(lyr.FullPath)
|
||||||
|
except Exception as ex:
|
||||||
|
obj_col = None
|
||||||
|
obj_col_src = "fail:{}".format(ex)
|
||||||
|
print("[STYLES] obj {} color src={} val={}".format(
|
||||||
|
str(obj.Id)[:8], obj_col_src, obj_col))
|
||||||
|
# Per-Object: frischen SectionStyle bauen wie in layer_builder
|
||||||
|
style = SS()
|
||||||
|
if pattern_name == "None" or not enabled:
|
||||||
|
_try_set(style, ("HatchIndex", "HatchPatternIndex"), -1)
|
||||||
else:
|
else:
|
||||||
if pat_idx >= 0:
|
if pat_idx >= 0:
|
||||||
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), pat_idx)
|
_try_set(style, ("HatchIndex", "HatchPatternIndex"), pat_idx)
|
||||||
_try_set_attr(a, ("SectionHatchScale", "HatchPatternScale"), scale_v)
|
_try_set(style, ("HatchScale", "HatchPatternScale"), scale_v)
|
||||||
_try_set_attr(a, ("SectionHatchRotation", "HatchPatternRotation"), rot_rad)
|
_try_set(style, ("HatchRotationRadians", "HatchRotation",
|
||||||
if col is not None:
|
"HatchAngle"), rot_rad)
|
||||||
_try_set_attr(a, ("SectionFillColor", "SectionHatchColor",
|
if obj_col is not None:
|
||||||
"HatchColor"), col)
|
# Display- UND Print-Color setzen damit beide matchen
|
||||||
|
_try_set(style, ("HatchPatternColor", "HatchColor",
|
||||||
|
"FillColor"), obj_col)
|
||||||
|
_try_set(style, ("HatchPatternPrintColor",), obj_col)
|
||||||
|
_try_set(style, ("BoundaryVisible",), bool(boundary_visible))
|
||||||
try:
|
try:
|
||||||
doc.Objects.ModifyAttributes(obj, a, True)
|
_try_set(style, ("BoundaryWidthScale",),
|
||||||
n_ok += 1
|
float(boundary_width_scale))
|
||||||
except Exception as ex:
|
except Exception: pass
|
||||||
print("[GESTALTUNG] SectionStyle ModifyAttributes:", ex)
|
# Boundary-Farbe: NUR setzen wenn User explizit eine Override-Farbe
|
||||||
|
# gewaehlt hat. Sonst lassen wir Rhinos Default (schwarz) greifen
|
||||||
|
# damit Boundary visuell unterscheidbar von der Hatch-Pattern-Farbe
|
||||||
|
# bleibt. (Sonst wuerde HatchPatternColor=Layer + BoundaryColor=Layer
|
||||||
|
# die Schnittflaeche als einfarbige Flaeche erscheinen lassen.)
|
||||||
|
bcol = None
|
||||||
|
if boundary_color_hex:
|
||||||
|
try: bcol = _hex_to_color(boundary_color_hex)
|
||||||
|
except Exception: bcol = None
|
||||||
|
if bcol is not None:
|
||||||
|
_try_set(style, ("BoundaryColor",), bcol)
|
||||||
|
_try_set(style, ("BoundaryPrintColor",), bcol)
|
||||||
|
# Background-Fill: User-Override (hex) → SolidColor-Mode + Farbe
|
||||||
|
# Sonst Transparent (Viewport-Mode, Default)
|
||||||
|
if background_color_hex:
|
||||||
|
try:
|
||||||
|
bgcol = _hex_to_color(background_color_hex)
|
||||||
|
except Exception:
|
||||||
|
bgcol = None
|
||||||
|
if bgcol is not None:
|
||||||
|
_try_set(style, ("BackgroundFillColor",), bgcol)
|
||||||
|
_try_set(style, ("BackgroundFillPrintColor",), bgcol)
|
||||||
|
# FillMode auf SolidColor via Enum (mehrere Namens-Varianten)
|
||||||
|
for en_cls in ("SectionBackgroundFillMode",
|
||||||
|
"BackgroundFillMode"):
|
||||||
|
try:
|
||||||
|
E = getattr(Rhino.DocObjects, en_cls, None)
|
||||||
|
if E is not None:
|
||||||
|
_try_set(style, ("BackgroundFillMode",),
|
||||||
|
E.SolidColor)
|
||||||
|
break
|
||||||
|
except Exception: pass
|
||||||
|
via = _apply_custom(obj, style)
|
||||||
|
print("[STYLES] obj {}: set custom via {} (hatch_idx={})".format(
|
||||||
|
str(obj.Id)[:8], via, pat_idx))
|
||||||
|
if via: n_ok += 1
|
||||||
|
|
||||||
print("[GESTALTUNG] SectionStyle auf {} Objekt(e) appliziert".format(n_ok))
|
print("[STYLES] SectionStyle auf {} Objekt(e) appliziert".format(n_ok))
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
self._send_selection()
|
self._send_selection()
|
||||||
|
|
||||||
@@ -1560,7 +1871,7 @@ def _install_selection_listener(bridge):
|
|||||||
# Selection-Refresh wird via Idle-Event debounced:
|
# Selection-Refresh wird via Idle-Event debounced:
|
||||||
# Rhino feuert pro Object-Select/Deselect einzeln. Bei mass-Delete von
|
# Rhino feuert pro Object-Select/Deselect einzeln. Bei mass-Delete von
|
||||||
# 327 Objekten = 327 refresh-Calls → 327 IPC-Sends in den WebView →
|
# 327 Objekten = 327 refresh-Calls → 327 IPC-Sends in den WebView →
|
||||||
# UI haengt + Command-History wird mit '[GESTALTUNG] sel: n=N'
|
# UI haengt + Command-History wird mit '[STYLES] sel: n=N'
|
||||||
# zugemuellt. Wir setzen nur ein Dirty-Flag und feuern EINMAL beim
|
# zugemuellt. Wir setzen nur ein Dirty-Flag und feuern EINMAL beim
|
||||||
# naechsten Idle-Tick.
|
# naechsten Idle-Tick.
|
||||||
def refresh(*args):
|
def refresh(*args):
|
||||||
@@ -1587,7 +1898,7 @@ def _install_selection_listener(bridge):
|
|||||||
Rhino.RhinoApp.Idle += on_idle_flush
|
Rhino.RhinoApp.Idle += on_idle_flush
|
||||||
sc.sticky["_gestaltung_idle_attached"] = True
|
sc.sticky["_gestaltung_idle_attached"] = True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Idle-Hook fail:", ex)
|
print("[STYLES] Idle-Hook fail:", ex)
|
||||||
|
|
||||||
def on_replace(sender, args):
|
def on_replace(sender, args):
|
||||||
"""Sync Curve↔Hatch bei Move/Replace:
|
"""Sync Curve↔Hatch bei Move/Replace:
|
||||||
@@ -1632,7 +1943,7 @@ def _install_selection_listener(bridge):
|
|||||||
try:
|
try:
|
||||||
new_curves = new_obj.Geometry.Get3dCurves(True)
|
new_curves = new_obj.Geometry.Get3dCurves(True)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] hatch.Get3dCurves:", ex)
|
print("[STYLES] hatch.Get3dCurves:", ex)
|
||||||
return
|
return
|
||||||
if not new_curves or len(new_curves) == 0:
|
if not new_curves or len(new_curves) == 0:
|
||||||
return
|
return
|
||||||
@@ -1641,14 +1952,14 @@ def _install_selection_listener(bridge):
|
|||||||
try:
|
try:
|
||||||
doc2.Objects.Replace(owner_id, new_curve)
|
doc2.Objects.Replace(owner_id, new_curve)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] hatch→curve replace:", ex)
|
print("[STYLES] hatch→curve replace:", ex)
|
||||||
finally:
|
finally:
|
||||||
_processing.discard(owner_id)
|
_processing.discard(owner_id)
|
||||||
return
|
return
|
||||||
hatch_id_str = a.GetUserString(_FILL_KEY)
|
hatch_id_str = a.GetUserString(_FILL_KEY)
|
||||||
if not hatch_id_str:
|
if not hatch_id_str:
|
||||||
return
|
return
|
||||||
print("[GESTALTUNG] on_replace fuer Curve mit Fill")
|
print("[STYLES] on_replace fuer Curve mit Fill")
|
||||||
try:
|
try:
|
||||||
hatch_id = System.Guid(hatch_id_str)
|
hatch_id = System.Guid(hatch_id_str)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1679,7 +1990,7 @@ def _install_selection_listener(bridge):
|
|||||||
try:
|
try:
|
||||||
doc.Objects.Replace(hatch_id, new_hatches[0])
|
doc.Objects.Replace(hatch_id, new_hatches[0])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Hatch-Update:", ex)
|
print("[STYLES] Hatch-Update:", ex)
|
||||||
finally:
|
finally:
|
||||||
_processing.discard(hatch_id)
|
_processing.discard(hatch_id)
|
||||||
|
|
||||||
@@ -1721,7 +2032,7 @@ def _install_selection_listener(bridge):
|
|||||||
hatch_id_str = _lookup_hatch_for_curve(obj.Id)
|
hatch_id_str = _lookup_hatch_for_curve(obj.Id)
|
||||||
if not hatch_id_str:
|
if not hatch_id_str:
|
||||||
return
|
return
|
||||||
print("[GESTALTUNG] on_delete: hatch via sticky map gefunden")
|
print("[STYLES] on_delete: hatch via sticky map gefunden")
|
||||||
|
|
||||||
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
|
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
|
||||||
if hatch_id_str:
|
if hatch_id_str:
|
||||||
@@ -1738,10 +2049,10 @@ def _install_selection_listener(bridge):
|
|||||||
_processing.add(hatch_id)
|
_processing.add(hatch_id)
|
||||||
try:
|
try:
|
||||||
ok = doc.Objects.Delete(hatch_id, True)
|
ok = doc.Objects.Delete(hatch_id, True)
|
||||||
print("[GESTALTUNG] Curve geloescht -> Hatch {} ({})".format(
|
print("[STYLES] Curve geloescht -> Hatch {} ({})".format(
|
||||||
"weg" if ok else "konnte nicht geloescht werden", hatch_id))
|
"weg" if ok else "konnte nicht geloescht werden", hatch_id))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Hatch-Loeschen:", ex)
|
print("[STYLES] Hatch-Loeschen:", ex)
|
||||||
finally:
|
finally:
|
||||||
_processing.discard(hatch_id)
|
_processing.discard(hatch_id)
|
||||||
_unlink_curve(obj.Id)
|
_unlink_curve(obj.Id)
|
||||||
@@ -1765,7 +2076,7 @@ def _install_selection_listener(bridge):
|
|||||||
finally:
|
finally:
|
||||||
_processing.discard(owner_id)
|
_processing.discard(owner_id)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] Curve-Verweis aufraeumen:", ex)
|
print("[STYLES] Curve-Verweis aufraeumen:", ex)
|
||||||
|
|
||||||
def on_add(sender, args):
|
def on_add(sender, args):
|
||||||
"""Auto-Fill bzw. Drag-Recovery: neues Objekt -> ggf. Hatch erzeugen.
|
"""Auto-Fill bzw. Drag-Recovery: neues Objekt -> ggf. Hatch erzeugen.
|
||||||
@@ -1792,10 +2103,10 @@ def _install_selection_listener(bridge):
|
|||||||
try:
|
try:
|
||||||
ok = _restore_hatch_from_pending(doc, obj, pending)
|
ok = _restore_hatch_from_pending(doc, obj, pending)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] on_add restore Exception:", ex)
|
print("[STYLES] on_add restore Exception:", ex)
|
||||||
ok = False
|
ok = False
|
||||||
if ok:
|
if ok:
|
||||||
print("[GESTALTUNG] Drag-Recovery: Hatch wiederhergestellt fuer {}".format(obj.Id))
|
print("[STYLES] Drag-Recovery: Hatch wiederhergestellt fuer {}".format(obj.Id))
|
||||||
b = sc.sticky.get("gestaltung_bridge")
|
b = sc.sticky.get("gestaltung_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b._send_selection()
|
try: b._send_selection()
|
||||||
@@ -1806,10 +2117,34 @@ def _install_selection_listener(bridge):
|
|||||||
# Waehrend Move/Rotate werden Sub-Volumen erzeugt die kein Auto-Fill
|
# Waehrend Move/Rotate werden Sub-Volumen erzeugt die kein Auto-Fill
|
||||||
# brauchen, und elemente uebernimmt die Coupling.
|
# brauchen, und elemente uebernimmt die Coupling.
|
||||||
if sc.sticky.get("_dossier_user_transform_active"): return
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
|
|
||||||
|
# 2b) Default-Pen ("Stift fuer neue Objekte") auf frisch gezeichnete
|
||||||
|
# 2D-Objekte stempeln (Kurven, Text, Hatch, Bemassung ...), solange
|
||||||
|
# aktiv. Ausgeschlossen: 3D-Volumen (Pen ist ein 2D-Begriff) und
|
||||||
|
# DOSSIER-Element-Geometrie (dossier_element_type), die elemente.py setzt.
|
||||||
|
pen = _default_pen()
|
||||||
|
g = obj.Geometry
|
||||||
|
is_3d = isinstance(g, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD))
|
||||||
|
if _default_pen_active(pen) and g is not None and not is_3d:
|
||||||
|
try: dossier_type = obj.Attributes.GetUserString("dossier_element_type") or ""
|
||||||
|
except Exception: dossier_type = ""
|
||||||
|
if not dossier_type:
|
||||||
|
try:
|
||||||
|
a = obj.Attributes.Duplicate()
|
||||||
|
_apply_pen_to_attrs(doc, a, pen)
|
||||||
|
_processing.add(obj.Id)
|
||||||
|
try:
|
||||||
|
doc.Objects.ModifyAttributes(obj, a, True)
|
||||||
|
finally:
|
||||||
|
_processing.discard(obj.Id)
|
||||||
|
doc.Views.Redraw()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STYLES] on_add default-pen:", ex)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ok = _apply_ebene_fill(doc, obj)
|
ok = _apply_ebene_fill(doc, obj)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] on_add Exception:", ex)
|
print("[STYLES] on_add Exception:", ex)
|
||||||
return
|
return
|
||||||
if ok:
|
if ok:
|
||||||
b = sc.sticky.get("gestaltung_bridge")
|
b = sc.sticky.get("gestaltung_bridge")
|
||||||
@@ -1854,7 +2189,7 @@ def _install_selection_listener(bridge):
|
|||||||
finally:
|
finally:
|
||||||
_processing.discard(obj.Id)
|
_processing.discard(obj.Id)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] on_modify_attrs plot-sync:", ex)
|
print("[STYLES] on_modify_attrs plot-sync:", ex)
|
||||||
|
|
||||||
# --- (1) Layer-Wechsel -> Hatch mitziehen ---
|
# --- (1) Layer-Wechsel -> Hatch mitziehen ---
|
||||||
if old_lyr == new_lyr:
|
if old_lyr == new_lyr:
|
||||||
@@ -1883,16 +2218,16 @@ def _install_selection_listener(bridge):
|
|||||||
doc.Objects.ModifyAttributes(hatch_obj, ha, True)
|
doc.Objects.ModifyAttributes(hatch_obj, ha, True)
|
||||||
finally:
|
finally:
|
||||||
_processing.discard(hatch_id)
|
_processing.discard(hatch_id)
|
||||||
print("[GESTALTUNG] Curve {} Layer geaendert -> Hatch mitgezogen".format(obj.Id))
|
print("[STYLES] Curve {} Layer geaendert -> Hatch mitgezogen".format(obj.Id))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] on_modify_attrs:", ex)
|
print("[STYLES] on_modify_attrs:", ex)
|
||||||
return
|
return
|
||||||
# Falls die neue Ebene andere Fill-Settings hat (Pattern/Skala/Drehung),
|
# Falls die neue Ebene andere Fill-Settings hat (Pattern/Skala/Drehung),
|
||||||
# die Hatch entsprechend an die neue Layer-Definition angleichen.
|
# die Hatch entsprechend an die neue Layer-Definition angleichen.
|
||||||
try:
|
try:
|
||||||
refresh_layer_fills(doc)
|
refresh_layer_fills(doc)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] on_modify_attrs refresh:", ex)
|
print("[STYLES] on_modify_attrs refresh:", ex)
|
||||||
|
|
||||||
Rhino.RhinoDoc.SelectObjects += refresh
|
Rhino.RhinoDoc.SelectObjects += refresh
|
||||||
Rhino.RhinoDoc.DeselectObjects += refresh
|
Rhino.RhinoDoc.DeselectObjects += refresh
|
||||||
@@ -1902,7 +2237,7 @@ def _install_selection_listener(bridge):
|
|||||||
Rhino.RhinoDoc.AddRhinoObject += on_add
|
Rhino.RhinoDoc.AddRhinoObject += on_add
|
||||||
Rhino.RhinoDoc.ModifyObjectAttributes += on_modify_attrs
|
Rhino.RhinoDoc.ModifyObjectAttributes += on_modify_attrs
|
||||||
sc.sticky[flag] = True
|
sc.sticky[flag] = True
|
||||||
print("[GESTALTUNG] Listener aktiv (Selection + Hatch-Live-Update + Ebene-Auto-Fill + Layer-Sync)")
|
print("[STYLES] Listener active (Selection + Hatch-Live-Update + Ebene-Auto-Fill + Layer-Sync)")
|
||||||
|
|
||||||
|
|
||||||
def _bridge_factory():
|
def _bridge_factory():
|
||||||
+6
-4
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
swisstopo.py
|
swisstopo.py
|
||||||
STAC-API-Client + GeoTIFF/XYZ-Parser + Mesh-Builder fuer swisstopo-Daten.
|
STAC-API-Client + GeoTIFF/XYZ-Parser + Mesh-Builder fuer swisstopo-Daten.
|
||||||
@@ -707,7 +709,7 @@ def merge_grids(grids):
|
|||||||
|
|
||||||
def mesh_from_grid(grid, origin_shift=(0, 0, 0), unit_scale=1.0):
|
def mesh_from_grid(grid, origin_shift=(0, 0, 0), unit_scale=1.0):
|
||||||
"""Baut ein Rhino-Mesh aus dem XYZ-Grid. origin_shift wird auf jeden
|
"""Baut ein Rhino-Mesh aus dem XYZ-Grid. origin_shift wird auf jeden
|
||||||
Vertex angewendet (typisch: bbox-Center zu Welt-0/0/0 schieben).
|
Vertex applied (typisch: bbox-Center zu Welt-0/0/0 schieben).
|
||||||
unit_scale: Skalierung von Meter (Quelle XYZ) auf Doc-Units. Bei
|
unit_scale: Skalierung von Meter (Quelle XYZ) auf Doc-Units. Bei
|
||||||
mm-Doc = 1000, bei m-Doc = 1.0 ."""
|
mm-Doc = 1000, bei m-Doc = 1.0 ."""
|
||||||
es = grid["es"]; ns = grid["ns"]
|
es = grid["es"]; ns = grid["ns"]
|
||||||
@@ -897,7 +899,7 @@ def generate_patch_from_contours(doc, contour_curves, progress=None):
|
|||||||
brep = rg.Brep.CreatePatch(
|
brep = rg.Brep.CreatePatch(
|
||||||
geom_list, u_spans, v_spans, doc.ModelAbsoluteTolerance)
|
geom_list, u_spans, v_spans, doc.ModelAbsoluteTolerance)
|
||||||
if brep is None:
|
if brep is None:
|
||||||
if progress: progress("Patch fehlgeschlagen (None zurueck)")
|
if progress: progress("Patch failed (None zurueck)")
|
||||||
return None
|
return None
|
||||||
gid = doc.Objects.AddBrep(brep)
|
gid = doc.Objects.AddBrep(brep)
|
||||||
if gid and gid != System.Guid.Empty:
|
if gid and gid != System.Guid.Empty:
|
||||||
@@ -1025,7 +1027,7 @@ def volumize_terrain_object(doc, top_obj, depth_doc, progress=None):
|
|||||||
old_id = top_obj.Id
|
old_id = top_obj.Id
|
||||||
new_gid = doc.Objects.AddMesh(vol, attrs)
|
new_gid = doc.Objects.AddMesh(vol, attrs)
|
||||||
if new_gid is None or new_gid == System.Guid.Empty:
|
if new_gid is None or new_gid == System.Guid.Empty:
|
||||||
if progress: progress("Volumize: AddMesh fehlgeschlagen")
|
if progress: progress("Volumize: AddMesh failed")
|
||||||
return None
|
return None
|
||||||
doc.Objects.Delete(old_id, True)
|
doc.Objects.Delete(old_id, True)
|
||||||
new_obj = doc.Objects.Find(new_gid)
|
new_obj = doc.Objects.Find(new_gid)
|
||||||
@@ -1135,7 +1137,7 @@ def _geotiff_to_png(tif_path, max_dim=2048):
|
|||||||
img.width, img.height))
|
img.width, img.height))
|
||||||
return png_path
|
return png_path
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("[SWISSTOPO] Pillow nicht verfuegbar — versuche Eto.Drawing")
|
print("[SWISSTOPO] Pillow not available — versuche Eto.Drawing")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SWISSTOPO] Pillow-convert fail:", ex)
|
print("[SWISSTOPO] Pillow-convert fail:", ex)
|
||||||
# --- Variante 2: Eto.Drawing (Mac NSImage liest TIFF)
|
# --- Variante 2: Eto.Drawing (Mac NSImage liest TIFF)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Display-Mode-Templates
|
||||||
|
|
||||||
|
## dossier_plan.ini — was wir wollen
|
||||||
|
|
||||||
|
Das Plugin liest **`dossier_plan.ini`** aus diesem Ordner und lädt den
|
||||||
|
Dossier-Plan-Display-Mode daraus. So fügst du den Mode hinzu:
|
||||||
|
|
||||||
|
1. In Rhino: Display-Mode-Editor öffnen (Settings → View → Display Modes)
|
||||||
|
2. „Dossier Plan" auswählen (oder einen neuen Mode anlegen + benennen)
|
||||||
|
3. Settings so einstellen wie du sie haben willst:
|
||||||
|
- General → Visibility → „Show HiddenLines" **AUS**
|
||||||
|
- Clipping plane objects → „Use section styles" **AN**
|
||||||
|
- Curves, Mesh-Wires, Iso-Curves: alles aus
|
||||||
|
- Hintergrund weiss
|
||||||
|
- Was auch immer du brauchst…
|
||||||
|
4. Apply + OK
|
||||||
|
5. Display-Mode-Editor: rechts-klick auf „Dossier Plan" → **Export…**
|
||||||
|
6. Datei speichern als: `~/STUDIO/DOSSIER/rhino/templates/dossier_plan.ini`
|
||||||
|
|
||||||
|
Beim nächsten Reload (`_RunPythonScript /…/_reset_panels.py`) lädt das
|
||||||
|
Plugin den Mode aus dieser ini direkt. Name + Guid werden automatisch
|
||||||
|
auf „Dossier Plan" + `d0551e72-7e72-4170-b1a4-d0551e72d055` umgesetzt, alles
|
||||||
|
andere bleibt 1:1 wie du's exportiert hast.
|
||||||
|
|
||||||
|
## Fallback wenn keine ini da
|
||||||
|
|
||||||
|
Wenn `dossier_plan.ini` nicht existiert, klont das Plugin den
|
||||||
|
**Technical**-Mode und patcht ihn programmatisch (mit den paar Settings
|
||||||
|
die wir kennen). Funktioniert aber nicht so robust — Template ist
|
||||||
|
sauberer.
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d]
|
||||||
|
PipelineId=e1eb7363-87f2-4a2b-a861-256e77835369
|
||||||
|
SupportsShading=y
|
||||||
|
SupportsStereo=y
|
||||||
|
AddToMenu=y
|
||||||
|
AllowObjectAssignment=y
|
||||||
|
ShadedPipelineRequired=y
|
||||||
|
WireframePipelineRequired=y
|
||||||
|
PipelineLocked=y
|
||||||
|
Order=-5
|
||||||
|
DerivedFrom=00000000-0000-0000-0000-000000000000
|
||||||
|
Name=Dossier 3D
|
||||||
|
XrayAllObjects=n
|
||||||
|
IgnoreHighlights=n
|
||||||
|
DisableConduits=n
|
||||||
|
DisableTransparency=n
|
||||||
|
BBoxMode=0
|
||||||
|
RealtimeDisplayId=00000000-0000-0000-0000-000000000000
|
||||||
|
SupportsShadeCmd=y
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Lighting]
|
||||||
|
ShadowIntensity=40
|
||||||
|
ShadowClippingRadius=0
|
||||||
|
ShadowClippingUsage=0
|
||||||
|
PerPixelLighting=n
|
||||||
|
TransparencyTolerance=40
|
||||||
|
ShadowBlur=0
|
||||||
|
ShadowBias=10,12,0
|
||||||
|
ShowLights=n
|
||||||
|
UseHiddenLights=n
|
||||||
|
UseLightColor=n
|
||||||
|
LightingScheme=2
|
||||||
|
Luminosity=0
|
||||||
|
AmbientColor=0,0,0
|
||||||
|
LightCount=0
|
||||||
|
CastShadows=y
|
||||||
|
ShadowMapSize=2048
|
||||||
|
SkylightShadowQuality=4
|
||||||
|
NumSamples=4
|
||||||
|
ShadowMapType=2
|
||||||
|
ShadowBitDepth=32
|
||||||
|
ShadowColor=0,0,0
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects]
|
||||||
|
CPHidePoints=n
|
||||||
|
CPHighlight=y
|
||||||
|
CPHidden=n
|
||||||
|
nCPWireThickness=1
|
||||||
|
LockedUsage=2
|
||||||
|
LockedTrans=50
|
||||||
|
GhostLockedObjects=n
|
||||||
|
ClipSectionUsage=0
|
||||||
|
CPColor=0,0,0
|
||||||
|
eCVStyle=102
|
||||||
|
nCVSize=3
|
||||||
|
LayersFollowLockUsage=n
|
||||||
|
LockedObjectsBehind=n
|
||||||
|
LockedColor=100,100,100
|
||||||
|
CPSolidLines=n
|
||||||
|
CPSingleColor=n
|
||||||
|
CPHideSurface=n
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Annotations]
|
||||||
|
DotBorderColor=-1
|
||||||
|
ShowText=y
|
||||||
|
ShowAnnotations=y
|
||||||
|
DotTextColor=-1
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Curves]
|
||||||
|
ShowCurves=y
|
||||||
|
CurveThicknessUsage=0
|
||||||
|
LineJoinStyle=0
|
||||||
|
LineEndCapStyle=0
|
||||||
|
CurvePattern=-1
|
||||||
|
CurveTrans=0
|
||||||
|
CurveThickness=1
|
||||||
|
CurveColor=0,0,0
|
||||||
|
SingleCurveColor=n
|
||||||
|
ShowCurvatureHair=n
|
||||||
|
CurveThicknessScale=1
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Meshes]
|
||||||
|
ShowMeshNakedEdges=n
|
||||||
|
MeshNonmanifoldEdgeColor=0,0,0
|
||||||
|
MeshNakedEdgeColor=0,0,0
|
||||||
|
MeshEdgeColor=0,0,0
|
||||||
|
MeshNonmanifoldEdgeColorReduction=0
|
||||||
|
MeshNakedEdgeColorReduction=0
|
||||||
|
MeshEdgeColorReduction=0
|
||||||
|
MeshNonmanifoldEdgeThickness=0
|
||||||
|
MeshNakedEdgeThickness=2
|
||||||
|
MeshEdgeThickness=2
|
||||||
|
ShowMeshNonmanifoldEdges=n
|
||||||
|
ShowMeshEdges=n
|
||||||
|
MeshVertexSize=0
|
||||||
|
ShowMeshVertices=n
|
||||||
|
ShowMeshWires=n
|
||||||
|
MeshWirePattern=-1
|
||||||
|
MeshWireThickness=1
|
||||||
|
MeshWireColor=0,0,0
|
||||||
|
SingleMeshWireColor=n
|
||||||
|
HighlightMeshes=n
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Points]
|
||||||
|
PCSize=2
|
||||||
|
ShowPoints=y
|
||||||
|
PointStyle=51
|
||||||
|
PointSize=3
|
||||||
|
PCGripStyle=102
|
||||||
|
PCGripSize=2
|
||||||
|
PCStyle=50
|
||||||
|
ShowPointClouds=y
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\SubD]
|
||||||
|
CreaseVisible=n
|
||||||
|
CreaseUsage=0
|
||||||
|
CreaseColor=255,255,255
|
||||||
|
CreaseColorReduction=0
|
||||||
|
CreaseThickness=2
|
||||||
|
CreaseThicknessScale=1
|
||||||
|
CreaseApplyPattern=y
|
||||||
|
NonmanifoldVisible=n
|
||||||
|
NonmanifoldUsage=0
|
||||||
|
NonmanifoldColor=255,255,255
|
||||||
|
NonmanifoldColorReduction=0
|
||||||
|
NonmanifoldThickness=1
|
||||||
|
NonmanifoldThicknessScale=1
|
||||||
|
NonmanifoldApplyPattern=y
|
||||||
|
BoundaryVisible=n
|
||||||
|
BoundaryUsage=0
|
||||||
|
BoundaryColor=255,255,255
|
||||||
|
SmoothColorReduction=0
|
||||||
|
SmoothColor=255,255,255
|
||||||
|
ShowSymmetryAxis=y
|
||||||
|
SmoothVisible=n
|
||||||
|
SubDThicknessUsage=0
|
||||||
|
BoundaryColorReduction=0
|
||||||
|
BoundaryThickness=2
|
||||||
|
SmoothThickness=1
|
||||||
|
BoundaryThicknessScale=2
|
||||||
|
BoundaryApplyPattern=y
|
||||||
|
ShowReflectedPlane=y
|
||||||
|
SmoothUsage=0
|
||||||
|
PlaneColorUsage=2
|
||||||
|
SymmetryAxisThickness=0.02500000037252903
|
||||||
|
ReflectedColor=64,64,64
|
||||||
|
ColorPercentage=30
|
||||||
|
AxisColor=255,0,0
|
||||||
|
SmoothThicknessScale=1
|
||||||
|
SmoothApplyPattern=y
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Surfaces]
|
||||||
|
EdgeColorReduction=0
|
||||||
|
SurfaceKappaHair=n
|
||||||
|
HighlightSurfaces=n
|
||||||
|
ShowIsocurves=n
|
||||||
|
IsoThicknessUsed=n
|
||||||
|
IsocurveThickness=1
|
||||||
|
IsoUThickness=1
|
||||||
|
IsoVThickness=1
|
||||||
|
IsoWThickness=1
|
||||||
|
SingleIsoColor=n
|
||||||
|
IsoColor=0,0,0
|
||||||
|
IsoColorsUsed=n
|
||||||
|
IsoUColor=0,0,0
|
||||||
|
IsoVColor=0,0,0
|
||||||
|
IsoWColor=0,0,0
|
||||||
|
IsoPatternUsed=n
|
||||||
|
IsocurvePattern=-1
|
||||||
|
IsoUPattern=-1
|
||||||
|
IsoVPattern=-1
|
||||||
|
IsoWPattern=-1
|
||||||
|
ShowEdges=y
|
||||||
|
ShowNakedEdges=n
|
||||||
|
ShowTangentEdges=n
|
||||||
|
ShowTangentSeams=n
|
||||||
|
ShowNonmanifoldEdges=n
|
||||||
|
ShowEdgeEndpoints=n
|
||||||
|
EdgeThickness=1
|
||||||
|
EdgeColorUsage=0
|
||||||
|
NakedEdgeThickness=2
|
||||||
|
NakedEdgeColorUsage=0
|
||||||
|
NakedEdgeColorReduction=0
|
||||||
|
EdgeColor=0,0,0
|
||||||
|
NakedEdgeColor=0,0,0
|
||||||
|
NonmanifoldEdgeColor=0,0,0
|
||||||
|
EdgePattern=-1
|
||||||
|
NakedEdgePattern=-1
|
||||||
|
NonmanifoldEdgePattern=-1
|
||||||
|
SurfaceThicknessUsage=0
|
||||||
|
SurfaceEdgeThicknessScale=2
|
||||||
|
SurfaceEdgeApplyPattern=y
|
||||||
|
SurfaceNakedEdgeUseNormalEdgeThickness=y
|
||||||
|
SurfaceNakedEdgeThicknessScale=2
|
||||||
|
SurfaceNakedEdgeApplyPattern=y
|
||||||
|
SurfaceIsoUThicknessScale=1
|
||||||
|
SurfaceIsoVThicknessScale=1
|
||||||
|
SurfaceIsoWThicknessScale=1
|
||||||
|
SurfaceIsoUApplyPattern=y
|
||||||
|
SurfaceIsoVApplyPattern=y
|
||||||
|
SurfaceIsoWApplyPattern=y
|
||||||
|
ShowFlatSurfaceIsos=n
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Objects\Technical]
|
||||||
|
TEThickness=1
|
||||||
|
TCThickness=1
|
||||||
|
TSThickness=1
|
||||||
|
TIThickness=1
|
||||||
|
THColor=0,0,0
|
||||||
|
TEColor=0,0,0
|
||||||
|
TSiColor=0,0,0
|
||||||
|
TCColor=0,0,0
|
||||||
|
TSColor=0,0,0
|
||||||
|
TIColor=0,0,0
|
||||||
|
TechnicalMask=14
|
||||||
|
TechnicalUsageMask=0
|
||||||
|
THThickness=1
|
||||||
|
TSiThickness=1
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading]
|
||||||
|
ParallelLineRotation=0
|
||||||
|
CullBackfaces=n
|
||||||
|
ShadeVertexColors=n
|
||||||
|
SingleWireColor=n
|
||||||
|
WireColor=0,0,0
|
||||||
|
ShadeSurface=y
|
||||||
|
UseObjectMaterial=n
|
||||||
|
UseObjectBFMaterial=n
|
||||||
|
BakeTextures=y
|
||||||
|
ShowDecals=y
|
||||||
|
SurfaceColorWriting=y
|
||||||
|
ShadingEffect=0
|
||||||
|
ParallelLineWidth=2
|
||||||
|
ParallelLineSeparation=3
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material]
|
||||||
|
BackIsCustom=y
|
||||||
|
FrontIsCustom=n
|
||||||
|
UseBackMaterial=y
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Back Material]
|
||||||
|
ShineIntensity=100
|
||||||
|
Luminosity=0
|
||||||
|
Reflectivity=0
|
||||||
|
Transparency=0
|
||||||
|
Specular=255,255,255
|
||||||
|
Shine=0
|
||||||
|
Diffuse=126,126,126
|
||||||
|
OverrideObjectReflectivity=y
|
||||||
|
OverrideObjectTransparency=y
|
||||||
|
OverrideObjectColor=n
|
||||||
|
FlatShaded=n
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Back Material\BitmapTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Back Material\EmapTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Back Material\TransparencyTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Front Material]
|
||||||
|
Diffuse=126,126,126
|
||||||
|
Specular=255,255,255
|
||||||
|
Shine=128
|
||||||
|
ShineIntensity=100
|
||||||
|
Reflectivity=0
|
||||||
|
Transparency=0
|
||||||
|
FlatShaded=y
|
||||||
|
OverrideObjectColor=n
|
||||||
|
OverrideObjectTransparency=y
|
||||||
|
OverrideObjectReflectivity=y
|
||||||
|
Luminosity=0
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Front Material\BitmapTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Front Material\EmapTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\Shading\Material\Front Material\TransparencyTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d\View settings]
|
||||||
|
ShowClippingPlanes=n
|
||||||
|
FlipGlasses=n
|
||||||
|
AGViewingMode=0
|
||||||
|
AGColorMode=0
|
||||||
|
StereoParallax=1
|
||||||
|
StereoSeparation=1
|
||||||
|
StereoModeEnabled=0
|
||||||
|
BackgroundBitmap=
|
||||||
|
GradBotRight=210,210,210
|
||||||
|
WzColor=0,0,150
|
||||||
|
GridTrans=60
|
||||||
|
WyColor=75,150,75
|
||||||
|
WxColor=150,75,75
|
||||||
|
WorldAxesColor=0
|
||||||
|
GridPlaneColor=0,0,0
|
||||||
|
PlaneUsesGridColor=n
|
||||||
|
AxesPercentage=100
|
||||||
|
PlaneVisibility=0
|
||||||
|
GridPlaneTrans=90
|
||||||
|
DrawTransGridPlane=n
|
||||||
|
GradTopRight=240,240,240
|
||||||
|
GradBotLeft=210,210,210
|
||||||
|
GradTopLeft=240,240,240
|
||||||
|
SolidColor=250,250,250
|
||||||
|
FillMode=2
|
||||||
|
CustomLinearWorkflowPostProcessGamma=2.200000047683716
|
||||||
|
CustomLinearWorkflowPreProcessGamma=2.200000047683716
|
||||||
|
CustomLinearWorkflowPostProcessFrameBuffer=n
|
||||||
|
CustomLinearWorkflowPreProcessTextures=y
|
||||||
|
CustomLinearWorkflowPreProcessColors=y
|
||||||
|
LinearWorkflowUsage=0
|
||||||
|
CustomGroundPlaneShadowOnly=y
|
||||||
|
CustomGroundPlaneAutomaticAltitude=y
|
||||||
|
CustomGroundPlaneAltitude=0
|
||||||
|
CustomGroundPlaneShow=y
|
||||||
|
GroundPlaneUsage=1
|
||||||
|
UseDocumentGrid=n
|
||||||
|
DrawGrid=n
|
||||||
|
DrawAxes=n
|
||||||
|
DrawZAxis=n
|
||||||
|
DrawWorldAxes=n
|
||||||
|
ShowGridOnTop=n
|
||||||
|
ShowTransGrid=n
|
||||||
|
BlendGrid=n
|
||||||
|
VertScale=1
|
||||||
|
HorzScale=1
|
||||||
|
ClippingCPColor=255,255,255
|
||||||
|
ClippingEdgeColor=0,0,0
|
||||||
|
ClippingSurfaceColor=128,128,128
|
||||||
|
ClippingEdgeThickness=3
|
||||||
|
ClippingCPTrans=95
|
||||||
|
ClippingCPUsage=0
|
||||||
|
ClippingEdgesUsage=2
|
||||||
|
ClippingSurfaceUsage=0
|
||||||
|
ClippingShowCP=n
|
||||||
|
ClippingClipSelected=y
|
||||||
|
ClippingShowXEdges=y
|
||||||
|
ClippingShowXSurface=y
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055]
|
||||||
|
PipelineId=e1eb7363-87f2-4a2b-a861-256e77835369
|
||||||
|
SupportsShading=y
|
||||||
|
SupportsStereo=y
|
||||||
|
AddToMenu=y
|
||||||
|
AllowObjectAssignment=y
|
||||||
|
ShadedPipelineRequired=y
|
||||||
|
WireframePipelineRequired=y
|
||||||
|
PipelineLocked=y
|
||||||
|
Order=-6
|
||||||
|
DerivedFrom=00000000-0000-0000-0000-000000000000
|
||||||
|
Name=Dossier Plan
|
||||||
|
XrayAllObjects=n
|
||||||
|
IgnoreHighlights=n
|
||||||
|
DisableConduits=n
|
||||||
|
DisableTransparency=n
|
||||||
|
BBoxMode=0
|
||||||
|
RealtimeDisplayId=00000000-0000-0000-0000-000000000000
|
||||||
|
SupportsShadeCmd=y
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Lighting]
|
||||||
|
ShadowIntensity=100
|
||||||
|
ShadowClippingRadius=0
|
||||||
|
ShadowClippingUsage=0
|
||||||
|
PerPixelLighting=n
|
||||||
|
TransparencyTolerance=40
|
||||||
|
ShadowBlur=0
|
||||||
|
ShadowBias=10,12,0
|
||||||
|
ShowLights=n
|
||||||
|
UseHiddenLights=n
|
||||||
|
UseLightColor=n
|
||||||
|
LightingScheme=0
|
||||||
|
Luminosity=0
|
||||||
|
AmbientColor=0,0,0
|
||||||
|
LightCount=0
|
||||||
|
CastShadows=n
|
||||||
|
ShadowMapSize=2048
|
||||||
|
SkylightShadowQuality=4
|
||||||
|
NumSamples=4
|
||||||
|
ShadowMapType=2
|
||||||
|
ShadowBitDepth=32
|
||||||
|
ShadowColor=0,0,0
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects]
|
||||||
|
CPHidePoints=n
|
||||||
|
CPHighlight=y
|
||||||
|
CPHidden=n
|
||||||
|
nCPWireThickness=1
|
||||||
|
LockedUsage=2
|
||||||
|
LockedTrans=50
|
||||||
|
GhostLockedObjects=n
|
||||||
|
ClipSectionUsage=0
|
||||||
|
CPColor=0,0,0
|
||||||
|
eCVStyle=102
|
||||||
|
nCVSize=3
|
||||||
|
LayersFollowLockUsage=n
|
||||||
|
LockedObjectsBehind=n
|
||||||
|
LockedColor=100,100,100
|
||||||
|
CPSolidLines=n
|
||||||
|
CPSingleColor=n
|
||||||
|
CPHideSurface=n
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Annotations]
|
||||||
|
DotBorderColor=-1
|
||||||
|
ShowText=y
|
||||||
|
ShowAnnotations=y
|
||||||
|
DotTextColor=-1
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Curves]
|
||||||
|
ShowCurves=y
|
||||||
|
CurveThicknessUsage=0
|
||||||
|
LineJoinStyle=0
|
||||||
|
LineEndCapStyle=0
|
||||||
|
CurvePattern=-1
|
||||||
|
CurveTrans=0
|
||||||
|
CurveThickness=1
|
||||||
|
CurveColor=0,0,0
|
||||||
|
SingleCurveColor=n
|
||||||
|
ShowCurvatureHair=n
|
||||||
|
CurveThicknessScale=1
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Meshes]
|
||||||
|
ShowMeshNakedEdges=n
|
||||||
|
MeshNonmanifoldEdgeColor=0,0,0
|
||||||
|
MeshNakedEdgeColor=0,0,0
|
||||||
|
MeshEdgeColor=0,0,0
|
||||||
|
MeshNonmanifoldEdgeColorReduction=0
|
||||||
|
MeshNakedEdgeColorReduction=0
|
||||||
|
MeshEdgeColorReduction=0
|
||||||
|
MeshNonmanifoldEdgeThickness=0
|
||||||
|
MeshNakedEdgeThickness=2
|
||||||
|
MeshEdgeThickness=2
|
||||||
|
ShowMeshNonmanifoldEdges=n
|
||||||
|
ShowMeshEdges=n
|
||||||
|
MeshVertexSize=0
|
||||||
|
ShowMeshVertices=n
|
||||||
|
ShowMeshWires=n
|
||||||
|
MeshWirePattern=-1
|
||||||
|
MeshWireThickness=1
|
||||||
|
MeshWireColor=0,0,0
|
||||||
|
SingleMeshWireColor=n
|
||||||
|
HighlightMeshes=n
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Points]
|
||||||
|
PCSize=2
|
||||||
|
ShowPoints=y
|
||||||
|
PointStyle=51
|
||||||
|
PointSize=3
|
||||||
|
PCGripStyle=102
|
||||||
|
PCGripSize=2
|
||||||
|
PCStyle=50
|
||||||
|
ShowPointClouds=y
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\SubD]
|
||||||
|
CreaseVisible=n
|
||||||
|
CreaseUsage=0
|
||||||
|
CreaseColor=255,255,255
|
||||||
|
CreaseColorReduction=0
|
||||||
|
CreaseThickness=2
|
||||||
|
CreaseThicknessScale=1
|
||||||
|
CreaseApplyPattern=y
|
||||||
|
NonmanifoldVisible=n
|
||||||
|
NonmanifoldUsage=0
|
||||||
|
NonmanifoldColor=255,255,255
|
||||||
|
NonmanifoldColorReduction=0
|
||||||
|
NonmanifoldThickness=1
|
||||||
|
NonmanifoldThicknessScale=1
|
||||||
|
NonmanifoldApplyPattern=y
|
||||||
|
BoundaryVisible=n
|
||||||
|
BoundaryUsage=0
|
||||||
|
BoundaryColor=255,255,255
|
||||||
|
SmoothColorReduction=0
|
||||||
|
SmoothColor=255,255,255
|
||||||
|
ShowSymmetryAxis=y
|
||||||
|
SmoothVisible=n
|
||||||
|
SubDThicknessUsage=0
|
||||||
|
BoundaryColorReduction=0
|
||||||
|
BoundaryThickness=2
|
||||||
|
SmoothThickness=1
|
||||||
|
BoundaryThicknessScale=2
|
||||||
|
BoundaryApplyPattern=y
|
||||||
|
ShowReflectedPlane=y
|
||||||
|
SmoothUsage=0
|
||||||
|
PlaneColorUsage=2
|
||||||
|
SymmetryAxisThickness=0.02500000037252903
|
||||||
|
ReflectedColor=64,64,64
|
||||||
|
ColorPercentage=30
|
||||||
|
AxisColor=255,0,0
|
||||||
|
SmoothThicknessScale=1
|
||||||
|
SmoothApplyPattern=y
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Surfaces]
|
||||||
|
EdgeColorReduction=0
|
||||||
|
SurfaceKappaHair=n
|
||||||
|
HighlightSurfaces=n
|
||||||
|
ShowIsocurves=n
|
||||||
|
IsoThicknessUsed=n
|
||||||
|
IsocurveThickness=1
|
||||||
|
IsoUThickness=1
|
||||||
|
IsoVThickness=1
|
||||||
|
IsoWThickness=1
|
||||||
|
SingleIsoColor=n
|
||||||
|
IsoColor=0,0,0
|
||||||
|
IsoColorsUsed=n
|
||||||
|
IsoUColor=0,0,0
|
||||||
|
IsoVColor=0,0,0
|
||||||
|
IsoWColor=0,0,0
|
||||||
|
IsoPatternUsed=n
|
||||||
|
IsocurvePattern=-1
|
||||||
|
IsoUPattern=-1
|
||||||
|
IsoVPattern=-1
|
||||||
|
IsoWPattern=-1
|
||||||
|
ShowEdges=y
|
||||||
|
ShowNakedEdges=n
|
||||||
|
ShowTangentEdges=n
|
||||||
|
ShowTangentSeams=n
|
||||||
|
ShowNonmanifoldEdges=n
|
||||||
|
ShowEdgeEndpoints=n
|
||||||
|
EdgeThickness=1
|
||||||
|
EdgeColorUsage=0
|
||||||
|
NakedEdgeThickness=2
|
||||||
|
NakedEdgeColorUsage=0
|
||||||
|
NakedEdgeColorReduction=0
|
||||||
|
EdgeColor=0,0,0
|
||||||
|
NakedEdgeColor=0,0,0
|
||||||
|
NonmanifoldEdgeColor=0,0,0
|
||||||
|
EdgePattern=-1
|
||||||
|
NakedEdgePattern=-1
|
||||||
|
NonmanifoldEdgePattern=-1
|
||||||
|
SurfaceThicknessUsage=0
|
||||||
|
SurfaceEdgeThicknessScale=2
|
||||||
|
SurfaceEdgeApplyPattern=y
|
||||||
|
SurfaceNakedEdgeUseNormalEdgeThickness=y
|
||||||
|
SurfaceNakedEdgeThicknessScale=2
|
||||||
|
SurfaceNakedEdgeApplyPattern=y
|
||||||
|
SurfaceIsoUThicknessScale=1
|
||||||
|
SurfaceIsoVThicknessScale=1
|
||||||
|
SurfaceIsoWThicknessScale=1
|
||||||
|
SurfaceIsoUApplyPattern=y
|
||||||
|
SurfaceIsoVApplyPattern=y
|
||||||
|
SurfaceIsoWApplyPattern=y
|
||||||
|
ShowFlatSurfaceIsos=n
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Objects\Technical]
|
||||||
|
TEThickness=1
|
||||||
|
TCThickness=1
|
||||||
|
TSThickness=1
|
||||||
|
TIThickness=1
|
||||||
|
THColor=0,0,0
|
||||||
|
TEColor=0,0,0
|
||||||
|
TSiColor=0,0,0
|
||||||
|
TCColor=0,0,0
|
||||||
|
TSColor=0,0,0
|
||||||
|
TIColor=0,0,0
|
||||||
|
TechnicalMask=14
|
||||||
|
TechnicalUsageMask=0
|
||||||
|
THThickness=1
|
||||||
|
TSiThickness=1
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading]
|
||||||
|
ParallelLineRotation=0
|
||||||
|
CullBackfaces=n
|
||||||
|
ShadeVertexColors=n
|
||||||
|
SingleWireColor=n
|
||||||
|
WireColor=0,0,0
|
||||||
|
ShadeSurface=y
|
||||||
|
UseObjectMaterial=n
|
||||||
|
UseObjectBFMaterial=n
|
||||||
|
BakeTextures=y
|
||||||
|
ShowDecals=y
|
||||||
|
SurfaceColorWriting=y
|
||||||
|
ShadingEffect=0
|
||||||
|
ParallelLineWidth=2
|
||||||
|
ParallelLineSeparation=3
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material]
|
||||||
|
BackIsCustom=y
|
||||||
|
FrontIsCustom=n
|
||||||
|
UseBackMaterial=y
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Back Material]
|
||||||
|
ShineIntensity=100
|
||||||
|
Luminosity=0
|
||||||
|
Reflectivity=0
|
||||||
|
Transparency=0
|
||||||
|
Specular=255,255,255
|
||||||
|
Shine=0
|
||||||
|
Diffuse=126,126,126
|
||||||
|
OverrideObjectReflectivity=y
|
||||||
|
OverrideObjectTransparency=y
|
||||||
|
OverrideObjectColor=n
|
||||||
|
FlatShaded=n
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Back Material\BitmapTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Back Material\EmapTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Back Material\TransparencyTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Front Material]
|
||||||
|
Diffuse=126,126,126
|
||||||
|
Specular=255,255,255
|
||||||
|
Shine=128
|
||||||
|
ShineIntensity=100
|
||||||
|
Reflectivity=0
|
||||||
|
Transparency=0
|
||||||
|
FlatShaded=y
|
||||||
|
OverrideObjectColor=n
|
||||||
|
OverrideObjectTransparency=y
|
||||||
|
OverrideObjectReflectivity=y
|
||||||
|
Luminosity=0
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Front Material\BitmapTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Front Material\EmapTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\Shading\Material\Front Material\TransparencyTexture]
|
||||||
|
=
|
||||||
|
[DisplayMode\d0551e72-7e72-4170-b1a4-d0551e72d055\View settings]
|
||||||
|
ShowClippingPlanes=n
|
||||||
|
FlipGlasses=n
|
||||||
|
AGViewingMode=0
|
||||||
|
AGColorMode=0
|
||||||
|
StereoParallax=1
|
||||||
|
StereoSeparation=1
|
||||||
|
StereoModeEnabled=0
|
||||||
|
BackgroundBitmap=
|
||||||
|
GradBotRight=140,140,140
|
||||||
|
WzColor=0,0,150
|
||||||
|
GridTrans=60
|
||||||
|
WyColor=75,150,75
|
||||||
|
WxColor=150,75,75
|
||||||
|
WorldAxesColor=0
|
||||||
|
GridPlaneColor=0,0,0
|
||||||
|
PlaneUsesGridColor=n
|
||||||
|
AxesPercentage=100
|
||||||
|
PlaneVisibility=0
|
||||||
|
GridPlaneTrans=90
|
||||||
|
DrawTransGridPlane=n
|
||||||
|
GradTopRight=200,200,200
|
||||||
|
GradBotLeft=140,140,140
|
||||||
|
GradTopLeft=200,200,200
|
||||||
|
SolidColor=230,230,230
|
||||||
|
FillMode=2
|
||||||
|
CustomLinearWorkflowPostProcessGamma=2.200000047683716
|
||||||
|
CustomLinearWorkflowPreProcessGamma=2.200000047683716
|
||||||
|
CustomLinearWorkflowPostProcessFrameBuffer=n
|
||||||
|
CustomLinearWorkflowPreProcessTextures=y
|
||||||
|
CustomLinearWorkflowPreProcessColors=y
|
||||||
|
LinearWorkflowUsage=0
|
||||||
|
CustomGroundPlaneShadowOnly=y
|
||||||
|
CustomGroundPlaneAutomaticAltitude=y
|
||||||
|
CustomGroundPlaneAltitude=0
|
||||||
|
CustomGroundPlaneShow=y
|
||||||
|
GroundPlaneUsage=1
|
||||||
|
UseDocumentGrid=n
|
||||||
|
DrawGrid=n
|
||||||
|
DrawAxes=n
|
||||||
|
DrawZAxis=n
|
||||||
|
DrawWorldAxes=n
|
||||||
|
ShowGridOnTop=n
|
||||||
|
ShowTransGrid=n
|
||||||
|
BlendGrid=n
|
||||||
|
VertScale=1
|
||||||
|
HorzScale=1
|
||||||
|
ClippingCPColor=255,255,255
|
||||||
|
ClippingEdgeColor=0,0,0
|
||||||
|
ClippingSurfaceColor=128,128,128
|
||||||
|
ClippingEdgeThickness=3
|
||||||
|
ClippingCPTrans=95
|
||||||
|
ClippingCPUsage=0
|
||||||
|
ClippingEdgesUsage=2
|
||||||
|
ClippingSurfaceUsage=0
|
||||||
|
ClippingShowCP=n
|
||||||
|
ClippingClipSelected=y
|
||||||
|
ClippingShowXEdges=y
|
||||||
|
ClippingShowXSurface=y
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
text_create.py
|
text_create.py
|
||||||
Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog.
|
Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
text_editor.py
|
text_editor.py
|
||||||
React-WYSIWYG-Editor in Satellite-WebView (Topmost). User picked Frame
|
React-WYSIWYG-Editor in Satellite-WebView (Topmost). User picked Frame
|
||||||
@@ -62,7 +64,7 @@ def _ensure_double_click_hook():
|
|||||||
if not sc.sticky.get("dossier_text_idle_registered"):
|
if not sc.sticky.get("dossier_text_idle_registered"):
|
||||||
Rhino.RhinoApp.Idle += _on_idle_check_pending_edit
|
Rhino.RhinoApp.Idle += _on_idle_check_pending_edit
|
||||||
sc.sticky["dossier_text_idle_registered"] = True
|
sc.sticky["dossier_text_idle_registered"] = True
|
||||||
print("[TEXT-HOOK] Doppelklick-Hook installiert")
|
print("[TEXT-HOOK] Double-click hook installed")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT-HOOK] install:", ex)
|
print("[TEXT-HOOK] install:", ex)
|
||||||
|
|
||||||
@@ -91,9 +93,9 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
self._initial_settings = settings
|
self._initial_settings = settings
|
||||||
self._fonts = fonts
|
self._fonts = fonts
|
||||||
self._form_ref = None
|
self._form_ref = None
|
||||||
self._edit_obj_id = edit_obj_id # bei Doppelklick-Edit gesetzt
|
self._edit_obj_id = edit_obj_id # bei Doppelklick-Edit set
|
||||||
self._initial_text = initial_text
|
self._initial_text = initial_text
|
||||||
self._initial_runs = initial_runs # rich-format-Runs falls vorhanden
|
self._initial_runs = initial_runs # rich-format-Runs falls present
|
||||||
self._initial_html = initial_html # 1:1 Editor-HTML beim Reopen
|
self._initial_html = initial_html # 1:1 Editor-HTML beim Reopen
|
||||||
|
|
||||||
def set_form(self, form):
|
def set_form(self, form):
|
||||||
@@ -202,7 +204,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
st.get("underline"))
|
st.get("underline"))
|
||||||
|
|
||||||
# 3. Text-Wrap im Frame — NACH dem Content damit es nicht
|
# 3. Text-Wrap im Frame — NACH dem Content damit es nicht
|
||||||
# durch RichText-Set zurueckgesetzt wird. Beide Setter
|
# durch RichText-Set zurueckset wird. Beide Setter
|
||||||
# versuchen (verschiedene Rhino-Versions-APIs).
|
# versuchen (verschiedene Rhino-Versions-APIs).
|
||||||
applied_w = None
|
applied_w = None
|
||||||
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
|
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
|
||||||
@@ -272,7 +274,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
break
|
break
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
if applied_scale is None:
|
if applied_scale is None:
|
||||||
print("[TEXT-EDITOR] AnnotationScaling-Property nicht gefunden")
|
print("[TEXT-EDITOR] AnnotationScaling-Property not found")
|
||||||
|
|
||||||
attrs = Rhino.DocObjects.ObjectAttributes()
|
attrs = Rhino.DocObjects.ObjectAttributes()
|
||||||
col = st.get("color") # [r,g,b] oder None
|
col = st.get("color") # [r,g,b] oder None
|
||||||
@@ -571,7 +573,7 @@ def open_for_edit(obj):
|
|||||||
if rj: initial_runs = json.loads(rj)
|
if rj: initial_runs = json.loads(rj)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT-EDITOR] read runs:", ex)
|
print("[TEXT-EDITOR] read runs:", ex)
|
||||||
# Editor-innerHTML (Round-Trip-Konservierung): wenn vorhanden,
|
# Editor-innerHTML (Round-Trip-Konservierung): wenn present,
|
||||||
# wird der Editor exakt mit diesem HTML geoeffnet
|
# wird der Editor exakt mit diesem HTML geoeffnet
|
||||||
initial_html = None
|
initial_html = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
|||||||
#! python 3
|
#! python 3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
werkzeuge.py
|
werkzeuge.py
|
||||||
WERKZEUGE-Panel: Architektur-orientierte Toolbar als React-WebView.
|
WERKZEUGE-Panel: Architektur-orientierte Toolbar als React-WebView.
|
||||||
@@ -43,11 +45,11 @@ class WerkzeugeBridge(panel_base.BaseBridge):
|
|||||||
if cmd.startswith("_") and "\n" not in cmd and ";" not in cmd:
|
if cmd.startswith("_") and "\n" not in cmd and ";" not in cmd:
|
||||||
try:
|
try:
|
||||||
Rhino.RhinoApp.RunScript(cmd, False)
|
Rhino.RhinoApp.RunScript(cmd, False)
|
||||||
print("[WERKZEUGE] {}".format(cmd))
|
print("[TOOLS] {}".format(cmd))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[WERKZEUGE] RunScript-Fehler:", ex)
|
print("[TOOLS] RunScript-Fehler:", ex)
|
||||||
else:
|
else:
|
||||||
print("[WERKZEUGE] Befehl ignoriert (kein '_' Praefix oder unsicher):", cmd)
|
print("[TOOLS] Befehl ignoriert (kein '_' Praefix oder unsicher):", cmd)
|
||||||
|
|
||||||
|
|
||||||
def _bridge_factory():
|
def _bridge_factory():
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""
|
||||||
|
treppe_grips.py
|
||||||
|
Display-Conduit fuer gruene Marker an Treppen-Achsen. Visuelle
|
||||||
|
Indikation wie bei Waenden, aber keine eigene Drag-Logik — der normale
|
||||||
|
Partnership-Cascade (elemente._on_select_objects) + Pure-Transform-Pfad
|
||||||
|
verschieben die Treppe bereits sauber.
|
||||||
|
|
||||||
|
Marker-Logik pro Treppen-Art:
|
||||||
|
- gerade : PointAtStart, PointAtEnd der Linie
|
||||||
|
- L (3-Pt): poly[0] (Start), poly[1] (Eck), poly[2] (Ende) — alle 3
|
||||||
|
damit das Eck einzeln gegriffen werden kann
|
||||||
|
- L (4-Pt): alle 4 Punkte (Start, Lauf1-Ende, Lauf2-Anfang, Ende)
|
||||||
|
- Wendel : poly[1] (Start), poly[2] (Ende) — poly[0] ist Rotations-
|
||||||
|
zentrum, nicht der Treppen-Anfang
|
||||||
|
"""
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Display as rd
|
||||||
|
import Rhino.DocObjects as rdoc
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
import scriptcontext as sc
|
||||||
|
import System.Drawing as SD
|
||||||
|
|
||||||
|
|
||||||
|
_MARKER_RADIUS_PX = 7
|
||||||
|
_MARKER_FILL = SD.Color.FromArgb(220, 95, 168, 150) # petrol-gruen, gleich wie wand_grips
|
||||||
|
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
|
||||||
|
|
||||||
|
|
||||||
|
def _treppe_endpoints(axis_obj):
|
||||||
|
"""Liefert Liste von Point3d. Beachtet treppe_art + Polyline-Punktzahl."""
|
||||||
|
if axis_obj is None or axis_obj.IsDeleted: return []
|
||||||
|
a = axis_obj.Attributes
|
||||||
|
if a.GetUserString("dossier_element_type") != "treppe_axis": return []
|
||||||
|
geom = axis_obj.Geometry
|
||||||
|
if not isinstance(geom, rg.Curve): return []
|
||||||
|
art = a.GetUserString("dossier_treppe_art") or "gerade"
|
||||||
|
try:
|
||||||
|
if art == "wendel":
|
||||||
|
ok, poly = geom.TryGetPolyline()
|
||||||
|
if not ok or poly is None or poly.Count != 3: return []
|
||||||
|
return [poly[1], poly[2]]
|
||||||
|
if art == "l":
|
||||||
|
ok, poly = geom.TryGetPolyline()
|
||||||
|
if not ok or poly is None: return []
|
||||||
|
return [poly[i] for i in range(poly.Count)]
|
||||||
|
return [geom.PointAtStart, geom.PointAtEnd]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _enumerator_all():
|
||||||
|
"""Iterator-Settings die hidden + locked Objekte mit einschliessen —
|
||||||
|
Mac-Default skipt sonst hidden-Layer-Objekte."""
|
||||||
|
s = rdoc.ObjectEnumeratorSettings()
|
||||||
|
s.HiddenObjects = True
|
||||||
|
s.LockedObjects = True
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
class _TreppeEndpointConduit(rd.DisplayConduit):
|
||||||
|
"""Zeichnet gruene Marker an allen selektierten Treppen-Achsen."""
|
||||||
|
|
||||||
|
def DrawForeground(self, e):
|
||||||
|
try:
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
seen = set()
|
||||||
|
for obj in sel:
|
||||||
|
a = obj.Attributes
|
||||||
|
eid = a.GetUserString("dossier_element_id") or ""
|
||||||
|
if not eid or eid in seen: continue
|
||||||
|
# Source-Axis via element_id finden — auch wenn auf hidden
|
||||||
|
# Layer (User hat z.B. nur 2D-Plansymbol selektiert).
|
||||||
|
axis = None
|
||||||
|
for o in doc.Objects.GetObjectList(_enumerator_all()):
|
||||||
|
if o is None or o.IsDeleted: continue
|
||||||
|
try:
|
||||||
|
a2 = o.Attributes
|
||||||
|
if a2.GetUserString("dossier_element_id") == eid and \
|
||||||
|
a2.GetUserString("dossier_element_type") == "treppe_axis":
|
||||||
|
axis = o; break
|
||||||
|
except Exception: continue
|
||||||
|
if axis is None: continue
|
||||||
|
seen.add(eid)
|
||||||
|
for pt in _treppe_endpoints(axis):
|
||||||
|
try:
|
||||||
|
e.Display.DrawPoint(pt,
|
||||||
|
rd.PointStyle.RoundControlPoint,
|
||||||
|
_MARKER_RADIUS_PX, _MARKER_FILL)
|
||||||
|
except Exception:
|
||||||
|
try: e.Display.DrawDot(pt, "●", _MARKER_FILL, _MARKER_BORDER)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STAIR-GRIPS] DrawForeground:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
_STICKY_CONDUIT = "_dossier_treppe_grips_conduit"
|
||||||
|
|
||||||
|
|
||||||
|
def install_handlers():
|
||||||
|
"""Idempotente Registrierung. Bei Modul-Reload alten Conduit zuerst
|
||||||
|
disablen, dann neuen anhaengen."""
|
||||||
|
try:
|
||||||
|
old = sc.sticky.get(_STICKY_CONDUIT)
|
||||||
|
if old is not None:
|
||||||
|
try: old.Enabled = False
|
||||||
|
except Exception: pass
|
||||||
|
conduit = _TreppeEndpointConduit()
|
||||||
|
conduit.Enabled = True
|
||||||
|
sc.sticky[_STICKY_CONDUIT] = conduit
|
||||||
|
print("[STAIR-GRIPS] Endpoint conduit active")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STAIR-GRIPS] install:", ex)
|
||||||
+92
-84
@@ -1,5 +1,7 @@
|
|||||||
#! python3
|
#! python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
"""
|
"""
|
||||||
wand_grips.py
|
wand_grips.py
|
||||||
Custom Endpoint-Grips fuer Waende — Display-Conduit + MouseCallback Overlay.
|
Custom Endpoint-Grips fuer Waende — Display-Conduit + MouseCallback Overlay.
|
||||||
@@ -94,53 +96,44 @@ def _find_axis_for_obj(doc, obj):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _curve_endpoints(curve):
|
def _axis_vertices(geom):
|
||||||
"""Liefert (start_pt, end_pt) fuer eine wand_axis. Funktioniert fuer
|
"""Liefert die Vertices der wand_axis-Curve als Liste.
|
||||||
LineCurve, PolylineCurve, NurbsCurve etc — alle Curve-Typen haben
|
- PolylineCurve: alle Vertices
|
||||||
PointAtStart/PointAtEnd. Bei degenerierten Curves None."""
|
- LineCurve / sonstige Curve: [Start, End] (zwei-Vertex-Faelle)
|
||||||
if curve is None: return None, None
|
Returnt [] bei degeneriertem Input."""
|
||||||
|
if geom is None: return []
|
||||||
try:
|
try:
|
||||||
return curve.PointAtStart, curve.PointAtEnd
|
if isinstance(geom, rg.PolylineCurve):
|
||||||
|
poly = geom.ToPolyline()
|
||||||
|
if poly is None or poly.Count < 2: return []
|
||||||
|
return list(poly)
|
||||||
|
p_start = geom.PointAtStart
|
||||||
|
p_end = geom.PointAtEnd
|
||||||
|
return [p_start, p_end]
|
||||||
except Exception:
|
except Exception:
|
||||||
return None, None
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _replace_axis_endpoint(doc, axis_obj, kind, new_pt):
|
def _replace_axis_vertex(doc, axis_obj, vertex_idx, new_pt):
|
||||||
"""Tauscht den Start- (kind='start') oder Endpunkt (kind='end') der
|
"""Tauscht den Vertex an Index `vertex_idx` der wand_axis-Curve gegen
|
||||||
wand_axis-Curve gegen new_pt. Geht intelligent um mit:
|
new_pt. Funktioniert fuer Linien (idx 0/1) und Polylinien (alle idx).
|
||||||
- LineCurve: erzeuge neue Line vom fixen Punkt zum neuen Punkt
|
Setzt die neue Geometrie via Objects.Replace — feuert
|
||||||
- PolylineCurve: ersetze ersten/letzten Vertex, Rest bleibt
|
|
||||||
- andere Curve-Typen: aktuell nur Line-Fallback (Erst/Letzt-Vertex
|
|
||||||
rekonstruieren)
|
|
||||||
Setzt die neue Geometrie via Objects.Replace — das feuert
|
|
||||||
ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft."""
|
ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft."""
|
||||||
if axis_obj is None or axis_obj.IsDeleted: return False
|
if axis_obj is None or axis_obj.IsDeleted: return False
|
||||||
geom = axis_obj.Geometry
|
geom = axis_obj.Geometry
|
||||||
if geom is None: return False
|
if geom is None: return False
|
||||||
try:
|
try:
|
||||||
# PolylineCurve mit > 2 Vertices: ersten/letzten Vertex ersetzen
|
pts = _axis_vertices(geom)
|
||||||
if isinstance(geom, rg.PolylineCurve):
|
if not pts: return False
|
||||||
poly = geom.ToPolyline()
|
if vertex_idx < 0 or vertex_idx >= len(pts): return False
|
||||||
if poly is None or poly.Count < 2: return False
|
pts[vertex_idx] = new_pt
|
||||||
pts = list(poly)
|
if len(pts) == 2:
|
||||||
if kind == "start":
|
new_curve = rg.LineCurve(pts[0], pts[1])
|
||||||
pts[0] = new_pt
|
|
||||||
else:
|
else:
|
||||||
pts[-1] = new_pt
|
new_curve = rg.PolylineCurve(rg.Polyline(pts))
|
||||||
new_poly = rg.Polyline(pts)
|
|
||||||
new_curve = rg.PolylineCurve(new_poly)
|
|
||||||
else:
|
|
||||||
# LineCurve oder unbekannter Typ → reduziere auf Line zwischen
|
|
||||||
# neuem + altem fixen Punkt.
|
|
||||||
p_start, p_end = _curve_endpoints(geom)
|
|
||||||
if p_start is None or p_end is None: return False
|
|
||||||
if kind == "start":
|
|
||||||
new_curve = rg.LineCurve(new_pt, p_end)
|
|
||||||
else:
|
|
||||||
new_curve = rg.LineCurve(p_start, new_pt)
|
|
||||||
return doc.Objects.Replace(axis_obj.Id, new_curve)
|
return doc.Objects.Replace(axis_obj.Id, new_curve)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[WAND_GRIPS] replace endpoint:", ex)
|
print("[WALL-GRIPS] replace vertex:", ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -153,15 +146,17 @@ class _EndpointConduit(rd.DisplayConduit):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
rd.DisplayConduit.__init__(self)
|
rd.DisplayConduit.__init__(self)
|
||||||
self.hot_key = None # (axis_id_str, kind) — fuer Hover
|
self.hot_key = None # (axis_id_str, vidx) — fuer Hover
|
||||||
self.drag_key = None # (axis_id_str, kind) — waehrend aktivem Drag
|
self.drag_key = None # (axis_id_str, vidx) — waehrend aktivem Drag
|
||||||
self.drag_preview = None # rg.Line — Live-Vorschau waehrend GetPoint
|
self.drag_preview = None # Liste von rg.Line — Live-Vorschau (Linien
|
||||||
|
# zu Nachbar-Vertices waehrend GetPoint)
|
||||||
|
|
||||||
def _collect_endpoints(self, doc):
|
def _collect_grip_points(self, doc):
|
||||||
"""Liefert Liste von (axis_obj, kind, world_pt) fuer alle selektier-
|
"""Liefert Liste von (axis_obj, vertex_idx, world_pt) fuer ALLE
|
||||||
ten Waende. Iteriert die Selektion + dedupliziert Achsen (jede
|
Vertices aller selektierten Waende — fuer Polyline-Waende ist
|
||||||
Wand erscheint nur einmal, auch wenn mehrere Volumen mit-selek-
|
jeder Knick ein eigener Grip. Iteriert die Selektion + dedupli-
|
||||||
tiert sind)."""
|
ziert Achsen (jede Wand erscheint nur einmal, auch wenn mehrere
|
||||||
|
Volumen mit-selektiert sind)."""
|
||||||
out = []
|
out = []
|
||||||
seen_axis = set()
|
seen_axis = set()
|
||||||
try:
|
try:
|
||||||
@@ -173,24 +168,21 @@ class _EndpointConduit(rd.DisplayConduit):
|
|||||||
aid = str(axis.Id)
|
aid = str(axis.Id)
|
||||||
if aid in seen_axis: continue
|
if aid in seen_axis: continue
|
||||||
seen_axis.add(aid)
|
seen_axis.add(aid)
|
||||||
p_start, p_end = _curve_endpoints(axis.Geometry)
|
for i, pt in enumerate(_axis_vertices(axis.Geometry)):
|
||||||
if p_start is not None:
|
out.append((axis, i, pt))
|
||||||
out.append((axis, "start", p_start))
|
|
||||||
if p_end is not None:
|
|
||||||
out.append((axis, "end", p_end))
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def DrawForeground(self, e):
|
def DrawForeground(self, e):
|
||||||
try:
|
try:
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None: return
|
if doc is None: return
|
||||||
for axis, kind, pt in self._collect_endpoints(doc):
|
for axis, vidx, pt in self._collect_grip_points(doc):
|
||||||
aid = str(axis.Id)
|
aid = str(axis.Id)
|
||||||
# Skip den gerade gezogenen Marker — der wird via
|
# Skip den gerade gezogenen Marker — der wird via
|
||||||
# drag_preview separat dargestellt.
|
# drag_preview separat dargestellt.
|
||||||
if self.drag_key and self.drag_key == (aid, kind):
|
if self.drag_key and self.drag_key == (aid, vidx):
|
||||||
continue
|
continue
|
||||||
is_hot = self.hot_key and self.hot_key == (aid, kind)
|
is_hot = self.hot_key and self.hot_key == (aid, vidx)
|
||||||
r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX
|
r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX
|
||||||
fill = _MARKER_HOVER if is_hot else _MARKER_FILL
|
fill = _MARKER_HOVER if is_hot else _MARKER_FILL
|
||||||
# DrawPoint mit RoundControlPoint = gefuellter Kreis +
|
# DrawPoint mit RoundControlPoint = gefuellter Kreis +
|
||||||
@@ -202,13 +194,14 @@ class _EndpointConduit(rd.DisplayConduit):
|
|||||||
# Fallback fuer aeltere Rhino-Versionen: einfacher
|
# Fallback fuer aeltere Rhino-Versionen: einfacher
|
||||||
# DrawDot mit Label "●"
|
# DrawDot mit Label "●"
|
||||||
e.Display.DrawDot(pt, "●", fill, _MARKER_BORDER)
|
e.Display.DrawDot(pt, "●", fill, _MARKER_BORDER)
|
||||||
# Drag-Preview-Linie waehrend GetPoint aktiv ist
|
# Drag-Preview-Linien waehrend GetPoint aktiv ist
|
||||||
if self.drag_preview is not None:
|
if self.drag_preview:
|
||||||
|
for line in self.drag_preview:
|
||||||
try:
|
try:
|
||||||
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
|
e.Display.DrawLine(line, _MARKER_HOVER, 2)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[WAND_GRIPS] DrawForeground:", ex)
|
print("[WALL-GRIPS] DrawForeground:", ex)
|
||||||
|
|
||||||
|
|
||||||
# --- Mouse-Handler --------------------------------------------------------
|
# --- Mouse-Handler --------------------------------------------------------
|
||||||
@@ -224,21 +217,21 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
|
|||||||
self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point
|
self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point
|
||||||
|
|
||||||
def _hit_test(self, view, screen_pt):
|
def _hit_test(self, view, screen_pt):
|
||||||
"""Liefert (axis, kind, world_pt) wenn screen_pt nahe eines Endpoint-
|
"""Liefert (axis, vertex_idx, world_pt) wenn screen_pt nahe eines
|
||||||
Markers liegt, sonst None. Iteriert die aktuelle Conduit-Liste."""
|
Vertex-Markers liegt, sonst None."""
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None: return None
|
if doc is None: return None
|
||||||
try:
|
try:
|
||||||
vp = view.ActiveViewport
|
vp = view.ActiveViewport
|
||||||
except Exception: return None
|
except Exception: return None
|
||||||
thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX
|
thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX
|
||||||
for axis, kind, world_pt in self.conduit._collect_endpoints(doc):
|
for axis, vidx, world_pt in self.conduit._collect_grip_points(doc):
|
||||||
try:
|
try:
|
||||||
s = vp.WorldToClient(world_pt)
|
s = vp.WorldToClient(world_pt)
|
||||||
dx = s.X - screen_pt.X
|
dx = s.X - screen_pt.X
|
||||||
dy = s.Y - screen_pt.Y
|
dy = s.Y - screen_pt.Y
|
||||||
if (dx * dx + dy * dy) <= thresh2:
|
if (dx * dx + dy * dy) <= thresh2:
|
||||||
return axis, kind, world_pt
|
return axis, vidx, world_pt
|
||||||
except Exception: continue
|
except Exception: continue
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -272,46 +265,61 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
|
|||||||
# Default-Klick (Selection) abwuergen — wir uebernehmen
|
# Default-Klick (Selection) abwuergen — wir uebernehmen
|
||||||
try: e.Cancel = True
|
try: e.Cancel = True
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
axis, kind, world_pt = hit
|
axis, vidx, world_pt = hit
|
||||||
self._start_drag(view.Document, axis, kind, world_pt)
|
self._start_drag(view.Document, axis, vidx, world_pt)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[WAND_GRIPS] OnMouseDown:", ex)
|
print("[WALL-GRIPS] OnMouseDown:", ex)
|
||||||
|
|
||||||
def _start_drag(self, doc, axis, kind, anchor_pt):
|
def _start_drag(self, doc, axis, vertex_idx, anchor_pt):
|
||||||
"""Startet eine Rhino-GetPoint-Interaktion um den neuen Endpunkt
|
"""Startet eine Rhino-GetPoint-Interaktion um den Vertex zu
|
||||||
zu picken. Der ANDERE Endpunkt (Fix-Punkt) wird als BasePoint
|
verschieben. BasePoint-Strategie:
|
||||||
gesetzt — damit kriegt der User Tracking-Linie, Ortho-Mode etc.
|
- End-Vertex (idx 0 oder letzter): gegenueberliegender End-Vertex
|
||||||
wie bei _Move."""
|
→ User bekommt Tracking-Linie + Wand-Laenge wie bei _Move
|
||||||
|
- Mittel-Vertex (Polyline-Knick): Vertex selbst, plus Live-Preview
|
||||||
|
zu beiden Nachbar-Vertices damit beide Segmente sichtbar mit-
|
||||||
|
schwingen."""
|
||||||
if doc is None: return
|
if doc is None: return
|
||||||
geom = axis.Geometry
|
geom = axis.Geometry
|
||||||
if geom is None: return
|
if geom is None: return
|
||||||
p_start, p_end = _curve_endpoints(geom)
|
pts = _axis_vertices(geom)
|
||||||
if p_start is None or p_end is None: return
|
if not pts or vertex_idx < 0 or vertex_idx >= len(pts): return
|
||||||
fixed_pt = p_end if kind == "start" else p_start
|
is_first = vertex_idx == 0
|
||||||
# Conduit-State: drag-Marker hervorheben + Preview-Linie
|
is_last = vertex_idx == len(pts) - 1
|
||||||
self.conduit.drag_key = (str(axis.Id), kind)
|
prev_pt = pts[vertex_idx - 1] if not is_first else None
|
||||||
self.conduit.drag_preview = rg.Line(fixed_pt, anchor_pt)
|
next_pt = pts[vertex_idx + 1] if not is_last else None
|
||||||
|
if is_first: base_pt = next_pt
|
||||||
|
elif is_last: base_pt = prev_pt
|
||||||
|
else: base_pt = anchor_pt
|
||||||
|
# Conduit-State: drag-Marker hervorheben + Preview-Linien
|
||||||
|
self.conduit.drag_key = (str(axis.Id), vertex_idx)
|
||||||
|
self.conduit.drag_preview = []
|
||||||
|
if prev_pt is not None:
|
||||||
|
self.conduit.drag_preview.append(rg.Line(prev_pt, anchor_pt))
|
||||||
|
if next_pt is not None:
|
||||||
|
self.conduit.drag_preview.append(rg.Line(next_pt, anchor_pt))
|
||||||
self._busy = True
|
self._busy = True
|
||||||
try:
|
try:
|
||||||
gp = Rhino.Input.Custom.GetPoint()
|
gp = Rhino.Input.Custom.GetPoint()
|
||||||
gp.SetCommandPrompt("Wand-Endpunkt: neuer Punkt (Esc=Abbruch)")
|
gp.SetCommandPrompt("Wand-Vertex: neuer Punkt (Esc=Abbruch)")
|
||||||
gp.SetBasePoint(fixed_pt, True)
|
gp.SetBasePoint(base_pt, True)
|
||||||
gp.DrawLineFromPoint(fixed_pt, True)
|
gp.DrawLineFromPoint(base_pt, True)
|
||||||
# Live-Preview ueber Conduit (zusaetzlich zu Rhinos eigener
|
|
||||||
# Tracking-Linie) — sieht ueblich, hilft beim Verstehen welcher
|
|
||||||
# Endpunkt sich bewegt.
|
|
||||||
def _on_mouse_move(sender, args):
|
def _on_mouse_move(sender, args):
|
||||||
try:
|
try:
|
||||||
self.conduit.drag_preview = rg.Line(fixed_pt, args.Point)
|
preview = []
|
||||||
|
if prev_pt is not None:
|
||||||
|
preview.append(rg.Line(prev_pt, args.Point))
|
||||||
|
if next_pt is not None:
|
||||||
|
preview.append(rg.Line(next_pt, args.Point))
|
||||||
|
self.conduit.drag_preview = preview
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
try: gp.MouseMove += _on_mouse_move
|
try: gp.MouseMove += _on_mouse_move
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
res = gp.Get()
|
res = gp.Get()
|
||||||
if res == Rhino.Input.GetResult.Point:
|
if res == Rhino.Input.GetResult.Point:
|
||||||
new_pt = gp.Point()
|
new_pt = gp.Point()
|
||||||
_replace_axis_endpoint(doc, axis, kind, new_pt)
|
_replace_axis_vertex(doc, axis, vertex_idx, new_pt)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[WAND_GRIPS] _start_drag:", ex)
|
print("[WALL-GRIPS] _start_drag:", ex)
|
||||||
finally:
|
finally:
|
||||||
self.conduit.drag_key = None
|
self.conduit.drag_key = None
|
||||||
self.conduit.drag_preview = None
|
self.conduit.drag_preview = None
|
||||||
@@ -346,6 +354,6 @@ def install_handlers():
|
|||||||
handler.Enabled = True
|
handler.Enabled = True
|
||||||
sc.sticky[_STICKY_CONDUIT] = conduit
|
sc.sticky[_STICKY_CONDUIT] = conduit
|
||||||
sc.sticky[_STICKY_HANDLER] = handler
|
sc.sticky[_STICKY_HANDLER] = handler
|
||||||
print("[WAND_GRIPS] Endpoint-Conduit + Mouse-Handler aktiv")
|
print("[WALL-GRIPS] Endpoint conduit + mouse handler active")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[WAND_GRIPS] install:", ex)
|
print("[WALL-GRIPS] install:", ex)
|
||||||
|
|||||||
@@ -0,0 +1,562 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
# Copyright (C) 2026 Karim Gabriele Varano
|
||||||
|
"""
|
||||||
|
welcome.py
|
||||||
|
Welcome-Screen + Shortcuts-Cheatsheet als WebView-Dialog im DOSSIER-Style
|
||||||
|
(passend zum Splashscreen — Petrol-Gradient, Mono-Font).
|
||||||
|
|
||||||
|
Funktionen:
|
||||||
|
- show_welcome() — erscheint NACH dem Splash (eigener Idle-Timer), einmal
|
||||||
|
pro Version. User kann "nicht mehr anzeigen" rechts unten anklicken.
|
||||||
|
- show_cheatsheet() — DOSSIER-Shortcut-Liste, aufrufbar via dkeys-Alias.
|
||||||
|
|
||||||
|
Marker-Datei fuer "schon gesehen" wird in
|
||||||
|
~/Library/Application Support/ch.gabrielevarano.Dossier/welcome_shown abgelegt.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import Rhino
|
||||||
|
|
||||||
|
|
||||||
|
DOSSIER_VERSION = "0.6.3"
|
||||||
|
DOSSIER_GITHUB = "https://github.com/karimgvarano/DOSSIER"
|
||||||
|
DOSSIER_SUPPORT_EMAIL = "karim@gabrielevarano.ch"
|
||||||
|
|
||||||
|
_WELCOME_DIR = os.path.expanduser(
|
||||||
|
"~/Library/Application Support/ch.gabrielevarano.Dossier")
|
||||||
|
_WELCOME_FLAG = os.path.join(_WELCOME_DIR, "welcome_shown.txt")
|
||||||
|
_WELCOME_OPTOUT = os.path.join(_WELCOME_DIR, "welcome_dontshow.txt")
|
||||||
|
_SPLASH_MIN_DELAY_SEC = 3.5
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_SHORTCUTS_JSON = os.path.join(_HERE, "aliases", "shortcuts_default.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _has_optout():
|
||||||
|
return os.path.exists(_WELCOME_OPTOUT)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_seen_version(version):
|
||||||
|
try:
|
||||||
|
if not os.path.exists(_WELCOME_FLAG): return False
|
||||||
|
with open(_WELCOME_FLAG, "r") as f:
|
||||||
|
return f.read().strip() == version
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_seen(version):
|
||||||
|
try:
|
||||||
|
os.makedirs(_WELCOME_DIR, exist_ok=True)
|
||||||
|
with open(_WELCOME_FLAG, "w") as f:
|
||||||
|
f.write(version)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] mark-seen err:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_optout():
|
||||||
|
try:
|
||||||
|
os.makedirs(_WELCOME_DIR, exist_ok=True)
|
||||||
|
with open(_WELCOME_OPTOUT, "w") as f:
|
||||||
|
f.write("1")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] optout-write err:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_shortcuts():
|
||||||
|
try:
|
||||||
|
with open(_SHORTCUTS_JSON, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
items = []
|
||||||
|
for k, v in data.items():
|
||||||
|
if k.startswith("_") or not isinstance(v, dict): continue
|
||||||
|
items.append({
|
||||||
|
"id": k,
|
||||||
|
"trigger": v.get("trigger", ""),
|
||||||
|
"label": v.get("label", k),
|
||||||
|
"type": v.get("type", ""),
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] shortcuts-load err:", ex)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# ---- HTML — DOSSIER-Style passend zum Splash ----------------------------
|
||||||
|
|
||||||
|
_WELCOME_HTML = """<!DOCTYPE html>
|
||||||
|
<html lang="de"><head><meta charset="utf-8"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&family=Playfair+Display:wght@400;500&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
|
||||||
|
--paper: #fff; --paper-mute: rgba(255,255,255,0.78); --paper-faint: rgba(255,255,255,0.5);
|
||||||
|
--font-display: Krungthep, 'Archivo Black', sans-serif;
|
||||||
|
--font-serif: 'Playfair Display', serif;
|
||||||
|
--font-mono: 'DM Mono', 'Menlo', monospace;
|
||||||
|
}}
|
||||||
|
* {{ box-sizing:border-box; }}
|
||||||
|
html, body {{
|
||||||
|
margin:0; padding:0; width:100%; height:100%; background:transparent !important;
|
||||||
|
color:var(--paper); overflow:hidden; font-family:var(--font-mono); user-select:none;
|
||||||
|
-webkit-user-select:none;
|
||||||
|
}}
|
||||||
|
.frame {{
|
||||||
|
box-sizing:border-box; width:100%; height:100%; padding:28px 32px 24px;
|
||||||
|
display:flex; flex-direction:column;
|
||||||
|
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
|
||||||
|
border-radius:16px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.18);
|
||||||
|
}}
|
||||||
|
.brand-row {{ display:flex; align-items:baseline; justify-content:space-between; gap:12px; }}
|
||||||
|
.brand {{
|
||||||
|
font-family:var(--font-display); font-size:32px; letter-spacing:-0.01em;
|
||||||
|
line-height:1; color:var(--paper);
|
||||||
|
}}
|
||||||
|
.brand-dot {{ color:var(--accent-deep); }}
|
||||||
|
.version {{
|
||||||
|
font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em;
|
||||||
|
color:var(--paper-mute); text-transform:uppercase;
|
||||||
|
}}
|
||||||
|
.title {{
|
||||||
|
font-family:var(--font-serif); font-size:22px; line-height:1.3;
|
||||||
|
color:var(--paper); margin-top:20px; font-weight:500;
|
||||||
|
}}
|
||||||
|
.intro {{
|
||||||
|
font-size:11px; line-height:1.65; color:var(--paper-mute); margin-top:10px;
|
||||||
|
letter-spacing:0.02em;
|
||||||
|
}}
|
||||||
|
.section-title {{
|
||||||
|
font-size:9px; letter-spacing:0.18em; text-transform:uppercase;
|
||||||
|
color:var(--paper-faint); margin:22px 0 10px;
|
||||||
|
}}
|
||||||
|
.links {{ display:flex; flex-direction:column; gap:8px; }}
|
||||||
|
a {{ color:inherit; text-decoration:none; }}
|
||||||
|
.link {{
|
||||||
|
display:flex; align-items:flex-start; gap:12px;
|
||||||
|
padding:10px 14px; border-radius:6px; cursor:pointer;
|
||||||
|
background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.12);
|
||||||
|
transition:background 0.15s;
|
||||||
|
color:var(--paper); text-decoration:none;
|
||||||
|
}}
|
||||||
|
.link:hover {{ background:rgba(255,255,255,0.16); }}
|
||||||
|
.link-icon {{
|
||||||
|
font-family:var(--font-display); font-size:14px; color:var(--accent-deep);
|
||||||
|
background:var(--paper); width:24px; height:24px; border-radius:50%;
|
||||||
|
display:flex; align-items:center; justify-content:center; flex-shrink:0;
|
||||||
|
margin-top:1px;
|
||||||
|
}}
|
||||||
|
.link-content {{ flex:1; min-width:0; }}
|
||||||
|
.link-title {{ font-size:12px; color:var(--paper); font-weight:500; }}
|
||||||
|
.link-desc {{ font-size:10px; color:var(--paper-mute); margin-top:2px; }}
|
||||||
|
kbd {{
|
||||||
|
background:rgba(0,0,0,0.18); padding:1px 6px; border-radius:3px;
|
||||||
|
font-family:var(--font-mono); font-size:10px; color:var(--paper);
|
||||||
|
border:1px solid rgba(255,255,255,0.15);
|
||||||
|
}}
|
||||||
|
.footer {{
|
||||||
|
margin-top:auto; display:flex; align-items:center; justify-content:space-between;
|
||||||
|
padding-top:18px; gap:12px;
|
||||||
|
}}
|
||||||
|
.footer-meta {{
|
||||||
|
font-size:9px; letter-spacing:0.14em; color:var(--paper-faint);
|
||||||
|
text-transform:uppercase;
|
||||||
|
}}
|
||||||
|
.optout {{
|
||||||
|
display:flex; align-items:center; gap:6px; cursor:pointer;
|
||||||
|
font-size:10px; color:var(--paper-mute); user-select:none;
|
||||||
|
}}
|
||||||
|
.optout:hover {{ color:var(--paper); }}
|
||||||
|
.optout input {{ accent-color:var(--paper); margin:0; }}
|
||||||
|
.win-ctrl {{
|
||||||
|
position:absolute; top:14px; right:16px; display:flex; gap:6px; z-index:20;
|
||||||
|
}}
|
||||||
|
.win-btn {{
|
||||||
|
width:22px; height:22px; border-radius:50%; cursor:pointer;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
background:rgba(0,0,0,0.18); border:1px solid rgba(255,255,255,0.18);
|
||||||
|
color:var(--paper); font-family:var(--font-mono); font-size:13px;
|
||||||
|
text-decoration:none; transition:background 0.12s;
|
||||||
|
line-height:1; user-select:none;
|
||||||
|
}}
|
||||||
|
.win-btn:hover {{ background:rgba(0,0,0,0.32); }}
|
||||||
|
</style></head><body>
|
||||||
|
<div class="frame">
|
||||||
|
<div class="win-ctrl">
|
||||||
|
<a class="win-btn" href="dossier:close" title="Schliessen">×</a>
|
||||||
|
</div>
|
||||||
|
<div class="brand-row">
|
||||||
|
<div class="brand">DOSSIER<span class="brand-dot">.</span></div>
|
||||||
|
<div class="version">Version {ver}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title">Willkommen im Studio</div>
|
||||||
|
<div class="intro">
|
||||||
|
DOSSIER ist dein Architektur-Studio-Plugin fuer Rhino 8 —
|
||||||
|
Waende, Decken, Treppen, Fenster, Tueren, Raumstempel,
|
||||||
|
Layouts. Alles aus einer Hand, im selben Stil.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">Einstieg</div>
|
||||||
|
<div class="links">
|
||||||
|
<a class="link" href="dossier:cheatsheet">
|
||||||
|
<div class="link-icon">⌘</div>
|
||||||
|
<div class="link-content">
|
||||||
|
<div class="link-title">Shortcuts & Cheatsheet</div>
|
||||||
|
<div class="link-desc">Tippe <kbd>dkeys</kbd> im Command-Prompt fuer die volle Liste</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="link" href="{github}" target="_blank">
|
||||||
|
<div class="link-icon">i</div>
|
||||||
|
<div class="link-content">
|
||||||
|
<div class="link-title">Einfuehrung & Doku</div>
|
||||||
|
<div class="link-desc">{github}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="link" href="{github}/releases" target="_blank">
|
||||||
|
<div class="link-icon">v</div>
|
||||||
|
<div class="link-content">
|
||||||
|
<div class="link-title">Changelog</div>
|
||||||
|
<div class="link-desc">Was ist neu in dieser Version</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="link" href="mailto:{email}" target="_blank">
|
||||||
|
<div class="link-icon">?</div>
|
||||||
|
<div class="link-content">
|
||||||
|
<div class="link-title">Support & Problem melden</div>
|
||||||
|
<div class="link-desc">{email} oder GitHub-Issues</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<div class="footer-meta">AGPL-3.0 · Karim Gabriele Varano</div>
|
||||||
|
<label class="optout">
|
||||||
|
<input type="checkbox" id="optout" onchange="window.location='dossier:optout?'+this.checked"/>
|
||||||
|
Nicht mehr anzeigen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body></html>"""
|
||||||
|
|
||||||
|
|
||||||
|
_CHEATSHEET_HTML = """<!DOCTYPE html>
|
||||||
|
<html><head><meta charset="utf-8"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=DM+Mono:ital,wght@0,300;0,400;0,500&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
--accent: #5fa896; --accent-soft: #6fb5a3; --accent-deep: #2f5d54;
|
||||||
|
--paper: #fff; --paper-mute: rgba(255,255,255,0.78); --paper-faint: rgba(255,255,255,0.5);
|
||||||
|
--font-display: Krungthep, 'Archivo Black', sans-serif;
|
||||||
|
--font-mono: 'DM Mono', 'Menlo', monospace;
|
||||||
|
}}
|
||||||
|
* {{ box-sizing:border-box; }}
|
||||||
|
html, body {{
|
||||||
|
margin:0; padding:0; width:100%; height:100%; background:transparent !important;
|
||||||
|
color:var(--paper); overflow:auto; font-family:var(--font-mono);
|
||||||
|
}}
|
||||||
|
.frame {{
|
||||||
|
box-sizing:border-box; min-height:100%; padding:24px 28px;
|
||||||
|
background: radial-gradient(120% 140% at 0% 0%, var(--accent-soft) 0%, var(--accent) 55%, var(--accent-deep) 130%);
|
||||||
|
border-radius:16px;
|
||||||
|
}}
|
||||||
|
.brand-row {{ display:flex; align-items:baseline; justify-content:space-between; gap:12px; }}
|
||||||
|
.brand {{ font-family:var(--font-display); font-size:24px; line-height:1; color:var(--paper); }}
|
||||||
|
.brand-dot {{ color:var(--accent-deep); }}
|
||||||
|
.version {{ font-family:var(--font-mono); font-size:10px; letter-spacing:0.10em; color:var(--paper-mute); text-transform:uppercase; }}
|
||||||
|
h2 {{
|
||||||
|
font-size:10px; letter-spacing:0.18em; color:var(--paper); margin:18px 0 8px;
|
||||||
|
text-transform:uppercase; font-weight:500;
|
||||||
|
}}
|
||||||
|
table {{ width:100%; border-collapse:collapse; }}
|
||||||
|
td {{ padding:5px 8px; border-bottom:1px solid rgba(255,255,255,0.12); vertical-align:middle; }}
|
||||||
|
td:first-child {{ width:170px; }}
|
||||||
|
kbd {{
|
||||||
|
background:rgba(0,0,0,0.18); padding:2px 8px; border-radius:3px;
|
||||||
|
font-family:var(--font-mono); font-size:11px; color:var(--paper);
|
||||||
|
border:1px solid rgba(255,255,255,0.18);
|
||||||
|
}}
|
||||||
|
.lab {{ color:var(--paper); font-size:11px; }}
|
||||||
|
.badge {{
|
||||||
|
font-size:9px; padding:1px 5px; border-radius:2px; margin-left:6px;
|
||||||
|
background:rgba(255,255,255,0.12); color:var(--paper-mute);
|
||||||
|
font-family:var(--font-mono);
|
||||||
|
}}
|
||||||
|
.win-ctrl {{
|
||||||
|
position:fixed; top:14px; right:18px; display:flex; gap:6px; z-index:20;
|
||||||
|
}}
|
||||||
|
.win-btn {{
|
||||||
|
width:22px; height:22px; border-radius:50%; cursor:pointer;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
background:rgba(0,0,0,0.22); border:1px solid rgba(255,255,255,0.18);
|
||||||
|
color:var(--paper); font-family:var(--font-mono); font-size:13px;
|
||||||
|
text-decoration:none; transition:background 0.12s;
|
||||||
|
line-height:1; user-select:none;
|
||||||
|
}}
|
||||||
|
.win-btn:hover {{ background:rgba(0,0,0,0.38); }}
|
||||||
|
</style></head><body>
|
||||||
|
<div class="frame">
|
||||||
|
<div class="win-ctrl">
|
||||||
|
<a class="win-btn" href="dossier:back" title="Zurueck">‹</a>
|
||||||
|
<a class="win-btn" href="dossier:close" title="Schliessen">×</a>
|
||||||
|
</div>
|
||||||
|
<div class="brand-row">
|
||||||
|
<div class="brand">DOSSIER<span class="brand-dot">.</span> Shortcuts</div>
|
||||||
|
<div class="version">v {ver}</div>
|
||||||
|
</div>
|
||||||
|
{sections}
|
||||||
|
</div></body></html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def _build_cheatsheet_html():
|
||||||
|
items = _load_shortcuts()
|
||||||
|
groups = {
|
||||||
|
"DOSSIER BIM": [],
|
||||||
|
"2D-Werkzeuge": [],
|
||||||
|
"Views & Navigation": [],
|
||||||
|
"Modify-Tools": [],
|
||||||
|
"Sonstige Aliases": [],
|
||||||
|
}
|
||||||
|
bim_ids = {"wand", "tuer", "fenster", "decke", "treppe", "stuetze",
|
||||||
|
"traeger", "raum", "symbol", "stempel", "dach", "aussparung"}
|
||||||
|
view_ids = {"view_plan", "view_3d", "view_material", "zoom_ext",
|
||||||
|
"zoom_sel", "geschoss_up", "geschoss_down",
|
||||||
|
"panel_layer", "panel_elemente"}
|
||||||
|
twod_ids = {"text", "line", "arc", "rectangle", "polyline", "curve",
|
||||||
|
"hatch", "polygon", "ellipse", "circle"}
|
||||||
|
for it in items:
|
||||||
|
i = it["id"]
|
||||||
|
if i in bim_ids: groups["DOSSIER BIM"].append(it)
|
||||||
|
elif i in view_ids: groups["Views & Navigation"].append(it)
|
||||||
|
elif i.startswith("mod_"): groups["Modify-Tools"].append(it)
|
||||||
|
elif i in twod_ids or i.endswith("_alias"): groups["2D-Werkzeuge"].append(it)
|
||||||
|
else: groups["Sonstige Aliases"].append(it)
|
||||||
|
|
||||||
|
def _row(it):
|
||||||
|
trig = it["trigger"]
|
||||||
|
trig = trig.replace("Cmd+", "⌘+").replace("Shift+", "⇧+").replace("Alt+", "⌥+")
|
||||||
|
return ('<tr><td><kbd>{}</kbd></td>'
|
||||||
|
'<td class="lab">{}</td></tr>'
|
||||||
|
.format(trig, it["label"]))
|
||||||
|
|
||||||
|
sections = []
|
||||||
|
for gname, gitems in groups.items():
|
||||||
|
if not gitems: continue
|
||||||
|
rows = "".join(_row(it) for it in gitems)
|
||||||
|
sections.append('<h2>{}</h2><table>{}</table>'.format(gname, rows))
|
||||||
|
return _CHEATSHEET_HTML.format(ver=DOSSIER_VERSION, sections="".join(sections))
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Dialog-Anzeige ------------------------------------------------------
|
||||||
|
|
||||||
|
def _try_borderless_mac(form):
|
||||||
|
"""Borderless NSWindow + transparenten Hintergrund (analog _startup_splash)."""
|
||||||
|
try:
|
||||||
|
import System
|
||||||
|
nsw = getattr(form, "ControlObject", None)
|
||||||
|
if nsw is None: return False
|
||||||
|
# StyleMask = 0 (Borderless)
|
||||||
|
try:
|
||||||
|
cur = nsw.StyleMask
|
||||||
|
nsw.StyleMask = System.Enum.ToObject(type(cur), 0)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] StyleMask:", ex)
|
||||||
|
# Transparent background damit border-radius vom HTML sichtbar
|
||||||
|
for prop, val in [("TitlebarAppearsTransparent", True),
|
||||||
|
("IsOpaque", False), ("HasShadow", True),
|
||||||
|
("MovableByWindowBackground", True)]:
|
||||||
|
try: setattr(nsw, prop, val)
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
tv_type = type(nsw.TitleVisibility)
|
||||||
|
nsw.TitleVisibility = System.Enum.ToObject(tv_type, 1)
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
from AppKit import NSColor as _NSC
|
||||||
|
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
|
||||||
|
if clear is not None: nsw.BackgroundColor = clear
|
||||||
|
except Exception: pass
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] borderless:", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _webview_transparent(web):
|
||||||
|
"""WKWebView vollstaendig transparent — KVC drawsBackground=NO,
|
||||||
|
UnderPageBackgroundColor=Clear, Layer.BackgroundColor=CGColor.Clear."""
|
||||||
|
wk = getattr(web, "ControlObject", None)
|
||||||
|
if wk is None: return
|
||||||
|
try:
|
||||||
|
from Foundation import NSNumber, NSString
|
||||||
|
try: wk.SetValueForKey(NSNumber.FromBoolean(False), NSString("drawsBackground"))
|
||||||
|
except Exception as ex: print("[WELCOME] KVC drawsBackground:", ex)
|
||||||
|
except Exception as ex: print("[WELCOME] Foundation:", ex)
|
||||||
|
try:
|
||||||
|
from AppKit import NSColor as _NSC
|
||||||
|
clear = getattr(_NSC, "Clear", None) or getattr(_NSC, "ClearColor", None)
|
||||||
|
if clear is not None:
|
||||||
|
try: wk.UnderPageBackgroundColor = clear
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
layer = getattr(wk, "Layer", None)
|
||||||
|
if layer is not None:
|
||||||
|
layer.BackgroundColor = clear.CGColor
|
||||||
|
layer.Opaque = False
|
||||||
|
except Exception as ex: print("[WELCOME] Layer:", ex)
|
||||||
|
except Exception as ex: print("[WELCOME] NSColor:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _show_html_form(title, html, width=620, height=720, on_navigating=None,
|
||||||
|
borderless=True):
|
||||||
|
"""Eto.Forms.Form mit WebView + Inline-HTML. Optional borderless +
|
||||||
|
Navigation-Hook fuer custom URL-Schemes."""
|
||||||
|
try:
|
||||||
|
import Eto.Forms as ef
|
||||||
|
import Eto.Drawing as ed
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] Eto.Forms not available:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
form = ef.Form()
|
||||||
|
form.Title = title
|
||||||
|
form.ClientSize = ed.Size(width, height)
|
||||||
|
form.Topmost = False
|
||||||
|
form.Resizable = False
|
||||||
|
if borderless:
|
||||||
|
try: form.WindowStyle = getattr(ef.WindowStyle, "None")
|
||||||
|
except Exception: pass
|
||||||
|
for attr, val in (("Minimizable", False), ("Maximizable", False),
|
||||||
|
("Closeable", False), ("ShowInTaskbar", False)):
|
||||||
|
try: setattr(form, attr, val)
|
||||||
|
except Exception: pass
|
||||||
|
try: form.BackgroundColor = ed.Colors.Transparent
|
||||||
|
except Exception: pass
|
||||||
|
web = ef.WebView()
|
||||||
|
web.Size = ed.Size(width, height)
|
||||||
|
if borderless:
|
||||||
|
try: web.BackgroundColor = ed.Colors.Transparent
|
||||||
|
except Exception: pass
|
||||||
|
if on_navigating is not None:
|
||||||
|
try: web.DocumentLoading += on_navigating
|
||||||
|
except Exception as ex: print("[WELCOME] nav-hook:", ex)
|
||||||
|
try: web.LoadHtml(html)
|
||||||
|
except Exception as e: print("[WELCOME] LoadHtml:", e)
|
||||||
|
form.Content = web
|
||||||
|
try: form.Owner = Rhino.UI.RhinoEtoApp.MainWindow
|
||||||
|
except Exception: pass
|
||||||
|
form.Show()
|
||||||
|
if borderless:
|
||||||
|
_try_borderless_mac(form)
|
||||||
|
_webview_transparent(web)
|
||||||
|
try: ef.Application.Instance.RunIteration()
|
||||||
|
except Exception: pass
|
||||||
|
return form
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] form show:", ex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def show_welcome(force=False):
|
||||||
|
"""Zeigt Welcome NACH Splash. Erscheint bei jedem Start ausser der
|
||||||
|
User klickt 'Nicht mehr anzeigen' (= optout-File).
|
||||||
|
WICHTIG: UI muss auf Main-Thread laufen (Mac Cocoa) — Rhino-Idle-Event
|
||||||
|
feuert dort, deshalb defern wir die Anzeige."""
|
||||||
|
if not force and _has_optout():
|
||||||
|
print("[WELCOME] optout active ({}) — skip".format(_WELCOME_OPTOUT))
|
||||||
|
return
|
||||||
|
print("[WELCOME] geplant — Anzeige nach Splash (>{:.1f}s)".format(_SPLASH_MIN_DELAY_SEC))
|
||||||
|
|
||||||
|
import time
|
||||||
|
state = {"start": time.time(), "fired": False}
|
||||||
|
def _on_idle(sender, e):
|
||||||
|
if state["fired"]: return
|
||||||
|
if time.time() - state["start"] < _SPLASH_MIN_DELAY_SEC: return
|
||||||
|
state["fired"] = True
|
||||||
|
try:
|
||||||
|
Rhino.RhinoApp.Idle -= _on_idle
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
print("[WELCOME] Anzeige starten")
|
||||||
|
_show_welcome_now()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] show err:", ex)
|
||||||
|
try:
|
||||||
|
Rhino.RhinoApp.Idle += _on_idle
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WELCOME] idle-hook err:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
def _show_welcome_now():
|
||||||
|
html = _WELCOME_HTML.format(
|
||||||
|
ver=DOSSIER_VERSION, github=DOSSIER_GITHUB, email=DOSSIER_SUPPORT_EMAIL)
|
||||||
|
form_ref = [None]
|
||||||
|
def _on_nav(sender, e):
|
||||||
|
try:
|
||||||
|
url = e.Uri.ToString() if hasattr(e, "Uri") else str(getattr(e, "Url", ""))
|
||||||
|
except Exception:
|
||||||
|
url = ""
|
||||||
|
if not url: return
|
||||||
|
if url.startswith("dossier:optout"):
|
||||||
|
# Optout-Checkbox-Klick. URL-Form: dossier:optout?true/false
|
||||||
|
checked = url.endswith("true")
|
||||||
|
if checked: _write_optout()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if os.path.exists(_WELCOME_OPTOUT):
|
||||||
|
os.remove(_WELCOME_OPTOUT)
|
||||||
|
except Exception: pass
|
||||||
|
try: e.Cancel = True
|
||||||
|
except Exception: pass
|
||||||
|
elif url.startswith("dossier:cheatsheet"):
|
||||||
|
try: e.Cancel = True
|
||||||
|
except Exception: pass
|
||||||
|
show_cheatsheet()
|
||||||
|
try:
|
||||||
|
if form_ref[0] is not None: form_ref[0].Close()
|
||||||
|
except Exception: pass
|
||||||
|
elif url.startswith("dossier:close"):
|
||||||
|
try: e.Cancel = True
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
if form_ref[0] is not None: form_ref[0].Close()
|
||||||
|
except Exception: pass
|
||||||
|
form_ref[0] = _show_html_form("Willkommen bei DOSSIER", html, 600, 620,
|
||||||
|
on_navigating=_on_nav)
|
||||||
|
|
||||||
|
|
||||||
|
def show_cheatsheet():
|
||||||
|
html = _build_cheatsheet_html()
|
||||||
|
form_ref = [None]
|
||||||
|
def _on_nav(sender, e):
|
||||||
|
try:
|
||||||
|
url = e.Uri.ToString() if hasattr(e, "Uri") else str(getattr(e, "Url", ""))
|
||||||
|
except Exception:
|
||||||
|
url = ""
|
||||||
|
if not url: return
|
||||||
|
if url.startswith("dossier:close"):
|
||||||
|
try: e.Cancel = True
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
if form_ref[0] is not None: form_ref[0].Close()
|
||||||
|
except Exception: pass
|
||||||
|
elif url.startswith("dossier:back"):
|
||||||
|
try: e.Cancel = True
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
if form_ref[0] is not None: form_ref[0].Close()
|
||||||
|
except Exception: pass
|
||||||
|
try: _show_welcome_now()
|
||||||
|
except Exception as ex: print("[WELCOME] back:", ex)
|
||||||
|
form_ref[0] = _show_html_form("DOSSIER Shortcuts", html, 640, 760,
|
||||||
|
on_navigating=_on_nav)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
show_cheatsheet()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user