Compare commits
39 Commits
e1b63aa4e6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -26,6 +26,12 @@ __pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# Rhino-Testdateien (rhino/-Ordner)
|
||||
rhino/*.3dm
|
||||
rhino/*.3dm.thumb.png
|
||||
rhino/*.3dmbak
|
||||
rhino/dossier.project.json
|
||||
|
||||
# Claude Code
|
||||
.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/>.
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "dossier-launcher",
|
||||
"private": true,
|
||||
"version": "0.6.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -3,6 +3,7 @@ name = "dossier-launcher"
|
||||
version = "0.6.3"
|
||||
description = "Dossier — Projekt-Launcher fuer Rhino"
|
||||
authors = ["Karim Gabriele Varano"]
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
fn main() {
|
||||
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.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
@@ -292,6 +294,13 @@ fn plugin_loaded_marker_path() -> PathBuf {
|
||||
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> {
|
||||
let settings = load_settings();
|
||||
// XML-Edit nur sinnvoll wenn Rhino nicht laeuft (sonst ueberschreibt's
|
||||
@@ -312,8 +321,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).
|
||||
let show_splash = settings.auto_load_plugin;
|
||||
let marker = plugin_loaded_marker_path();
|
||||
let owner_marker = splash_owner_marker_path();
|
||||
if show_splash {
|
||||
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") {
|
||||
let _ = splash.show();
|
||||
}
|
||||
@@ -342,6 +354,7 @@ fn open_rhino_internal(app: &tauri::AppHandle, path3dm: &str) -> Result<(), Stri
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
let _ = fs::remove_file(&marker);
|
||||
let _ = fs::remove_file(&owner_marker);
|
||||
if let Some(splash) = app_clone.get_webview_window("splash") {
|
||||
let _ = splash.hide();
|
||||
}
|
||||
|
||||
@@ -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-
|
||||
// Unterstuetzung und damit `tauri::generate_context!` korrekt aufgeloest wird).
|
||||
fn main() {
|
||||
|
||||
@@ -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 { invoke } from '@tauri-apps/api/core'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
@@ -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,
|
||||
// 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 { 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 { createRoot } from 'react-dom/client'
|
||||
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
|
||||
// (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 react from '@vitejs/plugin-react'
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "dossier",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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 gesetzt)")
|
||||
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' nicht gefunden")
|
||||
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
|
||||
# -*- 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
|
||||
neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos
|
||||
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) gesetzt")
|
||||
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 gesetzt (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) angewendet")
|
||||
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
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
about.py
|
||||
About-Dialog als Eto-Form + WebView. Vom DOSSIER-Logo-Klick in der
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
ausschnitte.py
|
||||
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#! python 3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
clean.py
|
||||
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#! python 3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
clean_layers.py
|
||||
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
dimensionen.py
|
||||
DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild.
|
||||
@@ -584,6 +586,10 @@ def _install_listeners(bridge):
|
||||
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
|
||||
# Tick zwischen den einzelnen Deletes. CommandEnd refresht.
|
||||
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")
|
||||
if b is not None:
|
||||
try: b.tick_idle()
|
||||
@@ -593,6 +599,15 @@ def _install_listeners(bridge):
|
||||
# Swisstopo-Import feuert tausende Selection-Events → bail.
|
||||
if sc.sticky.get("dossier_swisstopo_busy"): 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")
|
||||
if b is not None:
|
||||
try: b._send_state(force=True)
|
||||
|
||||
+3416
-79
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
elemente_properties.py
|
||||
Properties-Satellite-Window. Zeigt die Property-Forms (WallProperties,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
elemente_uebersicht.py
|
||||
BIM-artiger Project Browser: alle Smart-Elemente in einem Tree
|
||||
@@ -29,6 +31,7 @@ _KIND_MAP = {
|
||||
"stuetze_point": "stuetze",
|
||||
"traeger_axis": "traeger",
|
||||
"raum_outline": "raum",
|
||||
"stempel": "stempel",
|
||||
"decke_aussparung_outline": "aussparung",
|
||||
"oeffnung_point": "oeffnung", # wird zu fenster/tuer aufgeloest
|
||||
}
|
||||
@@ -114,7 +117,39 @@ def _build_overview(doc):
|
||||
out_geschosse.append({
|
||||
"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):
|
||||
@@ -173,6 +208,88 @@ class ElementeUebersichtBridge(panel_base.BaseBridge):
|
||||
print("[UEBERSICHT] zoom:", ex)
|
||||
except Exception as 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():
|
||||
|
||||
+275
-69
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
gestaltung.py
|
||||
GESTALTUNG-Panel: Attribute der Selektion (Farbe, Stiftdicke, Linientyp,
|
||||
@@ -860,6 +862,11 @@ def _selection_summary(doc):
|
||||
sec_patterns = set()
|
||||
sec_scales = 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'
|
||||
# (offene Kurve), '3d' (Brep/Extrusion/Mesh — Volumen mit Schnittflaeche),
|
||||
# 'other'. Aggregiert ueber alle Selektions-Objekte zu kind=
|
||||
@@ -926,52 +933,93 @@ def _selection_summary(doc):
|
||||
else:
|
||||
geometry_kinds.add('other')
|
||||
|
||||
# Section-Style aus Object-Attributes lesen (Rhino 8, mit Fallbacks
|
||||
# fuer Property-Namen die je nach Build variieren).
|
||||
# Section-Style aus Object-Attributes lesen — Rhino 8 Mac packt die
|
||||
# Settings in ein SectionStyle-Objekt (via GetCustomSectionStyle),
|
||||
# NICHT in direkte Attribute-Properties wie das alte API.
|
||||
if is_3d:
|
||||
src_attr = None
|
||||
try:
|
||||
src_attr = getattr(a, "SectionAttributesSource", None)
|
||||
except Exception: src_attr = None
|
||||
src_is_object = False
|
||||
if src_attr is not None:
|
||||
try:
|
||||
src_name = str(src_attr).lower()
|
||||
if "layer" in src_name: sec_sources.add("layer")
|
||||
elif "object" in src_name: sec_sources.add("object")
|
||||
if "object" in src_name:
|
||||
sec_sources.add("object"); src_is_object = True
|
||||
elif "layer" in src_name:
|
||||
sec_sources.add("layer")
|
||||
except Exception: pass
|
||||
# Hatch-Index/Scale/Rotation
|
||||
hidx = None
|
||||
for n in ("SectionHatchIndex", "HatchPatternIndex"):
|
||||
if hasattr(a, n):
|
||||
|
||||
# Wenn Source=FromObject: aus dem Custom-SectionStyle lesen.
|
||||
# Sonst (FromLayer): vom Layer.GetCustomSectionStyle() lesen damit
|
||||
# die UI auch im Layer-Modus den effektiven Hatch zeigt.
|
||||
css = None
|
||||
try:
|
||||
if src_is_object and hasattr(a, "GetCustomSectionStyle"):
|
||||
css = a.GetCustomSectionStyle()
|
||||
if css is None:
|
||||
# Fallback: Layer-SectionStyle
|
||||
try:
|
||||
v = getattr(a, n)
|
||||
if v is not None: hidx = int(v); break
|
||||
lyr = doc.Layers[obj.Attributes.LayerIndex]
|
||||
if hasattr(lyr, "GetCustomSectionStyle"):
|
||||
css = lyr.GetCustomSectionStyle()
|
||||
except Exception: pass
|
||||
if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count:
|
||||
sec_enabled.add(True)
|
||||
try: sec_patterns.add(doc.HatchPatterns[hidx].Name)
|
||||
except Exception: pass
|
||||
elif hidx == -1:
|
||||
sec_enabled.add(False)
|
||||
for n, target in (
|
||||
(("SectionHatchScale", "HatchPatternScale"), sec_scales),
|
||||
(("SectionHatchRotation", "HatchPatternRotation"), sec_rots),
|
||||
):
|
||||
for nn in n:
|
||||
if hasattr(a, nn):
|
||||
except Exception: pass
|
||||
|
||||
if css is not None:
|
||||
# HatchIndex
|
||||
hidx = None
|
||||
for n in ("HatchIndex", "HatchPatternIndex"):
|
||||
if hasattr(css, n):
|
||||
try:
|
||||
v = float(getattr(a, nn))
|
||||
target.add(round(v, 4)
|
||||
if target is sec_scales
|
||||
else round(math.degrees(v), 2))
|
||||
break
|
||||
v = getattr(css, n)
|
||||
if v is not None: hidx = int(v); break
|
||||
except Exception: pass
|
||||
for n in ("SectionFillColor", "SectionHatchColor", "HatchColor"):
|
||||
if hasattr(a, n):
|
||||
try:
|
||||
c = _color_to_hex(getattr(a, n))
|
||||
if c: sec_colors.add(c); break
|
||||
if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count:
|
||||
sec_enabled.add(True)
|
||||
try: sec_patterns.add(doc.HatchPatterns[hidx].Name)
|
||||
except Exception: pass
|
||||
elif hidx == -1:
|
||||
sec_enabled.add(False)
|
||||
# Scale
|
||||
for n in ("HatchScale", "HatchPatternScale"):
|
||||
if hasattr(css, n):
|
||||
try: sec_scales.add(round(float(getattr(css, n)), 4)); break
|
||||
except Exception: pass
|
||||
# Rotation (rad → deg)
|
||||
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:
|
||||
c = _color_to_hex(getattr(css, n))
|
||||
if c: sec_colors.add(c); break
|
||||
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
|
||||
if _is_closed_planar_curve(obj.Geometry):
|
||||
@@ -1072,6 +1120,10 @@ def _selection_summary(doc):
|
||||
"sectionPattern": single(sec_patterns),
|
||||
"sectionScale": single(sec_scales),
|
||||
"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": (
|
||||
'mixed' if len(geometry_kinds & {'curve', 'curveOpen', '3d'}) > 1
|
||||
@@ -1159,6 +1211,10 @@ class GestaltungBridge(panel_base.BaseBridge):
|
||||
p.get("pattern"),
|
||||
p.get("scale"),
|
||||
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):
|
||||
@@ -1478,71 +1534,221 @@ class GestaltungBridge(panel_base.BaseBridge):
|
||||
# ---- SectionStyle (per-Object, Rhino 8) -------------------------------
|
||||
|
||||
def _set_section_style(self, enabled, source, color_hex,
|
||||
pattern_name=None, scale=None, rotation_deg=None):
|
||||
"""Setzt Per-Object SectionStyle-Properties auf die selektierten
|
||||
3D-Objekte. Rhino 8 expone diese Properties auf ObjectAttributes
|
||||
unter teils variierenden Namen — wir versuchen die bekannten."""
|
||||
pattern_name=None, scale=None, rotation_deg=None,
|
||||
boundary_visible=True, boundary_width_scale=1.0,
|
||||
boundary_color_hex=None,
|
||||
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
|
||||
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||
is_layer_source = (source == "layer")
|
||||
print("[GESTALTUNG] _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("[GESTALTUNG] 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("[GESTALTUNG] Source-Enum: Rhino.DocObjects.{}".format(cls_name))
|
||||
break
|
||||
except Exception: pass
|
||||
if SAS is None:
|
||||
print("[GESTALTUNG] 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("[GESTALTUNG] RhinoObject.{}: {}".format(
|
||||
meth, hasattr(o, meth)))
|
||||
try:
|
||||
a = o.Attributes
|
||||
for meth in ("SetCustomSectionStyle", "RemoveCustomSectionStyle"):
|
||||
print("[GESTALTUNG] Attributes.{}: {}".format(
|
||||
meth, hasattr(a, meth)))
|
||||
except Exception: pass
|
||||
self._ss_api_logged = True
|
||||
|
||||
# Hatch-Pattern-Index ermitteln
|
||||
pat_idx = -1
|
||||
if pattern_name and pattern_name not in ("None", ""):
|
||||
try: pat_idx = doc.HatchPatterns.Find(pattern_name, True)
|
||||
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
|
||||
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
|
||||
|
||||
def _try_set_attr(a, names, value):
|
||||
def _try_set(target, names, value):
|
||||
for n in names:
|
||||
if hasattr(a, n):
|
||||
if hasattr(target, n):
|
||||
try:
|
||||
setattr(a, n, value)
|
||||
setattr(target, n, value)
|
||||
return n
|
||||
except Exception: pass
|
||||
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("[GESTALTUNG] 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("[GESTALTUNG] 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("[GESTALTUNG] post via {} (modify_ok={}): Source={} Got={}".format(
|
||||
via, ok_modify, src, got))
|
||||
except Exception as ex:
|
||||
print("[GESTALTUNG] 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("[GESTALTUNG] set Source.FromLayer fail:", ex)
|
||||
doc.Objects.ModifyAttributes(obj, a, True)
|
||||
return "Attributes.RemoveCustomSectionStyle+FromLayer"
|
||||
except Exception as ex:
|
||||
print("[GESTALTUNG] attr.RemoveCustomSectionStyle fail:", ex)
|
||||
return None
|
||||
|
||||
n_ok = 0
|
||||
for obj in objs:
|
||||
geom = obj.Geometry
|
||||
if not isinstance(geom, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD)):
|
||||
continue
|
||||
a = obj.Attributes.Duplicate()
|
||||
|
||||
# Source: FromLayer vs FromObject — verschiedene Enum-Namen
|
||||
if is_layer_source:
|
||||
# Versuche SectionAttributesSource auf FromLayer
|
||||
_try_set_attr(a, ("SectionAttributesSource",),
|
||||
Rhino.DocObjects.SectionAttributesSource.FromLayer
|
||||
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 0)
|
||||
# Custom entfernen → Layer-SectionStyle wird wirksam
|
||||
via = _remove_custom(obj)
|
||||
print("[GESTALTUNG] obj {}: remove custom via {}".format(
|
||||
str(obj.Id)[:8], via))
|
||||
if via: n_ok += 1
|
||||
continue
|
||||
# Default-Farbe = Layer-Farbe wenn der User keine Override-Farbe
|
||||
# gewaehlt hat. Section-Style hat keine "ByLayer"-Source-Option,
|
||||
# also setzen wir die echte Layer-Farbe explizit auf den Style.
|
||||
obj_col = col
|
||||
obj_col_src = "user-override" if col is not None else "n/a"
|
||||
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("[GESTALTUNG] 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:
|
||||
_try_set_attr(a, ("SectionAttributesSource",),
|
||||
Rhino.DocObjects.SectionAttributesSource.FromObject
|
||||
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 1)
|
||||
|
||||
if not enabled or pattern_name == "None":
|
||||
# Hatch-Index auf -1 = keine Fuellung
|
||||
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), -1)
|
||||
else:
|
||||
if pat_idx >= 0:
|
||||
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), pat_idx)
|
||||
_try_set_attr(a, ("SectionHatchScale", "HatchPatternScale"), scale_v)
|
||||
_try_set_attr(a, ("SectionHatchRotation", "HatchPatternRotation"), rot_rad)
|
||||
if col is not None:
|
||||
_try_set_attr(a, ("SectionFillColor", "SectionHatchColor",
|
||||
"HatchColor"), col)
|
||||
|
||||
if pat_idx >= 0:
|
||||
_try_set(style, ("HatchIndex", "HatchPatternIndex"), pat_idx)
|
||||
_try_set(style, ("HatchScale", "HatchPatternScale"), scale_v)
|
||||
_try_set(style, ("HatchRotationRadians", "HatchRotation",
|
||||
"HatchAngle"), rot_rad)
|
||||
if obj_col is not None:
|
||||
# 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:
|
||||
doc.Objects.ModifyAttributes(obj, a, True)
|
||||
n_ok += 1
|
||||
except Exception as ex:
|
||||
print("[GESTALTUNG] SectionStyle ModifyAttributes:", ex)
|
||||
_try_set(style, ("BoundaryWidthScale",),
|
||||
float(boundary_width_scale))
|
||||
except Exception: pass
|
||||
# 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("[GESTALTUNG] 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))
|
||||
doc.Views.Redraw()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#! python 3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
inspect_section.py
|
||||
Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
kamera.py
|
||||
Kamera-Panel: liest/setzt Viewport-Kamera (Position, Target, Projektion,
|
||||
|
||||
+25
-19
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
layer_builder.py
|
||||
Layer-Struktur:
|
||||
@@ -157,6 +159,9 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
|
||||
pat = (section_cfg.get("hatchPattern") or "None").strip()
|
||||
show = bool(section_cfg.get("boundaryShow", True))
|
||||
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
|
||||
if pat == "None" and not show:
|
||||
@@ -194,17 +199,20 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
|
||||
rot_deg = float(section_cfg.get("hatchRotation") or 0)
|
||||
_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")
|
||||
if hatch_color:
|
||||
col = _color(hatch_color)
|
||||
set_color = _try_set(style, ("HatchColor", "FillColor"), col)
|
||||
# Source auf "FromObject" — sonst nutzt Rhino den Layer-Color
|
||||
src_from_object = _enum_int(
|
||||
(("DocObjects", "ObjectColorSource"), "ColorFromObject"))
|
||||
if src_from_object is not None:
|
||||
_try_set(style, ("HatchColorSource", "FillColorSource"), src_from_object)
|
||||
print(diag, "HatchColor via {}".format(set_color))
|
||||
elif layer_color is not None:
|
||||
col = _color(layer_color) if isinstance(layer_color, str) else layer_color
|
||||
else:
|
||||
col = None
|
||||
if col is not None:
|
||||
set_color = _try_set(style, ("HatchPatternColor", "HatchColor", "FillColor"), col)
|
||||
print(diag, "HatchColor via {} (default=layer)".format(set_color))
|
||||
|
||||
# Background (viewport=0/transparent vs object=1)
|
||||
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))
|
||||
|
||||
if show:
|
||||
# Boundary-Color: setze Color + Source auf FromObject
|
||||
# Boundary-Color: User-Override oder Layer-Farbe als Default
|
||||
bc = section_cfg.get("boundaryColor")
|
||||
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,
|
||||
("BoundaryColor", "OutlineColor", "EdgeColor"), col)
|
||||
src_from_object = _enum_int(
|
||||
(("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))
|
||||
("BoundaryColor", "OutlineColor", "EdgeColor"), bcol)
|
||||
print(diag, "BoundaryColor via {} (default=layer)".format(set_to))
|
||||
|
||||
# Width-Scale auf PlotWeight uebertragen (RW8 hat keine WidthScale direkt;
|
||||
# alternative Property-Namen probieren)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
layouts.py
|
||||
LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python3
|
||||
# -*- 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)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
mass_style.py
|
||||
Globale Mass-Stil-Presets fuer Dossier — speichert pro Dokument benannte
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
masse_settings.py
|
||||
Satellite-Fenster fuer das Bearbeiten der Masse-Presets
|
||||
|
||||
+15
-4
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
massstab.py
|
||||
MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports.
|
||||
@@ -888,15 +890,24 @@ def _apply_scale(doc, vp, ratio):
|
||||
apply_scaled_hatches(doc, float(ratio))
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] Hatch-Rescale:", ex)
|
||||
# Neuen Wert persistieren — sowohl per-Viewport (fuer das Dropdown,
|
||||
# damit jeder Viewport seinen eigenen Massstab behaelt) als auch als
|
||||
# globaler "letzter Wert" (Legacy-Key; wird von Plotweight/Hatch-Rescale
|
||||
# doc-weit benutzt — dort ist nur EIN Faktor sinnvoll).
|
||||
# Neuen Wert ZUERST persistieren — sowohl per-Viewport (fuer das
|
||||
# Dropdown, damit jeder Viewport seinen eigenen Massstab behaelt) als
|
||||
# auch als globaler "letzter Wert". WICHTIG: vor dem Raumstempel-
|
||||
# Regen weil _resolve_raum_text_height_m get_applied_scale_ratio()
|
||||
# liest — sonst regennt mit ALTER Skala.
|
||||
_write_user_scale(doc, ratio)
|
||||
try:
|
||||
_set_applied_scale_for_vp(doc, vp.Name, float(ratio))
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] 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("[MASSSTAB] {} masstab-Raum/Raeume regenned".format(n_regen))
|
||||
except Exception as ex:
|
||||
print("[MASSSTAB] Raumstempel-Regen:", ex)
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
print("[MASSSTAB] Skala 1:{:.2f} gesetzt (Faktor {:.4f}, soll-frustum {:.4f} {})".format(
|
||||
|
||||
+517
-1
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
oberleiste.py
|
||||
OBERLEISTE-Panel: horizontale Top-Bar mit Architektur-Kontext-Controls.
|
||||
@@ -183,6 +185,404 @@ def _import_display_modes(paths):
|
||||
return count
|
||||
|
||||
|
||||
# Fest-Guids fuer die Dossier-Display-Modes damit Re-Imports denselben Slot
|
||||
# wiederverwenden statt Duplikate zu erzeugen.
|
||||
_DOSSIER_PLAN_GUID = "d0551e72-7e72-4170-b1a4-d0551e72d055"
|
||||
_DOSSIER_3D_GUID = "d0551e72-7e72-4170-b1a4-3d3d3d3d3d3d"
|
||||
_DOSSIER_MATERIAL_GUID = "d0551e72-7e72-4170-b1a4-555555555555"
|
||||
_DOSSIER_RAYTRACING_GUID = "d0551e72-7e72-4170-b1a4-666666666666"
|
||||
|
||||
# Registrierte Dossier-Display-Modes: (name, guid, template_basename, default_pipeline_fallback)
|
||||
# Material + Raytracing haben (noch) kein Template — werden vom Fallback-Base
|
||||
# (Rendered / Raytraced) geklont. User kann sie in Rhino anpassen und spaeter
|
||||
# als Template exportieren.
|
||||
_DOSSIER_DISPLAY_MODES = (
|
||||
("Dossier Plan", _DOSSIER_PLAN_GUID, "dossier_plan.ini", "Technical"),
|
||||
("Dossier 3D", _DOSSIER_3D_GUID, "dossier_3d.ini", "Shaded"),
|
||||
("Dossier Material", _DOSSIER_MATERIAL_GUID, "dossier_material.ini", "Rendered"),
|
||||
("Dossier Raytracing", _DOSSIER_RAYTRACING_GUID, "dossier_raytracing.ini", "Raytraced"),
|
||||
)
|
||||
|
||||
def _apply_dossier_plan_attrs(dmd):
|
||||
"""Wendet die Dossier-Plan-Visual-Settings auf einen DisplayMode an.
|
||||
Wird sowohl beim Erstanlegen als auch bei jedem Reload aufgerufen —
|
||||
so propagieren Attribut-Aenderungen aus dem Code automatisch."""
|
||||
try:
|
||||
from Rhino.Display import DisplayModeDescription
|
||||
except Exception:
|
||||
return
|
||||
attrs = dmd.DisplayAttributes
|
||||
# DEBUG: einmal pro Session alle relevanten Property-Namen loggen damit
|
||||
# wir sehen welche tatsaechlich existieren (Mac vs Win Rhino unterscheidet
|
||||
# sich) — sonst werden Set-Attempts still verschluckt.
|
||||
if not getattr(_apply_dossier_plan_attrs, "_props_logged", False):
|
||||
try:
|
||||
relevant = sorted([n for n in dir(attrs)
|
||||
if not n.startswith("_")
|
||||
and ("idden" in n or "ngent" in n or "ilho" in n
|
||||
or "ire" in n or "soc" in n or "dge" in n
|
||||
or "ech" in n or "hade" in n or "ection" in n)])
|
||||
print("[OBERLEISTE] Plan-Mode Attrs-Inventar:", ", ".join(relevant))
|
||||
# Sub-Objekt-Settings sind in Rhino 8 oft eigene Klassen
|
||||
for sub in ("CurveSettings", "ObjectSettings", "ShadingSettings",
|
||||
"MeshSpecificAttributes"):
|
||||
if hasattr(attrs, sub):
|
||||
obj = getattr(attrs, sub)
|
||||
sub_props = sorted([n for n in dir(obj)
|
||||
if not n.startswith("_")
|
||||
and ("idden" in n or "ngent" in n
|
||||
or "soc" in n or "ire" in n
|
||||
or "dge" in n)])
|
||||
if sub_props:
|
||||
print("[OBERLEISTE] Plan-Mode {}:".format(sub),
|
||||
", ".join(sub_props))
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode inspect:", ex)
|
||||
_apply_dossier_plan_attrs._props_logged = True
|
||||
# Surfaces gefuellt + weiss + kein Shading
|
||||
try: attrs.ShadingEnabled = False
|
||||
except Exception: pass
|
||||
try: import System.Drawing as SD
|
||||
except Exception: SD = None
|
||||
if SD:
|
||||
try:
|
||||
white = SD.Color.FromArgb(255, 255, 255, 255)
|
||||
try: attrs.ShadeSurfaceColor = white
|
||||
except Exception: pass
|
||||
try: attrs.BackgroundColor = white
|
||||
except Exception: pass
|
||||
try: attrs.BackgroundColorTop = white
|
||||
except Exception: pass
|
||||
try: attrs.BackgroundColorBottom = white
|
||||
except Exception: pass
|
||||
except Exception: pass
|
||||
# Wires + Isocurves AUS — sonst Plan-Linien-Noise.
|
||||
# Property-Namen wie sie auf Mac Rhino 8 tatsaechlich existieren
|
||||
# (per Inspektion verifiziert — frueher hatten wir falsche Schreibweisen
|
||||
# die das try/except still verschluckt hat).
|
||||
try: attrs.ShowIsoCurves = False # NICHT ShowIsocurves!
|
||||
except Exception: pass
|
||||
try: attrs.SurfaceIsoThicknessUsed = False
|
||||
except Exception: pass
|
||||
try: attrs.SurfaceIsoColorsUsed = False
|
||||
except Exception: pass
|
||||
try: attrs.ShowTangentEdges = False
|
||||
except Exception: pass
|
||||
try: attrs.ShowTangentSeams = False
|
||||
except Exception: pass
|
||||
try: attrs.ShowSurfaceEdges = True
|
||||
except Exception: pass
|
||||
try: attrs.ShowSurfaceEdge = True # Singular existiert auch
|
||||
except Exception: pass
|
||||
# Mesh-Wires AUS — die liegen auf dem Sub-Objekt MeshSpecificAttributes,
|
||||
# nicht direkt auf attrs. Das sind in Top-Views haeufig die "feinen
|
||||
# Punkte" auf Brep-Wand-Volumen (Rhino mesht intern fuer Display).
|
||||
try:
|
||||
if hasattr(attrs, "MeshSpecificAttributes"):
|
||||
attrs.MeshSpecificAttributes.ShowMeshWires = False
|
||||
attrs.MeshSpecificAttributes.ShowMeshVertices = False
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode MeshSpecificAttributes:", ex)
|
||||
# Section-Styles MUESSEN aktiv sein damit Custom-Section-Styles
|
||||
# (per-Layer oder per-Object) tatsaechlich gerendert werden. Default
|
||||
# ist False — d.h. Section-Hatches werden zugewiesen aber nicht angezeigt.
|
||||
# Diagnose: vorher + nachher loggen weil auf Mac Rhino set+UpdateDisplayMode
|
||||
# diesen Wert manchmal nicht persistiert (wir patchen darum auch direkt
|
||||
# die ini beim Erstanlegen).
|
||||
pre_uss = None
|
||||
try: pre_uss = bool(attrs.UseSectionStyles)
|
||||
except Exception: pass
|
||||
try: attrs.UseSectionStyles = True
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode UseSectionStyles set:", ex)
|
||||
try: post_uss = bool(attrs.UseSectionStyles)
|
||||
except Exception: post_uss = None
|
||||
print("[OBERLEISTE] Plan-Mode UseSectionStyles pre={} post={}".format(
|
||||
pre_uss, post_uss))
|
||||
# Clipping-Edges + Fills sichtbar
|
||||
try: attrs.ShowClippingEdges = True
|
||||
except Exception: pass
|
||||
try: attrs.ShowClippingFills = True
|
||||
except Exception: pass
|
||||
try: attrs.ShowClipIntersectionEdges = True
|
||||
except Exception: pass
|
||||
try: attrs.ShowClipIntersectionSurfaces = True
|
||||
except Exception: pass
|
||||
# Linewidths an — Lineweights-Toggle wirkt
|
||||
try: attrs.ShowLineWidths = True
|
||||
except Exception: pass
|
||||
try:
|
||||
DisplayModeDescription.UpdateDisplayMode(dmd)
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode update:", ex)
|
||||
|
||||
|
||||
_TEMPLATES_DIR = os.path.join(_HERE, "templates")
|
||||
|
||||
|
||||
def _ensure_dossier_display_mode(target_name, target_guid, template_basename,
|
||||
fallback_base_name):
|
||||
"""Stellt sicher dass ein Dossier-Display-Mode existiert.
|
||||
|
||||
Strategie: wenn eine Template-ini im Repo existiert
|
||||
(rhino/templates/<template_basename>), laden wir die. Sonst Fallback auf
|
||||
Clone-eines-Built-in-Mode + ini-Patch. Template ist die bevorzugte
|
||||
Methode weil sich Mac-Rhino-Display-Mode-Properties via Python-API
|
||||
unzuverlaessig setzen lassen.
|
||||
|
||||
Args:
|
||||
target_name: 'Dossier Plan' / 'Dossier 3D' / ...
|
||||
target_guid: Fix-Guid die wir fuer Re-Imports wiederverwenden
|
||||
template_basename: 'dossier_plan.ini' / 'dossier_3d.ini'
|
||||
fallback_base_name: 'Technical' / 'Shaded' — Mode zum Klonen
|
||||
"""
|
||||
print("[OBERLEISTE] {}: check...".format(target_name))
|
||||
try:
|
||||
from Rhino.Display import DisplayModeDescription
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] {}: DMD nicht verfuegbar: {}".format(target_name, ex))
|
||||
return False
|
||||
import re
|
||||
template_ini_path = os.path.join(_TEMPLATES_DIR, template_basename)
|
||||
try:
|
||||
import System
|
||||
target_guid_obj = System.Guid(target_guid)
|
||||
except Exception:
|
||||
target_guid_obj = None
|
||||
# Template-Datei vorhanden? Wenn ja, Hash davon als "version key"
|
||||
# benutzen — wir nur neu importieren wenn sich die Template-Datei
|
||||
# geaendert hat.
|
||||
template_exists = os.path.isfile(template_ini_path)
|
||||
print("[OBERLEISTE] {} template: {}".format(target_name,
|
||||
"found at " + template_ini_path if template_exists else "missing"))
|
||||
# Fallback-Clone nur fuer "Dossier Plan" erlauben. Bei anderen Modes
|
||||
# ohne Template skippen — sonst klonen wir z.B. den Raytraced-Mode mit
|
||||
# Cycles-PipelineId, was Rhinos Display-Mode-State auf Mac korrumpieren
|
||||
# und ALLE Modes nach Restart verschwinden lassen kann. User soll den
|
||||
# Mode in Rhino bauen + per "Save As" -> templates/<basename> exportieren.
|
||||
if not template_exists and target_name != "Dossier Plan":
|
||||
print("[OBERLEISTE] {}: kein Template → skip (in Rhino bauen, "
|
||||
"Display-Mode -> Save As -> {})".format(
|
||||
target_name, template_ini_path))
|
||||
return False
|
||||
# Schon registriert?
|
||||
try:
|
||||
existing = None
|
||||
for dm in DisplayModeDescription.GetDisplayModes():
|
||||
try:
|
||||
if dm.EnglishName == target_name or dm.LocalName == target_name:
|
||||
existing = dm; break
|
||||
if target_guid_obj is not None and dm.Id == target_guid_obj:
|
||||
existing = dm; break
|
||||
except Exception: pass
|
||||
if existing is not None:
|
||||
# Mode existiert bereits — in Ruhe lassen. User kann manuell
|
||||
# loeschen + reloaden wenn er das Template neu laden will.
|
||||
# Vermeidet delete-loop wenn das Template ini-Werte hat die mein
|
||||
# alter check als "falsch" einstufte.
|
||||
print("[OBERLEISTE] Plan-Mode: existing gefunden, keine Aktion (manuell loeschen fuer Refresh)")
|
||||
return True
|
||||
# Sonst kein existing → vor dem Import alle Orphan-Modes mit unserer
|
||||
# Guid ODER Namen "Dossier Plan" wegputzen (alte kaputte Versionen
|
||||
# ohne Namen sind zuvor manchmal liegen geblieben → Duplikate +
|
||||
# Rhino-Crashes beim Klick).
|
||||
try:
|
||||
cleanup_count = 0
|
||||
for dm in list(DisplayModeDescription.GetDisplayModes()):
|
||||
should_delete = False
|
||||
try:
|
||||
if dm.EnglishName == target_name or dm.LocalName == target_name:
|
||||
should_delete = True
|
||||
elif target_guid_obj is not None and dm.Id == target_guid_obj:
|
||||
should_delete = True
|
||||
except Exception: pass
|
||||
if should_delete:
|
||||
try:
|
||||
DisplayModeDescription.DeleteDisplayMode(dm.Id)
|
||||
cleanup_count += 1
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode cleanup delete fail:", ex)
|
||||
if cleanup_count > 0:
|
||||
print("[OBERLEISTE] Plan-Mode: {} Orphan-Mode(s) entfernt vor Import".format(
|
||||
cleanup_count))
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode cleanup:", ex)
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode list:", ex)
|
||||
return False
|
||||
# ----------------------------------------------------------------
|
||||
# SOURCE: Template-ini bevorzugt (User hat den Mode manuell gebaut +
|
||||
# exportiert) — sonst Fallback auf Technical-Clone.
|
||||
# ----------------------------------------------------------------
|
||||
import tempfile
|
||||
tmp_path = os.path.join(tempfile.gettempdir(), template_basename)
|
||||
base = None
|
||||
if template_exists:
|
||||
# Template-ini lesen + ueberschreiben den tmp_path
|
||||
try:
|
||||
with open(template_ini_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
print("[OBERLEISTE] {}: Template geladen ({} bytes)".format(
|
||||
target_name, len(content)))
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] {} Template read: {}".format(target_name, ex))
|
||||
return False
|
||||
else:
|
||||
# Fallback: einen Base-Mode exportieren + patchen
|
||||
try:
|
||||
all_modes = list(DisplayModeDescription.GetDisplayModes())
|
||||
except Exception: all_modes = []
|
||||
for prefer in (fallback_base_name, "Technical", "Pen", "Shaded"):
|
||||
for dm in all_modes:
|
||||
try:
|
||||
if dm.EnglishName == prefer:
|
||||
base = dm; break
|
||||
except Exception: pass
|
||||
if base is not None: break
|
||||
if base is None:
|
||||
print("[OBERLEISTE] Plan-Mode: kein Basis-Modus gefunden")
|
||||
return False
|
||||
try:
|
||||
ok_export = False
|
||||
try:
|
||||
ok_export = bool(DisplayModeDescription.ExportToFile(base, tmp_path))
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode ExportToFile:", ex)
|
||||
if not ok_export:
|
||||
print("[OBERLEISTE] Plan-Mode: ExportToFile failed")
|
||||
return False
|
||||
with open(tmp_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode fallback read ini:", ex)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Name-Feld ersetzen (verschiedene moegliche Keys)
|
||||
import re
|
||||
try:
|
||||
content = re.sub(
|
||||
r'(?i)^(\s*Name\s*=\s*)(.*)$',
|
||||
r'\1' + target_name, content, count=1, flags=re.MULTILINE)
|
||||
except Exception: pass
|
||||
# Mode-Guid PRAEZISE per Section-Header finden — nicht irgendeine
|
||||
# Guid im ini erwischen (PipelineId etc. sind eigene Guids die wir
|
||||
# NICHT anfassen duerfen, sonst geht die Pipeline-Referenz kaputt
|
||||
# und Rhino verliert den Mode bzw. crasht beim Klick).
|
||||
try:
|
||||
old_guid_match = re.search(
|
||||
r'\[DisplayMode\\([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})',
|
||||
content)
|
||||
if old_guid_match:
|
||||
old_guid = old_guid_match.group(1)
|
||||
content = content.replace(old_guid, target_guid)
|
||||
content = content.replace(old_guid.upper(),
|
||||
target_guid.upper())
|
||||
else:
|
||||
print("[OBERLEISTE] {}: kein DisplayMode-Section-Header "
|
||||
"gefunden — Mode-Guid nicht ersetzt".format(target_name))
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] {} Guid-Replace: {}".format(target_name, ex))
|
||||
# Plan-Mode-Settings in der ini patchen — Rhino-DisplayMode-ini hat
|
||||
# nested Sections wie [DisplayMode\<guid>\Objects\Surfaces]. Wir
|
||||
# patchen nur EXISTIERENDE Keys (Rhino's Parser stripped unbekannte
|
||||
# Keys beim Re-Import wieder).
|
||||
def _ini_replace(content, key, value):
|
||||
"""Ersetzt erstes Vorkommen von key=... mit key=value (case-insens).
|
||||
Liefert (content, found_bool)."""
|
||||
pat = r'(?im)^(\s*' + re.escape(key) + r'\s*=\s*)(.*)$'
|
||||
new_content, n = re.subn(pat, r'\g<1>' + str(value),
|
||||
content, count=1)
|
||||
return new_content, (n > 0)
|
||||
|
||||
# Bei Template-Pfad: NUR Name+Guid normalisieren, KEINE inhaltlichen
|
||||
# Patches (User hat das Template bewusst so konfiguriert).
|
||||
# Bei Fallback-Pfad fuer Dossier Plan (Technical-Clone): die Plan-
|
||||
# spezifischen Settings forcen. Andere Modes (Material/Raytracing)
|
||||
# uebernehmen den Fallback-Base 1:1 — sonst wuerden Plan-Settings
|
||||
# wie TechnicalMask in eine Rendered/Raytraced-ini gequetscht und
|
||||
# alles zerschiessen.
|
||||
if not template_exists and target_name == "Dossier Plan":
|
||||
for key, val in (
|
||||
("ClipSectionUsage", "1"),
|
||||
("TechnicalMask", "15"),
|
||||
("ShowIsocurves", "n"),
|
||||
("ShowTangentEdges", "n"),
|
||||
("ShowTangentSeams", "n"),
|
||||
("ShowMeshWires", "n"),
|
||||
("ShowMeshEdges", "n"),
|
||||
("ShadeSurface", "n"),
|
||||
("ClippingShowXEdges", "y"),
|
||||
("ClippingShowXSurface", "y"),
|
||||
):
|
||||
try:
|
||||
content, found = _ini_replace(content, key, val)
|
||||
if found:
|
||||
print("[OBERLEISTE] Plan-Mode ini {}={}".format(key, val))
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode ini-set {}={} fail: {}".format(
|
||||
key, val, ex))
|
||||
try:
|
||||
with open(tmp_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode write ini:", ex)
|
||||
return False
|
||||
# Import
|
||||
try:
|
||||
ok_import = bool(DisplayModeDescription.ImportFromFile(tmp_path))
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode ImportFromFile:", ex)
|
||||
return False
|
||||
if not ok_import:
|
||||
print("[OBERLEISTE] Plan-Mode: ImportFromFile gab False")
|
||||
return False
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode clone:", ex)
|
||||
return False
|
||||
# Neu importierten Mode holen + tweaken
|
||||
try:
|
||||
dmd = None
|
||||
if target_guid_obj is not None:
|
||||
try: dmd = DisplayModeDescription.GetDisplayMode(target_guid_obj)
|
||||
except Exception: pass
|
||||
if dmd is None:
|
||||
for dm in DisplayModeDescription.GetDisplayModes():
|
||||
try:
|
||||
if dm.EnglishName == target_name or dm.LocalName == target_name:
|
||||
dmd = dm; break
|
||||
except Exception: pass
|
||||
if dmd is None:
|
||||
print("[OBERLEISTE] Plan-Mode: nach Import nicht gefunden")
|
||||
return False
|
||||
# KEIN _apply_dossier_plan_attrs() hier — der wuerde
|
||||
# UpdateDisplayMode() aufrufen und die ini-Werte (ClipSectionUsage,
|
||||
# TechnicalMask) mit den Python-Default-Attrs ueberschreiben.
|
||||
# Die ini hat schon alle richtigen Werte.
|
||||
src = "Template" if template_exists else (base.EnglishName if base else "?")
|
||||
print("[OBERLEISTE] Display-Mode 'Dossier Plan' angelegt (Basis: {})".format(src))
|
||||
# Sanity-Check: liste alle Display-Modes auf damit wir sehen ob unser
|
||||
# Mode wirklich registriert ist (und mit welchem Namen).
|
||||
try:
|
||||
names = []
|
||||
for dm_check in DisplayModeDescription.GetDisplayModes():
|
||||
try:
|
||||
names.append(dm_check.EnglishName)
|
||||
except Exception: pass
|
||||
print("[OBERLEISTE] Plan-Mode: alle registrierten Modes: {}".format(
|
||||
", ".join(names)))
|
||||
except Exception: pass
|
||||
# Cache invalidieren damit das Dropdown ihn sieht
|
||||
try:
|
||||
global _display_modes_cache
|
||||
_display_modes_cache = None
|
||||
except Exception: pass
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] Plan-Mode tweak:", ex)
|
||||
return False
|
||||
|
||||
|
||||
_THUMB_SIZE = (480, 320) # 3:2 — kompakt fuer Launcher-Cards
|
||||
|
||||
|
||||
@@ -360,15 +760,104 @@ def _layout_name_to_guid(name):
|
||||
return None
|
||||
|
||||
|
||||
_LAYOUT_MARKER_PATH = os.path.expanduser(
|
||||
"~/Library/Application Support/ch.gabrielevarano.Dossier/layout_marker.json")
|
||||
|
||||
_RHINO_SETTINGS_XML = os.path.expanduser(
|
||||
"~/Library/Application Support/McNeel/Rhinoceros/8.0/settings/"
|
||||
"settings-Scheme__Default.xml")
|
||||
|
||||
|
||||
def _rhino_last_restored_layout_guid():
|
||||
"""Liest aus Rhinos settings-Scheme__Default.xml die zuletzt restorete
|
||||
Layout-GUID. Rhino schreibt diesen Eintrag nach jedem erfolgreichen
|
||||
Restore (sowohl beim App-Start als auch nach `_-WindowLayout`-Command).
|
||||
Wenn die GUID == unser Ziel ist, hat Rhino's Auto-Restore das Layout
|
||||
schon beim Cold-Start angewendet → wir koennen den teuren Command-
|
||||
Apply skippen."""
|
||||
try:
|
||||
if not os.path.isfile(_RHINO_SETTINGS_XML): return None
|
||||
with open(_RHINO_SETTINGS_XML, "rb") as f:
|
||||
txt = f.read().decode("utf-8", errors="replace")
|
||||
# <entry key="LastRestoredWindowLayout">b6b68c03-...</entry>
|
||||
import re
|
||||
m = re.search(
|
||||
r'<entry\s+key="LastRestoredWindowLayout">\s*([0-9a-fA-F\-]+)\s*</entry>',
|
||||
txt)
|
||||
if m: return m.group(1).strip().lower()
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] last-restored read:", ex)
|
||||
return None
|
||||
|
||||
|
||||
def _is_layout_recently_applied(name, max_age_sec=600):
|
||||
"""True wenn das gegebene Layout vor < max_age_sec Sekunden via DOSSIER
|
||||
appliziert wurde (Marker-File). Wird benutzt um den teuren
|
||||
`_-WindowLayout`-RunScript bei Quick-Restarts zu skippen — Rhino haelt
|
||||
die Panel-Positionen dann meistens noch in seinem internen State."""
|
||||
try:
|
||||
if not os.path.isfile(_LAYOUT_MARKER_PATH): return False
|
||||
import time as _t, json as _j
|
||||
with open(_LAYOUT_MARKER_PATH, "rb") as f:
|
||||
data = _j.loads(f.read().decode("utf-8"))
|
||||
if not isinstance(data, dict): return False
|
||||
if (data.get("layoutName") or "") != (name or ""): return False
|
||||
try: age = _t.time() - float(data.get("appliedAt", 0))
|
||||
except Exception: return False
|
||||
return 0 <= age <= max_age_sec
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] layout-marker read:", ex)
|
||||
return False
|
||||
|
||||
|
||||
def _mark_layout_applied(name):
|
||||
"""Schreibt den Marker nach erfolgreichem Apply."""
|
||||
try:
|
||||
import time as _t, json as _j
|
||||
d = os.path.dirname(_LAYOUT_MARKER_PATH)
|
||||
if not os.path.isdir(d):
|
||||
try: os.makedirs(d)
|
||||
except Exception: pass
|
||||
with open(_LAYOUT_MARKER_PATH, "wb") as f:
|
||||
f.write(_j.dumps({
|
||||
"layoutName": name,
|
||||
"appliedAt": _t.time(),
|
||||
}, ensure_ascii=False).encode("utf-8"))
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] layout-marker write:", ex)
|
||||
|
||||
|
||||
def _apply_window_layout(name):
|
||||
"""Wendet ein benanntes Window-Layout an. Probiert mehrere Wege weil
|
||||
Mac Rhino 8 keine offizielle Python-API dafuer exponiert und die
|
||||
Scripted-Commands je nach Rhino-Version variieren. STOP on success —
|
||||
pruefen via RunScript-Return-Value oder via fehlerfreiem API-Call."""
|
||||
pruefen via RunScript-Return-Value oder via fehlerfreiem API-Call.
|
||||
Schreibt nach erfolgreichem Apply einen Marker fuer Quick-Restart-Skip
|
||||
(siehe _is_layout_recently_applied).
|
||||
"""
|
||||
if not name:
|
||||
print("[OBERLEISTE] apply_window_layout: leerer Name")
|
||||
return False
|
||||
# KEIN Content-Skip mehr: Mac Rhino's Auto-Restore aus LastRestoredWindow
|
||||
# Layout dockt Eto-WebView-Panels NICHT korrekt — sie spawnen floating in
|
||||
# der Mitte statt an die persistierte Dock-Position. Nur der explizite
|
||||
# `_-WindowLayout`-RunScript triggert den vollen Re-Dock-Cycle der auch
|
||||
# WebViews korrekt platziert. Die 3s sind der Preis fuer korrektes UI.
|
||||
import time as _t_wl
|
||||
_t_wl_start = _t_wl.time()
|
||||
_ret = False
|
||||
try:
|
||||
_ret = _apply_window_layout_impl(name)
|
||||
finally:
|
||||
try: panel_base._t_mark("window_layout", name, _t_wl_start)
|
||||
except Exception: pass
|
||||
if _ret:
|
||||
try: _mark_layout_applied(name)
|
||||
except Exception: pass
|
||||
return _ret
|
||||
|
||||
|
||||
def _apply_window_layout_impl(name):
|
||||
guid = _layout_name_to_guid(name)
|
||||
print("[OBERLEISTE] apply_window_layout: name='{}' guid='{}'".format(
|
||||
name, guid))
|
||||
@@ -896,6 +1385,7 @@ class OberleisteBridge(panel_base.BaseBridge):
|
||||
# DIESER Rhino-Session geschehen (sticky-flag = process-lifetime).
|
||||
# Mac Rhino persistiert die Window-Anordnung zwischen Sessions
|
||||
# NICHT zuverlaessig — der Cold-Start-Apply muss jedes Mal laufen.
|
||||
# Die 3s Wartezeit verdeckt jetzt der Splash-Screen optisch.
|
||||
try:
|
||||
cfg = _settings_load()
|
||||
if not sc.sticky.get("_dossier_layout_applied"):
|
||||
@@ -903,6 +1393,8 @@ class OberleisteBridge(panel_base.BaseBridge):
|
||||
if cfg.get("autoApplyLayout") and layout_name:
|
||||
sc.sticky["_dossier_layout_applied"] = True
|
||||
_apply_window_layout(layout_name)
|
||||
try: _mark_layout_applied(layout_name)
|
||||
except Exception: pass
|
||||
# Viewport-Colors einmalig pro Session auto-applien (wenn aktiviert)
|
||||
if (cfg.get("autoApplyViewColors") and
|
||||
not sc.sticky.get("_dossier_view_colors_applied")):
|
||||
@@ -910,6 +1402,14 @@ class OberleisteBridge(panel_base.BaseBridge):
|
||||
sc.sticky["_dossier_view_colors_applied"] = True
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] auto-apply (layout/colors):", ex)
|
||||
# Splash-Screen (falls noch offen) jetzt wegmachen — Layout-Apply ist
|
||||
# durch, Panels sind in finaler Position. hide() dispatcht intern
|
||||
# auf Main-Thread via Rhino.Idle (Mac-safe).
|
||||
try:
|
||||
import _startup_splash as _ss
|
||||
_ss.hide()
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] splash hide:", ex)
|
||||
self._send_state(force=True)
|
||||
|
||||
def handle(self, data):
|
||||
@@ -1730,5 +2230,21 @@ def _bridge_factory():
|
||||
return b
|
||||
|
||||
|
||||
# Custom Display-Mode 'Dossier Plan' beim Modul-Load registrieren — laeuft
|
||||
# bei jedem startup.py oder _reset_panels.py, unabhaengig davon ob das
|
||||
# Panel jemals geoeffnet wird. Funktion ist idempotent.
|
||||
# Alle Dossier-Display-Modes registrieren (Plan, 3D, ...)
|
||||
import time as _t_dm
|
||||
_t_dm_start = _t_dm.time()
|
||||
for _name, _guid, _tmpl, _fallback in _DOSSIER_DISPLAY_MODES:
|
||||
try:
|
||||
_ensure_dossier_display_mode(_name, _guid, _tmpl, _fallback)
|
||||
except Exception as _ex:
|
||||
print("[OBERLEISTE] ensure {}: {}".format(_name, _ex))
|
||||
try:
|
||||
panel_base._t_mark("display_modes", "oberleiste", _t_dm_start)
|
||||
except Exception: pass
|
||||
|
||||
|
||||
panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory,
|
||||
icon_spec=("menu", "#2f5d54"))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python3
|
||||
# -*- 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
|
||||
Polylinien (Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege).
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
overrides.py
|
||||
Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides /
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
overrides_panel.py
|
||||
OVERRIDES-Panel: Rule-Editor fuer grafische Overrides.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
panel_base.py
|
||||
Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
rhinopanel.py
|
||||
Oeffnet das EBENEN-Panel (Zeichnungsebenen + globale Ebenen).
|
||||
@@ -16,6 +18,7 @@ if _HERE not in sys.path:
|
||||
|
||||
import panel_base
|
||||
import layer_builder
|
||||
import library
|
||||
|
||||
PANEL_GUID_STR = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718"
|
||||
# Zweites Panel fuer Zeichnungsebenen (Geschoss-Liste + Clipping). UX-Split
|
||||
@@ -300,6 +303,10 @@ _PROJECT_SETTINGS_DEFAULTS = {
|
||||
"schnittDepthBack": 8.0,
|
||||
"schnittHeightMin": -1.0,
|
||||
"schnittHeightMax": 12.0,
|
||||
# Arbeitseinheit. DOSSIER-Default ist Meter (Architektur-Standard).
|
||||
# Beim Doc-Open prueft startup.py ob doc.ModelUnitSystem dem hier
|
||||
# entspricht — sonst Dialog mit "Umstellen"-Option.
|
||||
"unit": "meters", # "meters" | "millimeters" | "centimeters"
|
||||
},
|
||||
"materials": [],
|
||||
"project": {
|
||||
@@ -316,6 +323,44 @@ _PROJECT_SETTINGS_DEFAULTS = {
|
||||
}
|
||||
|
||||
|
||||
# Mapping Setting-String → Rhino.UnitSystem Enum (lazy import vermeidet
|
||||
# Bootstrap-Problem wenn Rhino.dll noch nicht da ist)
|
||||
_UNIT_STRING_TO_ENUM = None
|
||||
|
||||
|
||||
def _unit_string_to_enum():
|
||||
global _UNIT_STRING_TO_ENUM
|
||||
if _UNIT_STRING_TO_ENUM is not None:
|
||||
return _UNIT_STRING_TO_ENUM
|
||||
try:
|
||||
import Rhino
|
||||
_UNIT_STRING_TO_ENUM = {
|
||||
"meters": Rhino.UnitSystem.Meters,
|
||||
"millimeters": Rhino.UnitSystem.Millimeters,
|
||||
"centimeters": Rhino.UnitSystem.Centimeters,
|
||||
}
|
||||
except Exception:
|
||||
_UNIT_STRING_TO_ENUM = {}
|
||||
return _UNIT_STRING_TO_ENUM
|
||||
|
||||
|
||||
def get_project_unit(doc):
|
||||
"""Liefert die Arbeitseinheit aus den Project-Settings als String
|
||||
('meters'|'millimeters'|'centimeters'). Default 'meters'.
|
||||
"""
|
||||
ps = load_project_settings(doc) or {}
|
||||
d = ps.get("defaults") or {}
|
||||
u = (d.get("unit") or "meters").lower()
|
||||
if u not in ("meters", "millimeters", "centimeters"):
|
||||
u = "meters"
|
||||
return u
|
||||
|
||||
|
||||
def get_project_unit_enum(doc):
|
||||
"""Wie get_project_unit, aber als Rhino.UnitSystem Enum."""
|
||||
return _unit_string_to_enum().get(get_project_unit(doc))
|
||||
|
||||
|
||||
def _normalize_project_meta(p):
|
||||
"""Garantiert das project-Schema. Strings werden gestripped, mum als
|
||||
float (default 0.0 wenn nicht parsebar)."""
|
||||
@@ -916,6 +961,17 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
lib_root = library.library_root()
|
||||
except Exception:
|
||||
lib_items = []; lib_root = ""
|
||||
# Raumstempel-Stile + Stempel-Stile + Fonts aus elemente fuer die
|
||||
# "Raumstile" und "Stempelstile" Tabs
|
||||
try:
|
||||
import elemente as _el
|
||||
raum_stile = _el.load_raum_stempel_stile(doc)
|
||||
stempel_stile = _el.load_stempel_stile(doc)
|
||||
fonts_list = _el._list_system_fonts()
|
||||
except Exception:
|
||||
raum_stile = []
|
||||
stempel_stile = []
|
||||
fonts_list = []
|
||||
params = {
|
||||
"defaults": current.get("defaults", {}),
|
||||
"project": current.get("project", {}),
|
||||
@@ -926,6 +982,9 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
"linetypes": _list_linetypes_full(doc),
|
||||
"libraryItems": lib_items,
|
||||
"libraryRoot": lib_root,
|
||||
"raumStempelStile": raum_stile,
|
||||
"stempelStile": stempel_stile,
|
||||
"fonts": fonts_list,
|
||||
}
|
||||
def on_save(updated):
|
||||
doc2 = Rhino.RhinoDoc.ActiveDoc
|
||||
@@ -1033,6 +1092,209 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
self._delete_library_item(p)
|
||||
elif t == "SAVE_SELECTION_AS_LIBRARY":
|
||||
self._save_selection_as_library(p)
|
||||
elif t == "SAVE_RAUM_STIL":
|
||||
self._raum_stil_save(p)
|
||||
elif t == "DELETE_RAUM_STIL":
|
||||
self._raum_stil_delete(p)
|
||||
elif t == "DUPLICATE_RAUM_STIL":
|
||||
self._raum_stil_duplicate(p)
|
||||
elif t == "REORDER_RAUM_STILE":
|
||||
self._raum_stil_reorder(p)
|
||||
elif t == "SAVE_STEMPEL_STIL":
|
||||
self._stempel_stil_save(p)
|
||||
elif t == "DELETE_STEMPEL_STIL":
|
||||
self._stempel_stil_delete(p)
|
||||
elif t == "DUPLICATE_STEMPEL_STIL":
|
||||
self._stempel_stil_duplicate(p)
|
||||
elif t == "REORDER_STEMPEL_STILE":
|
||||
self._stempel_stil_reorder(p)
|
||||
|
||||
# ---- Raumstempel-Stile (Settings-Tab "Raumstile") ----
|
||||
# Dispatcht direkt zu elemente.py — kein Roundtrip via Elemente-
|
||||
# Bridge noetig. Nach jeder Op wird die neue Liste an's Frontend
|
||||
# gepushed via STILE_UPDATED.
|
||||
def _raum_stil_send_updated(self):
|
||||
try:
|
||||
import elemente as _el
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
self.send("STILE_UPDATED",
|
||||
{"raumStempelStile": _el.load_raum_stempel_stile(d)})
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] STILE_UPDATED:", ex)
|
||||
|
||||
def _raum_stil_save(self, p):
|
||||
try:
|
||||
import elemente as _el
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
name = (p.get("name") or "").strip()
|
||||
if not name: return
|
||||
sid = (p.get("id") or "").strip() or (
|
||||
"stil_" + __import__("uuid").uuid4().hex[:8])
|
||||
stile = _el.load_raum_stempel_stile(d)
|
||||
settings = p.get("settings") or {}
|
||||
new_stil = {"id": sid, "name": name}
|
||||
for f in _el._RAUM_STIL_FIELDS:
|
||||
if f in settings: new_stil[f] = settings[f]
|
||||
found = False
|
||||
for i, s in enumerate(stile):
|
||||
if s.get("id") == sid:
|
||||
stile[i] = new_stil; found = True; break
|
||||
if not found: stile.append(new_stil)
|
||||
_el.save_raum_stempel_stile(d, stile)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] raum_stil save:", ex)
|
||||
self._raum_stil_send_updated()
|
||||
|
||||
def _raum_stil_delete(self, p):
|
||||
try:
|
||||
import elemente as _el
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
sid = (p.get("id") or "").strip()
|
||||
if not sid: return
|
||||
stile = _el.load_raum_stempel_stile(d)
|
||||
stile = [s for s in stile if s.get("id") != sid]
|
||||
_el.save_raum_stempel_stile(d, stile)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] raum_stil delete:", ex)
|
||||
self._raum_stil_send_updated()
|
||||
|
||||
def _raum_stil_duplicate(self, p):
|
||||
try:
|
||||
import elemente as _el
|
||||
import uuid as _uuid
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
sid = (p.get("id") or "").strip()
|
||||
if not sid: return
|
||||
stile = _el.load_raum_stempel_stile(d)
|
||||
src = next((s for s in stile if s.get("id") == sid), None)
|
||||
if src is None: return
|
||||
new_name = (p.get("newName") or "").strip() or (
|
||||
"{} (Kopie)".format(src.get("name", "Stil")))
|
||||
new_stil = dict(src)
|
||||
new_stil["id"] = "stil_" + _uuid.uuid4().hex[:8]
|
||||
new_stil["name"] = new_name
|
||||
try:
|
||||
i = next(j for j, s in enumerate(stile) if s.get("id") == sid)
|
||||
stile.insert(i + 1, new_stil)
|
||||
except StopIteration:
|
||||
stile.append(new_stil)
|
||||
_el.save_raum_stempel_stile(d, stile)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] raum_stil duplicate:", ex)
|
||||
self._raum_stil_send_updated()
|
||||
|
||||
def _raum_stil_reorder(self, p):
|
||||
try:
|
||||
import elemente as _el
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
new_order = p.get("ids") or []
|
||||
if not isinstance(new_order, list) or not new_order: return
|
||||
stile = _el.load_raum_stempel_stile(d)
|
||||
by_id = {s.get("id"): s for s in stile if s.get("id")}
|
||||
ordered = []; seen = set()
|
||||
for i in new_order:
|
||||
if i in by_id and i not in seen:
|
||||
ordered.append(by_id[i]); seen.add(i)
|
||||
for s in stile:
|
||||
if s.get("id") and s["id"] not in seen:
|
||||
ordered.append(s); seen.add(s["id"])
|
||||
_el.save_raum_stempel_stile(d, ordered)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] raum_stil reorder:", ex)
|
||||
self._raum_stil_send_updated()
|
||||
|
||||
# ---- Stempel-Stile (analog Raum-Stil-Pattern) ----
|
||||
def _stempel_stil_send_updated(self):
|
||||
try:
|
||||
import elemente as _el
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
self.send("STEMPEL_STILE_UPDATED",
|
||||
{"stempelStile": _el.load_stempel_stile(d)})
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] STEMPEL_STILE_UPDATED:", ex)
|
||||
|
||||
def _stempel_stil_save(self, p):
|
||||
try:
|
||||
import elemente as _el
|
||||
import uuid as _uuid
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
name = (p.get("name") or "").strip()
|
||||
if not name: return
|
||||
sid = (p.get("id") or "").strip() or (
|
||||
"stestil_" + _uuid.uuid4().hex[:8])
|
||||
settings = p.get("settings") or {}
|
||||
stile = _el.load_stempel_stile(d)
|
||||
new_stil = {"id": sid, "name": name}
|
||||
for f in _el._STEMPEL_STIL_FIELDS:
|
||||
if f in settings: new_stil[f] = settings[f]
|
||||
found = False
|
||||
for i, s in enumerate(stile):
|
||||
if s.get("id") == sid:
|
||||
stile[i] = new_stil; found = True; break
|
||||
if not found: stile.append(new_stil)
|
||||
_el.save_stempel_stile(d, stile)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] stempel_stil save:", ex)
|
||||
self._stempel_stil_send_updated()
|
||||
|
||||
def _stempel_stil_delete(self, p):
|
||||
try:
|
||||
import elemente as _el
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
sid = (p.get("id") or "").strip()
|
||||
if not sid: return
|
||||
stile = _el.load_stempel_stile(d)
|
||||
stile = [s for s in stile if s.get("id") != sid]
|
||||
_el.save_stempel_stile(d, stile)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] stempel_stil delete:", ex)
|
||||
self._stempel_stil_send_updated()
|
||||
|
||||
def _stempel_stil_duplicate(self, p):
|
||||
try:
|
||||
import elemente as _el
|
||||
import uuid as _uuid
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
sid = (p.get("id") or "").strip()
|
||||
if not sid: return
|
||||
stile = _el.load_stempel_stile(d)
|
||||
src = next((s for s in stile if s.get("id") == sid), None)
|
||||
if src is None: return
|
||||
new_name = (p.get("newName") or "").strip() or (
|
||||
"{} (Kopie)".format(src.get("name", "Stil")))
|
||||
new_stil = dict(src)
|
||||
new_stil["id"] = "stestil_" + _uuid.uuid4().hex[:8]
|
||||
new_stil["name"] = new_name
|
||||
try:
|
||||
i = next(j for j, s in enumerate(stile) if s.get("id") == sid)
|
||||
stile.insert(i + 1, new_stil)
|
||||
except StopIteration:
|
||||
stile.append(new_stil)
|
||||
_el.save_stempel_stile(d, stile)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] stempel_stil duplicate:", ex)
|
||||
self._stempel_stil_send_updated()
|
||||
|
||||
def _stempel_stil_reorder(self, p):
|
||||
try:
|
||||
import elemente as _el
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
new_order = p.get("ids") or []
|
||||
if not isinstance(new_order, list) or not new_order: return
|
||||
stile = _el.load_stempel_stile(d)
|
||||
by_id = {s.get("id"): s for s in stile if s.get("id")}
|
||||
ordered = []; seen = set()
|
||||
for i in new_order:
|
||||
if i in by_id and i not in seen:
|
||||
ordered.append(by_id[i]); seen.add(i)
|
||||
for s in stile:
|
||||
if s.get("id") and s["id"] not in seen:
|
||||
ordered.append(s); seen.add(s["id"])
|
||||
_el.save_stempel_stile(d, ordered)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] stempel_stil reorder:", ex)
|
||||
self._stempel_stil_send_updated()
|
||||
|
||||
def _pick_texture(self, payload):
|
||||
slot = payload.get("slot") or "diffuse"
|
||||
try:
|
||||
@@ -1609,6 +1871,10 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
updated = p.get("ebene") or {}
|
||||
orig_code = p.get("originalCode") or updated.get("code")
|
||||
if not (isinstance(updated, dict) and updated.get("code")): return
|
||||
# DEBUG: zeigt was tatsaechlich vom Frontend ankommt
|
||||
_sec = updated.get("section") or {}
|
||||
print("[EBENEN-SETTINGS] _persist code={} sec.hatch={} sec.color={}".format(
|
||||
updated.get("code"), _sec.get("hatchPattern"), _sec.get("hatchColor")))
|
||||
e_raw = doc.Strings.GetValue("dossier_ebenen")
|
||||
if not e_raw: return
|
||||
try: e_list = json.loads(e_raw)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
schnitt_grips.py
|
||||
Endpoint-Grips fuer Schnitt/Ansicht-Symbole im Plan.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
schnitte.py
|
||||
Schnitte + Ansichten als Zeichnungsebenen-Typ.
|
||||
@@ -291,6 +293,17 @@ def activate_schnitt(doc, z, skip_view=False):
|
||||
vp.ChangeToParallelProjection(True)
|
||||
vp.SetCameraLocations(target, cam_pos)
|
||||
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
|
||||
# macht ZoomBoundingBox auch Sinn — Rhino passt das FOV-Frame
|
||||
# entsprechend an.
|
||||
|
||||
+174
-4
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
startup.py
|
||||
Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine
|
||||
@@ -13,16 +15,26 @@ import json
|
||||
import Rhino
|
||||
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).
|
||||
try:
|
||||
import _startup_splash as _splash_first
|
||||
_splash_first.show()
|
||||
except Exception as _ex_splash:
|
||||
print("[STARTUP] splash early:", _ex_splash)
|
||||
|
||||
# DIAGNOSE — welcher Python-Engine laeuft hier wirklich? Einmalig beim Start.
|
||||
print("[STARTUP] Python: {}".format(sys.version))
|
||||
print("[STARTUP] Implementation: {}".format(
|
||||
sys.implementation.name if hasattr(sys, "implementation") else "n/a (IPy2)"))
|
||||
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
|
||||
_UI_FILE = os.path.join(_HERE, "DOSSIERUI.rhw")
|
||||
|
||||
@@ -74,6 +86,139 @@ def _migrate_active_doc(*_):
|
||||
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 nicht verfuegbar:", 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:", ex); return
|
||||
|
||||
if mode_plan is None and mode_3d is None:
|
||||
print("[STARTUP] view-modes: keine Dossier-Modes registriert — 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) gesetzt".format(n_set))
|
||||
|
||||
|
||||
_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 gesetzt). 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 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 lesen:", 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 hat Unit-Umstellung verweigert — Doc bleibt {}".format(current))
|
||||
except Exception as ex:
|
||||
print("[STARTUP] unit-check dialog:", ex)
|
||||
|
||||
|
||||
def _on_doc_opened(sender, e):
|
||||
"""Greift bei jedem geoeffneten Doc nach Rhino-Start. Migration ist
|
||||
idempotent (Flag in doc.Strings)."""
|
||||
@@ -81,6 +226,8 @@ def _on_doc_opened(sender, e):
|
||||
doc = e.Document if hasattr(e, "Document") else Rhino.RhinoDoc.ActiveDoc
|
||||
import panel_base
|
||||
panel_base.migrate_to_dossier(doc)
|
||||
_assign_default_display_modes(doc)
|
||||
_check_doc_unit(doc)
|
||||
except Exception as ex:
|
||||
print("[STARTUP] _on_doc_opened:", ex)
|
||||
|
||||
@@ -103,6 +250,7 @@ def _load_all(sender, e):
|
||||
Rhino.RhinoApp.Idle -= _load_all
|
||||
except Exception:
|
||||
pass
|
||||
# Splash wird ganz oben in startup.py (vor diesem Idle) gezeigt.
|
||||
print("[STARTUP] Lade DOSSIER-Panels...")
|
||||
# Migration einmal fuer das beim Start aktive Doc
|
||||
_migrate_active_doc()
|
||||
@@ -126,19 +274,41 @@ def _load_all(sender, e):
|
||||
print("[STARTUP] Keine dossier.project.json — alle Module laden")
|
||||
# massstab.py wird als Library mitgeladen (von oberleiste/ausschnitte/...)
|
||||
# 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:
|
||||
py_name = _MODULE_TO_PY[mod_id]
|
||||
_t_imp = _t.time()
|
||||
try:
|
||||
__import__(py_name)
|
||||
_pb._t_mark("import", mod_id, _t_imp)
|
||||
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
|
||||
except Exception as ex:
|
||||
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
|
||||
# Text-Editor Doppelklick-Hook fuer DOSSIER-Texte
|
||||
_t_te = _t.time()
|
||||
try:
|
||||
import text_editor
|
||||
text_editor._ensure_double_click_hook()
|
||||
_pb._t_mark("hook", "text_editor", _t_te)
|
||||
except Exception as 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:", 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:", ex)
|
||||
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
|
||||
_hint_dossier_ui()
|
||||
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
swisstopo.py
|
||||
STAC-API-Client + GeoTIFF/XYZ-Parser + Mesh-Builder fuer swisstopo-Daten.
|
||||
|
||||
@@ -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
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
text_create.py
|
||||
Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
text_editor.py
|
||||
React-WYSIWYG-Editor in Satellite-WebView (Topmost). User picked Frame
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#! 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 Endpunkt-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.
|
||||
|
||||
Endpunkt-Logik pro Treppen-Art:
|
||||
- gerade : PointAtStart, PointAtEnd der Linie
|
||||
- L : poly[0] (Start), poly[2] (Ende) — poly[1] ist der Eck-Punkt
|
||||
- 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.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 fuer Treppen-Start + -Ende. Beachtet
|
||||
treppe_art (Wendel hat anderes Polyline-Schema)."""
|
||||
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]]
|
||||
# gerade + L → Start- und End-Punkt der Curve sind die Treppen-Enden
|
||||
return [geom.PointAtStart, geom.PointAtEnd]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
class _TreppeEndpointConduit(rd.DisplayConduit):
|
||||
"""Zeichnet gruene Endpunkt-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 (kann anderer Obj sein
|
||||
# wenn User nur Volume oder 2D-Symbol selektiert hat)
|
||||
axis = None
|
||||
for o in doc.Objects:
|
||||
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("[TREPPE_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("[TREPPE_GRIPS] Endpoint-Conduit aktiv")
|
||||
except Exception as ex:
|
||||
print("[TREPPE_GRIPS] install:", ex)
|
||||
+89
-81
@@ -1,5 +1,7 @@
|
||||
#! python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
wand_grips.py
|
||||
Custom Endpoint-Grips fuer Waende — Display-Conduit + MouseCallback Overlay.
|
||||
@@ -94,53 +96,44 @@ def _find_axis_for_obj(doc, obj):
|
||||
return None
|
||||
|
||||
|
||||
def _curve_endpoints(curve):
|
||||
"""Liefert (start_pt, end_pt) fuer eine wand_axis. Funktioniert fuer
|
||||
LineCurve, PolylineCurve, NurbsCurve etc — alle Curve-Typen haben
|
||||
PointAtStart/PointAtEnd. Bei degenerierten Curves None."""
|
||||
if curve is None: return None, None
|
||||
def _axis_vertices(geom):
|
||||
"""Liefert die Vertices der wand_axis-Curve als Liste.
|
||||
- PolylineCurve: alle Vertices
|
||||
- LineCurve / sonstige Curve: [Start, End] (zwei-Vertex-Faelle)
|
||||
Returnt [] bei degeneriertem Input."""
|
||||
if geom is None: return []
|
||||
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:
|
||||
return None, None
|
||||
return []
|
||||
|
||||
|
||||
def _replace_axis_endpoint(doc, axis_obj, kind, new_pt):
|
||||
"""Tauscht den Start- (kind='start') oder Endpunkt (kind='end') der
|
||||
wand_axis-Curve gegen new_pt. Geht intelligent um mit:
|
||||
- LineCurve: erzeuge neue Line vom fixen Punkt zum neuen Punkt
|
||||
- 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
|
||||
def _replace_axis_vertex(doc, axis_obj, vertex_idx, new_pt):
|
||||
"""Tauscht den Vertex an Index `vertex_idx` der wand_axis-Curve gegen
|
||||
new_pt. Funktioniert fuer Linien (idx 0/1) und Polylinien (alle idx).
|
||||
Setzt die neue Geometrie via Objects.Replace — feuert
|
||||
ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft."""
|
||||
if axis_obj is None or axis_obj.IsDeleted: return False
|
||||
geom = axis_obj.Geometry
|
||||
if geom is None: return False
|
||||
try:
|
||||
# PolylineCurve mit > 2 Vertices: ersten/letzten Vertex ersetzen
|
||||
if isinstance(geom, rg.PolylineCurve):
|
||||
poly = geom.ToPolyline()
|
||||
if poly is None or poly.Count < 2: return False
|
||||
pts = list(poly)
|
||||
if kind == "start":
|
||||
pts[0] = new_pt
|
||||
else:
|
||||
pts[-1] = new_pt
|
||||
new_poly = rg.Polyline(pts)
|
||||
new_curve = rg.PolylineCurve(new_poly)
|
||||
pts = _axis_vertices(geom)
|
||||
if not pts: return False
|
||||
if vertex_idx < 0 or vertex_idx >= len(pts): return False
|
||||
pts[vertex_idx] = new_pt
|
||||
if len(pts) == 2:
|
||||
new_curve = rg.LineCurve(pts[0], pts[1])
|
||||
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)
|
||||
new_curve = rg.PolylineCurve(rg.Polyline(pts))
|
||||
return doc.Objects.Replace(axis_obj.Id, new_curve)
|
||||
except Exception as ex:
|
||||
print("[WAND_GRIPS] replace endpoint:", ex)
|
||||
print("[WAND_GRIPS] replace vertex:", ex)
|
||||
return False
|
||||
|
||||
|
||||
@@ -153,15 +146,17 @@ class _EndpointConduit(rd.DisplayConduit):
|
||||
|
||||
def __init__(self):
|
||||
rd.DisplayConduit.__init__(self)
|
||||
self.hot_key = None # (axis_id_str, kind) — fuer Hover
|
||||
self.drag_key = None # (axis_id_str, kind) — waehrend aktivem Drag
|
||||
self.drag_preview = None # rg.Line — Live-Vorschau waehrend GetPoint
|
||||
self.hot_key = None # (axis_id_str, vidx) — fuer Hover
|
||||
self.drag_key = None # (axis_id_str, vidx) — waehrend aktivem Drag
|
||||
self.drag_preview = None # Liste von rg.Line — Live-Vorschau (Linien
|
||||
# zu Nachbar-Vertices waehrend GetPoint)
|
||||
|
||||
def _collect_endpoints(self, doc):
|
||||
"""Liefert Liste von (axis_obj, kind, world_pt) fuer alle selektier-
|
||||
ten Waende. Iteriert die Selektion + dedupliziert Achsen (jede
|
||||
Wand erscheint nur einmal, auch wenn mehrere Volumen mit-selek-
|
||||
tiert sind)."""
|
||||
def _collect_grip_points(self, doc):
|
||||
"""Liefert Liste von (axis_obj, vertex_idx, world_pt) fuer ALLE
|
||||
Vertices aller selektierten Waende — fuer Polyline-Waende ist
|
||||
jeder Knick ein eigener Grip. Iteriert die Selektion + dedupli-
|
||||
ziert Achsen (jede Wand erscheint nur einmal, auch wenn mehrere
|
||||
Volumen mit-selektiert sind)."""
|
||||
out = []
|
||||
seen_axis = set()
|
||||
try:
|
||||
@@ -173,24 +168,21 @@ class _EndpointConduit(rd.DisplayConduit):
|
||||
aid = str(axis.Id)
|
||||
if aid in seen_axis: continue
|
||||
seen_axis.add(aid)
|
||||
p_start, p_end = _curve_endpoints(axis.Geometry)
|
||||
if p_start is not None:
|
||||
out.append((axis, "start", p_start))
|
||||
if p_end is not None:
|
||||
out.append((axis, "end", p_end))
|
||||
for i, pt in enumerate(_axis_vertices(axis.Geometry)):
|
||||
out.append((axis, i, pt))
|
||||
return out
|
||||
|
||||
def DrawForeground(self, e):
|
||||
try:
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
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)
|
||||
# Skip den gerade gezogenen Marker — der wird via
|
||||
# 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
|
||||
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
|
||||
fill = _MARKER_HOVER if is_hot else _MARKER_FILL
|
||||
# DrawPoint mit RoundControlPoint = gefuellter Kreis +
|
||||
@@ -202,11 +194,12 @@ class _EndpointConduit(rd.DisplayConduit):
|
||||
# Fallback fuer aeltere Rhino-Versionen: einfacher
|
||||
# DrawDot mit Label "●"
|
||||
e.Display.DrawDot(pt, "●", fill, _MARKER_BORDER)
|
||||
# Drag-Preview-Linie waehrend GetPoint aktiv ist
|
||||
if self.drag_preview is not None:
|
||||
try:
|
||||
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
|
||||
except Exception: pass
|
||||
# Drag-Preview-Linien waehrend GetPoint aktiv ist
|
||||
if self.drag_preview:
|
||||
for line in self.drag_preview:
|
||||
try:
|
||||
e.Display.DrawLine(line, _MARKER_HOVER, 2)
|
||||
except Exception: pass
|
||||
except Exception as ex:
|
||||
print("[WAND_GRIPS] DrawForeground:", ex)
|
||||
|
||||
@@ -224,21 +217,21 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
|
||||
self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point
|
||||
|
||||
def _hit_test(self, view, screen_pt):
|
||||
"""Liefert (axis, kind, world_pt) wenn screen_pt nahe eines Endpoint-
|
||||
Markers liegt, sonst None. Iteriert die aktuelle Conduit-Liste."""
|
||||
"""Liefert (axis, vertex_idx, world_pt) wenn screen_pt nahe eines
|
||||
Vertex-Markers liegt, sonst None."""
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return None
|
||||
try:
|
||||
vp = view.ActiveViewport
|
||||
except Exception: return None
|
||||
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:
|
||||
s = vp.WorldToClient(world_pt)
|
||||
dx = s.X - screen_pt.X
|
||||
dy = s.Y - screen_pt.Y
|
||||
if (dx * dx + dy * dy) <= thresh2:
|
||||
return axis, kind, world_pt
|
||||
return axis, vidx, world_pt
|
||||
except Exception: continue
|
||||
return None
|
||||
|
||||
@@ -272,44 +265,59 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
|
||||
# Default-Klick (Selection) abwuergen — wir uebernehmen
|
||||
try: e.Cancel = True
|
||||
except Exception: pass
|
||||
axis, kind, world_pt = hit
|
||||
self._start_drag(view.Document, axis, kind, world_pt)
|
||||
axis, vidx, world_pt = hit
|
||||
self._start_drag(view.Document, axis, vidx, world_pt)
|
||||
except Exception as ex:
|
||||
print("[WAND_GRIPS] OnMouseDown:", ex)
|
||||
|
||||
def _start_drag(self, doc, axis, kind, anchor_pt):
|
||||
"""Startet eine Rhino-GetPoint-Interaktion um den neuen Endpunkt
|
||||
zu picken. Der ANDERE Endpunkt (Fix-Punkt) wird als BasePoint
|
||||
gesetzt — damit kriegt der User Tracking-Linie, Ortho-Mode etc.
|
||||
wie bei _Move."""
|
||||
def _start_drag(self, doc, axis, vertex_idx, anchor_pt):
|
||||
"""Startet eine Rhino-GetPoint-Interaktion um den Vertex zu
|
||||
verschieben. BasePoint-Strategie:
|
||||
- End-Vertex (idx 0 oder letzter): gegenueberliegender End-Vertex
|
||||
→ 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
|
||||
geom = axis.Geometry
|
||||
if geom is None: return
|
||||
p_start, p_end = _curve_endpoints(geom)
|
||||
if p_start is None or p_end is None: return
|
||||
fixed_pt = p_end if kind == "start" else p_start
|
||||
# Conduit-State: drag-Marker hervorheben + Preview-Linie
|
||||
self.conduit.drag_key = (str(axis.Id), kind)
|
||||
self.conduit.drag_preview = rg.Line(fixed_pt, anchor_pt)
|
||||
pts = _axis_vertices(geom)
|
||||
if not pts or vertex_idx < 0 or vertex_idx >= len(pts): return
|
||||
is_first = vertex_idx == 0
|
||||
is_last = vertex_idx == len(pts) - 1
|
||||
prev_pt = pts[vertex_idx - 1] if not is_first else None
|
||||
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
|
||||
try:
|
||||
gp = Rhino.Input.Custom.GetPoint()
|
||||
gp.SetCommandPrompt("Wand-Endpunkt: neuer Punkt (Esc=Abbruch)")
|
||||
gp.SetBasePoint(fixed_pt, True)
|
||||
gp.DrawLineFromPoint(fixed_pt, True)
|
||||
# Live-Preview ueber Conduit (zusaetzlich zu Rhinos eigener
|
||||
# Tracking-Linie) — sieht ueblich, hilft beim Verstehen welcher
|
||||
# Endpunkt sich bewegt.
|
||||
gp.SetCommandPrompt("Wand-Vertex: neuer Punkt (Esc=Abbruch)")
|
||||
gp.SetBasePoint(base_pt, True)
|
||||
gp.DrawLineFromPoint(base_pt, True)
|
||||
def _on_mouse_move(sender, args):
|
||||
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
|
||||
try: gp.MouseMove += _on_mouse_move
|
||||
except Exception: pass
|
||||
res = gp.Get()
|
||||
if res == Rhino.Input.GetResult.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:
|
||||
print("[WAND_GRIPS] _start_drag:", ex)
|
||||
finally:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#! python 3
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2026 Karim Gabriele Varano
|
||||
"""
|
||||
werkzeuge.py
|
||||
WERKZEUGE-Panel: Architektur-orientierte Toolbar als React-WebView.
|
||||
|
||||
+171
-34
@@ -1,54 +1,191 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function Row({ icon, label, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '8px 0',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<Icon name={icon} size={14}
|
||||
style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
|
||||
<span style={{
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
fontWeight: 600, width: 70, flexShrink: 0,
|
||||
}}>{label}</span>
|
||||
<span style={{ flex: 1, fontSize: 12, minWidth: 0,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function VersionPill({ label, version }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
height: 26, padding: '0 12px 0 10px',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
fontWeight: 600,
|
||||
}}>{label}</span>
|
||||
<span style={{
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
fontSize: 11, fontWeight: 500,
|
||||
color: 'var(--accent-light)',
|
||||
}}>v{version}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AboutApp() {
|
||||
useEffect(() => { notifyReady() }, [])
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '28px 32px',
|
||||
padding: '36px 36px 28px',
|
||||
fontFamily: 'var(--font)', color: 'var(--text-primary)',
|
||||
background: 'var(--bg-panel)', minHeight: '100vh',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex', flexDirection: 'column', gap: 28,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
marginBottom: 4 }}>
|
||||
<span style={{
|
||||
fontFamily: "Krungthep, 'Archivo Black', sans-serif",
|
||||
fontSize: 32, letterSpacing: '-0.02em', lineHeight: 1,
|
||||
{/* Logo + Tagline */}
|
||||
<div>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
marginBottom: 6,
|
||||
}}>
|
||||
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.06em', textTransform: 'uppercase',
|
||||
marginBottom: 22 }}>
|
||||
Teil von OpenStudio
|
||||
<span style={{
|
||||
fontFamily: "Krungthep, 'Archivo Black', sans-serif",
|
||||
fontSize: 38, letterSpacing: '-0.02em', lineHeight: 1,
|
||||
}}>
|
||||
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: "'Playfair Display', Georgia, serif",
|
||||
fontStyle: 'italic',
|
||||
fontSize: 14, color: 'var(--text-secondary)',
|
||||
lineHeight: 1.4,
|
||||
}}>
|
||||
Architektur-Studio für Rhino 8
|
||||
</div>
|
||||
<div style={{
|
||||
marginTop: 10,
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.1em', textTransform: 'uppercase',
|
||||
}}>
|
||||
Teil von <a href="https://openbureau.ch" target="_blank" rel="noreferrer"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
openbureau
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr',
|
||||
gap: '8px 16px', fontSize: 12, marginBottom: 22 }}>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Launcher</span>
|
||||
<span style={{ fontFamily: 'DM Mono, monospace' }}>v{__LAUNCHER_VERSION__}</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Plugin</span>
|
||||
<span style={{ fontFamily: 'DM Mono, monospace' }}>v{__APP_VERSION__}</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Autor</span>
|
||||
<span>Karim Gabriele Varano</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Website</span>
|
||||
<a href="https://gabrielevarano.ch" target="_blank" rel="noreferrer"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
gabrielevarano.ch
|
||||
</a>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Lizenz</span>
|
||||
<span>Proprietär — © 2026 Karim Gabriele Varano</span>
|
||||
{/* Versions */}
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<VersionPill label="Plugin" version={__APP_VERSION__} />
|
||||
<VersionPill label="Launcher" version={__LAUNCHER_VERSION__} />
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)',
|
||||
lineHeight: 1.5,
|
||||
paddingTop: 14,
|
||||
borderTop: '1px solid var(--border-light)' }}>
|
||||
Rhino 8 Plugin für architektonische Workflows — Wände, Decken,
|
||||
Öffnungen, Räume, SIA 416, Plan-Layouts. Schwester-App: Rapport.
|
||||
{/* Meta-Rows */}
|
||||
<div>
|
||||
<Row icon="person" label="Autor">
|
||||
Karim Gabriele Varano
|
||||
</Row>
|
||||
<Row icon="public" label="Web">
|
||||
<a href="https://dossier.openbureau.ch" target="_blank" rel="noreferrer"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
dossier.openbureau.ch
|
||||
</a>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* Lizenz — Dual: AGPL-3.0 + Commercial */}
|
||||
<div style={{
|
||||
padding: '14px 16px',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderLeft: '3px solid var(--accent)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8,
|
||||
}}>
|
||||
<Icon name="copyright" size={13} style={{ color: 'var(--accent)' }} />
|
||||
<span style={{
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
fontWeight: 600,
|
||||
}}>Lizenz · Dual</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>AGPL-3.0</span>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
· Source available
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
lineHeight: 1.55, marginBottom: 10 }}>
|
||||
Frei nutzbar, modifizierbar und weitergebbar unter den Bedingungen
|
||||
der <a href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
target="_blank" rel="noreferrer"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
GNU Affero General Public License v3
|
||||
</a>. Abgeleitete Werke müssen unter derselben Lizenz veröffentlicht
|
||||
werden.
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
height: 1, background: 'var(--border-light)', margin: '6px 0 10px',
|
||||
}} />
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Kommerzielle Lizenz</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
lineHeight: 1.55 }}>
|
||||
Für proprietäre Integrationen, geschlossene Forks oder Nutzungen die
|
||||
nicht mit AGPL-3.0 vereinbar sind, ist eine kommerzielle Lizenz
|
||||
erforderlich. Kontakt:{' '}
|
||||
<a href="mailto:karim@gabrielevarano.ch"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
karim@gabrielevarano.ch
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||
marginTop: 10, paddingTop: 8,
|
||||
borderTop: '1px solid var(--border-light)' }}>
|
||||
© 2026 Karim Gabriele Varano
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
marginTop: 'auto',
|
||||
paddingTop: 16,
|
||||
borderTop: '1px solid var(--border-light)',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.05em',
|
||||
}}>
|
||||
<Icon name="favorite" size={10} style={{ color: 'var(--accent)' }} />
|
||||
<span>made in Switzerland</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+9
-1
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import EbenenManager from './components/EbenenManager'
|
||||
import {
|
||||
@@ -21,7 +23,13 @@ const INITIAL_EBENEN = [
|
||||
{ code: '31', name: 'Dächer', color: '#7a4a3a', lw: 0.35, visible: true, locked: false },
|
||||
{ code: '35', name: 'Träger', color: '#a87858', lw: 0.50, visible: true, locked: false },
|
||||
{ code: '50', name: 'Text', color: '#d0d0d0', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '60', name: 'Plangrafik', color: '#c0a040', lw: 0.13, visible: true, locked: false },
|
||||
// Raum-Familie: RAEUME (Nutzflaechen HNF/NNF/VF/FF) + GF (Geschossflaeche
|
||||
// gross) + AGF (Aussengeschossflaeche). Wird vom Elemente-Modul auto-
|
||||
// erzeugt sobald der erste Raum mit entsprechendem SIA-Tag entsteht.
|
||||
{ code: '60', name: 'RAEUME', color: '#7a8a9a', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '61', name: 'GF', color: '#a0a0a0', lw: 0.18, visible: true, locked: false },
|
||||
{ code: '62', name: 'AGF', color: '#90b090', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '80', name: 'Plangrafik', color: '#c0a040', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '90', name: 'Referenzen', color: '#585860', lw: 0.13, visible: true, locked: false },
|
||||
{ code: '99', name: 'Konstruktion', color: '#404048', lw: 0.13, visible: true, locked: false },
|
||||
]
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState } from 'react'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
import { BarToggle, BarCombo, BAR_H } from './components/BarControls'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[AusschnittSettings] →', type, payload); return }
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
function Field({ label, hint, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '6px 0' }}>
|
||||
@@ -85,42 +100,38 @@ export default function AusschnittSettingsApp() {
|
||||
value={snap.scale || ''}
|
||||
onChange={(ev) => set({ scale: ev.target.value })}
|
||||
placeholder="1:50"
|
||||
style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font-mono)', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, fontFamily: 'var(--font-mono)', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="DARSTELLUNG"
|
||||
hint="SIA-400 Detaillierungsgrad fuer diesen Ausschnitt — leer = beim Restore nicht aendern">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={snap.darstellung || ''}
|
||||
onChange={(ev) => set({ darstellung: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => set({ darstellung: v })}>
|
||||
<option value="">— nicht aendern —</option>
|
||||
<option value="einfach">Einfach (1:100)</option>
|
||||
<option value="standard">Standard (1:50)</option>
|
||||
<option value="detail">Detail (1:20)</option>
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Field>
|
||||
|
||||
<Field label="BILDSCHIRMMODUS"
|
||||
hint="Display-Mode des Viewports beim Wiederherstellen">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={snap.displayMode || ''}
|
||||
onChange={(ev) => {
|
||||
const dm = displayModes.find(d => d.id === ev.target.value)
|
||||
onChange={(v) => {
|
||||
const dm = displayModes.find(d => d.id === v)
|
||||
set({
|
||||
displayMode: ev.target.value || null,
|
||||
displayMode: v || null,
|
||||
displayModeName: dm ? dm.name : null,
|
||||
})
|
||||
}}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
}}>
|
||||
<option value="">— unverändert —</option>
|
||||
{displayModes.map(dm => (
|
||||
<option key={dm.id} value={dm.id}>{dm.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Field>
|
||||
|
||||
<SectionLabel>Grafische Overrides</SectionLabel>
|
||||
@@ -142,29 +153,25 @@ export default function AusschnittSettingsApp() {
|
||||
{snap.applyOverrides && (
|
||||
<>
|
||||
<Field label="OVERRIDES STATUS">
|
||||
<select
|
||||
value={snap.overridesEnabled ? 'on' : 'off'}
|
||||
onChange={(ev) => set({ overridesEnabled: ev.target.value === 'on' })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
<option value="on">AN</option>
|
||||
<option value="off">AUS</option>
|
||||
</select>
|
||||
<BarToggle label="AN"
|
||||
active={!!snap.overridesEnabled}
|
||||
onClick={() => set({ overridesEnabled: true })} />
|
||||
<BarToggle label="AUS"
|
||||
active={!snap.overridesEnabled}
|
||||
onClick={() => set({ overridesEnabled: false })} />
|
||||
</Field>
|
||||
|
||||
<Field label="OVERRIDES PRESET"
|
||||
hint="Leer = kein Preset (Doc-Rules bleiben)">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={snap.overridesPreset || ''}
|
||||
onChange={(ev) => set({ overridesPreset: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
disabled={!snap.overridesEnabled}
|
||||
>
|
||||
onChange={(v) => set({ overridesPreset: v })}
|
||||
disabled={!snap.overridesEnabled}>
|
||||
<option value="">— kein Preset —</option>
|
||||
{overridesPresets.map(name => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
@@ -173,16 +180,14 @@ export default function AusschnittSettingsApp() {
|
||||
|
||||
<Field label="KOMBI"
|
||||
hint='"Eigene" = die per Snap gespeicherte Sichtbarkeit. Ein Preset überschreibt diese beim Wiederherstellen.'>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={snap.layerCombination || ''}
|
||||
onChange={(ev) => set({ layerCombination: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => set({ layerCombination: v })}>
|
||||
<option value="">— Eigene Sichtbarkeit —</option>
|
||||
{layerKombis.map(name => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
@@ -194,8 +199,8 @@ export default function AusschnittSettingsApp() {
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={saveAndClose}>Übernehmen</button>
|
||||
<BarToggle label="Abbrechen" onClick={() => send('CANCEL', {})} />
|
||||
<BarToggle label="Übernehmen" active onClick={saveAndClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+9
-11
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
@@ -103,13 +105,13 @@ function OrientationBadge({ orientation }) {
|
||||
title={variant.title}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 24, height: 24, flexShrink: 0,
|
||||
width: 20, height: 20, flexShrink: 0,
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-input)',
|
||||
color: variant.color,
|
||||
}}
|
||||
>
|
||||
<Icon name={variant.icon} size={14} />
|
||||
<Icon name={variant.icon} size={12} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -123,13 +125,13 @@ function AusschnittCard({ snap, onClick, onContextMenu, onMenuClick, onRename, o
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 8px',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '2px 8px 2px 4px',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-input)',
|
||||
cursor: 'grab', userSelect: 'none',
|
||||
marginBottom: 4,
|
||||
marginBottom: 3,
|
||||
opacity: dragging ? 0.4 : 1,
|
||||
transition: 'background 0.14s, border-color 0.14s, opacity 0.14s',
|
||||
}}
|
||||
@@ -364,13 +366,9 @@ export default function AusschnitteApp() {
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
|
||||
{/* Save-Bar als Card */}
|
||||
{/* Save-Bar — kein Outer-Border mehr, nur das Pill-Input + Add-Button. */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: 8,
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
background: 'var(--bg-section)',
|
||||
marginBottom: 8,
|
||||
marginTop: 6,
|
||||
}}>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton } from './components/BarControls'
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import EbenenSettingsDialog from './components/EbenenSettingsDialog'
|
||||
import { notifyReady, onMessage } from './lib/rhinoBridge'
|
||||
|
||||
function bridgeSend(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
import { notifyReady, onMessage, send as bridgeSend } from './lib/rhinoBridge'
|
||||
|
||||
export default function EbenenSettingsApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
|
||||
+886
-140
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
@@ -33,7 +35,10 @@ export default function ElementePropertiesApp() {
|
||||
geschosse={state.geschosse || []}
|
||||
materials={state.materials || []}
|
||||
hatchPatterns={state.hatchPatterns}
|
||||
fonts={state.fonts || []}
|
||||
oeffStyles={state.oeffStyles || []}
|
||||
raumStempelStile={state.raumStempelStile || []}
|
||||
stempelStile={state.stempelStile || []}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton } from './components/BarControls'
|
||||
@@ -8,7 +10,7 @@ import { onMessage, notifyReady, send } from './lib/rhinoBridge'
|
||||
|
||||
const KIND_ORDER = [
|
||||
'wand', 'decke', 'dach', 'fenster', 'tuer', 'aussparung',
|
||||
'treppe', 'stuetze', 'traeger', 'raum',
|
||||
'treppe', 'stuetze', 'traeger', 'raum', 'stempel',
|
||||
]
|
||||
|
||||
const KIND_META = {
|
||||
@@ -22,6 +24,7 @@ const KIND_META = {
|
||||
stuetze: { icon: 'square_foot', label: 'Stützen', color: '#c87050' },
|
||||
traeger: { icon: 'horizontal_rule', label: 'Träger', color: '#a87858' },
|
||||
raum: { icon: 'crop_free', label: 'Räume', color: '#5fa896' },
|
||||
stempel: { icon: 'receipt_long', label: 'Stempel', color: '#5fa896' },
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +41,7 @@ export default function ElementeUebersichtApp() {
|
||||
|
||||
const items = state.items || []
|
||||
const geschosse = state.geschosse || []
|
||||
const siaBilanz = state.siaBilanz || {}
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let r = items
|
||||
@@ -124,6 +128,9 @@ export default function ElementeUebersichtApp() {
|
||||
}} />
|
||||
<BarButton icon="unfold_more" onClick={expandAll} title="Alle aufklappen" />
|
||||
<BarButton icon="unfold_less" onClick={collapseAll} title="Alle einklappen" />
|
||||
<BarButton icon="file_download"
|
||||
onClick={() => send('EXPORT_BILANZ', {})}
|
||||
title="SIA-416 Bilanz als CSV exportieren (eine Spalte pro Geschoss + Total)" />
|
||||
</div>
|
||||
{/* Kind-Filter Chips */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||
@@ -161,6 +168,7 @@ export default function ElementeUebersichtApp() {
|
||||
const total = Object.values(groupForG).reduce((s, arr) => s + arr.length, 0)
|
||||
if (total === 0) return null
|
||||
const gOpen = expanded[g.id] !== false // default: open
|
||||
const bilanz = siaBilanz[g.id]
|
||||
return (
|
||||
<div key={g.id}>
|
||||
<div onClick={() => toggle(g.id)}
|
||||
@@ -190,6 +198,51 @@ export default function ElementeUebersichtApp() {
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
}}>{total}</span>
|
||||
</div>
|
||||
{/* SIA-416 Bilanz pro Geschoss — read-only Mini-Tabelle,
|
||||
nur angezeigt wenn mindestens ein klassifizierter Raum */}
|
||||
{gOpen && bilanz && (bilanz.hnf + bilanz.nnf + bilanz.vf + bilanz.ff) > 0 && (
|
||||
<div style={{
|
||||
display: 'flex', flexWrap: 'wrap', gap: 10,
|
||||
padding: '4px 14px 6px',
|
||||
background: 'var(--bg-section)',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
fontSize: 9, fontFamily: 'DM Mono, monospace',
|
||||
color: 'var(--text-muted)',
|
||||
}}>
|
||||
{bilanz.hnf > 0 && (
|
||||
<span title="Hauptnutzflaeche (SIA 416)">
|
||||
HNF <strong style={{color:'var(--text-primary)'}}>{bilanz.hnf.toFixed(1)}</strong> m²
|
||||
</span>
|
||||
)}
|
||||
{bilanz.nnf > 0 && (
|
||||
<span title="Nebennutzflaeche">
|
||||
NNF <strong style={{color:'var(--text-primary)'}}>{bilanz.nnf.toFixed(1)}</strong> m²
|
||||
</span>
|
||||
)}
|
||||
{bilanz.vf > 0 && (
|
||||
<span title="Verkehrsflaeche">
|
||||
VF <strong style={{color:'var(--text-primary)'}}>{bilanz.vf.toFixed(1)}</strong> m²
|
||||
</span>
|
||||
)}
|
||||
{bilanz.ff > 0 && (
|
||||
<span title="Funktionsflaeche">
|
||||
FF <strong style={{color:'var(--text-primary)'}}>{bilanz.ff.toFixed(1)}</strong> m²
|
||||
</span>
|
||||
)}
|
||||
{bilanz.nf > 0 && (
|
||||
<span title="Nutzflaeche = HNF + NNF"
|
||||
style={{ paddingLeft: 4, borderLeft: '1px solid var(--border)' }}>
|
||||
NF <strong style={{color:'var(--accent)'}}>{bilanz.nf.toFixed(1)}</strong> m²
|
||||
</span>
|
||||
)}
|
||||
{bilanz.ohne > 0 && (
|
||||
<span title="Raeume ohne SIA-Klassifikation"
|
||||
style={{ opacity: 0.6 }}>
|
||||
— {bilanz.ohne.toFixed(1)} m²
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{gOpen && KIND_ORDER.map(k => {
|
||||
const arr = groupForG[k]
|
||||
if (!arr || arr.length === 0) return null
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import GeschossDialog from './components/GeschossDialog'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function bridgeSend(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||
const json = JSON.stringify({ type, payload })
|
||||
document.title = 'RHINOMSG::' + json
|
||||
}
|
||||
import { notifyReady, send as bridgeSend } from './lib/rhinoBridge'
|
||||
|
||||
// recalcOkff direkt hier — gleiche Logik wie in ZeichnungsebenenApp.jsx,
|
||||
// damit der Dialog die OKFF-Werte beim Editieren live updaten kann.
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import GeschossSettingsDialog from './components/GeschossSettingsDialog'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function bridgeSend(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||
const json = JSON.stringify({ type, payload })
|
||||
document.title = 'RHINOMSG::' + json
|
||||
}
|
||||
import { notifyReady, send as bridgeSend } from './lib/rhinoBridge'
|
||||
|
||||
export default function GeschossSettingsApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
|
||||
+104
-21
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarCombo, BarToggle, BarButton } from './components/BarControls'
|
||||
@@ -311,14 +313,20 @@ function HatchEditor({ sel, enabled, source, color, pattern, scale, rotation,
|
||||
dropdownOptions.push({ value: currentValue, label: currentValue })
|
||||
}
|
||||
const applyPattern = (newValue) => {
|
||||
// Pattern-Pick: KEIN color senden (null) damit Backend per-Object die
|
||||
// Layer-Farbe nimmt. Color-Override greift nur wenn User explizit den
|
||||
// ColorBar anklickt → apply({color: ...}).
|
||||
if (newValue === '__layer__') setter(true, 'layer', null, null, null, null)
|
||||
else if (newValue === 'None') setter(false, source, null, null, scale, rotation)
|
||||
else setter(true, 'object', color, newValue, scale, rotation)
|
||||
else setter(true, 'object', null, newValue, scale, rotation)
|
||||
}
|
||||
const apply = (over) => setter(
|
||||
true,
|
||||
over.source ?? (source === 'layer' ? 'object' : source),
|
||||
(over.source ?? source) === 'layer' ? null : (over.color ?? color),
|
||||
// Layer-Source: null. Object-Source: nur expliziter Override-Color senden,
|
||||
// sonst null (= Backend nimmt Layer-Farbe).
|
||||
(over.source ?? source) === 'layer' ? null
|
||||
: (over.color !== undefined ? over.color : null),
|
||||
over.pattern ?? (objectPat === 'None' ? 'Solid' : objectPat),
|
||||
over.scale ?? scale,
|
||||
over.rotation ?? rotation,
|
||||
@@ -399,18 +407,89 @@ function FillBlock({ sel }) {
|
||||
|
||||
function SectionBlock({ sel }) {
|
||||
const color = sel.sectionColor || sel.layerColor || '#cccccc'
|
||||
return <HatchEditor
|
||||
sel={sel}
|
||||
enabled={sel.sectionEnabled === true}
|
||||
source={sel.sectionSource || 'layer'}
|
||||
color={color}
|
||||
pattern={sel.sectionPattern || 'Solid'}
|
||||
scale={sel.sectionScale ?? 1.0}
|
||||
rotation={sel.sectionRotation ?? 0.0}
|
||||
patternList={sel.hatchPatterns || ['Solid']}
|
||||
layerHint="Pattern, Skalierung & Farbe folgen Layer-SectionStyle"
|
||||
setter={setSectionStyle}
|
||||
/>
|
||||
const enabled = sel.sectionEnabled === true
|
||||
const isLayerSource = (sel.sectionSource || 'layer') === 'layer'
|
||||
// Boundary + Background Helper: setter mit allen aktuellen Werten,
|
||||
// overrides als opts.
|
||||
const callSetter = (over = {}) => {
|
||||
const opts = {
|
||||
boundaryVisible: over.boundaryVisible !== undefined
|
||||
? over.boundaryVisible : (sel.sectionBoundaryVisible !== false),
|
||||
boundaryWidthScale: over.boundaryWidthScale !== undefined
|
||||
? over.boundaryWidthScale : (sel.sectionBoundaryWidthScale ?? 1.0),
|
||||
boundaryColor: over.boundaryColor !== undefined
|
||||
? over.boundaryColor : sel.sectionBoundaryColor,
|
||||
backgroundColor: over.backgroundColor !== undefined
|
||||
? over.backgroundColor : sel.sectionBackgroundColor,
|
||||
}
|
||||
setSectionStyle(
|
||||
enabled, sel.sectionSource || 'layer', sel.sectionColor || null,
|
||||
sel.sectionPattern || 'Solid',
|
||||
sel.sectionScale ?? 1.0, sel.sectionRotation ?? 0.0,
|
||||
opts)
|
||||
}
|
||||
const boundaryVisible = sel.sectionBoundaryVisible !== false
|
||||
const boundaryColor = sel.sectionBoundaryColor || '#000000'
|
||||
const boundaryWidthScale = sel.sectionBoundaryWidthScale ?? 1.0
|
||||
const backgroundColor = sel.sectionBackgroundColor || null
|
||||
return <>
|
||||
{/* Schnittkante (Boundary) — Outline des Section-Cut, oben weil
|
||||
visuell zuerst wahrgenommen */}
|
||||
<SectionHead title="Schnittkante" />
|
||||
{!isLayerSource && enabled ? (
|
||||
<div style={{ padding: '0 14px 8px', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<BarToggle label={boundaryVisible ? 'Sichtbar' : 'Versteckt'}
|
||||
icon={boundaryVisible ? 'visibility' : 'visibility_off'}
|
||||
active={boundaryVisible}
|
||||
onClick={() => callSetter({ boundaryVisible: !boundaryVisible })} />
|
||||
{boundaryVisible && (
|
||||
<>
|
||||
<Icon name="line_weight" size={14}
|
||||
style={{ color: 'var(--text-muted)', flexShrink: 0 }}
|
||||
title="Schnittkanten-Breite" />
|
||||
<NumInput
|
||||
value={boundaryWidthScale} step={0.5} min={0.1}
|
||||
onCommit={(v) => callSetter({ boundaryWidthScale: v })}
|
||||
width={56}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{boundaryVisible && (
|
||||
<ColorBar
|
||||
color={boundaryColor}
|
||||
onChange={(c) => callSetter({ boundaryColor: c })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '0 14px 8px', fontSize: 10, color: 'var(--text-muted)',
|
||||
fontStyle: 'italic' }}>
|
||||
Aktiviere ein Pattern damit die Schnittkante eingestellt werden kann.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section Style (Hatch-Fill) */}
|
||||
<SectionHead title="Section Style" />
|
||||
<HatchEditor
|
||||
sel={sel}
|
||||
enabled={enabled}
|
||||
source={sel.sectionSource || 'layer'}
|
||||
color={color}
|
||||
pattern={sel.sectionPattern || 'Solid'}
|
||||
scale={sel.sectionScale ?? 1.0}
|
||||
rotation={sel.sectionRotation ?? 0.0}
|
||||
patternList={sel.hatchPatterns || ['Solid']}
|
||||
layerHint="Pattern, Skalierung & Farbe folgen Layer-SectionStyle"
|
||||
setter={setSectionStyle}
|
||||
/>
|
||||
|
||||
{/* Fill — Objekt-Pen (Color/LW/Linetype) als 'Fill' weil dessen
|
||||
Color in Plan-Mode visuell die Schnittflaeche fuellt. */}
|
||||
<SectionHead title="Fill" />
|
||||
<PenBlock sel={sel} />
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -449,7 +528,7 @@ export default function GestaltungApp() {
|
||||
const kind = sel.geometryKind || 'curve'
|
||||
const showFill = kind === 'curve'
|
||||
const showSection = kind === '3d'
|
||||
const penLabel = (kind === '3d') ? 'Boundary' : 'Pen'
|
||||
const penLabel = (kind === '3d') ? 'Background' : 'Pen'
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
@@ -468,14 +547,18 @@ export default function GestaltungApp() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<SectionHead title={penLabel} />
|
||||
<PenBlock sel={sel} />
|
||||
{/* PenBlock fuer 2D/Curves oben, fuer 3D innerhalb der
|
||||
SectionBlock als 'Fill' weiter unten (User-Erfahrung:
|
||||
ObjektFarbe fuehlt sich wie Section-Fill an). */}
|
||||
{kind !== '3d' && (
|
||||
<>
|
||||
<SectionHead title={penLabel} />
|
||||
<PenBlock sel={sel} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{showSection && (
|
||||
<>
|
||||
<SectionHead title="Section Style" />
|
||||
<SectionBlock sel={sel} />
|
||||
</>
|
||||
<SectionBlock sel={sel} />
|
||||
)}
|
||||
|
||||
<SectionHead title="Effects" />
|
||||
|
||||
+49
-77
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './components/BarControls'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
setKameraViewport, setKameraProjection, setKameraIso,
|
||||
@@ -13,6 +16,18 @@ const labelXs = {
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
function NumberField({ label, value, onCommit, suffix, step = 0.1 }) {
|
||||
const [draft, setDraft] = useState(value != null ? value.toFixed(3) : '')
|
||||
useEffect(() => {
|
||||
@@ -37,8 +52,7 @@ function NumberField({ label, value, onCommit, suffix, step = 0.1 }) {
|
||||
if (ev.key === 'Enter') commit()
|
||||
if (ev.key === 'Escape') setDraft(value != null ? value.toFixed(3) : '')
|
||||
}}
|
||||
style={{ flex: 1, fontSize: 11, padding: '4px 8px',
|
||||
fontFamily: 'var(--font-mono)' }}
|
||||
style={{ ...pillInput, flex: 1, fontFamily: 'var(--font-mono)' }}
|
||||
/>
|
||||
{suffix && (
|
||||
<span style={{ fontSize: 9, color: 'var(--text-muted)', minWidth: 18 }}>
|
||||
@@ -123,32 +137,19 @@ export default function KameraApp() {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 600 }}>{vp.name || 'Unnamed'}</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
borderRadius: 999,
|
||||
border: '1px solid var(--border)',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setKameraProjection(false)}
|
||||
className={!isPar ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 12px', fontSize: 10, border: 'none',
|
||||
borderRadius: 0 }}
|
||||
>Perspektive</button>
|
||||
<button
|
||||
onClick={() => setKameraProjection(true)}
|
||||
className={isPar ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 12px', fontSize: 10, border: 'none',
|
||||
borderRadius: 0 }}
|
||||
>Parallel</button>
|
||||
</div>
|
||||
<BarToggle label="Perspektive"
|
||||
active={!isPar}
|
||||
onClick={() => setKameraProjection(false)} />
|
||||
<BarToggle label="Parallel"
|
||||
active={isPar}
|
||||
onClick={() => setKameraProjection(true)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan-Norden — Rotations-Winkel im Uhrzeigersinn von +Y */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<span style={labelXs}>Plan-Norden (Rotation von +Y, im Uhrzeigersinn)</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input
|
||||
type="number" min={0} max={360} step={0.5}
|
||||
value={northAngle.toFixed(1)}
|
||||
@@ -159,16 +160,12 @@ export default function KameraApp() {
|
||||
setKameraNorthAngle(a)
|
||||
}
|
||||
}}
|
||||
style={{ flex: 1, fontSize: 12, padding: '4px 8px',
|
||||
fontFamily: 'DM Mono, monospace' }}
|
||||
style={{ ...pillInput, flex: 1, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>°</span>
|
||||
<button
|
||||
<BarToggle label="Reset"
|
||||
onClick={() => { setNorthAngleState(0); setKameraNorthAngle(0) }}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '4px 10px', fontSize: 10 }}
|
||||
title="Norden zurueck auf +Y (0°)"
|
||||
>Reset</button>
|
||||
title="Norden zurueck auf +Y (0°)" />
|
||||
</div>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.4 }}>
|
||||
Norden = +Y bei 0°. Bei rotierten Projekten (z.B. swissBUILDINGS in
|
||||
@@ -180,20 +177,12 @@ export default function KameraApp() {
|
||||
{/* Iso-Quick-Picker */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<span style={labelXs}>Isometrie (Standard, true-iso 35°/45°)</span>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 4 }}>
|
||||
{[
|
||||
{ v: 'NW', label: 'NW' },
|
||||
{ v: 'NE', label: 'NE' },
|
||||
{ v: 'SE', label: 'SE' },
|
||||
{ v: 'SW', label: 'SW' },
|
||||
].map(o => (
|
||||
<button
|
||||
key={o.v}
|
||||
onClick={() => setKameraIso(o.v)}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '6px 0', fontSize: 11 }}
|
||||
title={`Isometrie aus ${o.label} (Kamera blickt Richtung Szene)`}
|
||||
>{o.label}</button>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{['NW', 'NE', 'SE', 'SW'].map(v => (
|
||||
<BarToggle key={v} label={v}
|
||||
onClick={() => setKameraIso(v)}
|
||||
title={`Isometrie aus ${v} (Kamera blickt Richtung Szene)`}
|
||||
minWidth={48} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,9 +194,9 @@ export default function KameraApp() {
|
||||
{/* Distance read-only */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 10px',
|
||||
padding: '6px 12px',
|
||||
background: 'var(--bg-section)',
|
||||
borderRadius: 6,
|
||||
borderRadius: 999,
|
||||
border: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<span style={labelXs}>Distanz</span>
|
||||
@@ -236,19 +225,13 @@ export default function KameraApp() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => kameraZoomExtents()}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '6px 12px', fontSize: 11 }}
|
||||
>
|
||||
<Icon name="zoom_out_map" size={13} />
|
||||
<span style={{ marginLeft: 6 }}>Zoom Extents</span>
|
||||
</button>
|
||||
<BarToggle icon="zoom_out_map" label="Zoom Extents"
|
||||
onClick={() => kameraZoomExtents()} />
|
||||
|
||||
{/* Presets */}
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 6,
|
||||
padding: 10, borderRadius: 6,
|
||||
padding: 10, borderRadius: 'var(--r-lg)',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border-light)',
|
||||
}}>
|
||||
@@ -260,41 +243,34 @@ export default function KameraApp() {
|
||||
value={presetName}
|
||||
onChange={(ev) => setPresetName(ev.target.value)}
|
||||
onKeyDown={(ev) => { if (ev.key === 'Enter') saveCurrent() }}
|
||||
style={{ flex: 1, fontSize: 11, padding: '4px 8px' }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
onClick={saveCurrent}
|
||||
<BarToggle label="Speichern"
|
||||
active={!!presetName.trim()}
|
||||
disabled={!presetName.trim()}
|
||||
className="btn-contained"
|
||||
style={{ padding: '4px 12px', fontSize: 11 }}
|
||||
>Aktuelle speichern</button>
|
||||
onClick={saveCurrent} />
|
||||
</div>
|
||||
{presets.length === 0 ? (
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
||||
Keine Presets gespeichert.
|
||||
</span>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{presets.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '4px 6px',
|
||||
borderRadius: 4,
|
||||
padding: '2px 6px 2px 4px',
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-item)',
|
||||
border: '1px solid transparent',
|
||||
border: '1px solid var(--border-light)',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
<BarButton icon="play_arrow"
|
||||
onClick={() => applyKameraPreset(p.id)}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '2px 8px', fontSize: 10 }}
|
||||
title="Anwenden"
|
||||
>
|
||||
<Icon name="play_arrow" size={11} />
|
||||
</button>
|
||||
title="Anwenden" />
|
||||
<span style={{ flex: 1, minWidth: 0,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap' }}>{p.name}</span>
|
||||
@@ -302,13 +278,9 @@ export default function KameraApp() {
|
||||
fontFamily: 'var(--font-mono)' }}>
|
||||
{p.parallel ? 'Par' : 'Persp'}
|
||||
</span>
|
||||
<button
|
||||
<BarButton icon="close"
|
||||
onClick={() => deleteKameraPreset(p.id)}
|
||||
className="btn-icon-xs"
|
||||
title="Loeschen"
|
||||
>
|
||||
<Icon name="close" size={11} />
|
||||
</button>
|
||||
title="Loeschen" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
+40
-40
@@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
import { BarToggle, BAR_H } from './components/BarControls'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[LayoutDialog] →', type, payload); return }
|
||||
@@ -9,6 +11,18 @@ function send(type, payload = {}) {
|
||||
|
||||
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
export default function LayoutDialogApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
const [mode, setMode] = useState(initial.mode || 'new')
|
||||
@@ -70,7 +84,7 @@ export default function LayoutDialogApp() {
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') submit() }}
|
||||
placeholder="z.B. Grundriss EG"
|
||||
autoFocus
|
||||
style={{ width: '100%', fontSize: 12, padding: '6px 8px' }}
|
||||
style={{ ...pillInput, width: '100%' }}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
@@ -78,19 +92,13 @@ export default function LayoutDialogApp() {
|
||||
<Field label="Papierformat">
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{PAPER_SIZES.map(f => (
|
||||
<button key={f}
|
||||
onClick={() => setFormat(f)}
|
||||
className={format === f ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '5px 12px', fontSize: 11 }}>
|
||||
{f}
|
||||
</button>
|
||||
<BarToggle key={f} label={f}
|
||||
active={format === f}
|
||||
onClick={() => setFormat(f)} />
|
||||
))}
|
||||
<button
|
||||
onClick={() => setFormat('custom')}
|
||||
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '5px 12px', fontSize: 11 }}>
|
||||
Eigene
|
||||
</button>
|
||||
<BarToggle label="Eigene"
|
||||
active={format === 'custom'}
|
||||
onClick={() => setFormat('custom')} />
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
@@ -101,16 +109,18 @@ export default function LayoutDialogApp() {
|
||||
type="text" value={cw}
|
||||
onChange={(e) => setCw(e.target.value)}
|
||||
placeholder="Breite"
|
||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
|
||||
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
|
||||
style={{ ...pillInput, flex: 1,
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
textAlign: 'right' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>×</span>
|
||||
<input
|
||||
type="text" value={ch}
|
||||
onChange={(e) => setCh(e.target.value)}
|
||||
placeholder="Höhe"
|
||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
|
||||
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
|
||||
style={{ ...pillInput, flex: 1,
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
textAlign: 'right' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10, width: 22 }}>mm</span>
|
||||
</div>
|
||||
@@ -118,22 +128,12 @@ export default function LayoutDialogApp() {
|
||||
) : (
|
||||
<Field label="Ausrichtung">
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
onClick={() => setLandscape(true)}
|
||||
className={landscape ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
|
||||
display: 'flex', gap: 6, alignItems: 'center',
|
||||
justifyContent: 'center' }}>
|
||||
<Icon name="crop_landscape" size={16} /> Quer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLandscape(false)}
|
||||
className={!landscape ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
|
||||
display: 'flex', gap: 6, alignItems: 'center',
|
||||
justifyContent: 'center' }}>
|
||||
<Icon name="crop_portrait" size={16} /> Hoch
|
||||
</button>
|
||||
<BarToggle icon="crop_landscape" label="Quer"
|
||||
active={landscape}
|
||||
onClick={() => setLandscape(true)} />
|
||||
<BarToggle icon="crop_portrait" label="Hoch"
|
||||
active={!landscape}
|
||||
onClick={() => setLandscape(false)} />
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
@@ -151,12 +151,12 @@ export default function LayoutDialogApp() {
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={submit}
|
||||
disabled={!editing && !name.trim()}
|
||||
title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}>
|
||||
{editing ? 'Anwenden' : 'Erstellen'}
|
||||
</button>
|
||||
<BarToggle label="Abbrechen" onClick={() => send('CANCEL', {})} />
|
||||
<BarToggle label={editing ? 'Anwenden' : 'Erstellen'}
|
||||
active
|
||||
disabled={!editing && !name.trim()}
|
||||
title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}
|
||||
onClick={submit} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+34
-79
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
import { BarButton, BarCombo, BAR_H } from './components/BarControls'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
listLayouts, deleteLayout, renameLayout, activateLayout,
|
||||
@@ -37,12 +40,12 @@ function OrientationBadge({ landscape }) {
|
||||
<span title={landscape ? 'Querformat' : 'Hochformat'}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 24, height: 24, flexShrink: 0,
|
||||
width: 20, height: 20, flexShrink: 0,
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-input)',
|
||||
color: 'var(--accent)',
|
||||
}}>
|
||||
<Icon name={landscape ? 'crop_landscape' : 'crop_portrait'} size={14} />
|
||||
<Icon name={landscape ? 'crop_landscape' : 'crop_portrait'} size={12} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -346,16 +349,12 @@ export default function LayoutsApp() {
|
||||
{folderName}
|
||||
</span>
|
||||
<span className="chip" style={{ fontSize: 8 }}>{items.length}</span>
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
<BarButton icon="more_vert"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
setCtxMenu({ x: ev.clientX, y: ev.clientY, kind: 'folder', id: folderName })
|
||||
}}
|
||||
title="Ordner-Aktionen"
|
||||
>
|
||||
<Icon name="more_vert" size={14} />
|
||||
</button>
|
||||
title="Ordner-Aktionen" />
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div style={{ marginTop: 6, display: 'flex', flexDirection: 'column' }}>
|
||||
@@ -461,26 +460,17 @@ export default function LayoutsApp() {
|
||||
{/* Details des aktuell gewaehlten Layouts */}
|
||||
{selected && (
|
||||
<div style={cardStyle}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 6 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 6 }}>
|
||||
<span style={{ ...labelXs, flex: 1 }}>
|
||||
Details · {selected.name}
|
||||
</span>
|
||||
<button
|
||||
<BarButton icon="add"
|
||||
onClick={() => addDetail(selected.id, null)}
|
||||
className="btn-icon-tonal"
|
||||
title="Neues Detail (zentriert auf Seite)"
|
||||
style={{ marginRight: 4 }}
|
||||
>
|
||||
<Icon name="add" size={13} />
|
||||
</button>
|
||||
<button
|
||||
title="Neues Detail (zentriert auf Seite)" />
|
||||
<BarButton icon="sync"
|
||||
onClick={() => syncLayout(selected.id)}
|
||||
className="btn-icon-tonal"
|
||||
disabled={details.length === 0}
|
||||
title="Alle Details mit ihren Ausschnitten neu synchronisieren"
|
||||
>
|
||||
<Icon name="sync" size={13} />
|
||||
</button>
|
||||
title="Alle Details mit ihren Ausschnitten neu synchronisieren" />
|
||||
</div>
|
||||
|
||||
{details.length === 0 ? (
|
||||
@@ -505,7 +495,7 @@ export default function LayoutsApp() {
|
||||
padding: '6px 8px',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span className="chip" style={{ flexShrink: 0 }}>#{i + 1}</span>
|
||||
@@ -518,38 +508,28 @@ export default function LayoutsApp() {
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', fontFamily: 'DM Mono, monospace' }}>
|
||||
{Math.round(d.width)}×{Math.round(d.height)}
|
||||
</span>
|
||||
<button
|
||||
className="btn-icon-sm btn-icon-danger"
|
||||
<BarButton icon="delete"
|
||||
onClick={() => {
|
||||
if (window.confirm('Detail loeschen?')) deleteDetail(selected.id, d.id)
|
||||
}}
|
||||
title="Detail loeschen"
|
||||
>
|
||||
<Icon name="delete" size={12} />
|
||||
</button>
|
||||
title="Detail loeschen" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={d.boundAusschnitt || ''}
|
||||
onChange={(e) => bindAusschnitt(selected.id, d.id, e.target.value || null)}
|
||||
style={{ flex: 1, fontSize: 11 }}
|
||||
title="Welcher Ausschnitt auf diesem Detail liegt"
|
||||
>
|
||||
onChange={(v) => bindAusschnitt(selected.id, d.id, v || null)}
|
||||
title="Welcher Ausschnitt auf diesem Detail liegt">
|
||||
<option value="">— kein Ausschnitt —</option>
|
||||
{snaps.map(s => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name}{s.folder ? ` · ${s.folder}` : ''}{s.scale ? ` · ${s.scale}` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
</BarCombo>
|
||||
<BarButton icon="sync"
|
||||
onClick={() => syncDetail(selected.id, d.id)}
|
||||
disabled={!d.boundAusschnitt}
|
||||
className="btn-icon-sm"
|
||||
title="Gebundenen Ausschnitt neu anwenden"
|
||||
>
|
||||
<Icon name="sync" size={12} />
|
||||
</button>
|
||||
title="Gebundenen Ausschnitt neu anwenden" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -573,49 +553,24 @@ export default function LayoutsApp() {
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
|
||||
Layouts
|
||||
</span>
|
||||
{/* PDF-Aktionen: feste Breite damit das Auswahl-Counter den Footer
|
||||
nicht horizontal verschiebt. */}
|
||||
<button
|
||||
{/* PDF-Aktionen — Pill-Stil */}
|
||||
<BarButton icon="picture_as_pdf"
|
||||
onClick={handleExportSelection}
|
||||
className="btn-icon-tonal"
|
||||
disabled={checked.size === 0}
|
||||
title={checked.size > 0
|
||||
? `Auswahl (${checked.size}) als ein PDF exportieren`
|
||||
: 'Erst Layouts ankreuzen'}
|
||||
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
|
||||
>
|
||||
<Icon name="picture_as_pdf" size={14} />
|
||||
{checked.size > 0 && (
|
||||
<span style={{ fontSize: 9, fontFamily: 'DM Mono, monospace' }}>
|
||||
{checked.size}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
: 'Erst Layouts ankreuzen'} />
|
||||
<BarButton icon="picture_as_pdf"
|
||||
onClick={() => exportPdfAll(300)}
|
||||
className="btn-icon-tonal"
|
||||
disabled={layouts.length === 0}
|
||||
title="Alle Layouts als ein PDF exportieren"
|
||||
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
|
||||
>
|
||||
<Icon name="picture_as_pdf" size={14} />
|
||||
<span style={{ fontSize: 9 }}>·∗</span>
|
||||
</button>
|
||||
title="Alle Layouts als ein PDF exportieren" />
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
<button
|
||||
<BarButton icon="create_new_folder"
|
||||
onClick={handleNewFolder}
|
||||
className="btn-icon-tonal"
|
||||
title="Neuer Ordner"
|
||||
>
|
||||
<Icon name="create_new_folder" size={14} />
|
||||
</button>
|
||||
<button
|
||||
title="Neuer Ordner" />
|
||||
<BarButton icon="refresh"
|
||||
onClick={() => listLayouts()}
|
||||
className="btn-icon-tonal"
|
||||
title="Aktualisieren"
|
||||
>
|
||||
<Icon name="refresh" size={14} />
|
||||
</button>
|
||||
title="Aktualisieren" />
|
||||
<button
|
||||
onClick={() => openLayoutDialog('new', null)}
|
||||
className="btn-add"
|
||||
@@ -660,14 +615,14 @@ function LayoutRow({ l, active, checked, dragging, forceEditName,
|
||||
onDoubleClick={onActivate}
|
||||
onContextMenu={onContextMenu}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 8px',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '2px 8px 2px 6px',
|
||||
border: '1px solid ' + (active ? 'var(--accent)' : 'var(--border-light)'),
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 999,
|
||||
background: active ? 'var(--bg-item-active)' : 'var(--bg-input)',
|
||||
cursor: 'grab',
|
||||
userSelect: 'none',
|
||||
marginBottom: 4,
|
||||
marginBottom: 3,
|
||||
opacity: dragging ? 0.4 : 1,
|
||||
transition: 'background 0.14s, border-color 0.14s, opacity 0.14s',
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import LibraryBrowser from './components/LibraryBrowser'
|
||||
import { notifyReady, onMessage, send } from './lib/rhinoBridge'
|
||||
|
||||
+34
-33
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarButton, BarCombo, BAR_H } from './components/BarControls'
|
||||
import { onMessage, notifyReady,
|
||||
masseSetActive as setActive,
|
||||
masseSavePreset as savePreset,
|
||||
@@ -19,6 +22,18 @@ const labelXs = {
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
function Row({ label, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
@@ -86,35 +101,27 @@ export default function MasseSettingsApp() {
|
||||
{/* Picker + Aktionen */}
|
||||
<Row label="Aktiv">
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={activeId || (active?.id || '')}
|
||||
onChange={(e) => setActive(e.target.value)}
|
||||
style={{ flex: 1, fontSize: 12 }}
|
||||
>
|
||||
onChange={(v) => setActive(v)}>
|
||||
{presets.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
</BarCombo>
|
||||
<BarButton icon="add"
|
||||
onClick={addNew}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '4px 8px' }}
|
||||
title="Neues Mass anlegen (mit aktuellen Werten als Vorlage)"
|
||||
><Icon name="add" size={13} /></button>
|
||||
<button
|
||||
title="Neues Mass anlegen (mit aktuellen Werten als Vorlage)" />
|
||||
<BarButton icon="delete"
|
||||
onClick={remove}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '4px 8px' }}
|
||||
title="Aktives Mass löschen"
|
||||
disabled={presets.length <= 1}
|
||||
><Icon name="delete" size={13} /></button>
|
||||
title="Aktives Mass löschen" />
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
{active && (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 10,
|
||||
padding: 12, borderRadius: 6,
|
||||
padding: 12, borderRadius: 'var(--r-lg)',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border-light)',
|
||||
}}>
|
||||
@@ -123,44 +130,38 @@ export default function MasseSettingsApp() {
|
||||
type="text"
|
||||
value={active.name}
|
||||
onChange={(e) => update({ name: e.target.value })}
|
||||
style={{ width: '100%', fontSize: 12, padding: '4px 8px' }}
|
||||
style={{ ...pillInput, width: '100%' }}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Row label="Raum-Rundung">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={active.raumRundung}
|
||||
onChange={(e) => update({ raumRundung: e.target.value })}
|
||||
style={{ width: '100%', fontSize: 12 }}
|
||||
>
|
||||
onChange={(v) => update({ raumRundung: v })}>
|
||||
{Object.entries(RAUM_RUNDUNGS_LABELS).map(([v, l]) => (
|
||||
<option key={v} value={v}>{l}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Row>
|
||||
|
||||
<Row label="Mass-Dezimalstellen">
|
||||
<select
|
||||
value={active.dimDezimalstellen}
|
||||
onChange={(e) => update({ dimDezimalstellen: parseInt(e.target.value, 10) })}
|
||||
style={{ width: '100%', fontSize: 12 }}
|
||||
>
|
||||
<BarCombo stretch
|
||||
value={String(active.dimDezimalstellen)}
|
||||
onChange={(v) => update({ dimDezimalstellen: parseInt(v, 10) })}>
|
||||
{[0, 1, 2, 3, 4].map(n => (
|
||||
<option key={n} value={n}>{n} {n === 1 ? 'Stelle' : 'Stellen'}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Row>
|
||||
|
||||
<Row label="Mass-Einheit">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={active.dimEinheit}
|
||||
onChange={(e) => update({ dimEinheit: e.target.value })}
|
||||
style={{ width: '100%', fontSize: 12 }}
|
||||
>
|
||||
onChange={(v) => update({ dimEinheit: v })}>
|
||||
<option value="m">m (Meter)</option>
|
||||
<option value="cm">cm (Zentimeter)</option>
|
||||
<option value="mm">mm (Millimeter)</option>
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+44
-68
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton, BarCombo, BAR_H } from './components/BarControls'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
requestMassstab, setMassstab,
|
||||
@@ -50,6 +53,18 @@ function parseScale(input) {
|
||||
return n > 0 ? n : null
|
||||
}
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function MassstabApp() {
|
||||
@@ -124,20 +139,6 @@ export default function MassstabApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Style-Bausteine ------------------------------------------------------
|
||||
const cellBtn = {
|
||||
fontSize: 11, padding: '0 8px', height: 24,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 4,
|
||||
background: 'var(--bg-item)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', color: 'var(--text-primary)', cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
const cellInput = {
|
||||
fontSize: 11, padding: '0 6px', height: 24, minWidth: 0,
|
||||
background: 'var(--bg-input)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', color: 'var(--text-primary)',
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%', height: '100%',
|
||||
@@ -163,13 +164,12 @@ export default function MassstabApp() {
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
|
||||
{/* Skala-Dropdown */}
|
||||
<select
|
||||
<BarCombo
|
||||
disabled={isPerspective}
|
||||
value={dropdownValue}
|
||||
onChange={(e) => applyDropdown(e.target.value)}
|
||||
style={{ ...cellInput, width: 80 }}
|
||||
title="Massstab wählen"
|
||||
>
|
||||
onChange={applyDropdown}
|
||||
width={84}
|
||||
title="Massstab wählen">
|
||||
<option value="__none__">1:?</option>
|
||||
{PRESETS.map(p => (
|
||||
<option key={p.value} value={String(p.value)}>{p.label}</option>
|
||||
@@ -177,7 +177,7 @@ export default function MassstabApp() {
|
||||
{appliedScale != null && !PRESETS.some(p => p.value === appliedScale) && (
|
||||
<option value={String(appliedScale)}>1:{appliedScale}</option>
|
||||
)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
|
||||
{/* Freitext */}
|
||||
<input
|
||||
@@ -188,49 +188,37 @@ export default function MassstabApp() {
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') applyDraft() }}
|
||||
onBlur={() => { if (draft) applyDraft() }}
|
||||
style={{ ...cellInput, width: 64 }}
|
||||
style={{ ...pillInput, width: 64 }}
|
||||
title="Eigenen Massstab eingeben (Enter)"
|
||||
/>
|
||||
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
|
||||
{/* Aktions-Buttons */}
|
||||
<button
|
||||
<BarToggle label="100%"
|
||||
disabled={isPerspective || !appliedScale}
|
||||
onClick={apply100}
|
||||
style={cellBtn}
|
||||
title={appliedScale
|
||||
? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})`
|
||||
: 'Erst einen Massstab wählen'}
|
||||
>100%</button>
|
||||
<button onClick={zoomExtents} style={cellBtn} title="Auf gesamten Inhalt zoomen">
|
||||
<Icon name="fit_screen" size={14} />
|
||||
</button>
|
||||
<button onClick={zoomSelection} style={cellBtn} title="Auf Selektion zoomen">
|
||||
<Icon name="center_focus_strong" size={14} />
|
||||
</button>
|
||||
: 'Erst einen Massstab wählen'} />
|
||||
<BarButton icon="fit_screen"
|
||||
onClick={zoomExtents}
|
||||
title="Auf gesamten Inhalt zoomen" />
|
||||
<BarButton icon="center_focus_strong"
|
||||
onClick={zoomSelection}
|
||||
title="Auf Selektion zoomen" />
|
||||
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
|
||||
{/* Print-View / Strichstaerken-Toggle
|
||||
Beide Icons werden permanent gerendert; nur display: none togglet,
|
||||
damit die Font-Ligatur nicht neu aufgeloest wird (sonst Flackern). */}
|
||||
<button
|
||||
{/* Print-View / Strichstaerken-Toggle */}
|
||||
<BarToggle
|
||||
icon={state.showLineweights ? 'print' : 'edit'}
|
||||
label={state.showLineweights ? 'Print' : 'Edit'}
|
||||
active={state.showLineweights}
|
||||
onClick={() => setShowLineweights(!state.showLineweights)}
|
||||
style={{
|
||||
...cellBtn,
|
||||
background: state.showLineweights ? 'var(--accent)' : 'var(--bg-item)',
|
||||
color: state.showLineweights ? '#fff' : 'var(--text-primary)',
|
||||
borderColor: state.showLineweights ? 'var(--accent)' : 'var(--border)',
|
||||
}}
|
||||
title={state.showLineweights
|
||||
? 'Strichstärken werden angezeigt (Print-View) — klicken zum Ausschalten'
|
||||
: 'Strichstärken als Hairlines (Edit-View) — klicken um Print-View zu zeigen'}
|
||||
>
|
||||
<Icon name="edit" size={14} style={{ display: state.showLineweights ? 'none' : 'inline-block' }} />
|
||||
<Icon name="print" size={14} style={{ display: state.showLineweights ? 'inline-block' : 'none' }} />
|
||||
<span style={{ fontSize: 10 }}>{state.showLineweights ? 'Print' : 'Edit'}</span>
|
||||
</button>
|
||||
: 'Strichstärken als Hairlines (Edit-View) — klicken um Print-View zu zeigen'} />
|
||||
|
||||
{/* Spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
@@ -246,24 +234,16 @@ export default function MassstabApp() {
|
||||
|
||||
{/* DPI-Popover */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button
|
||||
<BarToggle
|
||||
label={`${Math.round(state.dpi || 96)}dpi${state.dpiSource === 'auto' ? ' · auto' : ''}`}
|
||||
active={state.dpiSource === 'auto'}
|
||||
onClick={() => { setDpiDraft(String(state.dpi || 96)); setDpiOpen(o => !o) }}
|
||||
style={{ ...cellBtn, fontSize: 10, padding: '0 6px', height: 22,
|
||||
color: state.dpiSource === 'auto' ? 'var(--accent)'
|
||||
: state.dpiSource === 'manual' ? 'var(--text-primary)'
|
||||
: 'var(--text-muted)' }}
|
||||
title={`DPI Kalibrierung — aktuell ${Math.round(state.dpi || 96)} dpi (${state.dpiSource || 'default'})`}
|
||||
>
|
||||
{Math.round(state.dpi || 96)}dpi
|
||||
{state.dpiSource === 'auto' && (
|
||||
<span style={{ marginLeft: 3, fontSize: 8, opacity: 0.8 }}>auto</span>
|
||||
)}
|
||||
</button>
|
||||
title={`DPI Kalibrierung — aktuell ${Math.round(state.dpi || 96)} dpi (${state.dpiSource || 'default'})`} />
|
||||
{dpiOpen && (
|
||||
<div style={{
|
||||
position: 'absolute', right: 0, bottom: '100%', marginBottom: 4,
|
||||
background: 'var(--bg-panel)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', padding: 8, display: 'flex',
|
||||
borderRadius: 'var(--r-lg)', padding: 8, display: 'flex',
|
||||
flexDirection: 'column', gap: 6,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: 10, minWidth: 220,
|
||||
}}>
|
||||
@@ -277,17 +257,13 @@ export default function MassstabApp() {
|
||||
onChange={(e) => setDpiDraft(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') commitDpi() }}
|
||||
autoFocus
|
||||
style={{ ...cellInput, flex: 1 }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button onClick={commitDpi} style={{ ...cellBtn, fontSize: 10 }}>OK</button>
|
||||
<BarToggle label="OK" active onClick={commitDpi} />
|
||||
</div>
|
||||
<button
|
||||
<BarToggle icon="auto_fix_high" label="Auto-Detect (EDID)"
|
||||
onClick={() => { detectMassstabDpi(); setDpiOpen(false) }}
|
||||
style={{ ...cellBtn, fontSize: 10, justifyContent: 'flex-start' }}
|
||||
title="DPI automatisch über EDID des Bildschirms ermitteln"
|
||||
>
|
||||
<Icon name="auto_fix_high" size={12} /> Auto-Detect (EDID)
|
||||
</button>
|
||||
title="DPI automatisch über EDID des Bildschirms ermitteln" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarCombo, BarButton, BAR_H } from './components/BarControls'
|
||||
|
||||
+32
-27
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BAR_H } from './components/BarControls'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
@@ -7,6 +10,18 @@ function send(type, payload = {}) {
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
function Field({ label, hint, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}>
|
||||
@@ -35,13 +50,9 @@ function Radio({ value, options, onChange }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{options.map(o => (
|
||||
<button key={o.value}
|
||||
onClick={() => onChange(o.value)}
|
||||
className={value === o.value ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 10px', fontSize: 10 }}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
<BarToggle key={o.value} label={o.label}
|
||||
active={value === o.value}
|
||||
onClick={() => onChange(o.value)} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -168,36 +179,31 @@ export default function OsmApp() {
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }}
|
||||
placeholder="Adresse oder Ortsname"
|
||||
style={{ flex: 1, fontSize: 11, padding: '5px 8px' }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn-outlined"
|
||||
<BarToggle label={searching ? '…' : 'Suchen'}
|
||||
onClick={handleSearch}
|
||||
disabled={searching || !searchText.trim()}
|
||||
style={{ padding: '4px 10px', fontSize: 11 }}
|
||||
>
|
||||
{searching ? '…' : 'Suchen'}
|
||||
</button>
|
||||
disabled={searching || !searchText.trim()} />
|
||||
</Field>
|
||||
|
||||
<Field label="ODER LV95-KOORDS (E / N)"
|
||||
hint="Falls aus Swisstopo-Import übernommen">
|
||||
<input placeholder="E"
|
||||
onChange={(e) => handleManualCoords(e.target.value, center?.n || '')}
|
||||
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} />
|
||||
style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }} />
|
||||
<span style={{ color: 'var(--text-muted)' }}>/</span>
|
||||
<input placeholder="N"
|
||||
onChange={(e) => handleManualCoords(center?.e || '', e.target.value)}
|
||||
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} />
|
||||
style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }} />
|
||||
</Field>
|
||||
|
||||
{center && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 10px',
|
||||
padding: '8px 12px',
|
||||
background: 'var(--accent-dim)',
|
||||
border: '1px solid var(--accent-border)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 999,
|
||||
marginTop: 4,
|
||||
}}>
|
||||
<Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} />
|
||||
@@ -276,7 +282,7 @@ export default function OsmApp() {
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
background: 'var(--bg-base)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
color: 'var(--text-secondary)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}>
|
||||
@@ -296,13 +302,12 @@ export default function OsmApp() {
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)', flex: 1 }}>
|
||||
Quelle: Overpass-API · © OpenStreetMap-Mitwirkende (ODbL)
|
||||
</div>
|
||||
<button className="btn-text" onClick={() => send('CANCEL')}>Abbrechen</button>
|
||||
<button className="btn-contained"
|
||||
onClick={handleImport}
|
||||
disabled={running || !center}>
|
||||
<Icon name="download" size={12} />
|
||||
{running ? 'Lädt…' : 'Importieren'}
|
||||
</button>
|
||||
<BarToggle label="Abbrechen" onClick={() => send('CANCEL')} />
|
||||
<BarToggle icon="download"
|
||||
label={running ? 'Lädt…' : 'Importieren'}
|
||||
active
|
||||
disabled={running || !center}
|
||||
onClick={handleImport} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+101
-111
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
import { BarToggle, BarButton, BarCombo, BAR_H } from './components/BarControls'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
setOverridesEnabled, addRule, updateRule, deleteRule,
|
||||
@@ -29,6 +32,18 @@ const labelXs = {
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
|
||||
@@ -42,18 +57,14 @@ function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={t}
|
||||
onChange={(e) => onChange({ ...cond, type: e.target.value })}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => onChange({ ...cond, type: v })}>
|
||||
{COND_TYPES.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
{canRemove && (
|
||||
<button onClick={onRemove} className="btn-icon-danger"
|
||||
title="Diese Bedingung entfernen">
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
<BarButton icon="close" onClick={onRemove}
|
||||
title="Diese Bedingung entfernen" />
|
||||
)}
|
||||
</div>
|
||||
{t === 'user_string' && (
|
||||
@@ -61,34 +72,31 @@ function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
|
||||
type="text" placeholder="Key"
|
||||
value={cond?.key || ''}
|
||||
onChange={(e) => onChange({ ...cond, key: e.target.value })}
|
||||
style={{ width: '100%' }}
|
||||
style={{ ...pillInput, width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<select
|
||||
<BarCombo
|
||||
value={op}
|
||||
onChange={(e) => onChange({ ...cond, operator: e.target.value })}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
onChange={(v) => onChange({ ...cond, operator: v })}
|
||||
width={100}>
|
||||
{OPS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
{t === 'layer_name' ? (
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={cond?.value || ''}
|
||||
onChange={(e) => onChange({ ...cond, value: e.target.value })}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => onChange({ ...cond, value: v })}>
|
||||
<option value="">—</option>
|
||||
{(layers || []).map(l => (
|
||||
<option key={l.fullPath} value={l.fullPath}>{l.fullPath}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
) : (
|
||||
<input
|
||||
type="text" placeholder="Wert"
|
||||
value={cond?.value || ''}
|
||||
onChange={(e) => onChange({ ...cond, value: e.target.value })}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, minWidth: 0 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -124,16 +132,14 @@ function ConditionsEditor({ rule, layers, onChange }) {
|
||||
{conds.length > 1 && (
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>Logik:</span>
|
||||
<button
|
||||
<BarToggle label="AND"
|
||||
active={logic === 'and'}
|
||||
onClick={() => setLogic('and')}
|
||||
className={logic === 'and' ? 'btn-contained' : 'btn-outlined'}
|
||||
title="Alle Bedingungen müssen zutreffen"
|
||||
>AND</button>
|
||||
<button
|
||||
title="Alle Bedingungen müssen zutreffen" />
|
||||
<BarToggle label="OR"
|
||||
active={logic === 'or'}
|
||||
onClick={() => setLogic('or')}
|
||||
className={logic === 'or' ? 'btn-contained' : 'btn-outlined'}
|
||||
title="Mindestens eine Bedingung muss zutreffen"
|
||||
>OR</button>
|
||||
title="Mindestens eine Bedingung muss zutreffen" />
|
||||
</div>
|
||||
)}
|
||||
{conds.map((c, i) => (
|
||||
@@ -146,11 +152,11 @@ function ConditionsEditor({ rule, layers, onChange }) {
|
||||
canRemove={conds.length > 1}
|
||||
/>
|
||||
))}
|
||||
<button onClick={add} className="btn-outlined" style={{ alignSelf: 'flex-start' }}
|
||||
title="Weitere Bedingung hinzufügen">
|
||||
<Icon name="add" size={14} />
|
||||
<span>Bedingung</span>
|
||||
</button>
|
||||
<div style={{ alignSelf: 'flex-start' }}>
|
||||
<BarToggle icon="add" label="Bedingung"
|
||||
onClick={add}
|
||||
title="Weitere Bedingung hinzufügen" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -200,14 +206,16 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
type="color"
|
||||
value={a.color || '#888888'}
|
||||
onChange={(e) => setProp('color', e.target.value)}
|
||||
style={{ width: 36, height: 26, padding: 2, flexShrink: 0 }}
|
||||
style={{ width: 32, height: BAR_H, padding: 0, flexShrink: 0,
|
||||
border: '1px solid var(--border)', borderRadius: 999,
|
||||
background: 'var(--bg-input)' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={a.color || ''}
|
||||
placeholder="#rrggbb"
|
||||
onChange={(e) => setProp('color', e.target.value)}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, minWidth: 0, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
</ActionRow>
|
||||
|
||||
@@ -220,7 +228,7 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
type="number" step={0.05} min={0}
|
||||
value={a.lineweight ?? ''}
|
||||
onChange={(e) => setProp('lineweight', parseFloat(e.target.value) || 0)}
|
||||
style={{ width: 80 }}
|
||||
style={{ ...pillInput, width: 80, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>mm</span>
|
||||
</ActionRow>
|
||||
@@ -230,14 +238,12 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
active={'linetype' in a}
|
||||
onToggle={(e) => setProp('linetype', e.target.checked ? (a.linetype || 'Continuous') : '')}
|
||||
>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={a.linetype || ''}
|
||||
onChange={(e) => setProp('linetype', e.target.value)}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => setProp('linetype', v)}>
|
||||
<option value="">—</option>
|
||||
{(linetypes || []).map(lt => <option key={lt} value={lt}>{lt}</option>)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</ActionRow>
|
||||
|
||||
<ActionRow
|
||||
@@ -245,14 +251,12 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
active={'hatchPattern' in a}
|
||||
onToggle={(e) => setProp('hatchPattern', e.target.checked ? (a.hatchPattern || 'Solid') : '')}
|
||||
>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={a.hatchPattern || ''}
|
||||
onChange={(e) => setProp('hatchPattern', e.target.value)}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => setProp('hatchPattern', v)}>
|
||||
<option value="">—</option>
|
||||
{(hatchPatterns || []).map(hp => <option key={hp} value={hp}>{hp}</option>)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</ActionRow>
|
||||
|
||||
<ActionRow
|
||||
@@ -264,7 +268,7 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
type="number" step={0.1} min={0.001}
|
||||
value={a.hatchScale ?? ''}
|
||||
onChange={(e) => setProp('hatchScale', parseFloat(e.target.value) || 1.0)}
|
||||
style={{ width: 80 }}
|
||||
style={{ ...pillInput, width: 80, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
</ActionRow>
|
||||
|
||||
@@ -322,12 +326,11 @@ function RuleCard({ rule, index, total, layers, linetypes, hatchPatterns, onPatc
|
||||
value={rule.name || ''}
|
||||
placeholder="Regel-Name"
|
||||
onChange={(e) => onPatch({ ...rule, name: e.target.value })}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, minWidth: 0 }}
|
||||
/>
|
||||
<button onClick={() => setOpen(!open)} className="btn-icon"
|
||||
title={open ? 'Einklappen' : 'Bearbeiten'}>
|
||||
<Icon name={open ? 'expand_less' : 'edit'} size={14} />
|
||||
</button>
|
||||
<BarButton icon={open ? 'expand_less' : 'edit'}
|
||||
onClick={() => setOpen(!open)}
|
||||
title={open ? 'Einklappen' : 'Bearbeiten'} />
|
||||
</div>
|
||||
|
||||
{!open && (
|
||||
@@ -339,23 +342,22 @@ function RuleCard({ rule, index, total, layers, linetypes, hatchPatterns, onPatc
|
||||
|
||||
{open && (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 2, marginTop: 8 }}>
|
||||
<button onClick={() => onMoveUp()} disabled={index === 0}
|
||||
className="btn-icon-sm" title="Prio höher (nach oben)">
|
||||
<Icon name="arrow_upward" size={14} />
|
||||
</button>
|
||||
<button onClick={() => onMoveDown()} disabled={index === total - 1}
|
||||
className="btn-icon-sm" title="Prio tiefer (nach unten)">
|
||||
<Icon name="arrow_downward" size={14} />
|
||||
</button>
|
||||
<button onClick={() => onDuplicate()} className="btn-icon-sm" title="Duplizieren">
|
||||
<Icon name="content_copy" size={14} />
|
||||
</button>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 8 }}>
|
||||
<BarButton icon="arrow_upward"
|
||||
onClick={() => onMoveUp()}
|
||||
disabled={index === 0}
|
||||
title="Prio höher (nach oben)" />
|
||||
<BarButton icon="arrow_downward"
|
||||
onClick={() => onMoveDown()}
|
||||
disabled={index === total - 1}
|
||||
title="Prio tiefer (nach unten)" />
|
||||
<BarButton icon="content_copy"
|
||||
onClick={() => onDuplicate()}
|
||||
title="Duplizieren" />
|
||||
<div style={{ flex: 1 }} />
|
||||
<button onClick={() => { if (confirm(`Regel "${rule.name}" löschen?`)) onDelete() }}
|
||||
className="btn-icon-danger" title="Löschen">
|
||||
<Icon name="delete" size={14} />
|
||||
</button>
|
||||
<BarButton icon="delete"
|
||||
onClick={() => { if (confirm(`Regel "${rule.name}" löschen?`)) onDelete() }}
|
||||
title="Löschen" />
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
@@ -476,10 +478,9 @@ export default function OverridesApp() {
|
||||
borderRadius: 'var(--r-lg)',
|
||||
}}>
|
||||
<span style={labelXs}>Override-Kombinationen</span>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={selectedPreset}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
onChange={(v) => {
|
||||
setSelectedPreset(v)
|
||||
if (v) {
|
||||
loadPreset(v, 'replace')
|
||||
@@ -487,40 +488,36 @@ export default function OverridesApp() {
|
||||
clearOverrideRules()
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
title="Kombination zum Bearbeiten oeffnen"
|
||||
>
|
||||
title="Kombination zum Bearbeiten oeffnen">
|
||||
<option value="">— neu / keine —</option>
|
||||
{(state.presets || []).map(p => (
|
||||
<option key={p.name} value={p.name}>
|
||||
{p.name} ({p.ruleCount})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (selectedPreset) { savePreset(selectedPreset); return }
|
||||
const existing = (state.presets || []).map(p => p.name)
|
||||
const def = `Kombination ${existing.length + 1}`
|
||||
const name = window.prompt('Name für neue Kombination:', def)
|
||||
if (!name || !name.trim()) return
|
||||
const t = name.trim()
|
||||
if (existing.includes(t) && !window.confirm(`Kombination "${t}" überschreiben?`)) return
|
||||
savePreset(t)
|
||||
setSelectedPreset(t)
|
||||
}}
|
||||
disabled={state.rules.length === 0}
|
||||
className="btn-outlined"
|
||||
style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
||||
title={selectedPreset
|
||||
? `Änderungen in "${selectedPreset}" speichern`
|
||||
: 'Aktuelle Regeln als neue Kombination speichern'}
|
||||
>
|
||||
<Icon name="save" size={14} />
|
||||
<span>{selectedPreset ? 'Speichern' : 'Als Kombination speichern…'}</span>
|
||||
</button>
|
||||
<button
|
||||
<div style={{ flex: 1 }}>
|
||||
<BarToggle icon="save"
|
||||
label={selectedPreset ? 'Speichern' : 'Als Kombination speichern…'}
|
||||
active={state.rules.length > 0}
|
||||
disabled={state.rules.length === 0}
|
||||
onClick={() => {
|
||||
if (selectedPreset) { savePreset(selectedPreset); return }
|
||||
const existing = (state.presets || []).map(p => p.name)
|
||||
const def = `Kombination ${existing.length + 1}`
|
||||
const name = window.prompt('Name für neue Kombination:', def)
|
||||
if (!name || !name.trim()) return
|
||||
const t = name.trim()
|
||||
if (existing.includes(t) && !window.confirm(`Kombination "${t}" überschreiben?`)) return
|
||||
savePreset(t)
|
||||
setSelectedPreset(t)
|
||||
}}
|
||||
title={selectedPreset
|
||||
? `Änderungen in "${selectedPreset}" speichern`
|
||||
: 'Aktuelle Regeln als neue Kombination speichern'} />
|
||||
</div>
|
||||
<BarButton icon="delete"
|
||||
onClick={() => {
|
||||
if (!selectedPreset) return
|
||||
if (!window.confirm(`Kombination "${selectedPreset}" dauerhaft loeschen?`)) return
|
||||
@@ -528,11 +525,7 @@ export default function OverridesApp() {
|
||||
setSelectedPreset('')
|
||||
}}
|
||||
disabled={!selectedPreset}
|
||||
className="btn-icon-danger"
|
||||
title="Gewaehlte Kombination dauerhaft loeschen"
|
||||
>
|
||||
<Icon name="delete" size={14} />
|
||||
</button>
|
||||
title="Gewaehlte Kombination dauerhaft loeschen" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -551,10 +544,9 @@ export default function OverridesApp() {
|
||||
>
|
||||
<Icon name="add" size={16} />
|
||||
</button>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={selectedTemplate}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
onChange={(v) => {
|
||||
if (!v) { setSelectedTemplate(''); return }
|
||||
if (v === '__delete__') {
|
||||
if (!selectedTemplate) return
|
||||
@@ -566,9 +558,7 @@ export default function OverridesApp() {
|
||||
addFromTemplate(v)
|
||||
setSelectedTemplate(v)
|
||||
}}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
title="Regel aus Vorlage einfuegen"
|
||||
>
|
||||
title="Regel aus Vorlage einfuegen">
|
||||
<option value="">+ Aus Vorlage…</option>
|
||||
{(state.ruleTemplates || []).map(t => (
|
||||
<option key={t.name} value={t.name}>{t.name}</option>
|
||||
@@ -579,7 +569,7 @@ export default function OverridesApp() {
|
||||
<option value="__delete__">🗑 "{selectedTemplate}" loeschen</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic', lineHeight: 1.4 }}>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import ProjectSettingsDialog from './components/ProjectSettingsDialog'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function bridgeSend(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||
const json = JSON.stringify({ type, payload })
|
||||
document.title = 'RHINOMSG::' + json
|
||||
}
|
||||
import { notifyReady, send as bridgeSend } from './lib/rhinoBridge'
|
||||
|
||||
export default function ProjectSettingsApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
|
||||
+35
-30
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BAR_H } from './components/BarControls'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
@@ -7,6 +10,18 @@ function send(type, payload = {}) {
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
function Field({ label, hint, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}>
|
||||
@@ -35,14 +50,10 @@ function Radio({ value, options, onChange }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{options.map(o => (
|
||||
<button key={o.value}
|
||||
<BarToggle key={o.value} label={o.label}
|
||||
active={value === o.value}
|
||||
onClick={() => onChange(o.value)}
|
||||
className={value === o.value ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 10px', fontSize: 10 }}
|
||||
title={o.hint || ''}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
title={o.hint || ''} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -198,16 +209,11 @@ export default function SwisstopoApp() {
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }}
|
||||
placeholder="Adresse oder Ortsname"
|
||||
style={{ flex: 1, fontSize: 11, padding: '5px 8px' }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn-outlined"
|
||||
<BarToggle label={searching ? '…' : 'Suchen'}
|
||||
onClick={handleSearch}
|
||||
disabled={searching || !searchText.trim()}
|
||||
style={{ padding: '4px 10px', fontSize: 11 }}
|
||||
>
|
||||
{searching ? '…' : 'Suchen'}
|
||||
</button>
|
||||
disabled={searching || !searchText.trim()} />
|
||||
</Field>
|
||||
|
||||
<Field label="ODER LV95-KOORDS (E / N)"
|
||||
@@ -215,23 +221,23 @@ export default function SwisstopoApp() {
|
||||
<input
|
||||
placeholder="E"
|
||||
onChange={(e) => handleManualCoords(e.target.value, center?.n || '')}
|
||||
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }}
|
||||
style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)' }}>/</span>
|
||||
<input
|
||||
placeholder="N"
|
||||
onChange={(e) => handleManualCoords(center?.e || '', e.target.value)}
|
||||
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }}
|
||||
style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{center && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 10px',
|
||||
padding: '8px 12px',
|
||||
background: 'var(--accent-dim)',
|
||||
border: '1px solid var(--accent-border)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 999,
|
||||
marginTop: 4,
|
||||
}}>
|
||||
<Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} />
|
||||
@@ -395,7 +401,7 @@ export default function SwisstopoApp() {
|
||||
<input type="text"
|
||||
value={terrainVolumeDepth}
|
||||
onChange={(e) => setTerrainVolumeDepth(e.target.value)}
|
||||
style={{ width: 60, textAlign: 'right' }} />
|
||||
style={{ ...pillInput, width: 60, textAlign: 'right' }} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
m unter tiefstem Punkt
|
||||
</span>
|
||||
@@ -451,7 +457,7 @@ export default function SwisstopoApp() {
|
||||
fontSize: 10, fontFamily: 'DM Mono, monospace',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
padding: 8,
|
||||
maxHeight: 140,
|
||||
overflowY: 'auto',
|
||||
@@ -474,15 +480,14 @@ export default function SwisstopoApp() {
|
||||
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
{center ? `Tiles werden im Projekt-Ordner neben der .3dm gecacht (Fallback: ~/Library/Caches/Dossier/swisstopo/ wenn ungespeichert)` : 'Wähle zuerst einen Standort'}
|
||||
</div>
|
||||
<button className="btn-text" onClick={() => send('CANCEL', {})}
|
||||
disabled={running}>
|
||||
{done ? 'Schliessen' : 'Abbrechen'}
|
||||
</button>
|
||||
<button className="btn-contained" onClick={handleImport}
|
||||
disabled={!center || running}>
|
||||
<Icon name="download" size={13} />
|
||||
<span>{running ? 'Importiere…' : 'Importieren'}</span>
|
||||
</button>
|
||||
<BarToggle label={done ? 'Schliessen' : 'Abbrechen'}
|
||||
onClick={() => send('CANCEL', {})}
|
||||
disabled={running} />
|
||||
<BarToggle icon="download"
|
||||
label={running ? 'Importiere…' : 'Importieren'}
|
||||
active
|
||||
onClick={handleImport}
|
||||
disabled={!center || running} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import SymbolPicker from './components/SymbolPicker'
|
||||
import { notifyReady, onMessage, send } from './lib/rhinoBridge'
|
||||
|
||||
+5
-13
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle } from './components/BarControls'
|
||||
import { onMessage, notifyReady, send } from './lib/rhinoBridge'
|
||||
|
||||
const SYMBOL_GROUPS = [
|
||||
@@ -719,19 +722,8 @@ export default function TextEditorApp() {
|
||||
|
||||
{/* Bottom Buttons */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
|
||||
<Pill onClick={onCancel}>Abbrechen</Pill>
|
||||
<button onClick={onCommit}
|
||||
style={{
|
||||
height: BAR_H + 2, padding: '0 14px',
|
||||
background: 'var(--accent)',
|
||||
color: 'var(--bg-panel)',
|
||||
border: '1px solid var(--accent)',
|
||||
borderRadius: 999,
|
||||
fontSize: 11, fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
appearance: 'none', WebkitAppearance: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}}>Einfügen</button>
|
||||
<BarToggle label="Abbrechen" onClick={onCancel} />
|
||||
<BarToggle label="Einfügen" active onClick={onCommit} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+43
-23
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { notifyReady, runRhinoCommand } from './lib/rhinoBridge'
|
||||
@@ -52,34 +54,53 @@ const TOOLS = {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ToolButton({ icon, label, cmd, tip }) {
|
||||
function ToolPill({ icon, label, cmd, tip }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => runRhinoCommand(cmd)}
|
||||
title={`${tip} (${cmd})`}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--accent)'
|
||||
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
||||
e.currentTarget.style.background = 'var(--bg-input)'
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
background: 'var(--bg-item)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', color: 'var(--text-primary)',
|
||||
cursor: 'pointer', textAlign: 'left',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '5px 10px 5px 8px',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s, border-color 0.1s',
|
||||
fontSize: 11, fontWeight: 500,
|
||||
color: 'var(--text-primary)',
|
||||
whiteSpace: 'nowrap',
|
||||
appearance: 'none', WebkitAppearance: 'none',
|
||||
}}
|
||||
>
|
||||
<Icon name={icon} size={16} style={{ flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 500 }}>{label}</span>
|
||||
<Icon name={icon} size={14} style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupLabel({ children }) {
|
||||
function PillGroup({ label, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
padding: '8px 4px 4px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
}}>{children}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
<span style={{
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.08em', textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{label}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,20 +114,19 @@ export default function WerkzeugeApp() {
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%', height: '100%',
|
||||
display: 'flex', flexDirection: 'column', gap: 0,
|
||||
padding: 6,
|
||||
display: 'flex', flexDirection: 'column', gap: 10,
|
||||
padding: 10,
|
||||
fontFamily: 'var(--font)', color: 'var(--text-primary)',
|
||||
background: 'var(--bg-base)',
|
||||
boxSizing: 'border-box',
|
||||
overflowY: 'auto', overflowX: 'hidden',
|
||||
}}>
|
||||
{groups.map(([title, items], gi) => (
|
||||
<div key={title} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<GroupLabel>{title}</GroupLabel>
|
||||
{groups.map(([title, items]) => (
|
||||
<PillGroup key={title} label={title}>
|
||||
{items.map(([icon, label, cmd, tip]) => (
|
||||
<ToolButton key={cmd} icon={icon} label={label} cmd={cmd} tip={tip} />
|
||||
<ToolPill key={cmd} icon={icon} label={label} cmd={cmd} tip={tip} />
|
||||
))}
|
||||
</div>
|
||||
</PillGroup>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import GeschossManager from './components/GeschossManager'
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BarCombo, BAR_H } from './BarControls'
|
||||
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
// Erzeugt ein vollstaendiges Draft-Array fuer einen Preset.
|
||||
// Layer die im Preset nicht enthalten sind werden auf Default (visible=true,
|
||||
@@ -31,8 +46,6 @@ export default function AusschnittLayerDialog({
|
||||
const [filter, setFilter] = useState('')
|
||||
const [newName, setNewName] = useState('')
|
||||
|
||||
// Wenn die Layer-Liste (von Backend) sich aendert wegen Doc-Update,
|
||||
// resetten wir den Draft — aber nur wenn nicht dirty.
|
||||
useEffect(() => {
|
||||
if (dirty) return
|
||||
if (selectedPreset === null) {
|
||||
@@ -133,17 +146,17 @@ export default function AusschnittLayerDialog({
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="layers" size={16} style={{ color: 'var(--text-secondary)' }} />
|
||||
<Icon name="layers" size={16} style={{ color: 'var(--accent)' }} />
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
|
||||
{snapName}
|
||||
</span>
|
||||
{dirty && (
|
||||
<span style={{ fontSize: 10, color: 'var(--warn)',
|
||||
padding: '2px 6px', borderRadius: 'var(--r)',
|
||||
padding: '2px 6px', borderRadius: 999,
|
||||
background: 'var(--warn-dim)' }}
|
||||
title="Ungespeicherte Änderungen">●</span>
|
||||
)}
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
|
||||
<BarButton icon="close" onClick={onClose} title="Schliessen" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -156,11 +169,9 @@ export default function AusschnittLayerDialog({
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span className="label-xs" style={{ flexShrink: 0 }}>Kombination</span>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={selectedPreset || ''}
|
||||
onChange={(ev) => pickPreset(ev.target.value || null)}
|
||||
style={{ flex: 1, fontSize: 11 }}
|
||||
>
|
||||
onChange={(v) => pickPreset(v || null)}>
|
||||
<option value="">— Aktueller Zustand —</option>
|
||||
{presets.length > 0 && <option disabled>──────────</option>}
|
||||
{presets.map(p => (
|
||||
@@ -168,31 +179,20 @@ export default function AusschnittLayerDialog({
|
||||
{p.name} ({p.layers?.length || 0} Ebenen)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
{selectedPreset && (
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
<BarButton icon="delete"
|
||||
onClick={deleteSelected}
|
||||
title="Diese Kombination löschen"
|
||||
style={{ color: 'var(--danger)' }}
|
||||
>
|
||||
<Icon name="delete" size={13} />
|
||||
</button>
|
||||
title="Diese Kombination löschen" />
|
||||
)}
|
||||
</div>
|
||||
{selectedPreset && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<button
|
||||
className="btn-contained"
|
||||
onClick={savePresetChanges}
|
||||
<BarToggle icon="save" label="Speichern"
|
||||
active={dirty}
|
||||
disabled={!dirty}
|
||||
style={{ fontSize: 10, padding: '3px 10px',
|
||||
opacity: dirty ? 1 : 0.5 }}
|
||||
title={dirty ? `Änderungen in "${selectedPreset}" speichern` : 'Keine Änderungen'}
|
||||
>
|
||||
<Icon name="save" size={12} />
|
||||
<span>Speichern</span>
|
||||
</button>
|
||||
onClick={savePresetChanges}
|
||||
title={dirty ? `Änderungen in "${selectedPreset}" speichern` : 'Keine Änderungen'} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
Änderungen werden NICHT automatisch gespeichert.
|
||||
</span>
|
||||
@@ -204,17 +204,12 @@ export default function AusschnittLayerDialog({
|
||||
onChange={(ev) => setNewName(ev.target.value)}
|
||||
onKeyDown={(ev) => { if (ev.key === 'Enter') saveAsNew() }}
|
||||
placeholder="Aktuelle Auswahl als neue Kombination speichern…"
|
||||
style={{ flex: 1, fontSize: 10 }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn-outlined"
|
||||
<BarToggle icon="add" label="Neu"
|
||||
onClick={saveAsNew}
|
||||
disabled={!newName.trim()}
|
||||
title="Aktuelle Auswahl unter diesem Namen speichern"
|
||||
style={{ fontSize: 10, padding: '3px 8px' }}
|
||||
>
|
||||
<Icon name="add" size={12} /> Neu
|
||||
</button>
|
||||
title="Aktuelle Auswahl unter diesem Namen speichern" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -228,17 +223,17 @@ export default function AusschnittLayerDialog({
|
||||
value={filter}
|
||||
onChange={(ev) => setFilter(ev.target.value)}
|
||||
placeholder="Filter..."
|
||||
style={{ flex: 1, fontSize: 10, padding: '3px 6px' }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button className="btn-icon-xs" onClick={() => setAll('visible', true)} title="Alle (gefiltert) sichtbar">
|
||||
<Icon name="visibility" size={12} />
|
||||
</button>
|
||||
<button className="btn-icon-xs" onClick={() => setAll('visible', false)} title="Alle (gefiltert) ausblenden">
|
||||
<Icon name="visibility_off" size={12} />
|
||||
</button>
|
||||
<button className="btn-icon-xs" onClick={() => setAll('locked', false)} title="Alle (gefiltert) entsperren">
|
||||
<Icon name="lock_open" size={12} />
|
||||
</button>
|
||||
<BarButton icon="visibility"
|
||||
onClick={() => setAll('visible', true)}
|
||||
title="Alle (gefiltert) sichtbar" />
|
||||
<BarButton icon="visibility_off"
|
||||
onClick={() => setAll('visible', false)}
|
||||
title="Alle (gefiltert) ausblenden" />
|
||||
<BarButton icon="lock_open"
|
||||
onClick={() => setAll('locked', false)}
|
||||
title="Alle (gefiltert) entsperren" />
|
||||
</div>
|
||||
|
||||
{/* Layer-Liste */}
|
||||
@@ -254,7 +249,7 @@ export default function AusschnittLayerDialog({
|
||||
key={l.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '4px 14px',
|
||||
padding: '3px 14px',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
background: 'var(--bg-item)',
|
||||
opacity: l.visible ? 1 : 0.5,
|
||||
@@ -272,16 +267,16 @@ export default function AusschnittLayerDialog({
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
}} title={l.fullPath}>{l.fullPath || l.name}</span>
|
||||
<button
|
||||
className={`btn-icon-xs ${l.visible ? 'is-on' : ''}`}
|
||||
<BarButton
|
||||
icon={l.visible ? 'visibility' : 'visibility_off'}
|
||||
active={l.visible}
|
||||
onClick={() => toggle(l.id, 'visible')}
|
||||
title={l.visible ? 'Ausblenden' : 'Einblenden'}
|
||||
><Icon name={l.visible ? 'visibility' : 'visibility_off'} size={12} /></button>
|
||||
<button
|
||||
className={`btn-icon-xs ${l.locked ? 'is-on' : ''}`}
|
||||
title={l.visible ? 'Ausblenden' : 'Einblenden'} />
|
||||
<BarButton
|
||||
icon={l.locked ? 'lock' : 'lock_open'}
|
||||
active={l.locked}
|
||||
onClick={() => toggle(l.id, 'locked')}
|
||||
title={l.locked ? 'Entsperren' : 'Sperren'}
|
||||
><Icon name={l.locked ? 'lock' : 'lock_open'} size={12} /></button>
|
||||
title={l.locked ? 'Entsperren' : 'Sperren'} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -297,12 +292,11 @@ export default function AusschnittLayerDialog({
|
||||
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
{draft.filter(l => l.visible).length} / {draft.length} sichtbar
|
||||
</div>
|
||||
<button className="btn-text" onClick={onClose}>Schliessen</button>
|
||||
<button className="btn-contained" onClick={applyToDoc}
|
||||
title="Aktuelle Auswahl auf das Dokument anwenden">
|
||||
<Icon name="check" size={12} />
|
||||
<span>Auf Doc anwenden</span>
|
||||
</button>
|
||||
<BarToggle label="Schliessen" onClick={onClose} />
|
||||
<BarToggle icon="check" label="Auf Doc anwenden"
|
||||
active
|
||||
onClick={applyToDoc}
|
||||
title="Aktuelle Auswahl auf das Dokument anwenden" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import Icon from './Icon'
|
||||
|
||||
// Gemeinsame Toolbar-Primitiven für Panels im Oberleiste-Stil:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
export default function BottomBar({ onApply, dirty }) {
|
||||
return (
|
||||
<div style={{
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarCombo } from './BarControls'
|
||||
|
||||
export default function ConfirmDeleteEbene({ ebene, otherEbenen, onConfirm, onCancel }) {
|
||||
const [target, setTarget] = useState(otherEbenen[0]?.code ?? '_delete')
|
||||
@@ -42,28 +45,28 @@ export default function ConfirmDeleteEbene({ ebene, otherEbenen, onConfirm, onCa
|
||||
|
||||
<div style={{ padding: '10px 18px 14px', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<span className="label-xs">Inhalte auf der Ebene</span>
|
||||
<select value={target} onChange={ev => setTarget(ev.target.value)}>
|
||||
<BarCombo stretch
|
||||
value={target}
|
||||
onChange={(v) => setTarget(v)}>
|
||||
{otherEbenen.map(e => (
|
||||
<option key={e.code} value={e.code}>→ Verschieben nach {e.code}_{e.name}</option>
|
||||
))}
|
||||
<option value="_delete">⚠ Inhalte ebenfalls löschen</option>
|
||||
</select>
|
||||
</BarCombo>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex', gap: 4, padding: '10px 14px',
|
||||
display: 'flex', gap: 6, padding: '10px 14px',
|
||||
justifyContent: 'flex-end',
|
||||
borderTop: '1px solid var(--border-light)',
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<button className="btn-text" onClick={onCancel}>Abbrechen</button>
|
||||
<button
|
||||
className="btn-contained"
|
||||
style={isDelete ? { background: 'var(--danger)' } : undefined}
|
||||
<BarToggle label="Abbrechen" onClick={onCancel} />
|
||||
<BarToggle
|
||||
label="Löschen"
|
||||
active
|
||||
onClick={() => onConfirm(isDelete ? null : target)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useRef, useMemo, useEffect } from 'react'
|
||||
import Icon from './Icon'
|
||||
import ConfirmDeleteEbene from './ConfirmDeleteEbene'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarCombo } from './BarControls'
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, onClose, embedded = false }) {
|
||||
const [draft, setDraft] = useState(zeichnungsebenen.map(z => ({ ...z })))
|
||||
@@ -46,14 +49,16 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
.filter(z => z.isGeschoss)
|
||||
.reduce((s, z) => s + (z.hoehe ?? 0), 0)
|
||||
|
||||
// move-Spalte muss 2 BarButtons (BAR_H breit) + gap aufnehmen — sonst
|
||||
// ueberlappt der rechte Pfeil mit dem G-Haken in der Nachbarspalte.
|
||||
const col = {
|
||||
move: { width: 28, flexShrink: 0 },
|
||||
move: { width: BAR_H * 2 + 6, flexShrink: 0 },
|
||||
geschoss:{ width: 24, flexShrink: 0 },
|
||||
name: { flex: 1, minWidth: 60 },
|
||||
okff: { width: 50, flexShrink: 0 },
|
||||
hoehe: { width: 64, flexShrink: 0 },
|
||||
schnitt: { width: 64, flexShrink: 0 },
|
||||
del: { width: 22, flexShrink: 0 },
|
||||
del: { width: BAR_H, flexShrink: 0 },
|
||||
}
|
||||
|
||||
const wrapperStyle = embedded ? {
|
||||
@@ -81,20 +86,34 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
maxHeight: 'calc(100vh - 80px)',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
const numberInputStyle = {
|
||||
width: 44, height: BAR_H, textAlign: 'right',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 8px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<div style={innerStyle}>
|
||||
{/* Toolbar — Add-Buttons + Bau-Gesamthoehe. Kein Title-Header mehr;
|
||||
das Satelliten-Fenster bringt seinen eigenen mit. */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0,
|
||||
}}>
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 12, color: 'var(--text-primary)' }}>
|
||||
Zeichnungsebenen
|
||||
</span>
|
||||
<BarToggle icon="add" label="Geschoss" onClick={() => add(true)} />
|
||||
<BarToggle icon="add" label="Zeichnung" onClick={() => add(false)} />
|
||||
<div style={{ flex: 1 }} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
Gebäude {gesamthoehe.toFixed(2)} m
|
||||
</span>
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
@@ -119,13 +138,15 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
background: i % 2 === 0 ? 'var(--bg-item)' : 'var(--bg-dialog)',
|
||||
}}>
|
||||
<div style={{ ...col.move, display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<button className="btn-step" onClick={() => move(i, -1)} disabled={i === 0}>
|
||||
<Icon name="arrow_drop_up" size={14} />
|
||||
</button>
|
||||
<button className="btn-step" onClick={() => move(i, 1)} disabled={i === draft.length - 1}>
|
||||
<Icon name="arrow_drop_down" size={14} />
|
||||
</button>
|
||||
<div style={{ ...col.move, display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
<BarButton icon="arrow_drop_up"
|
||||
onClick={() => move(i, -1)}
|
||||
disabled={i === 0}
|
||||
title="Nach oben" />
|
||||
<BarButton icon="arrow_drop_down"
|
||||
onClick={() => move(i, 1)}
|
||||
disabled={i === draft.length - 1}
|
||||
title="Nach unten" />
|
||||
</div>
|
||||
|
||||
<div style={col.geschoss}>
|
||||
@@ -141,7 +162,19 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
<input
|
||||
value={z.name}
|
||||
onChange={ev => update(i, 'name', ev.target.value)}
|
||||
style={{ ...col.name, fontWeight: 600, fontSize: 11 }}
|
||||
style={{
|
||||
...col.name,
|
||||
height: BAR_H,
|
||||
fontWeight: 600, fontSize: 11,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ ...col.okff, color: z.isGeschoss ? 'var(--text-muted)' : 'transparent', fontSize: 11, fontFamily: 'var(--font-mono)', textAlign: 'right' }}>
|
||||
@@ -154,7 +187,7 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
<input type="number" step="0.05" min="0.5" max="20"
|
||||
value={z.hoehe ?? 3.0}
|
||||
onChange={ev => update(i, 'hoehe', parseFloat(ev.target.value) || z.hoehe || 3.0)}
|
||||
style={{ width: 44, textAlign: 'right' }}
|
||||
style={numberInputStyle}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>m</span>
|
||||
</>
|
||||
@@ -169,7 +202,7 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
<input type="number" step="0.05" min="0.1"
|
||||
value={z.schnitthoehe ?? 1.0}
|
||||
onChange={ev => update(i, 'schnitthoehe', parseFloat(ev.target.value) || 1.0)}
|
||||
style={{ width: 44, textAlign: 'right' }}
|
||||
style={numberInputStyle}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>m</span>
|
||||
</>
|
||||
@@ -179,9 +212,10 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
</div>
|
||||
|
||||
<div style={col.del}>
|
||||
<button className="btn-icon-sm" onClick={() => remove(i)} title="Löschen">
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
<BarButton icon="close"
|
||||
onClick={() => remove(i)}
|
||||
disabled={draft.length <= 1}
|
||||
title="Löschen" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -192,13 +226,9 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
padding: '10px 14px', borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-section)', flexShrink: 0,
|
||||
}}>
|
||||
<button className="btn-outlined" onClick={() => add(true)} style={{
|
||||
color: 'var(--accent-light)', borderColor: 'var(--accent-border)',
|
||||
}}>+ Geschoss</button>
|
||||
<button className="btn-outlined" onClick={() => add(false)}>+ Zeichnung</button>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={onClose}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={() => onSave(draft)}>Übernehmen</button>
|
||||
<BarToggle label="Abbrechen" onClick={onClose} />
|
||||
<BarToggle label="Übernehmen" active onClick={() => onSave(draft)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import ContextMenu from './ContextMenu'
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
/** Vertikales Feld-Layout: Label oben, Input darunter — passt in schmale Panels. */
|
||||
function Field({ label, hint, children }) {
|
||||
@@ -35,6 +38,19 @@ function Toggle({ label, checked, onChange, hint }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Pill-Input: rounded textfield im Stil der Oberleiste
|
||||
const pillInput = {
|
||||
height: BAR_H,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embedded = false }) {
|
||||
const [draft, setDraft] = useState({ ...geschoss })
|
||||
const set = (patch) => setDraft({ ...draft, ...patch })
|
||||
@@ -95,14 +111,14 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
padding: '10px 12px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="settings" size={14} style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
|
||||
<Icon name="settings" size={14} style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
<span style={{
|
||||
flex: 1, fontWeight: 600, fontSize: 11,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{geschoss.name}
|
||||
</span>
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button>
|
||||
<BarButton icon="close" onClick={onClose} title="Schliessen" />
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
@@ -111,7 +127,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
<input
|
||||
value={draft.name}
|
||||
onChange={(ev) => set({ name: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, fontWeight: 600, minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, fontWeight: 600, minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -145,7 +161,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.5" min="0.5"
|
||||
value={depthBack}
|
||||
onChange={(ev) => set({ depthBack: parseFloat(ev.target.value) || 8.0 })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -155,7 +171,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.1"
|
||||
value={heightMin}
|
||||
onChange={(ev) => set({ heightMin: parseFloat(ev.target.value) })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="HÖHE OBEN (m)">
|
||||
@@ -163,31 +179,31 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.1"
|
||||
value={heightMax}
|
||||
onChange={(ev) => set({ heightMax: parseFloat(ev.target.value) })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field label="BLICKRICHTUNG"
|
||||
hint="Wechselt zwischen den beiden Seiten der Schnittlinie">
|
||||
<button className={dirSign >= 0 ? 'btn-contained' : 'btn-outlined'}
|
||||
onClick={() => set({ dirSign: 1 })}
|
||||
style={{ flex: 1, fontSize: 11 }}>← Seite A</button>
|
||||
<button className={dirSign < 0 ? 'btn-contained' : 'btn-outlined'}
|
||||
onClick={() => set({ dirSign: -1 })}
|
||||
style={{ flex: 1, fontSize: 11 }}>Seite B →</button>
|
||||
<BarToggle label="← Seite A"
|
||||
active={dirSign >= 0}
|
||||
onClick={() => set({ dirSign: 1 })} />
|
||||
<BarToggle label="Seite B →"
|
||||
active={dirSign < 0}
|
||||
onClick={() => set({ dirSign: -1 })} />
|
||||
</Field>
|
||||
|
||||
<Field label="PROJEKTION"
|
||||
hint={projection === 'perspective'
|
||||
? 'Schnittperspektive — perspektivische Section mit gleichem Clipping. Cutaway-Visualisierung.'
|
||||
: 'Klassischer Schnitt — Parallelprojektion, masstabsgetreu.'}>
|
||||
<button className={projection === 'parallel' ? 'btn-contained' : 'btn-outlined'}
|
||||
onClick={() => set({ projection: 'parallel' })}
|
||||
style={{ flex: 1, fontSize: 11 }}>Parallel</button>
|
||||
<button className={projection === 'perspective' ? 'btn-contained' : 'btn-outlined'}
|
||||
onClick={() => set({ projection: 'perspective' })}
|
||||
style={{ flex: 1, fontSize: 11 }}>Perspektive</button>
|
||||
<BarToggle label="Parallel"
|
||||
active={projection === 'parallel'}
|
||||
onClick={() => set({ projection: 'parallel' })} />
|
||||
<BarToggle label="Perspektive"
|
||||
active={projection === 'perspective'}
|
||||
onClick={() => set({ projection: 'perspective' })} />
|
||||
</Field>
|
||||
|
||||
{projection === 'perspective' && (
|
||||
@@ -197,7 +213,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.1"
|
||||
value={cameraHeight}
|
||||
onChange={(ev) => set({ cameraHeight: parseFloat(ev.target.value) })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
@@ -213,7 +229,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.05" min="0.5" max="30"
|
||||
value={hoehe}
|
||||
onChange={(ev) => set({ hoehe: parseFloat(ev.target.value) || hoehe })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -222,7 +238,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.05" min="0.1"
|
||||
value={schnitt}
|
||||
onChange={(ev) => set({ schnitthoehe: parseFloat(ev.target.value) || 1.0 })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -250,7 +266,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.01"
|
||||
value={draft.projectZeroMum ?? 0}
|
||||
onChange={(ev) => set({ projectZeroMum: parseFloat(ev.target.value) || 0 })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0,
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0,
|
||||
fontFamily: 'var(--font-mono)' }}
|
||||
/>
|
||||
</Field>
|
||||
@@ -264,8 +280,8 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={onClose}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={() => {
|
||||
<BarToggle label="Abbrechen" onClick={onClose} />
|
||||
<BarToggle label="Übernehmen" active onClick={() => {
|
||||
// Numerische Felder NIEMALS als undefined/null rausgehen lassen —
|
||||
// sonst crasht der Plugin spaeter beim float()-Cast. Defaults
|
||||
// entsprechen den Werten die das UI auch ohne User-Input zeigt.
|
||||
@@ -289,7 +305,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
}
|
||||
}
|
||||
onSave(out)
|
||||
}}>Übernehmen</button>
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
export default function Icon({ name, size = 18, fill = 0, weight = 400, style }) {
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useMemo } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
@@ -7,6 +9,8 @@ import {
|
||||
renameHatch, deleteHatch, importHatchFile,
|
||||
listLibraryItems, addLibraryFile, updateLibraryItem, deleteLibraryItem,
|
||||
saveSelectionAsLibrary,
|
||||
saveRaumStil, deleteRaumStil, duplicateRaumStil, reorderRaumStile,
|
||||
saveStempelStil, deleteStempelStil, duplicateStempelStil, reorderStempelStile,
|
||||
} from '../lib/rhinoBridge'
|
||||
|
||||
/* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */
|
||||
@@ -644,6 +648,14 @@ export default function ProjectSettingsDialog({
|
||||
const [libRoot, setLibRoot] = useState(initial.libraryRoot || '')
|
||||
const [selLib, setSelLib] = useState(null)
|
||||
const builtin = initial.builtinMaterials || []
|
||||
// Raumstile-State: kommt von initial + wird per STATE-Refresh aktualisiert
|
||||
// wenn Backend CRUD-Op fertig ist. Drag-Reorder lokal + commit beim Drop.
|
||||
const [raumStile, setRaumStile] = useState(initial.raumStempelStile || [])
|
||||
const [dragStilIdx, setDragStilIdx] = useState(null)
|
||||
// Stempel-Stile (Bilanz-Stempel-Presets) — parallele Liste
|
||||
const [stempelStile, setStempelStile] = useState(initial.stempelStile || [])
|
||||
const [dragStempelIdx, setDragStempelIdx] = useState(null)
|
||||
const fontsList = initial.fonts || []
|
||||
|
||||
// Aktuell ausgewaehltes Material aus Selection ableiten
|
||||
const selectedMat = (() => {
|
||||
@@ -680,6 +692,12 @@ export default function ProjectSettingsDialog({
|
||||
onMessage('LIBRARY_ERROR', ({ msg }) => {
|
||||
if (msg) alert(msg)
|
||||
})
|
||||
onMessage('STILE_UPDATED', ({ raumStempelStile }) => {
|
||||
if (Array.isArray(raumStempelStile)) setRaumStile(raumStempelStile)
|
||||
})
|
||||
onMessage('STEMPEL_STILE_UPDATED', ({ stempelStile: ss }) => {
|
||||
if (Array.isArray(ss)) setStempelStile(ss)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Backend-File-Picker-Antwort: aktualisiert das Slot im aktuell
|
||||
@@ -749,11 +767,13 @@ export default function ProjectSettingsDialog({
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1,
|
||||
minHeight: 0, overflow: 'hidden' }}>
|
||||
<TabBar tabs={[
|
||||
{ key: 'defaults', label: 'Voreinstellungen' },
|
||||
{ key: 'materials', label: 'Materialien' },
|
||||
{ key: 'linetypes', label: 'Linientypen' },
|
||||
{ key: 'hatches', label: 'Schraffuren' },
|
||||
{ key: 'symbols', label: 'Symbole' },
|
||||
{ key: 'defaults', label: 'Voreinstellungen' },
|
||||
{ key: 'materials', label: 'Materialien' },
|
||||
{ key: 'linetypes', label: 'Linientypen' },
|
||||
{ key: 'hatches', label: 'Schraffuren' },
|
||||
{ key: 'symbols', label: 'Symbole' },
|
||||
{ key: 'raumstile', label: 'Raumstile' },
|
||||
{ key: 'stempelstile', label: 'Stempelstile' },
|
||||
]} active={tab} onChange={setTab} />
|
||||
|
||||
{/* Body */}
|
||||
@@ -797,6 +817,32 @@ export default function ProjectSettingsDialog({
|
||||
Voreinstellungen fuer neue Elemente. Pro-Element editierte
|
||||
Werte bleiben davon unberuehrt.
|
||||
</div>
|
||||
<DetailSection title="Arbeitseinheit">
|
||||
<div style={{ padding: '5px 0',
|
||||
display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ flex: 1, fontSize: 11,
|
||||
color: 'var(--text-primary)' }}>
|
||||
Einheit
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 3 }}>
|
||||
{[
|
||||
{ code: 'meters', label: 'Meter (m)' },
|
||||
{ code: 'centimeters', label: 'cm' },
|
||||
{ code: 'millimeters', label: 'mm' },
|
||||
].map(u => (
|
||||
<BarToggle key={u.code} label={u.label}
|
||||
active={(draft.defaults.unit || 'meters') === u.code}
|
||||
onClick={() => setDefault('unit', u.code)}
|
||||
title={`Doc-Unit auf ${u.label} einstellen`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||
lineHeight: 1.4, padding: '2px 0 4px' }}>
|
||||
Wenn ein geoeffnetes Doc nicht in dieser Einheit ist, fragt
|
||||
DOSSIER ob umgestellt werden soll. Architektur-Standard: Meter.
|
||||
</div>
|
||||
</DetailSection>
|
||||
<DetailSection title="Geschoss">
|
||||
<InlineNumberField label="Standard-Geschosshöhe"
|
||||
value={draft.defaults.geschossHoehe ?? 3.0}
|
||||
@@ -1313,6 +1359,228 @@ export default function ProjectSettingsDialog({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'raumstile' && (
|
||||
<div style={{ maxWidth: 760 }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
padding: '6px 0 10px', lineHeight: 1.5 }}>
|
||||
Raumstempel-Stile (Presets). Werden via Raum-Properties
|
||||
erstellt ("+ Aktuelle Settings als Stil speichern"), hier nur
|
||||
verwaltet. Drag zum Umsortieren — die Reihenfolge entspricht
|
||||
dem Dropdown in den Raum-Properties.
|
||||
</div>
|
||||
{raumStile.length === 0 ? (
|
||||
<div style={{ padding: 30, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 11 }}>
|
||||
Noch keine Stile gespeichert. Im Raum-Properties einen Raum
|
||||
konfigurieren und "+ Aktuelle Settings als Stil speichern".
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{raumStile.map((s, idx) => {
|
||||
const isDrag = dragStilIdx === idx
|
||||
return (
|
||||
<div key={s.id}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
setDragStilIdx(idx)
|
||||
try { e.dataTransfer.effectAllowed = 'move' } catch (_) {}
|
||||
}}
|
||||
onDragOver={(e) => { e.preventDefault() }}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
if (dragStilIdx == null || dragStilIdx === idx) return
|
||||
const next = [...raumStile]
|
||||
const [moved] = next.splice(dragStilIdx, 1)
|
||||
next.splice(idx, 0, moved)
|
||||
setRaumStile(next)
|
||||
reorderRaumStile(next.map(x => x.id))
|
||||
setDragStilIdx(null)
|
||||
}}
|
||||
onDragEnd={() => setDragStilIdx(null)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 10px',
|
||||
background: isDrag ? 'var(--bg-item-active)' : 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)',
|
||||
cursor: 'grab',
|
||||
opacity: isDrag ? 0.5 : 1,
|
||||
transition: 'opacity 0.15s',
|
||||
}}>
|
||||
<Icon name="drag_indicator" size={14}
|
||||
style={{ color: 'var(--text-muted)' }} />
|
||||
<input type="text" value={s.name}
|
||||
onChange={(e) => {
|
||||
const next = raumStile.map(x =>
|
||||
x.id === s.id ? { ...x, name: e.target.value } : x)
|
||||
setRaumStile(next)
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const newName = (e.target.value || '').trim()
|
||||
if (newName && newName !== s.name) {
|
||||
// Re-save mit gleicher id → effektiv rename
|
||||
const { id, name: _n, ...rest } = s
|
||||
saveRaumStil(id, newName, rest)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
|
||||
style={{ flex: 1, fontSize: 11, height: BAR_H,
|
||||
padding: '0 10px',
|
||||
fontFamily: s.font ? `"${s.font}", monospace` : 'inherit',
|
||||
fontWeight: s.bold ? 700 : 400,
|
||||
fontStyle: s.italic ? 'italic' : 'normal' }} />
|
||||
{/* Mini-Preview: Felder die in Layout vorkommen */}
|
||||
<span style={{ fontSize: 9,
|
||||
color: 'var(--text-muted)',
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
minWidth: 100, textAlign: 'right' }}>
|
||||
{(s.layout && s.layout.length)
|
||||
? s.layout.map(row => row.join('·')).join(' / ')
|
||||
: '—'}
|
||||
</span>
|
||||
<button onClick={() => duplicateRaumStil(s.id)}
|
||||
title="Duplizieren"
|
||||
style={{
|
||||
background: 'transparent', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', cursor: 'pointer',
|
||||
padding: '4px 8px', fontSize: 10,
|
||||
color: 'var(--text-primary)',
|
||||
}}>
|
||||
<Icon name="content_copy" size={11} />
|
||||
</button>
|
||||
<button onClick={() => {
|
||||
if (window.confirm(`Stil "${s.name}" löschen?`))
|
||||
deleteRaumStil(s.id)
|
||||
}}
|
||||
title="Löschen"
|
||||
style={{
|
||||
background: 'transparent', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', cursor: 'pointer',
|
||||
padding: '4px 8px', fontSize: 10,
|
||||
color: 'var(--text-danger, #c44)',
|
||||
}}>
|
||||
<Icon name="delete" size={11} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'stempelstile' && (
|
||||
<div style={{ maxWidth: 760 }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
padding: '6px 0 10px', lineHeight: 1.5 }}>
|
||||
Stempel-Stile (Presets) für SIA-Bilanz-Stempel. Werden via
|
||||
Stempel-Properties erstellt ("+ Aktuelle Settings als Stil
|
||||
speichern"), hier nur verwaltet. Drag zum Umsortieren —
|
||||
Reihenfolge entspricht dem Dropdown in den Stempel-Properties.
|
||||
</div>
|
||||
{stempelStile.length === 0 ? (
|
||||
<div style={{ padding: 30, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 11 }}>
|
||||
Noch keine Stempel-Stile gespeichert. Im Elemente-Panel
|
||||
einen Stempel platzieren, konfigurieren und im Stempel-
|
||||
Properties "+ Aktuelle Settings als Stil speichern".
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{stempelStile.map((s, idx) => {
|
||||
const isDrag = dragStempelIdx === idx
|
||||
// Show-Flags zaehlen fuer ein Mini-Preview
|
||||
const onCount = [
|
||||
'showHnf','showNnf','showNf','showVf','showFf',
|
||||
'showNgf','showGf','showAgf','showCount','showScope',
|
||||
].filter(k => s[k] !== false).length
|
||||
return (
|
||||
<div key={s.id}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
setDragStempelIdx(idx)
|
||||
try { e.dataTransfer.effectAllowed = 'move' } catch (_) {}
|
||||
}}
|
||||
onDragOver={(e) => { e.preventDefault() }}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
if (dragStempelIdx == null || dragStempelIdx === idx) return
|
||||
const next = [...stempelStile]
|
||||
const [moved] = next.splice(dragStempelIdx, 1)
|
||||
next.splice(idx, 0, moved)
|
||||
setStempelStile(next)
|
||||
reorderStempelStile(next.map(x => x.id))
|
||||
setDragStempelIdx(null)
|
||||
}}
|
||||
onDragEnd={() => setDragStempelIdx(null)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 10px',
|
||||
background: isDrag ? 'var(--bg-item-active)' : 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)',
|
||||
cursor: 'grab',
|
||||
opacity: isDrag ? 0.5 : 1,
|
||||
transition: 'opacity 0.15s',
|
||||
}}>
|
||||
<Icon name="drag_indicator" size={14}
|
||||
style={{ color: 'var(--text-muted)' }} />
|
||||
<input type="text" value={s.name}
|
||||
onChange={(e) => {
|
||||
const next = stempelStile.map(x =>
|
||||
x.id === s.id ? { ...x, name: e.target.value } : x)
|
||||
setStempelStile(next)
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const newName = (e.target.value || '').trim()
|
||||
if (newName && newName !== s.name) {
|
||||
const { id, name: _n, ...rest } = s
|
||||
saveStempelStil(id, newName, rest)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur() }}
|
||||
style={{ flex: 1, fontSize: 11, height: BAR_H,
|
||||
padding: '0 10px',
|
||||
fontFamily: s.font ? `"${s.font}", monospace` : 'inherit',
|
||||
fontWeight: s.bold ? 700 : 400,
|
||||
fontStyle: s.italic ? 'italic' : 'normal' }} />
|
||||
<span style={{ fontSize: 9,
|
||||
color: 'var(--text-muted)',
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
minWidth: 140, textAlign: 'right' }}>
|
||||
{s.header ? `"${s.header}"` : '—'} · {onCount} Felder
|
||||
</span>
|
||||
<button onClick={() => duplicateStempelStil(s.id)}
|
||||
title="Duplizieren"
|
||||
style={{
|
||||
background: 'transparent', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', cursor: 'pointer',
|
||||
padding: '4px 8px', fontSize: 10,
|
||||
color: 'var(--text-primary)',
|
||||
}}>
|
||||
<Icon name="content_copy" size={11} />
|
||||
</button>
|
||||
<button onClick={() => {
|
||||
if (window.confirm(`Stil "${s.name}" löschen?`))
|
||||
deleteStempelStil(s.id)
|
||||
}}
|
||||
title="Löschen"
|
||||
style={{
|
||||
background: 'transparent', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', cursor: 'pointer',
|
||||
padding: '4px 8px', fontSize: 10,
|
||||
color: 'var(--text-danger, #c44)',
|
||||
}}>
|
||||
<Icon name="delete" size={11} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer — Pill-Buttons */}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useMemo } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user