AGPL-3.0 Dual-Lizenz + Pill-Stil-UI + Section-Style-Overhaul + Plan-Mode-Template

Lizenz:
- AGPL-3.0 LICENSE-File im Repo-Root (GNU Volltext)
- SPDX-Header + Copyright in allen Source-Files (Python/JSX/JS/Rust)
- license-Feld in package.json + Cargo.toml
- About-App komplett neu: Dual-Lizenz-Block (AGPL + Commercial),
  openbureau-Branding, Version-Pills, made-in-Switzerland-Footer

UI-Restyle (3 Wellen) — alle Dialoge + Satellites + Panel-Sidebars
auf gemeinsamen Pill-Stil aus BarControls (BarToggle/BarButton/BarCombo):
- Welle 1: GeschossDialog/Settings, AusschnittSettings, LayoutDialog
- Welle 2: ConfirmDeleteEbene, Kamera, MasseSettings, Osm, Swisstopo,
  TextEditor, AusschnittLayerDialog, LayerCombinations
- Welle 3: LayoutsApp, MassstabApp, WerkzeugeApp, OverridesApp,
  ZeichnungsebenenApp; Werkzeuge mit ElementeApp-PillGroup-Layout

GeschossDialog Header-Refactor: +Geschoss/+Zeichnung in Toolbar oben,
move-Pfeile-Spalte breiter (kein Overlap mit G-Haken)

Ausschnitte Rows als Pills, kein Outer-Border ums Suchfeld

Section-Style komplett neu (gestaltung.py + GestaltungApp.jsx):
- ObjectSectionAttributesSource.FromObject (richtiger Enum-Name fuer Mac)
- HatchPatternPrintColor + BoundaryPrintColor mit-setzen (Display = Print)
- BoundaryColor nur bei explizitem User-Override, sonst Rhino-Default
- background_color_hex Parameter (BackgroundFillMode=SolidColor)
- Readback aus GetCustomSectionStyle statt direkt aus Attributes
- UI: Schnittkante > Section Style > Solid-Fill mit proper SectionHead
- 'Boundary' (3D Pen) -> 'Background' weil sich's wie Section-Hintergrund verhaelt

Plan-Mode 'Dossier Plan' via Template:
- rhino/templates/dossier_plan.ini wird direkt geladen
- Fallback auf Technical-Clone + ini-Patch wenn Template fehlt
- Auto-Cleanup von Orphan-Modes vor Import (Name- oder Guid-Match)
- ClipSectionUsage=1 + TechnicalMask=15 als bekannte Soll-Werte
- Bei Template-Pfad keine ini-Patches (1:1 wie User exportiert)
- Sanity-Print listet alle registrierten Modes nach Anlegen

Bridge-Unification: 4 Settings-Apps (Ebenen/Project/Geschoss*Dialog)
benutzen jetzt chunkende send() statt eigene bridgeSend ohne Chunk-
Logik -> grosse Payloads (Hatch-Refs etc.) kommen nicht mehr truncated
bei Python an (loeste 'JSON-Fehler char 990'-Regression in Ebenen-
Settings)

Library-Imports robust: 'import library' jetzt Top-Level in elemente.py
+ rhinopanel.py (statt Lazy in Methoden) -> 'No module named library'-
Crashes weg auch wenn sys.path zwischendurch resettet wird

Tools fuer Display-Mode-Maintenance:
- _clean_display_modes.py (loescht alle Custom-Modes, Built-ins bleiben)
- _inspect_plan_mode.py / _inspect_obj_section.py / _inspect_obj_boundary.py
  (Diagnose-Skripte fuer SectionStyle-Property-Reverse-Engineering)
- _reset_rhino_settings.sh (Backup + Nuke der Rhino-Settings als
  letzte Bastion gegen korrupte Display-Modes)
This commit is contained in:
2026-05-26 17:09:18 +02:00
parent e1b63aa4e6
commit 13a5e1eb7a
100 changed files with 3147 additions and 839 deletions
+661
View File
@@ -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/>.
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import js from '@eslint/js' import js from '@eslint/js'
import globals from 'globals' import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from 'eslint-plugin-react-hooks'
+1
View File
@@ -2,6 +2,7 @@
"name": "dossier-launcher", "name": "dossier-launcher",
"private": true, "private": true,
"version": "0.6.3", "version": "0.6.3",
"license": "AGPL-3.0-or-later",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+1
View File
@@ -3,6 +3,7 @@ name = "dossier-launcher"
version = "0.6.3" version = "0.6.3"
description = "Dossier — Projekt-Launcher fuer Rhino" description = "Dossier — Projekt-Launcher fuer Rhino"
authors = ["Karim Gabriele Varano"] authors = ["Karim Gabriele Varano"]
license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
[lib] [lib]
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
fn main() { fn main() {
tauri_build::build() tauri_build::build()
} }
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
// Verhindert ein extra Konsolen-Fenster auf Windows im Release-Build. // Verhindert ein extra Konsolen-Fenster auf Windows im Release-Build.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
// Tauri 2 Konvention: main.rs ist nur Einstieg, Logik in lib.rs (fuer Mobile- // Tauri 2 Konvention: main.rs ist nur Einstieg, Logik in lib.rs (fuer Mobile-
// Unterstuetzung und damit `tauri::generate_context!` korrekt aufgeloest wird). // Unterstuetzung und damit `tauri::generate_context!` korrekt aufgeloest wird).
fn main() { fn main() {
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import React, { useEffect, useState, useMemo, useCallback } from 'react' import React, { useEffect, useState, useMemo, useCallback } from 'react'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event' import { listen } from '@tauri-apps/api/event'
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
// Material-Symbols-Outlined-style Icons als Inline-SVG. Keine Font-Loads, // Material-Symbols-Outlined-style Icons als Inline-SVG. Keine Font-Loads,
// kein Codepoint-Mapping — sauber zu themen via currentColor + stroke-width. // kein Codepoint-Mapping — sauber zu themen via currentColor + stroke-width.
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback } from "react";
import { checkForAppUpdate, installAppUpdate, skipUpdateVersion, isTauri } from "../utils/updater.js"; import { checkForAppUpdate, installAppUpdate, skipUpdateVersion, isTauri } from "../utils/updater.js";
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import React from 'react' import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import App from './App.jsx' import App from './App.jsx'
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
// Shared helpers fuer den Tauri-Updater. Verwendet vom Auto-Check Modal // Shared helpers fuer den Tauri-Updater. Verwendet vom Auto-Check Modal
// (UpdateNotifier) und dem manuellen Check in den Einstellungen. // (UpdateNotifier) und dem manuellen Check in den Einstellungen.
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
+1
View File
@@ -2,6 +2,7 @@
"name": "dossier", "name": "dossier",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"license": "AGPL-3.0-or-later",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+66
View File
@@ -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.")
+67
View File
@@ -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)
+112
View File
@@ -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)
+96
View File
@@ -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)
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
"""Hilfsscript: alle Dossier-Panel-Registrierungs-Flags clearen + Module """Hilfsscript: alle Dossier-Panel-Registrierungs-Flags clearen + Module
neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos neu laden. Nuetzlich nach Icon-/Layout-Aenderungen. ABER: Rhinos
Panel-Manager cached die Icon-Bindung pro GUID — fuer NEUE Icons hilft Panel-Manager cached die Icon-Bindung pro GUID — fuer NEUE Icons hilft
+52
View File
@@ -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"
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
about.py about.py
About-Dialog als Eto-Form + WebView. Vom DOSSIER-Logo-Klick in der About-Dialog als Eto-Form + WebView. Vom DOSSIER-Logo-Klick in der
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
ausschnitte.py ausschnitte.py
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode, AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
+2
View File
@@ -1,4 +1,6 @@
#! python 3 #! python 3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
clean.py clean.py
Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste Loescht ALLE sticky-Eintraege der DOSSIER-Panels, damit der naechste
+2
View File
@@ -1,4 +1,6 @@
#! python 3 #! python 3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
clean_layers.py clean_layers.py
Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.) Loescht Rhino-Standardlayer (Default, Layer 01-05 usw.)
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
dimensionen.py dimensionen.py
DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild. DIMENSIONEN-Panel: Object Info Palette nach Vectorworks-Vorbild.
+3
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
elemente.py elemente.py
ELEMENTE-Panel: Smart Architektur-Elemente. ELEMENTE-Panel: Smart Architektur-Elemente.
@@ -22,6 +24,7 @@ if _HERE not in sys.path:
import panel_base import panel_base
import mass_style import mass_style
import library
PANEL_GUID_STR = "5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0" PANEL_GUID_STR = "5a6b7c8d-9e0f-4a1b-c2d3-e4f5061728e0"
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
elemente_properties.py elemente_properties.py
Properties-Satellite-Window. Zeigt die Property-Forms (WallProperties, Properties-Satellite-Window. Zeigt die Property-Forms (WallProperties,
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
elemente_uebersicht.py elemente_uebersicht.py
BIM-artiger Project Browser: alle Smart-Elemente in einem Tree BIM-artiger Project Browser: alle Smart-Elemente in einem Tree
+266 -60
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
gestaltung.py gestaltung.py
GESTALTUNG-Panel: Attribute der Selektion (Farbe, Stiftdicke, Linientyp, GESTALTUNG-Panel: Attribute der Selektion (Farbe, Stiftdicke, Linientyp,
@@ -860,6 +862,11 @@ def _selection_summary(doc):
sec_patterns = set() sec_patterns = set()
sec_scales = set() sec_scales = set()
sec_rots = set() sec_rots = set()
# Boundary-Subsettings (Schnittkante)
sec_bdy_visible = set()
sec_bdy_colors = set()
sec_bdy_widths = set()
sec_bg_colors = set() # Background-Fill (None wenn FillMode != SolidColor)
# Geometry-Kind-Klassifikation: 'curve' (closed planar 2D), 'curveOpen' # Geometry-Kind-Klassifikation: 'curve' (closed planar 2D), 'curveOpen'
# (offene Kurve), '3d' (Brep/Extrusion/Mesh — Volumen mit Schnittflaeche), # (offene Kurve), '3d' (Brep/Extrusion/Mesh — Volumen mit Schnittflaeche),
# 'other'. Aggregiert ueber alle Selektions-Objekte zu kind= # 'other'. Aggregiert ueber alle Selektions-Objekte zu kind=
@@ -926,25 +933,47 @@ def _selection_summary(doc):
else: else:
geometry_kinds.add('other') geometry_kinds.add('other')
# Section-Style aus Object-Attributes lesen (Rhino 8, mit Fallbacks # Section-Style aus Object-Attributes lesen Rhino 8 Mac packt die
# fuer Property-Namen die je nach Build variieren). # Settings in ein SectionStyle-Objekt (via GetCustomSectionStyle),
# NICHT in direkte Attribute-Properties wie das alte API.
if is_3d: if is_3d:
src_attr = None src_attr = None
try: try:
src_attr = getattr(a, "SectionAttributesSource", None) src_attr = getattr(a, "SectionAttributesSource", None)
except Exception: src_attr = None except Exception: src_attr = None
src_is_object = False
if src_attr is not None: if src_attr is not None:
try: try:
src_name = str(src_attr).lower() src_name = str(src_attr).lower()
if "layer" in src_name: sec_sources.add("layer") if "object" in src_name:
elif "object" in src_name: sec_sources.add("object") sec_sources.add("object"); src_is_object = True
elif "layer" in src_name:
sec_sources.add("layer")
except Exception: pass except Exception: pass
# Hatch-Index/Scale/Rotation
hidx = None # Wenn Source=FromObject: aus dem Custom-SectionStyle lesen.
for n in ("SectionHatchIndex", "HatchPatternIndex"): # Sonst (FromLayer): vom Layer.GetCustomSectionStyle() lesen damit
if hasattr(a, n): # die UI auch im Layer-Modus den effektiven Hatch zeigt.
css = None
try: try:
v = getattr(a, n) if src_is_object and hasattr(a, "GetCustomSectionStyle"):
css = a.GetCustomSectionStyle()
if css is None:
# Fallback: Layer-SectionStyle
try:
lyr = doc.Layers[obj.Attributes.LayerIndex]
if hasattr(lyr, "GetCustomSectionStyle"):
css = lyr.GetCustomSectionStyle()
except Exception: pass
except Exception: pass
if css is not None:
# HatchIndex
hidx = None
for n in ("HatchIndex", "HatchPatternIndex"):
if hasattr(css, n):
try:
v = getattr(css, n)
if v is not None: hidx = int(v); break if v is not None: hidx = int(v); break
except Exception: pass except Exception: pass
if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count: if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count:
@@ -953,25 +982,44 @@ def _selection_summary(doc):
except Exception: pass except Exception: pass
elif hidx == -1: elif hidx == -1:
sec_enabled.add(False) sec_enabled.add(False)
for n, target in ( # Scale
(("SectionHatchScale", "HatchPatternScale"), sec_scales), for n in ("HatchScale", "HatchPatternScale"):
(("SectionHatchRotation", "HatchPatternRotation"), sec_rots), if hasattr(css, n):
): try: sec_scales.add(round(float(getattr(css, n)), 4)); break
for nn in n:
if hasattr(a, nn):
try:
v = float(getattr(a, nn))
target.add(round(v, 4)
if target is sec_scales
else round(math.degrees(v), 2))
break
except Exception: pass except Exception: pass
for n in ("SectionFillColor", "SectionHatchColor", "HatchColor"): # Rotation (rad → deg)
if hasattr(a, n): for n in ("HatchRotationRadians", "HatchRotation", "HatchAngle"):
if hasattr(css, n):
try: sec_rots.add(round(math.degrees(float(getattr(css, n))), 2)); break
except Exception: pass
# Color
for n in ("HatchPatternColor", "HatchColor", "FillColor"):
if hasattr(css, n):
try: try:
c = _color_to_hex(getattr(a, n)) c = _color_to_hex(getattr(css, n))
if c: sec_colors.add(c); break if c: sec_colors.add(c); break
except Exception: pass except Exception: pass
# Boundary-Settings auslesen
if hasattr(css, "BoundaryVisible"):
try: sec_bdy_visible.add(bool(css.BoundaryVisible))
except Exception: pass
if hasattr(css, "BoundaryColor"):
try:
c = _color_to_hex(css.BoundaryColor)
if c: sec_bdy_colors.add(c)
except Exception: pass
if hasattr(css, "BoundaryWidthScale"):
try: sec_bdy_widths.add(round(float(css.BoundaryWidthScale), 2))
except Exception: pass
# Background nur lesen wenn FillMode != Viewport (sonst transparent)
try:
mode = getattr(css, "BackgroundFillMode", None)
if mode is not None and "viewport" not in str(mode).lower():
c = _color_to_hex(css.BackgroundFillColor)
if c: sec_bg_colors.add(c)
except Exception: pass
else:
sec_enabled.add(False)
# Fuellung # Fuellung
if _is_closed_planar_curve(obj.Geometry): if _is_closed_planar_curve(obj.Geometry):
@@ -1072,6 +1120,10 @@ def _selection_summary(doc):
"sectionPattern": single(sec_patterns), "sectionPattern": single(sec_patterns),
"sectionScale": single(sec_scales), "sectionScale": single(sec_scales),
"sectionRotation": single(sec_rots), "sectionRotation": single(sec_rots),
"sectionBoundaryVisible": single(sec_bdy_visible),
"sectionBoundaryColor": single(sec_bdy_colors),
"sectionBoundaryWidthScale": single(sec_bdy_widths),
"sectionBackgroundColor": single(sec_bg_colors),
# geometryKind: 'curve' | 'curveOpen' | '3d' | 'mixed' | 'other' # geometryKind: 'curve' | 'curveOpen' | '3d' | 'mixed' | 'other'
"geometryKind": ( "geometryKind": (
'mixed' if len(geometry_kinds & {'curve', 'curveOpen', '3d'}) > 1 'mixed' if len(geometry_kinds & {'curve', 'curveOpen', '3d'}) > 1
@@ -1159,6 +1211,10 @@ class GestaltungBridge(panel_base.BaseBridge):
p.get("pattern"), p.get("pattern"),
p.get("scale"), p.get("scale"),
p.get("rotation"), p.get("rotation"),
boundary_visible=p.get("boundaryVisible", True),
boundary_width_scale=p.get("boundaryWidthScale", 1.0),
boundary_color_hex=p.get("boundaryColor"),
background_color_hex=p.get("backgroundColor"),
) )
def _send_selection(self): def _send_selection(self):
@@ -1478,71 +1534,221 @@ class GestaltungBridge(panel_base.BaseBridge):
# ---- SectionStyle (per-Object, Rhino 8) ------------------------------- # ---- SectionStyle (per-Object, Rhino 8) -------------------------------
def _set_section_style(self, enabled, source, color_hex, def _set_section_style(self, enabled, source, color_hex,
pattern_name=None, scale=None, rotation_deg=None): pattern_name=None, scale=None, rotation_deg=None,
"""Setzt Per-Object SectionStyle-Properties auf die selektierten boundary_visible=True, boundary_width_scale=1.0,
3D-Objekte. Rhino 8 expone diese Properties auf ObjectAttributes boundary_color_hex=None,
unter teils variierenden Namen — wir versuchen die bekannten.""" background_color_hex=None):
"""Setzt einen Per-Object SectionStyle ueber die Rhino-8 API
(analog zu Layer.SetCustomSectionStyle). source='layer' entfernt
den Custom-Style → Layer-Default greift. source='object' setzt
einen frischen SectionStyle pro Objekt."""
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
objs = list(doc.Objects.GetSelectedObjects(False, False)) objs = list(doc.Objects.GetSelectedObjects(False, False))
is_layer_source = (source == "layer") is_layer_source = (source == "layer")
print("[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 # Hatch-Pattern-Index ermitteln
pat_idx = -1 pat_idx = -1
if pattern_name and pattern_name not in ("None", ""): if pattern_name and pattern_name not in ("None", ""):
try: pat_idx = doc.HatchPatterns.Find(pattern_name, True) try: pat_idx = doc.HatchPatterns.Find(pattern_name, True)
except Exception: pat_idx = -1 except Exception: pat_idx = -1
if pat_idx < 0 and pattern_name not in ("None", ""):
try: pat_idx = doc.HatchPatterns.Find("Solid", True)
except Exception: pat_idx = -1
col = _hex_to_color(color_hex) if color_hex else None col = _hex_to_color(color_hex) if color_hex else None
scale_v = float(scale) if scale is not None else 1.0 scale_v = float(scale) if scale is not None else 1.0
rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0 rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0
def _try_set_attr(a, names, value): def _try_set(target, names, value):
for n in names: for n in names:
if hasattr(a, n): if hasattr(target, n):
try: try:
setattr(a, n, value) setattr(target, n, value)
return n return n
except Exception: pass except Exception: pass
return None return None
def _apply_custom(obj, style):
"""Setzt Custom-SectionStyle + schaltet SectionAttributesSource
auf FromObject. Beides muss persistiert sein damit Rhino den
Custom-Style auch tatsaechlich rendert."""
try:
a = obj.Attributes.Duplicate()
if hasattr(a, "SetCustomSectionStyle"):
a.SetCustomSectionStyle(style)
# KRITISCH: Source auf FromObject — ohne das ignoriert Rhino
# den Custom-Style und nutzt weiter den Layer-Style.
if SAS is not None and hasattr(a, "SectionAttributesSource"):
try:
a.SectionAttributesSource = SAS.FromObject
except Exception as ex:
print("[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 n_ok = 0
for obj in objs: for obj in objs:
geom = obj.Geometry geom = obj.Geometry
if not isinstance(geom, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD)): if not isinstance(geom, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD)):
continue continue
a = obj.Attributes.Duplicate()
# Source: FromLayer vs FromObject — verschiedene Enum-Namen
if is_layer_source: if is_layer_source:
# Versuche SectionAttributesSource auf FromLayer # Custom entfernen → Layer-SectionStyle wird wirksam
_try_set_attr(a, ("SectionAttributesSource",), via = _remove_custom(obj)
Rhino.DocObjects.SectionAttributesSource.FromLayer print("[GESTALTUNG] obj {}: remove custom via {}".format(
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 0) str(obj.Id)[:8], via))
else: if via: n_ok += 1
_try_set_attr(a, ("SectionAttributesSource",), continue
Rhino.DocObjects.SectionAttributesSource.FromObject # Default-Farbe = Layer-Farbe wenn der User keine Override-Farbe
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 1) # gewaehlt hat. Section-Style hat keine "ByLayer"-Source-Option,
# also setzen wir die echte Layer-Farbe explizit auf den Style.
if not enabled or pattern_name == "None": obj_col = col
# Hatch-Index auf -1 = keine Fuellung obj_col_src = "user-override" if col is not None else "n/a"
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), -1) if obj_col is None:
try:
lyr = doc.Layers[obj.Attributes.LayerIndex]
obj_col = lyr.Color
obj_col_src = "layer({})".format(lyr.FullPath)
except Exception as ex:
obj_col = None
obj_col_src = "fail:{}".format(ex)
print("[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: else:
if pat_idx >= 0: if pat_idx >= 0:
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), pat_idx) _try_set(style, ("HatchIndex", "HatchPatternIndex"), pat_idx)
_try_set_attr(a, ("SectionHatchScale", "HatchPatternScale"), scale_v) _try_set(style, ("HatchScale", "HatchPatternScale"), scale_v)
_try_set_attr(a, ("SectionHatchRotation", "HatchPatternRotation"), rot_rad) _try_set(style, ("HatchRotationRadians", "HatchRotation",
if col is not None: "HatchAngle"), rot_rad)
_try_set_attr(a, ("SectionFillColor", "SectionHatchColor", if obj_col is not None:
"HatchColor"), col) # Display- UND Print-Color setzen damit beide matchen
_try_set(style, ("HatchPatternColor", "HatchColor",
"FillColor"), obj_col)
_try_set(style, ("HatchPatternPrintColor",), obj_col)
_try_set(style, ("BoundaryVisible",), bool(boundary_visible))
try: try:
doc.Objects.ModifyAttributes(obj, a, True) _try_set(style, ("BoundaryWidthScale",),
n_ok += 1 float(boundary_width_scale))
except Exception as ex: except Exception: pass
print("[GESTALTUNG] SectionStyle ModifyAttributes:", ex) # Boundary-Farbe: NUR setzen wenn User explizit eine Override-Farbe
# gewaehlt hat. Sonst lassen wir Rhinos Default (schwarz) greifen
# damit Boundary visuell unterscheidbar von der Hatch-Pattern-Farbe
# bleibt. (Sonst wuerde HatchPatternColor=Layer + BoundaryColor=Layer
# die Schnittflaeche als einfarbige Flaeche erscheinen lassen.)
bcol = None
if boundary_color_hex:
try: bcol = _hex_to_color(boundary_color_hex)
except Exception: bcol = None
if bcol is not None:
_try_set(style, ("BoundaryColor",), bcol)
_try_set(style, ("BoundaryPrintColor",), bcol)
# Background-Fill: User-Override (hex) → SolidColor-Mode + Farbe
# Sonst Transparent (Viewport-Mode, Default)
if background_color_hex:
try:
bgcol = _hex_to_color(background_color_hex)
except Exception:
bgcol = None
if bgcol is not None:
_try_set(style, ("BackgroundFillColor",), bgcol)
_try_set(style, ("BackgroundFillPrintColor",), bgcol)
# FillMode auf SolidColor via Enum (mehrere Namens-Varianten)
for en_cls in ("SectionBackgroundFillMode",
"BackgroundFillMode"):
try:
E = getattr(Rhino.DocObjects, en_cls, None)
if E is not None:
_try_set(style, ("BackgroundFillMode",),
E.SolidColor)
break
except Exception: pass
via = _apply_custom(obj, style)
print("[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)) print("[GESTALTUNG] SectionStyle auf {} Objekt(e) appliziert".format(n_ok))
doc.Views.Redraw() doc.Views.Redraw()
+2
View File
@@ -1,4 +1,6 @@
#! python 3 #! python 3
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
inspect_section.py inspect_section.py
Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log, Schreibt ALLE Eigenschaften der SectionStyle der aktuellen Ebene ins Log,
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
kamera.py kamera.py
Kamera-Panel: liest/setzt Viewport-Kamera (Position, Target, Projektion, Kamera-Panel: liest/setzt Viewport-Kamera (Position, Target, Projektion,
+25 -19
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
layer_builder.py layer_builder.py
Layer-Struktur: Layer-Struktur:
@@ -157,6 +159,9 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
pat = (section_cfg.get("hatchPattern") or "None").strip() pat = (section_cfg.get("hatchPattern") or "None").strip()
show = bool(section_cfg.get("boundaryShow", True)) show = bool(section_cfg.get("boundaryShow", True))
diag = "[SS:{}]".format(layer.Name if layer else "?") diag = "[SS:{}]".format(layer.Name if layer else "?")
# DEBUG: zeigt was an section_cfg ankommt (zur Diagnose des Hatch-Bugs)
print(diag, "section_cfg.hatchPattern='{}' scale={} rot={}".format(
pat, section_cfg.get("hatchScale"), section_cfg.get("hatchRotation")))
# Wenn weder Hatch noch Boundary → Custom-Style entfernen # Wenn weder Hatch noch Boundary → Custom-Style entfernen
if pat == "None" and not show: if pat == "None" and not show:
@@ -194,17 +199,20 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
rot_deg = float(section_cfg.get("hatchRotation") or 0) rot_deg = float(section_cfg.get("hatchRotation") or 0)
_try_set(style, ("HatchRotation", "HatchAngle"), math.radians(rot_deg)) _try_set(style, ("HatchRotation", "HatchAngle"), math.radians(rot_deg))
# Hatch-Color: explizit ColorFromObject setzen damit der eigene Wert greift # Hatch-Color: explizit setzen — wenn User keine Override-Farbe angegeben
# hat, nehmen wir die Layer-Farbe als Default (sonst rendert Rhino sonst
# schwarz). Section-Style hat keine ByLayer-Option, also Farbwert
# explizit reinkopieren.
hatch_color = section_cfg.get("hatchColor") hatch_color = section_cfg.get("hatchColor")
if hatch_color: if hatch_color:
col = _color(hatch_color) col = _color(hatch_color)
set_color = _try_set(style, ("HatchColor", "FillColor"), col) elif layer_color is not None:
# Source auf "FromObject" — sonst nutzt Rhino den Layer-Color col = _color(layer_color) if isinstance(layer_color, str) else layer_color
src_from_object = _enum_int( else:
(("DocObjects", "ObjectColorSource"), "ColorFromObject")) col = None
if src_from_object is not None: if col is not None:
_try_set(style, ("HatchColorSource", "FillColorSource"), src_from_object) set_color = _try_set(style, ("HatchPatternColor", "HatchColor", "FillColor"), col)
print(diag, "HatchColor via {}".format(set_color)) print(diag, "HatchColor via {} (default=layer)".format(set_color))
# Background (viewport=0/transparent vs object=1) # Background (viewport=0/transparent vs object=1)
bg = section_cfg.get("background") bg = section_cfg.get("background")
@@ -226,20 +234,18 @@ def _apply_section_style(doc, layer, section_cfg, layer_color):
print(diag, "BoundaryVisible={} via {}".format(show, set_show)) print(diag, "BoundaryVisible={} via {}".format(show, set_show))
if show: if show:
# Boundary-Color: setze Color + Source auf FromObject # Boundary-Color: User-Override oder Layer-Farbe als Default
bc = section_cfg.get("boundaryColor") bc = section_cfg.get("boundaryColor")
if bc: if bc:
col = _color(bc) bcol = _color(bc)
elif layer_color is not None:
bcol = _color(layer_color) if isinstance(layer_color, str) else layer_color
else:
bcol = None
if bcol is not None:
set_to = _try_set(style, set_to = _try_set(style,
("BoundaryColor", "OutlineColor", "EdgeColor"), col) ("BoundaryColor", "OutlineColor", "EdgeColor"), bcol)
src_from_object = _enum_int( print(diag, "BoundaryColor via {} (default=layer)".format(set_to))
(("DocObjects", "ObjectColorSource"), "ColorFromObject"))
if src_from_object is not None:
_try_set(style,
("BoundaryColorSource", "OutlineColorSource",
"EdgeColorSource"),
src_from_object)
print(diag, "BoundaryColor={} via {}".format(bc, set_to))
# Width-Scale auf PlotWeight uebertragen (RW8 hat keine WidthScale direkt; # Width-Scale auf PlotWeight uebertragen (RW8 hat keine WidthScale direkt;
# alternative Property-Namen probieren) # alternative Property-Namen probieren)
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
layouts.py layouts.py
LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken. LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken.
+2
View File
@@ -1,5 +1,7 @@
#! python3 #! python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
library.py Dossier-Library (Phase A: lokal, read-only) library.py Dossier-Library (Phase A: lokal, read-only)
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
mass_style.py mass_style.py
Globale Mass-Stil-Presets fuer Dossier speichert pro Dokument benannte Globale Mass-Stil-Presets fuer Dossier speichert pro Dokument benannte
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
masse_settings.py masse_settings.py
Satellite-Fenster fuer das Bearbeiten der Masse-Presets Satellite-Fenster fuer das Bearbeiten der Masse-Presets
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
massstab.py massstab.py
MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports. MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports.
+367
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
oberleiste.py oberleiste.py
OBERLEISTE-Panel: horizontale Top-Bar mit Architektur-Kontext-Controls. OBERLEISTE-Panel: horizontale Top-Bar mit Architektur-Kontext-Controls.
@@ -183,6 +185,363 @@ def _import_display_modes(paths):
return count return count
# Fest-Guid fuer 'Dossier Plan' damit Re-Imports denselben Slot
# wiederverwenden statt Duplikate zu erzeugen.
_DOSSIER_PLAN_GUID = "d0551e72-7e72-4170-b1a4-d0551e72d055"
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)
_TEMPLATE_INI_PATH = os.path.join(_HERE, "templates", "dossier_plan.ini")
def _ensure_dossier_plan_display_mode():
"""Stellt sicher dass der 'Dossier Plan' Display-Mode existiert.
Strategie: wenn eine Template-ini im Repo existiert
(rhino/templates/dossier_plan.ini), laden wir die. Sonst Fallback auf
Clone-Technical + ini-Patch. Template ist die bevorzugte Methode weil
sich Mac-Rhino-Display-Mode-Properties via Python-API unzuverlaessig
setzen lassen der User baut den Mode einmal manuell perfekt zusammen
und exportiert ihn dort hin.
"""
print("[OBERLEISTE] Plan-Mode: check...")
try:
from Rhino.Display import DisplayModeDescription
except Exception as ex:
print("[OBERLEISTE] Plan-Mode: DMD nicht verfuegbar:", ex)
return False
import re # fuer ini-checks unten
target_name = "Dossier Plan"
try:
import System
target_guid_obj = System.Guid(_DOSSIER_PLAN_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] Plan-Mode template: {}".format(
"found at " + _TEMPLATE_INI_PATH if template_exists else "missing → fallback"))
# 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(), "dossier_plan.ini")
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] Plan-Mode: Template geladen ({} bytes)".format(len(content)))
except Exception as ex:
print("[OBERLEISTE] Plan-Mode Template read:", ex)
return False
else:
# Fallback: Technical exportieren + patchen
try:
all_modes = list(DisplayModeDescription.GetDisplayModes())
except Exception: all_modes = []
for prefer in ("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
# Guid (im ini meist als [<guid>] Section-Header oder als "id="-Feld)
try:
# Bestehende Guid aus dem File extrahieren + ueberall ersetzen
old_guid_match = re.search(
r'([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, _DOSSIER_PLAN_GUID)
content = content.replace(old_guid.upper(),
_DOSSIER_PLAN_GUID.upper())
except Exception: pass
# 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 (Technical-Clone): die bekannten Settings forcen.
if not template_exists:
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 _THUMB_SIZE = (480, 320) # 3:2 — kompakt fuer Launcher-Cards
@@ -1730,5 +2089,13 @@ def _bridge_factory():
return b 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.
try: _ensure_dossier_plan_display_mode()
except Exception as ex:
print("[OBERLEISTE] ensure_dossier_plan_display_mode:", ex)
panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory, panel_base.register_and_open("oberleiste", "Oberleiste", PANEL_GUID_STR, _bridge_factory,
icon_spec=("menu", "#2f5d54")) icon_spec=("menu", "#2f5d54"))
+2
View File
@@ -1,5 +1,7 @@
#! python3 #! python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
OSM-Importer fuer Dossier holt OpenStreetMap-Daten via Overpass-API als OSM-Importer fuer Dossier holt OpenStreetMap-Daten via Overpass-API als
Polylinien (Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege). Polylinien (Strassen, Gebaeudeumrisse, Wasser, Gruenflaechen, Wege).
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
overrides.py overrides.py
Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides / Engine fuer regelbasierte grafische Overrides (ArchiCAD Graphical Overrides /
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
overrides_panel.py overrides_panel.py
OVERRIDES-Panel: Rule-Editor fuer grafische Overrides. OVERRIDES-Panel: Rule-Editor fuer grafische Overrides.
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
panel_base.py panel_base.py
Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView. Geteilte Infrastruktur fuer dockbare Rhino-Panels mit React-WebView.
+7
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
rhinopanel.py rhinopanel.py
Oeffnet das EBENEN-Panel (Zeichnungsebenen + globale Ebenen). Oeffnet das EBENEN-Panel (Zeichnungsebenen + globale Ebenen).
@@ -16,6 +18,7 @@ if _HERE not in sys.path:
import panel_base import panel_base
import layer_builder import layer_builder
import library
PANEL_GUID_STR = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718" PANEL_GUID_STR = "3a7f2e1d-4b8c-4d9e-a1b2-c3d4e5f60718"
# Zweites Panel fuer Zeichnungsebenen (Geschoss-Liste + Clipping). UX-Split # Zweites Panel fuer Zeichnungsebenen (Geschoss-Liste + Clipping). UX-Split
@@ -1609,6 +1612,10 @@ class EbenenBridge(panel_base.BaseBridge):
updated = p.get("ebene") or {} updated = p.get("ebene") or {}
orig_code = p.get("originalCode") or updated.get("code") orig_code = p.get("originalCode") or updated.get("code")
if not (isinstance(updated, dict) and updated.get("code")): return 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") e_raw = doc.Strings.GetValue("dossier_ebenen")
if not e_raw: return if not e_raw: return
try: e_list = json.loads(e_raw) try: e_list = json.loads(e_raw)
+2
View File
@@ -1,5 +1,7 @@
#! python3 #! python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
schnitt_grips.py schnitt_grips.py
Endpoint-Grips fuer Schnitt/Ansicht-Symbole im Plan. Endpoint-Grips fuer Schnitt/Ansicht-Symbole im Plan.
+2
View File
@@ -1,5 +1,7 @@
#! python3 #! python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
schnitte.py schnitte.py
Schnitte + Ansichten als Zeichnungsebenen-Typ. Schnitte + Ansichten als Zeichnungsebenen-Typ.
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
startup.py startup.py
Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine Laedt DOSSIER-Panels beim Rhino-Start. Liest pro geoeffnetem Dokument eine
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
swisstopo.py swisstopo.py
STAC-API-Client + GeoTIFF/XYZ-Parser + Mesh-Builder fuer swisstopo-Daten. STAC-API-Client + GeoTIFF/XYZ-Parser + Mesh-Builder fuer swisstopo-Daten.
+30
View File
@@ -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.
+323
View File
@@ -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
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
text_create.py text_create.py
Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog. Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog.
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
text_editor.py text_editor.py
React-WYSIWYG-Editor in Satellite-WebView (Topmost). User picked Frame React-WYSIWYG-Editor in Satellite-WebView (Topmost). User picked Frame
+2
View File
@@ -1,5 +1,7 @@
#! python3 #! python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
wand_grips.py wand_grips.py
Custom Endpoint-Grips fuer Waende Display-Conduit + MouseCallback Overlay. Custom Endpoint-Grips fuer Waende Display-Conduit + MouseCallback Overlay.
+2
View File
@@ -1,5 +1,7 @@
#! python 3 #! python 3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
werkzeuge.py werkzeuge.py
WERKZEUGE-Panel: Architektur-orientierte Toolbar als React-WebView. WERKZEUGE-Panel: Architektur-orientierte Toolbar als React-WebView.
+164 -27
View File
@@ -1,54 +1,191 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react' import { useEffect } from 'react'
import Icon from './components/Icon'
import { notifyReady } from './lib/rhinoBridge' 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() { export default function AboutApp() {
useEffect(() => { notifyReady() }, []) useEffect(() => { notifyReady() }, [])
return ( return (
<div style={{ <div style={{
padding: '28px 32px', padding: '36px 36px 28px',
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontFamily: 'var(--font)', color: 'var(--text-primary)',
background: 'var(--bg-panel)', minHeight: '100vh', background: 'var(--bg-panel)', minHeight: '100vh',
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'flex', flexDirection: 'column', gap: 28,
}}>
{/* Logo + Tagline */}
<div>
<div style={{
display: 'flex', alignItems: 'baseline', gap: 8,
marginBottom: 6,
}}> }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
marginBottom: 4 }}>
<span style={{ <span style={{
fontFamily: "Krungthep, 'Archivo Black', sans-serif", fontFamily: "Krungthep, 'Archivo Black', sans-serif",
fontSize: 32, letterSpacing: '-0.02em', lineHeight: 1, fontSize: 38, letterSpacing: '-0.02em', lineHeight: 1,
}}> }}>
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span> DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
</span> </span>
</div> </div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', <div style={{
letterSpacing: '0.06em', textTransform: 'uppercase', fontFamily: "'Playfair Display', Georgia, serif",
marginBottom: 22 }}> fontStyle: 'italic',
Teil von OpenStudio fontSize: 14, color: 'var(--text-secondary)',
lineHeight: 1.4,
}}>
Architektur-Studio für Rhino 8
</div> </div>
<div style={{
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', marginTop: 10,
gap: '8px 16px', fontSize: 12, marginBottom: 22 }}> fontSize: 10, color: 'var(--text-muted)',
<span style={{ color: 'var(--text-muted)' }}>Launcher</span> letterSpacing: '0.1em', textTransform: 'uppercase',
<span style={{ fontFamily: 'DM Mono, monospace' }}>v{__LAUNCHER_VERSION__}</span> }}>
<span style={{ color: 'var(--text-muted)' }}>Plugin</span> Teil von <a href="https://openbureau.ch" target="_blank" rel="noreferrer"
<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' }}> style={{ color: 'var(--accent)', textDecoration: 'none' }}>
gabrielevarano.ch openbureau
</a> </a>
<span style={{ color: 'var(--text-muted)' }}>Lizenz</span> </div>
<span>Proprietär © 2026 Karim Gabriele Varano</span>
</div> </div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', {/* Versions */}
lineHeight: 1.5, <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
paddingTop: 14, <VersionPill label="Plugin" version={__APP_VERSION__} />
<VersionPill label="Launcher" version={__LAUNCHER_VERSION__} />
</div>
{/* 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)' }}> borderTop: '1px solid var(--border-light)' }}>
Rhino 8 Plugin für architektonische Workflows Wände, Decken, © 2026 Karim Gabriele Varano
Öffnungen, Räume, SIA 416, Plan-Layouts. Schwester-App: Rapport. </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>
</div> </div>
) )
+2
View File
@@ -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 { useState, useEffect, useMemo } from 'react'
import EbenenManager from './components/EbenenManager' import EbenenManager from './components/EbenenManager'
import { import {
+40 -35
View File
@@ -1,11 +1,26 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { onMessage, notifyReady } from './lib/rhinoBridge' import { onMessage, notifyReady } from './lib/rhinoBridge'
import { BarToggle, BarCombo, BAR_H } from './components/BarControls'
function send(type, payload = {}) { function send(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[AusschnittSettings] →', type, payload); return } if (!window.RHINO_MODE) { console.log('[AusschnittSettings] →', type, payload); return }
document.title = 'RHINOMSG::' + JSON.stringify({ 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 }) { function Field({ label, hint, children }) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '6px 0' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '6px 0' }}>
@@ -85,42 +100,38 @@ export default function AusschnittSettingsApp() {
value={snap.scale || ''} value={snap.scale || ''}
onChange={(ev) => set({ scale: ev.target.value })} onChange={(ev) => set({ scale: ev.target.value })}
placeholder="1:50" 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>
<Field label="DARSTELLUNG" <Field label="DARSTELLUNG"
hint="SIA-400 Detaillierungsgrad fuer diesen Ausschnitt — leer = beim Restore nicht aendern"> hint="SIA-400 Detaillierungsgrad fuer diesen Ausschnitt — leer = beim Restore nicht aendern">
<select <BarCombo stretch
value={snap.darstellung || ''} value={snap.darstellung || ''}
onChange={(ev) => set({ darstellung: ev.target.value })} onChange={(v) => set({ darstellung: v })}>
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value=""> nicht aendern </option> <option value=""> nicht aendern </option>
<option value="einfach">Einfach (1:100)</option> <option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option> <option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option> <option value="detail">Detail (1:20)</option>
</select> </BarCombo>
</Field> </Field>
<Field label="BILDSCHIRMMODUS" <Field label="BILDSCHIRMMODUS"
hint="Display-Mode des Viewports beim Wiederherstellen"> hint="Display-Mode des Viewports beim Wiederherstellen">
<select <BarCombo stretch
value={snap.displayMode || ''} value={snap.displayMode || ''}
onChange={(ev) => { onChange={(v) => {
const dm = displayModes.find(d => d.id === ev.target.value) const dm = displayModes.find(d => d.id === v)
set({ set({
displayMode: ev.target.value || null, displayMode: v || null,
displayModeName: dm ? dm.name : null, displayModeName: dm ? dm.name : null,
}) })
}} }}>
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value=""> unverändert </option> <option value=""> unverändert </option>
{displayModes.map(dm => ( {displayModes.map(dm => (
<option key={dm.id} value={dm.id}>{dm.name}</option> <option key={dm.id} value={dm.id}>{dm.name}</option>
))} ))}
</select> </BarCombo>
</Field> </Field>
<SectionLabel>Grafische Overrides</SectionLabel> <SectionLabel>Grafische Overrides</SectionLabel>
@@ -142,29 +153,25 @@ export default function AusschnittSettingsApp() {
{snap.applyOverrides && ( {snap.applyOverrides && (
<> <>
<Field label="OVERRIDES STATUS"> <Field label="OVERRIDES STATUS">
<select <BarToggle label="AN"
value={snap.overridesEnabled ? 'on' : 'off'} active={!!snap.overridesEnabled}
onChange={(ev) => set({ overridesEnabled: ev.target.value === 'on' })} onClick={() => set({ overridesEnabled: true })} />
style={{ flex: 1, fontSize: 11, minWidth: 0 }} <BarToggle label="AUS"
> active={!snap.overridesEnabled}
<option value="on">AN</option> onClick={() => set({ overridesEnabled: false })} />
<option value="off">AUS</option>
</select>
</Field> </Field>
<Field label="OVERRIDES PRESET" <Field label="OVERRIDES PRESET"
hint="Leer = kein Preset (Doc-Rules bleiben)"> hint="Leer = kein Preset (Doc-Rules bleiben)">
<select <BarCombo stretch
value={snap.overridesPreset || ''} value={snap.overridesPreset || ''}
onChange={(ev) => set({ overridesPreset: ev.target.value })} onChange={(v) => set({ overridesPreset: v })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }} disabled={!snap.overridesEnabled}>
disabled={!snap.overridesEnabled}
>
<option value=""> kein Preset </option> <option value=""> kein Preset </option>
{overridesPresets.map(name => ( {overridesPresets.map(name => (
<option key={name} value={name}>{name}</option> <option key={name} value={name}>{name}</option>
))} ))}
</select> </BarCombo>
</Field> </Field>
</> </>
)} )}
@@ -173,16 +180,14 @@ export default function AusschnittSettingsApp() {
<Field label="KOMBI" <Field label="KOMBI"
hint='"Eigene" = die per Snap gespeicherte Sichtbarkeit. Ein Preset überschreibt diese beim Wiederherstellen.'> hint='"Eigene" = die per Snap gespeicherte Sichtbarkeit. Ein Preset überschreibt diese beim Wiederherstellen.'>
<select <BarCombo stretch
value={snap.layerCombination || ''} value={snap.layerCombination || ''}
onChange={(ev) => set({ layerCombination: ev.target.value })} onChange={(v) => set({ layerCombination: v })}>
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value=""> Eigene Sichtbarkeit </option> <option value=""> Eigene Sichtbarkeit </option>
{layerKombis.map(name => ( {layerKombis.map(name => (
<option key={name} value={name}>{name}</option> <option key={name} value={name}>{name}</option>
))} ))}
</select> </BarCombo>
</Field> </Field>
</div> </div>
@@ -194,8 +199,8 @@ export default function AusschnittSettingsApp() {
background: 'var(--bg-section)', background: 'var(--bg-section)',
}}> }}>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button> <BarToggle label="Abbrechen" onClick={() => send('CANCEL', {})} />
<button className="btn-contained" onClick={saveAndClose}>Übernehmen</button> <BarToggle label="Übernehmen" active onClick={saveAndClose} />
</div> </div>
</div> </div>
) )
+9 -11
View File
@@ -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 { useState, useEffect, useMemo } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu' import ContextMenu from './components/ContextMenu'
@@ -103,13 +105,13 @@ function OrientationBadge({ orientation }) {
title={variant.title} title={variant.title}
style={{ style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 24, height: 24, flexShrink: 0, width: 20, height: 20, flexShrink: 0,
borderRadius: 999, borderRadius: 999,
background: 'var(--bg-input)', background: 'var(--bg-input)',
color: variant.color, color: variant.color,
}} }}
> >
<Icon name={variant.icon} size={14} /> <Icon name={variant.icon} size={12} />
</span> </span>
) )
} }
@@ -123,13 +125,13 @@ function AusschnittCard({ snap, onClick, onContextMenu, onMenuClick, onRename, o
onDragStart={onDragStart} onDragStart={onDragStart}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 8px', padding: '2px 8px 2px 4px',
border: '1px solid var(--border-light)', border: '1px solid var(--border-light)',
borderRadius: 'var(--r)', borderRadius: 999,
background: 'var(--bg-input)', background: 'var(--bg-input)',
cursor: 'grab', userSelect: 'none', cursor: 'grab', userSelect: 'none',
marginBottom: 4, marginBottom: 3,
opacity: dragging ? 0.4 : 1, opacity: dragging ? 0.4 : 1,
transition: 'background 0.14s, border-color 0.14s, opacity 0.14s', transition: 'background 0.14s, border-color 0.14s, opacity 0.14s',
}} }}
@@ -364,13 +366,9 @@ export default function AusschnitteApp() {
position: 'relative', position: 'relative',
}}> }}>
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}> <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={{ <div style={{
display: 'flex', alignItems: 'center', gap: 6, display: 'flex', alignItems: 'center', gap: 6,
padding: 8,
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
background: 'var(--bg-section)',
marginBottom: 8, marginBottom: 8,
marginTop: 6, marginTop: 6,
}}> }}>
+2
View File
@@ -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 { useEffect, useState, useRef } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarToggle, BarButton } from './components/BarControls' import { BarToggle, BarButton } from './components/BarControls'
+3 -6
View File
@@ -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 { useEffect, useState, useRef } from 'react'
import EbenenSettingsDialog from './components/EbenenSettingsDialog' import EbenenSettingsDialog from './components/EbenenSettingsDialog'
import { notifyReady, onMessage } from './lib/rhinoBridge' import { notifyReady, onMessage, send as bridgeSend } from './lib/rhinoBridge'
function bridgeSend(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
}
export default function EbenenSettingsApp() { export default function EbenenSettingsApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
+2
View File
@@ -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 { useEffect, useRef, useState } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarToggle, BarButton, BarCombo } from './components/BarControls' import { BarToggle, BarButton, BarCombo } from './components/BarControls'
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { onMessage, notifyReady } from './lib/rhinoBridge' import { onMessage, notifyReady } from './lib/rhinoBridge'
+2
View File
@@ -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 { useEffect, useState, useMemo } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarToggle, BarButton } from './components/BarControls' import { BarToggle, BarButton } from './components/BarControls'
+3 -7
View File
@@ -1,12 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react' import { useEffect } from 'react'
import GeschossDialog from './components/GeschossDialog' import GeschossDialog from './components/GeschossDialog'
import { notifyReady } from './lib/rhinoBridge' import { notifyReady, send as bridgeSend } 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
}
// recalcOkff direkt hier gleiche Logik wie in ZeichnungsebenenApp.jsx, // recalcOkff direkt hier gleiche Logik wie in ZeichnungsebenenApp.jsx,
// damit der Dialog die OKFF-Werte beim Editieren live updaten kann. // damit der Dialog die OKFF-Werte beim Editieren live updaten kann.
+3 -7
View File
@@ -1,12 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react' import { useEffect } from 'react'
import GeschossSettingsDialog from './components/GeschossSettingsDialog' import GeschossSettingsDialog from './components/GeschossSettingsDialog'
import { notifyReady } from './lib/rhinoBridge' import { notifyReady, send as bridgeSend } 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
}
export default function GeschossSettingsApp() { export default function GeschossSettingsApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
+102 -8
View File
@@ -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 { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarCombo, BarToggle, BarButton } from './components/BarControls' 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 }) dropdownOptions.push({ value: currentValue, label: currentValue })
} }
const applyPattern = (newValue) => { 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) if (newValue === '__layer__') setter(true, 'layer', null, null, null, null)
else if (newValue === 'None') setter(false, source, null, null, scale, rotation) 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( const apply = (over) => setter(
true, true,
over.source ?? (source === 'layer' ? 'object' : source), 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.pattern ?? (objectPat === 'None' ? 'Solid' : objectPat),
over.scale ?? scale, over.scale ?? scale,
over.rotation ?? rotation, over.rotation ?? rotation,
@@ -399,9 +407,74 @@ function FillBlock({ sel }) {
function SectionBlock({ sel }) { function SectionBlock({ sel }) {
const color = sel.sectionColor || sel.layerColor || '#cccccc' const color = sel.sectionColor || sel.layerColor || '#cccccc'
return <HatchEditor 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} sel={sel}
enabled={sel.sectionEnabled === true} enabled={enabled}
source={sel.sectionSource || 'layer'} source={sel.sectionSource || 'layer'}
color={color} color={color}
pattern={sel.sectionPattern || 'Solid'} pattern={sel.sectionPattern || 'Solid'}
@@ -411,6 +484,30 @@ function SectionBlock({ sel }) {
layerHint="Pattern, Skalierung & Farbe folgen Layer-SectionStyle" layerHint="Pattern, Skalierung & Farbe folgen Layer-SectionStyle"
setter={setSectionStyle} setter={setSectionStyle}
/> />
{/* Hintergrund (Solid-Fill hinter dem Hatch-Pattern) */}
{!isLayerSource && enabled && (
<>
<SectionHead title="Solid-Fill" />
<div style={{ padding: '0 14px 12px', display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<BarToggle label={backgroundColor ? 'Solid' : 'Transparent'}
icon={backgroundColor ? 'format_color_fill' : 'check_box_outline_blank'}
active={!!backgroundColor}
onClick={() => callSetter({
backgroundColor: backgroundColor ? null : '#ffffff'
})} />
</div>
{backgroundColor && (
<ColorBar
color={backgroundColor}
onChange={(c) => callSetter({ backgroundColor: c })}
/>
)}
</div>
</>
)}
</>
} }
@@ -449,7 +546,7 @@ export default function GestaltungApp() {
const kind = sel.geometryKind || 'curve' const kind = sel.geometryKind || 'curve'
const showFill = kind === 'curve' const showFill = kind === 'curve'
const showSection = kind === '3d' const showSection = kind === '3d'
const penLabel = (kind === '3d') ? 'Boundary' : 'Pen' const penLabel = (kind === '3d') ? 'Background' : 'Pen'
return ( return (
<div style={{ <div style={{
@@ -472,10 +569,7 @@ export default function GestaltungApp() {
<PenBlock sel={sel} /> <PenBlock sel={sel} />
{showSection && ( {showSection && (
<>
<SectionHead title="Section Style" />
<SectionBlock sel={sel} /> <SectionBlock sel={sel} />
</>
)} )}
<SectionHead title="Effects" /> <SectionHead title="Effects" />
+49 -77
View File
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarToggle, BarButton, BAR_H } from './components/BarControls'
import { import {
onMessage, notifyReady, onMessage, notifyReady,
setKameraViewport, setKameraProjection, setKameraIso, setKameraViewport, setKameraProjection, setKameraIso,
@@ -13,6 +16,18 @@ const labelXs = {
fontWeight: 600, 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 }) { function NumberField({ label, value, onCommit, suffix, step = 0.1 }) {
const [draft, setDraft] = useState(value != null ? value.toFixed(3) : '') const [draft, setDraft] = useState(value != null ? value.toFixed(3) : '')
useEffect(() => { useEffect(() => {
@@ -37,8 +52,7 @@ function NumberField({ label, value, onCommit, suffix, step = 0.1 }) {
if (ev.key === 'Enter') commit() if (ev.key === 'Enter') commit()
if (ev.key === 'Escape') setDraft(value != null ? value.toFixed(3) : '') if (ev.key === 'Escape') setDraft(value != null ? value.toFixed(3) : '')
}} }}
style={{ flex: 1, fontSize: 11, padding: '4px 8px', style={{ ...pillInput, flex: 1, fontFamily: 'var(--font-mono)' }}
fontFamily: 'var(--font-mono)' }}
/> />
{suffix && ( {suffix && (
<span style={{ fontSize: 9, color: 'var(--text-muted)', minWidth: 18 }}> <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 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, fontWeight: 600 }}>{vp.name || 'Unnamed'}</span> <span style={{ fontSize: 13, fontWeight: 600 }}>{vp.name || 'Unnamed'}</span>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<div style={{ <BarToggle label="Perspektive"
display: 'flex', active={!isPar}
borderRadius: 999, onClick={() => setKameraProjection(false)} />
border: '1px solid var(--border)', <BarToggle label="Parallel"
overflow: 'hidden', active={isPar}
}}> onClick={() => setKameraProjection(true)} />
<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>
</div> </div>
</div> </div>
{/* Plan-Norden — Rotations-Winkel im Uhrzeigersinn von +Y */} {/* Plan-Norden — Rotations-Winkel im Uhrzeigersinn von +Y */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<span style={labelXs}>Plan-Norden (Rotation von +Y, im Uhrzeigersinn)</span> <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 <input
type="number" min={0} max={360} step={0.5} type="number" min={0} max={360} step={0.5}
value={northAngle.toFixed(1)} value={northAngle.toFixed(1)}
@@ -159,16 +160,12 @@ export default function KameraApp() {
setKameraNorthAngle(a) setKameraNorthAngle(a)
} }
}} }}
style={{ flex: 1, fontSize: 12, padding: '4px 8px', style={{ ...pillInput, flex: 1, fontFamily: 'DM Mono, monospace' }}
fontFamily: 'DM Mono, monospace' }}
/> />
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>°</span> <span style={{ fontSize: 11, color: 'var(--text-muted)' }}>°</span>
<button <BarToggle label="Reset"
onClick={() => { setNorthAngleState(0); setKameraNorthAngle(0) }} onClick={() => { setNorthAngleState(0); setKameraNorthAngle(0) }}
className="btn-outlined" title="Norden zurueck auf +Y (0°)" />
style={{ padding: '4px 10px', fontSize: 10 }}
title="Norden zurueck auf +Y (0°)"
>Reset</button>
</div> </div>
<span style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.4 }}> <span style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.4 }}>
Norden = +Y bei 0°. Bei rotierten Projekten (z.B. swissBUILDINGS in Norden = +Y bei 0°. Bei rotierten Projekten (z.B. swissBUILDINGS in
@@ -180,20 +177,12 @@ export default function KameraApp() {
{/* Iso-Quick-Picker */} {/* Iso-Quick-Picker */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<span style={labelXs}>Isometrie (Standard, true-iso 35°/45°)</span> <span style={labelXs}>Isometrie (Standard, true-iso 35°/45°)</span>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 4 }}> <div style={{ display: 'flex', gap: 4 }}>
{[ {['NW', 'NE', 'SE', 'SW'].map(v => (
{ v: 'NW', label: 'NW' }, <BarToggle key={v} label={v}
{ v: 'NE', label: 'NE' }, onClick={() => setKameraIso(v)}
{ v: 'SE', label: 'SE' }, title={`Isometrie aus ${v} (Kamera blickt Richtung Szene)`}
{ v: 'SW', label: 'SW' }, minWidth={48} />
].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> </div>
</div> </div>
@@ -205,9 +194,9 @@ export default function KameraApp() {
{/* Distance read-only */} {/* Distance read-only */}
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 8,
padding: '6px 10px', padding: '6px 12px',
background: 'var(--bg-section)', background: 'var(--bg-section)',
borderRadius: 6, borderRadius: 999,
border: '1px solid var(--border-light)', border: '1px solid var(--border-light)',
}}> }}>
<span style={labelXs}>Distanz</span> <span style={labelXs}>Distanz</span>
@@ -236,19 +225,13 @@ export default function KameraApp() {
/> />
)} )}
<button <BarToggle icon="zoom_out_map" label="Zoom Extents"
onClick={() => kameraZoomExtents()} 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>
{/* Presets */} {/* Presets */}
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', gap: 6, display: 'flex', flexDirection: 'column', gap: 6,
padding: 10, borderRadius: 6, padding: 10, borderRadius: 'var(--r-lg)',
background: 'var(--bg-section)', background: 'var(--bg-section)',
border: '1px solid var(--border-light)', border: '1px solid var(--border-light)',
}}> }}>
@@ -260,41 +243,34 @@ export default function KameraApp() {
value={presetName} value={presetName}
onChange={(ev) => setPresetName(ev.target.value)} onChange={(ev) => setPresetName(ev.target.value)}
onKeyDown={(ev) => { if (ev.key === 'Enter') saveCurrent() }} onKeyDown={(ev) => { if (ev.key === 'Enter') saveCurrent() }}
style={{ flex: 1, fontSize: 11, padding: '4px 8px' }} style={{ ...pillInput, flex: 1 }}
/> />
<button <BarToggle label="Speichern"
onClick={saveCurrent} active={!!presetName.trim()}
disabled={!presetName.trim()} disabled={!presetName.trim()}
className="btn-contained" onClick={saveCurrent} />
style={{ padding: '4px 12px', fontSize: 11 }}
>Aktuelle speichern</button>
</div> </div>
{presets.length === 0 ? ( {presets.length === 0 ? (
<span style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}> <span style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
Keine Presets gespeichert. Keine Presets gespeichert.
</span> </span>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{presets.map(p => ( {presets.map(p => (
<div <div
key={p.id} key={p.id}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 6, display: 'flex', alignItems: 'center', gap: 6,
padding: '4px 6px', padding: '2px 6px 2px 4px',
borderRadius: 4, borderRadius: 999,
background: 'var(--bg-item)', background: 'var(--bg-item)',
border: '1px solid transparent', border: '1px solid var(--border-light)',
fontSize: 11, fontSize: 11,
}} }}
> >
<button <BarButton icon="play_arrow"
onClick={() => applyKameraPreset(p.id)} onClick={() => applyKameraPreset(p.id)}
className="btn-outlined" title="Anwenden" />
style={{ padding: '2px 8px', fontSize: 10 }}
title="Anwenden"
>
<Icon name="play_arrow" size={11} />
</button>
<span style={{ flex: 1, minWidth: 0, <span style={{ flex: 1, minWidth: 0,
overflow: 'hidden', textOverflow: 'ellipsis', overflow: 'hidden', textOverflow: 'ellipsis',
whiteSpace: 'nowrap' }}>{p.name}</span> whiteSpace: 'nowrap' }}>{p.name}</span>
@@ -302,13 +278,9 @@ export default function KameraApp() {
fontFamily: 'var(--font-mono)' }}> fontFamily: 'var(--font-mono)' }}>
{p.parallel ? 'Par' : 'Persp'} {p.parallel ? 'Par' : 'Persp'}
</span> </span>
<button <BarButton icon="close"
onClick={() => deleteKameraPreset(p.id)} onClick={() => deleteKameraPreset(p.id)}
className="btn-icon-xs" title="Loeschen" />
title="Loeschen"
>
<Icon name="close" size={11} />
</button>
</div> </div>
))} ))}
</div> </div>
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import AusschnittLayerDialog from './components/AusschnittLayerDialog' import AusschnittLayerDialog from './components/AusschnittLayerDialog'
import { onMessage, notifyReady } from './lib/rhinoBridge' import { onMessage, notifyReady } from './lib/rhinoBridge'
+39 -39
View File
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Icon from './components/Icon'
import { onMessage, notifyReady } from './lib/rhinoBridge' import { onMessage, notifyReady } from './lib/rhinoBridge'
import { BarToggle, BAR_H } from './components/BarControls'
function send(type, payload = {}) { function send(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[LayoutDialog] →', type, payload); return } 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 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() { export default function LayoutDialogApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
const [mode, setMode] = useState(initial.mode || 'new') const [mode, setMode] = useState(initial.mode || 'new')
@@ -70,7 +84,7 @@ export default function LayoutDialogApp() {
onKeyDown={(e) => { if (e.key === 'Enter') submit() }} onKeyDown={(e) => { if (e.key === 'Enter') submit() }}
placeholder="z.B. Grundriss EG" placeholder="z.B. Grundriss EG"
autoFocus autoFocus
style={{ width: '100%', fontSize: 12, padding: '6px 8px' }} style={{ ...pillInput, width: '100%' }}
/> />
</Field> </Field>
)} )}
@@ -78,19 +92,13 @@ export default function LayoutDialogApp() {
<Field label="Papierformat"> <Field label="Papierformat">
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{PAPER_SIZES.map(f => ( {PAPER_SIZES.map(f => (
<button key={f} <BarToggle key={f} label={f}
onClick={() => setFormat(f)} active={format === f}
className={format === f ? 'btn-contained' : 'btn-outlined'} onClick={() => setFormat(f)} />
style={{ padding: '5px 12px', fontSize: 11 }}>
{f}
</button>
))} ))}
<button <BarToggle label="Eigene"
onClick={() => setFormat('custom')} active={format === 'custom'}
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'} onClick={() => setFormat('custom')} />
style={{ padding: '5px 12px', fontSize: 11 }}>
Eigene
</button>
</div> </div>
</Field> </Field>
@@ -101,16 +109,18 @@ export default function LayoutDialogApp() {
type="text" value={cw} type="text" value={cw}
onChange={(e) => setCw(e.target.value)} onChange={(e) => setCw(e.target.value)}
placeholder="Breite" placeholder="Breite"
style={{ flex: 1, fontFamily: 'DM Mono, monospace', style={{ ...pillInput, flex: 1,
fontSize: 12, textAlign: 'right', padding: '6px 8px' }} fontFamily: 'DM Mono, monospace',
textAlign: 'right' }}
/> />
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>×</span> <span style={{ color: 'var(--text-muted)', fontSize: 11 }}>×</span>
<input <input
type="text" value={ch} type="text" value={ch}
onChange={(e) => setCh(e.target.value)} onChange={(e) => setCh(e.target.value)}
placeholder="Höhe" placeholder="Höhe"
style={{ flex: 1, fontFamily: 'DM Mono, monospace', style={{ ...pillInput, flex: 1,
fontSize: 12, textAlign: 'right', padding: '6px 8px' }} fontFamily: 'DM Mono, monospace',
textAlign: 'right' }}
/> />
<span style={{ color: 'var(--text-muted)', fontSize: 10, width: 22 }}>mm</span> <span style={{ color: 'var(--text-muted)', fontSize: 10, width: 22 }}>mm</span>
</div> </div>
@@ -118,22 +128,12 @@ export default function LayoutDialogApp() {
) : ( ) : (
<Field label="Ausrichtung"> <Field label="Ausrichtung">
<div style={{ display: 'flex', gap: 6 }}> <div style={{ display: 'flex', gap: 6 }}>
<button <BarToggle icon="crop_landscape" label="Quer"
onClick={() => setLandscape(true)} active={landscape}
className={landscape ? 'btn-contained' : 'btn-outlined'} onClick={() => setLandscape(true)} />
style={{ flex: 1, padding: '8px 12px', fontSize: 11, <BarToggle icon="crop_portrait" label="Hoch"
display: 'flex', gap: 6, alignItems: 'center', active={!landscape}
justifyContent: 'center' }}> onClick={() => setLandscape(false)} />
<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>
</div> </div>
</Field> </Field>
)} )}
@@ -151,12 +151,12 @@ export default function LayoutDialogApp() {
background: 'var(--bg-section)', background: 'var(--bg-section)',
}}> }}>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button> <BarToggle label="Abbrechen" onClick={() => send('CANCEL', {})} />
<button className="btn-contained" onClick={submit} <BarToggle label={editing ? 'Anwenden' : 'Erstellen'}
active
disabled={!editing && !name.trim()} disabled={!editing && !name.trim()}
title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}> title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}
{editing ? 'Anwenden' : 'Erstellen'} onClick={submit} />
</button>
</div> </div>
</div> </div>
) )
+34 -79
View File
@@ -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 { useEffect, useState, useRef } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu' import ContextMenu from './components/ContextMenu'
import { BarButton, BarCombo, BAR_H } from './components/BarControls'
import { import {
onMessage, notifyReady, onMessage, notifyReady,
listLayouts, deleteLayout, renameLayout, activateLayout, listLayouts, deleteLayout, renameLayout, activateLayout,
@@ -37,12 +40,12 @@ function OrientationBadge({ landscape }) {
<span title={landscape ? 'Querformat' : 'Hochformat'} <span title={landscape ? 'Querformat' : 'Hochformat'}
style={{ style={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 24, height: 24, flexShrink: 0, width: 20, height: 20, flexShrink: 0,
borderRadius: 999, borderRadius: 999,
background: 'var(--bg-input)', background: 'var(--bg-input)',
color: 'var(--accent)', color: 'var(--accent)',
}}> }}>
<Icon name={landscape ? 'crop_landscape' : 'crop_portrait'} size={14} /> <Icon name={landscape ? 'crop_landscape' : 'crop_portrait'} size={12} />
</span> </span>
) )
} }
@@ -346,16 +349,12 @@ export default function LayoutsApp() {
{folderName} {folderName}
</span> </span>
<span className="chip" style={{ fontSize: 8 }}>{items.length}</span> <span className="chip" style={{ fontSize: 8 }}>{items.length}</span>
<button <BarButton icon="more_vert"
className="btn-icon-sm"
onClick={(ev) => { onClick={(ev) => {
ev.stopPropagation() ev.stopPropagation()
setCtxMenu({ x: ev.clientX, y: ev.clientY, kind: 'folder', id: folderName }) setCtxMenu({ x: ev.clientX, y: ev.clientY, kind: 'folder', id: folderName })
}} }}
title="Ordner-Aktionen" title="Ordner-Aktionen" />
>
<Icon name="more_vert" size={14} />
</button>
</div> </div>
{!isCollapsed && ( {!isCollapsed && (
<div style={{ marginTop: 6, display: 'flex', flexDirection: 'column' }}> <div style={{ marginTop: 6, display: 'flex', flexDirection: 'column' }}>
@@ -461,26 +460,17 @@ export default function LayoutsApp() {
{/* Details des aktuell gewaehlten Layouts */} {/* Details des aktuell gewaehlten Layouts */}
{selected && ( {selected && (
<div style={cardStyle}> <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 }}> <span style={{ ...labelXs, flex: 1 }}>
Details · {selected.name} Details · {selected.name}
</span> </span>
<button <BarButton icon="add"
onClick={() => addDetail(selected.id, null)} onClick={() => addDetail(selected.id, null)}
className="btn-icon-tonal" title="Neues Detail (zentriert auf Seite)" />
title="Neues Detail (zentriert auf Seite)" <BarButton icon="sync"
style={{ marginRight: 4 }}
>
<Icon name="add" size={13} />
</button>
<button
onClick={() => syncLayout(selected.id)} onClick={() => syncLayout(selected.id)}
className="btn-icon-tonal"
disabled={details.length === 0} disabled={details.length === 0}
title="Alle Details mit ihren Ausschnitten neu synchronisieren" title="Alle Details mit ihren Ausschnitten neu synchronisieren" />
>
<Icon name="sync" size={13} />
</button>
</div> </div>
{details.length === 0 ? ( {details.length === 0 ? (
@@ -505,7 +495,7 @@ export default function LayoutsApp() {
padding: '6px 8px', padding: '6px 8px',
background: 'var(--bg-input)', background: 'var(--bg-input)',
border: '1px solid var(--border-light)', border: '1px solid var(--border-light)',
borderRadius: 'var(--r)', borderRadius: 'var(--r-lg)',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="chip" style={{ flexShrink: 0 }}>#{i + 1}</span> <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' }}> <span style={{ fontSize: 10, color: 'var(--text-muted)', fontFamily: 'DM Mono, monospace' }}>
{Math.round(d.width)}×{Math.round(d.height)} {Math.round(d.width)}×{Math.round(d.height)}
</span> </span>
<button <BarButton icon="delete"
className="btn-icon-sm btn-icon-danger"
onClick={() => { onClick={() => {
if (window.confirm('Detail loeschen?')) deleteDetail(selected.id, d.id) if (window.confirm('Detail loeschen?')) deleteDetail(selected.id, d.id)
}} }}
title="Detail loeschen" title="Detail loeschen" />
>
<Icon name="delete" size={12} />
</button>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<select <BarCombo stretch
value={d.boundAusschnitt || ''} value={d.boundAusschnitt || ''}
onChange={(e) => bindAusschnitt(selected.id, d.id, e.target.value || null)} onChange={(v) => bindAusschnitt(selected.id, d.id, v || null)}
style={{ flex: 1, fontSize: 11 }} title="Welcher Ausschnitt auf diesem Detail liegt">
title="Welcher Ausschnitt auf diesem Detail liegt"
>
<option value=""> kein Ausschnitt </option> <option value=""> kein Ausschnitt </option>
{snaps.map(s => ( {snaps.map(s => (
<option key={s.id} value={s.id}> <option key={s.id} value={s.id}>
{s.name}{s.folder ? ` · ${s.folder}` : ''}{s.scale ? ` · ${s.scale}` : ''} {s.name}{s.folder ? ` · ${s.folder}` : ''}{s.scale ? ` · ${s.scale}` : ''}
</option> </option>
))} ))}
</select> </BarCombo>
<button <BarButton icon="sync"
onClick={() => syncDetail(selected.id, d.id)} onClick={() => syncDetail(selected.id, d.id)}
disabled={!d.boundAusschnitt} disabled={!d.boundAusschnitt}
className="btn-icon-sm" title="Gebundenen Ausschnitt neu anwenden" />
title="Gebundenen Ausschnitt neu anwenden"
>
<Icon name="sync" size={12} />
</button>
</div> </div>
</div> </div>
))} ))}
@@ -573,49 +553,24 @@ export default function LayoutsApp() {
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}> <span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
Layouts Layouts
</span> </span>
{/* PDF-Aktionen: feste Breite damit das Auswahl-Counter den Footer {/* PDF-Aktionen — Pill-Stil */}
nicht horizontal verschiebt. */} <BarButton icon="picture_as_pdf"
<button
onClick={handleExportSelection} onClick={handleExportSelection}
className="btn-icon-tonal"
disabled={checked.size === 0} disabled={checked.size === 0}
title={checked.size > 0 title={checked.size > 0
? `Auswahl (${checked.size}) als ein PDF exportieren` ? `Auswahl (${checked.size}) als ein PDF exportieren`
: 'Erst Layouts ankreuzen'} : 'Erst Layouts ankreuzen'} />
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }} <BarButton icon="picture_as_pdf"
>
<Icon name="picture_as_pdf" size={14} />
{checked.size > 0 && (
<span style={{ fontSize: 9, fontFamily: 'DM Mono, monospace' }}>
{checked.size}
</span>
)}
</button>
<button
onClick={() => exportPdfAll(300)} onClick={() => exportPdfAll(300)}
className="btn-icon-tonal"
disabled={layouts.length === 0} disabled={layouts.length === 0}
title="Alle Layouts als ein PDF exportieren" 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>
<div style={{ width: 1, height: 18, background: 'var(--border)' }} /> <div style={{ width: 1, height: 18, background: 'var(--border)' }} />
<button <BarButton icon="create_new_folder"
onClick={handleNewFolder} onClick={handleNewFolder}
className="btn-icon-tonal" title="Neuer Ordner" />
title="Neuer Ordner" <BarButton icon="refresh"
>
<Icon name="create_new_folder" size={14} />
</button>
<button
onClick={() => listLayouts()} onClick={() => listLayouts()}
className="btn-icon-tonal" title="Aktualisieren" />
title="Aktualisieren"
>
<Icon name="refresh" size={14} />
</button>
<button <button
onClick={() => openLayoutDialog('new', null)} onClick={() => openLayoutDialog('new', null)}
className="btn-add" className="btn-add"
@@ -660,14 +615,14 @@ function LayoutRow({ l, active, checked, dragging, forceEditName,
onDoubleClick={onActivate} onDoubleClick={onActivate}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 8px', padding: '2px 8px 2px 6px',
border: '1px solid ' + (active ? 'var(--accent)' : 'var(--border-light)'), border: '1px solid ' + (active ? 'var(--accent)' : 'var(--border-light)'),
borderRadius: 'var(--r)', borderRadius: 999,
background: active ? 'var(--bg-item-active)' : 'var(--bg-input)', background: active ? 'var(--bg-item-active)' : 'var(--bg-input)',
cursor: 'grab', cursor: 'grab',
userSelect: 'none', userSelect: 'none',
marginBottom: 4, marginBottom: 3,
opacity: dragging ? 0.4 : 1, opacity: dragging ? 0.4 : 1,
transition: 'background 0.14s, border-color 0.14s, opacity 0.14s', transition: 'background 0.14s, border-color 0.14s, opacity 0.14s',
}} }}
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import LibraryBrowser from './components/LibraryBrowser' import LibraryBrowser from './components/LibraryBrowser'
import { notifyReady, onMessage, send } from './lib/rhinoBridge' import { notifyReady, onMessage, send } from './lib/rhinoBridge'
+34 -33
View File
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarButton, BarCombo, BAR_H } from './components/BarControls'
import { onMessage, notifyReady, import { onMessage, notifyReady,
masseSetActive as setActive, masseSetActive as setActive,
masseSavePreset as savePreset, masseSavePreset as savePreset,
@@ -19,6 +22,18 @@ const labelXs = {
fontWeight: 600, 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 }) { function Row({ label, children }) {
return ( return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
@@ -86,35 +101,27 @@ export default function MasseSettingsApp() {
{/* Picker + Aktionen */} {/* Picker + Aktionen */}
<Row label="Aktiv"> <Row label="Aktiv">
<div style={{ display: 'flex', gap: 4 }}> <div style={{ display: 'flex', gap: 4 }}>
<select <BarCombo stretch
value={activeId || (active?.id || '')} value={activeId || (active?.id || '')}
onChange={(e) => setActive(e.target.value)} onChange={(v) => setActive(v)}>
style={{ flex: 1, fontSize: 12 }}
>
{presets.map(p => ( {presets.map(p => (
<option key={p.id} value={p.id}>{p.name}</option> <option key={p.id} value={p.id}>{p.name}</option>
))} ))}
</select> </BarCombo>
<button <BarButton icon="add"
onClick={addNew} onClick={addNew}
className="btn-outlined" title="Neues Mass anlegen (mit aktuellen Werten als Vorlage)" />
style={{ padding: '4px 8px' }} <BarButton icon="delete"
title="Neues Mass anlegen (mit aktuellen Werten als Vorlage)"
><Icon name="add" size={13} /></button>
<button
onClick={remove} onClick={remove}
className="btn-outlined"
style={{ padding: '4px 8px' }}
title="Aktives Mass löschen"
disabled={presets.length <= 1} disabled={presets.length <= 1}
><Icon name="delete" size={13} /></button> title="Aktives Mass löschen" />
</div> </div>
</Row> </Row>
{active && ( {active && (
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', gap: 10, display: 'flex', flexDirection: 'column', gap: 10,
padding: 12, borderRadius: 6, padding: 12, borderRadius: 'var(--r-lg)',
background: 'var(--bg-section)', background: 'var(--bg-section)',
border: '1px solid var(--border-light)', border: '1px solid var(--border-light)',
}}> }}>
@@ -123,44 +130,38 @@ export default function MasseSettingsApp() {
type="text" type="text"
value={active.name} value={active.name}
onChange={(e) => update({ name: e.target.value })} onChange={(e) => update({ name: e.target.value })}
style={{ width: '100%', fontSize: 12, padding: '4px 8px' }} style={{ ...pillInput, width: '100%' }}
/> />
</Row> </Row>
<Row label="Raum-Rundung"> <Row label="Raum-Rundung">
<select <BarCombo stretch
value={active.raumRundung} value={active.raumRundung}
onChange={(e) => update({ raumRundung: e.target.value })} onChange={(v) => update({ raumRundung: v })}>
style={{ width: '100%', fontSize: 12 }}
>
{Object.entries(RAUM_RUNDUNGS_LABELS).map(([v, l]) => ( {Object.entries(RAUM_RUNDUNGS_LABELS).map(([v, l]) => (
<option key={v} value={v}>{l}</option> <option key={v} value={v}>{l}</option>
))} ))}
</select> </BarCombo>
</Row> </Row>
<Row label="Mass-Dezimalstellen"> <Row label="Mass-Dezimalstellen">
<select <BarCombo stretch
value={active.dimDezimalstellen} value={String(active.dimDezimalstellen)}
onChange={(e) => update({ dimDezimalstellen: parseInt(e.target.value, 10) })} onChange={(v) => update({ dimDezimalstellen: parseInt(v, 10) })}>
style={{ width: '100%', fontSize: 12 }}
>
{[0, 1, 2, 3, 4].map(n => ( {[0, 1, 2, 3, 4].map(n => (
<option key={n} value={n}>{n} {n === 1 ? 'Stelle' : 'Stellen'}</option> <option key={n} value={n}>{n} {n === 1 ? 'Stelle' : 'Stellen'}</option>
))} ))}
</select> </BarCombo>
</Row> </Row>
<Row label="Mass-Einheit"> <Row label="Mass-Einheit">
<select <BarCombo stretch
value={active.dimEinheit} value={active.dimEinheit}
onChange={(e) => update({ dimEinheit: e.target.value })} onChange={(v) => update({ dimEinheit: v })}>
style={{ width: '100%', fontSize: 12 }}
>
<option value="m">m (Meter)</option> <option value="m">m (Meter)</option>
<option value="cm">cm (Zentimeter)</option> <option value="cm">cm (Zentimeter)</option>
<option value="mm">mm (Millimeter)</option> <option value="mm">mm (Millimeter)</option>
</select> </BarCombo>
</Row> </Row>
</div> </div>
)} )}
+44 -68
View File
@@ -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 { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarToggle, BarButton, BarCombo, BAR_H } from './components/BarControls'
import { import {
onMessage, notifyReady, onMessage, notifyReady,
requestMassstab, setMassstab, requestMassstab, setMassstab,
@@ -50,6 +53,18 @@ function parseScale(input) {
return n > 0 ? n : null 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() { 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 ( return (
<div style={{ <div style={{
width: '100%', height: '100%', width: '100%', height: '100%',
@@ -163,13 +164,12 @@ export default function MassstabApp() {
<div style={{ width: 1, height: 18, background: 'var(--border)' }} /> <div style={{ width: 1, height: 18, background: 'var(--border)' }} />
{/* Skala-Dropdown */} {/* Skala-Dropdown */}
<select <BarCombo
disabled={isPerspective} disabled={isPerspective}
value={dropdownValue} value={dropdownValue}
onChange={(e) => applyDropdown(e.target.value)} onChange={applyDropdown}
style={{ ...cellInput, width: 80 }} width={84}
title="Massstab wählen" title="Massstab wählen">
>
<option value="__none__">1:?</option> <option value="__none__">1:?</option>
{PRESETS.map(p => ( {PRESETS.map(p => (
<option key={p.value} value={String(p.value)}>{p.label}</option> <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) && ( {appliedScale != null && !PRESETS.some(p => p.value === appliedScale) && (
<option value={String(appliedScale)}>1:{appliedScale}</option> <option value={String(appliedScale)}>1:{appliedScale}</option>
)} )}
</select> </BarCombo>
{/* Freitext */} {/* Freitext */}
<input <input
@@ -188,49 +188,37 @@ export default function MassstabApp() {
onChange={(e) => setDraft(e.target.value)} onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') applyDraft() }} onKeyDown={(e) => { if (e.key === 'Enter') applyDraft() }}
onBlur={() => { if (draft) applyDraft() }} onBlur={() => { if (draft) applyDraft() }}
style={{ ...cellInput, width: 64 }} style={{ ...pillInput, width: 64 }}
title="Eigenen Massstab eingeben (Enter)" title="Eigenen Massstab eingeben (Enter)"
/> />
<div style={{ width: 1, height: 18, background: 'var(--border)' }} /> <div style={{ width: 1, height: 18, background: 'var(--border)' }} />
{/* Aktions-Buttons */} {/* Aktions-Buttons */}
<button <BarToggle label="100%"
disabled={isPerspective || !appliedScale} disabled={isPerspective || !appliedScale}
onClick={apply100} onClick={apply100}
style={cellBtn}
title={appliedScale title={appliedScale
? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})` ? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})`
: 'Erst einen Massstab wählen'} : 'Erst einen Massstab wählen'} />
>100%</button> <BarButton icon="fit_screen"
<button onClick={zoomExtents} style={cellBtn} title="Auf gesamten Inhalt zoomen"> onClick={zoomExtents}
<Icon name="fit_screen" size={14} /> title="Auf gesamten Inhalt zoomen" />
</button> <BarButton icon="center_focus_strong"
<button onClick={zoomSelection} style={cellBtn} title="Auf Selektion zoomen"> onClick={zoomSelection}
<Icon name="center_focus_strong" size={14} /> title="Auf Selektion zoomen" />
</button>
<div style={{ width: 1, height: 18, background: 'var(--border)' }} /> <div style={{ width: 1, height: 18, background: 'var(--border)' }} />
{/* Print-View / Strichstaerken-Toggle {/* Print-View / Strichstaerken-Toggle */}
Beide Icons werden permanent gerendert; nur display: none togglet, <BarToggle
damit die Font-Ligatur nicht neu aufgeloest wird (sonst Flackern). */} icon={state.showLineweights ? 'print' : 'edit'}
<button label={state.showLineweights ? 'Print' : 'Edit'}
active={state.showLineweights}
onClick={() => setShowLineweights(!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 title={state.showLineweights
? 'Strichstärken werden angezeigt (Print-View) — klicken zum Ausschalten' ? 'Strichstärken werden angezeigt (Print-View) — klicken zum Ausschalten'
: 'Strichstärken als Hairlines (Edit-View) — klicken um Print-View zu zeigen'} : '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>
{/* Spacer */} {/* Spacer */}
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
@@ -246,24 +234,16 @@ export default function MassstabApp() {
{/* DPI-Popover */} {/* DPI-Popover */}
<div style={{ position: 'relative' }}> <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) }} onClick={() => { setDpiDraft(String(state.dpi || 96)); setDpiOpen(o => !o) }}
style={{ ...cellBtn, fontSize: 10, padding: '0 6px', height: 22, title={`DPI Kalibrierung — aktuell ${Math.round(state.dpi || 96)} dpi (${state.dpiSource || 'default'})`} />
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>
{dpiOpen && ( {dpiOpen && (
<div style={{ <div style={{
position: 'absolute', right: 0, bottom: '100%', marginBottom: 4, position: 'absolute', right: 0, bottom: '100%', marginBottom: 4,
background: 'var(--bg-panel)', border: '1px solid var(--border)', 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, flexDirection: 'column', gap: 6,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: 10, minWidth: 220, 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)} onChange={(e) => setDpiDraft(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') commitDpi() }} onKeyDown={(e) => { if (e.key === 'Enter') commitDpi() }}
autoFocus 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> </div>
<button <BarToggle icon="auto_fix_high" label="Auto-Detect (EDID)"
onClick={() => { detectMassstabDpi(); setDpiOpen(false) }} onClick={() => { detectMassstabDpi(); setDpiOpen(false) }}
style={{ ...cellBtn, fontSize: 10, justifyContent: 'flex-start' }} title="DPI automatisch über EDID des Bildschirms ermitteln" />
title="DPI automatisch über EDID des Bildschirms ermitteln"
>
<Icon name="auto_fix_high" size={12} /> Auto-Detect (EDID)
</button>
</div> </div>
)} )}
</div> </div>
+2
View File
@@ -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 { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarCombo, BarButton, BAR_H } from './components/BarControls' import { BarCombo, BarButton, BAR_H } from './components/BarControls'
+32 -27
View File
@@ -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 { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarToggle, BAR_H } from './components/BarControls'
import { onMessage, notifyReady } from './lib/rhinoBridge' import { onMessage, notifyReady } from './lib/rhinoBridge'
function send(type, payload = {}) { function send(type, payload = {}) {
@@ -7,6 +10,18 @@ function send(type, payload = {}) {
document.title = 'RHINOMSG::' + JSON.stringify({ 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 }) { function Field({ label, hint, children }) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}>
@@ -35,13 +50,9 @@ function Radio({ value, options, onChange }) {
return ( return (
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{options.map(o => ( {options.map(o => (
<button key={o.value} <BarToggle key={o.value} label={o.label}
onClick={() => onChange(o.value)} active={value === o.value}
className={value === o.value ? 'btn-contained' : 'btn-outlined'} onClick={() => onChange(o.value)} />
style={{ padding: '4px 10px', fontSize: 10 }}
>
{o.label}
</button>
))} ))}
</div> </div>
) )
@@ -168,36 +179,31 @@ export default function OsmApp() {
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }} onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }}
placeholder="Adresse oder Ortsname" placeholder="Adresse oder Ortsname"
style={{ flex: 1, fontSize: 11, padding: '5px 8px' }} style={{ ...pillInput, flex: 1 }}
/> />
<button <BarToggle label={searching ? '…' : 'Suchen'}
className="btn-outlined"
onClick={handleSearch} onClick={handleSearch}
disabled={searching || !searchText.trim()} disabled={searching || !searchText.trim()} />
style={{ padding: '4px 10px', fontSize: 11 }}
>
{searching ? '…' : 'Suchen'}
</button>
</Field> </Field>
<Field label="ODER LV95-KOORDS (E / N)" <Field label="ODER LV95-KOORDS (E / N)"
hint="Falls aus Swisstopo-Import übernommen"> hint="Falls aus Swisstopo-Import übernommen">
<input placeholder="E" <input placeholder="E"
onChange={(e) => handleManualCoords(e.target.value, center?.n || '')} 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> <span style={{ color: 'var(--text-muted)' }}>/</span>
<input placeholder="N" <input placeholder="N"
onChange={(e) => handleManualCoords(center?.e || '', e.target.value)} 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> </Field>
{center && ( {center && (
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 10px', padding: '8px 12px',
background: 'var(--accent-dim)', background: 'var(--accent-dim)',
border: '1px solid var(--accent-border)', border: '1px solid var(--accent-border)',
borderRadius: 'var(--r)', borderRadius: 999,
marginTop: 4, marginTop: 4,
}}> }}>
<Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} /> <Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} />
@@ -276,7 +282,7 @@ export default function OsmApp() {
fontFamily: 'DM Mono, monospace', fontFamily: 'DM Mono, monospace',
background: 'var(--bg-base)', background: 'var(--bg-base)',
border: '1px solid var(--border-light)', border: '1px solid var(--border-light)',
borderRadius: 'var(--r)', borderRadius: 'var(--r-lg)',
color: 'var(--text-secondary)', color: 'var(--text-secondary)',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
}}> }}>
@@ -296,13 +302,12 @@ export default function OsmApp() {
<div style={{ fontSize: 9, color: 'var(--text-muted)', flex: 1 }}> <div style={{ fontSize: 9, color: 'var(--text-muted)', flex: 1 }}>
Quelle: Overpass-API · © OpenStreetMap-Mitwirkende (ODbL) Quelle: Overpass-API · © OpenStreetMap-Mitwirkende (ODbL)
</div> </div>
<button className="btn-text" onClick={() => send('CANCEL')}>Abbrechen</button> <BarToggle label="Abbrechen" onClick={() => send('CANCEL')} />
<button className="btn-contained" <BarToggle icon="download"
onClick={handleImport} label={running ? 'Lädt…' : 'Importieren'}
disabled={running || !center}> active
<Icon name="download" size={12} /> disabled={running || !center}
{running ? 'Lädt…' : 'Importieren'} onClick={handleImport} />
</button>
</div> </div>
</div> </div>
) )
+88 -98
View File
@@ -1,6 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import ContextMenu from './components/ContextMenu' import ContextMenu from './components/ContextMenu'
import { BarToggle, BarButton, BarCombo, BAR_H } from './components/BarControls'
import { import {
onMessage, notifyReady, onMessage, notifyReady,
setOverridesEnabled, addRule, updateRule, deleteRule, setOverridesEnabled, addRule, updateRule, deleteRule,
@@ -29,6 +32,18 @@ const labelXs = {
fontWeight: 600, 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 }) { function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
@@ -42,18 +57,14 @@ function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
background: 'var(--bg-section)', background: 'var(--bg-section)',
}}> }}>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<select <BarCombo stretch
value={t} value={t}
onChange={(e) => onChange({ ...cond, type: e.target.value })} onChange={(v) => onChange({ ...cond, type: v })}>
style={{ flex: 1, minWidth: 0 }}
>
{COND_TYPES.map(c => <option key={c.value} value={c.value}>{c.label}</option>)} {COND_TYPES.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
</select> </BarCombo>
{canRemove && ( {canRemove && (
<button onClick={onRemove} className="btn-icon-danger" <BarButton icon="close" onClick={onRemove}
title="Diese Bedingung entfernen"> title="Diese Bedingung entfernen" />
<Icon name="close" size={14} />
</button>
)} )}
</div> </div>
{t === 'user_string' && ( {t === 'user_string' && (
@@ -61,34 +72,31 @@ function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
type="text" placeholder="Key" type="text" placeholder="Key"
value={cond?.key || ''} value={cond?.key || ''}
onChange={(e) => onChange({ ...cond, key: e.target.value })} onChange={(e) => onChange({ ...cond, key: e.target.value })}
style={{ width: '100%' }} style={{ ...pillInput, width: '100%' }}
/> />
)} )}
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<select <BarCombo
value={op} value={op}
onChange={(e) => onChange({ ...cond, operator: e.target.value })} onChange={(v) => onChange({ ...cond, operator: v })}
style={{ flexShrink: 0 }} width={100}>
>
{OPS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)} {OPS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
</select> </BarCombo>
{t === 'layer_name' ? ( {t === 'layer_name' ? (
<select <BarCombo stretch
value={cond?.value || ''} value={cond?.value || ''}
onChange={(e) => onChange({ ...cond, value: e.target.value })} onChange={(v) => onChange({ ...cond, value: v })}>
style={{ flex: 1, minWidth: 0 }}
>
<option value=""></option> <option value=""></option>
{(layers || []).map(l => ( {(layers || []).map(l => (
<option key={l.fullPath} value={l.fullPath}>{l.fullPath}</option> <option key={l.fullPath} value={l.fullPath}>{l.fullPath}</option>
))} ))}
</select> </BarCombo>
) : ( ) : (
<input <input
type="text" placeholder="Wert" type="text" placeholder="Wert"
value={cond?.value || ''} value={cond?.value || ''}
onChange={(e) => onChange({ ...cond, value: e.target.value })} onChange={(e) => onChange({ ...cond, value: e.target.value })}
style={{ flex: 1, minWidth: 0 }} style={{ ...pillInput, flex: 1, minWidth: 0 }}
/> />
)} )}
</div> </div>
@@ -124,16 +132,14 @@ function ConditionsEditor({ rule, layers, onChange }) {
{conds.length > 1 && ( {conds.length > 1 && (
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}> <div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>Logik:</span> <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>Logik:</span>
<button <BarToggle label="AND"
active={logic === 'and'}
onClick={() => setLogic('and')} onClick={() => setLogic('and')}
className={logic === 'and' ? 'btn-contained' : 'btn-outlined'} title="Alle Bedingungen müssen zutreffen" />
title="Alle Bedingungen müssen zutreffen" <BarToggle label="OR"
>AND</button> active={logic === 'or'}
<button
onClick={() => setLogic('or')} onClick={() => setLogic('or')}
className={logic === 'or' ? 'btn-contained' : 'btn-outlined'} title="Mindestens eine Bedingung muss zutreffen" />
title="Mindestens eine Bedingung muss zutreffen"
>OR</button>
</div> </div>
)} )}
{conds.map((c, i) => ( {conds.map((c, i) => (
@@ -146,11 +152,11 @@ function ConditionsEditor({ rule, layers, onChange }) {
canRemove={conds.length > 1} canRemove={conds.length > 1}
/> />
))} ))}
<button onClick={add} className="btn-outlined" style={{ alignSelf: 'flex-start' }} <div style={{ alignSelf: 'flex-start' }}>
title="Weitere Bedingung hinzufügen"> <BarToggle icon="add" label="Bedingung"
<Icon name="add" size={14} /> onClick={add}
<span>Bedingung</span> title="Weitere Bedingung hinzufügen" />
</button> </div>
</div> </div>
) )
} }
@@ -200,14 +206,16 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
type="color" type="color"
value={a.color || '#888888'} value={a.color || '#888888'}
onChange={(e) => setProp('color', e.target.value)} 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 <input
type="text" type="text"
value={a.color || ''} value={a.color || ''}
placeholder="#rrggbb" placeholder="#rrggbb"
onChange={(e) => setProp('color', e.target.value)} onChange={(e) => setProp('color', e.target.value)}
style={{ flex: 1, minWidth: 0 }} style={{ ...pillInput, flex: 1, minWidth: 0, fontFamily: 'DM Mono, monospace' }}
/> />
</ActionRow> </ActionRow>
@@ -220,7 +228,7 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
type="number" step={0.05} min={0} type="number" step={0.05} min={0}
value={a.lineweight ?? ''} value={a.lineweight ?? ''}
onChange={(e) => setProp('lineweight', parseFloat(e.target.value) || 0)} 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> <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>mm</span>
</ActionRow> </ActionRow>
@@ -230,14 +238,12 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
active={'linetype' in a} active={'linetype' in a}
onToggle={(e) => setProp('linetype', e.target.checked ? (a.linetype || 'Continuous') : '')} onToggle={(e) => setProp('linetype', e.target.checked ? (a.linetype || 'Continuous') : '')}
> >
<select <BarCombo stretch
value={a.linetype || ''} value={a.linetype || ''}
onChange={(e) => setProp('linetype', e.target.value)} onChange={(v) => setProp('linetype', v)}>
style={{ flex: 1, minWidth: 0 }}
>
<option value=""></option> <option value=""></option>
{(linetypes || []).map(lt => <option key={lt} value={lt}>{lt}</option>)} {(linetypes || []).map(lt => <option key={lt} value={lt}>{lt}</option>)}
</select> </BarCombo>
</ActionRow> </ActionRow>
<ActionRow <ActionRow
@@ -245,14 +251,12 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
active={'hatchPattern' in a} active={'hatchPattern' in a}
onToggle={(e) => setProp('hatchPattern', e.target.checked ? (a.hatchPattern || 'Solid') : '')} onToggle={(e) => setProp('hatchPattern', e.target.checked ? (a.hatchPattern || 'Solid') : '')}
> >
<select <BarCombo stretch
value={a.hatchPattern || ''} value={a.hatchPattern || ''}
onChange={(e) => setProp('hatchPattern', e.target.value)} onChange={(v) => setProp('hatchPattern', v)}>
style={{ flex: 1, minWidth: 0 }}
>
<option value=""></option> <option value=""></option>
{(hatchPatterns || []).map(hp => <option key={hp} value={hp}>{hp}</option>)} {(hatchPatterns || []).map(hp => <option key={hp} value={hp}>{hp}</option>)}
</select> </BarCombo>
</ActionRow> </ActionRow>
<ActionRow <ActionRow
@@ -264,7 +268,7 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
type="number" step={0.1} min={0.001} type="number" step={0.1} min={0.001}
value={a.hatchScale ?? ''} value={a.hatchScale ?? ''}
onChange={(e) => setProp('hatchScale', parseFloat(e.target.value) || 1.0)} onChange={(e) => setProp('hatchScale', parseFloat(e.target.value) || 1.0)}
style={{ width: 80 }} style={{ ...pillInput, width: 80, fontFamily: 'DM Mono, monospace' }}
/> />
</ActionRow> </ActionRow>
@@ -322,12 +326,11 @@ function RuleCard({ rule, index, total, layers, linetypes, hatchPatterns, onPatc
value={rule.name || ''} value={rule.name || ''}
placeholder="Regel-Name" placeholder="Regel-Name"
onChange={(e) => onPatch({ ...rule, name: e.target.value })} 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" <BarButton icon={open ? 'expand_less' : 'edit'}
title={open ? 'Einklappen' : 'Bearbeiten'}> onClick={() => setOpen(!open)}
<Icon name={open ? 'expand_less' : 'edit'} size={14} /> title={open ? 'Einklappen' : 'Bearbeiten'} />
</button>
</div> </div>
{!open && ( {!open && (
@@ -339,23 +342,22 @@ function RuleCard({ rule, index, total, layers, linetypes, hatchPatterns, onPatc
{open && ( {open && (
<> <>
<div style={{ display: 'flex', alignItems: 'center', gap: 2, marginTop: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 8 }}>
<button onClick={() => onMoveUp()} disabled={index === 0} <BarButton icon="arrow_upward"
className="btn-icon-sm" title="Prio höher (nach oben)"> onClick={() => onMoveUp()}
<Icon name="arrow_upward" size={14} /> disabled={index === 0}
</button> title="Prio höher (nach oben)" />
<button onClick={() => onMoveDown()} disabled={index === total - 1} <BarButton icon="arrow_downward"
className="btn-icon-sm" title="Prio tiefer (nach unten)"> onClick={() => onMoveDown()}
<Icon name="arrow_downward" size={14} /> disabled={index === total - 1}
</button> title="Prio tiefer (nach unten)" />
<button onClick={() => onDuplicate()} className="btn-icon-sm" title="Duplizieren"> <BarButton icon="content_copy"
<Icon name="content_copy" size={14} /> onClick={() => onDuplicate()}
</button> title="Duplizieren" />
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<button onClick={() => { if (confirm(`Regel "${rule.name}" löschen?`)) onDelete() }} <BarButton icon="delete"
className="btn-icon-danger" title="Löschen"> onClick={() => { if (confirm(`Regel "${rule.name}" löschen?`)) onDelete() }}
<Icon name="delete" size={14} /> title="Löschen" />
</button>
</div> </div>
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 10 }}> <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 10 }}>
@@ -476,10 +478,9 @@ export default function OverridesApp() {
borderRadius: 'var(--r-lg)', borderRadius: 'var(--r-lg)',
}}> }}>
<span style={labelXs}>Override-Kombinationen</span> <span style={labelXs}>Override-Kombinationen</span>
<select <BarCombo stretch
value={selectedPreset} value={selectedPreset}
onChange={(e) => { onChange={(v) => {
const v = e.target.value
setSelectedPreset(v) setSelectedPreset(v)
if (v) { if (v) {
loadPreset(v, 'replace') loadPreset(v, 'replace')
@@ -487,18 +488,20 @@ export default function OverridesApp() {
clearOverrideRules() clearOverrideRules()
} }
}} }}
style={{ width: '100%' }} title="Kombination zum Bearbeiten oeffnen">
title="Kombination zum Bearbeiten oeffnen"
>
<option value=""> neu / keine </option> <option value=""> neu / keine </option>
{(state.presets || []).map(p => ( {(state.presets || []).map(p => (
<option key={p.name} value={p.name}> <option key={p.name} value={p.name}>
{p.name} ({p.ruleCount}) {p.name} ({p.ruleCount})
</option> </option>
))} ))}
</select> </BarCombo>
<div style={{ display: 'flex', gap: 6 }}> <div style={{ display: 'flex', gap: 6 }}>
<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={() => { onClick={() => {
if (selectedPreset) { savePreset(selectedPreset); return } if (selectedPreset) { savePreset(selectedPreset); return }
const existing = (state.presets || []).map(p => p.name) const existing = (state.presets || []).map(p => p.name)
@@ -510,17 +513,11 @@ export default function OverridesApp() {
savePreset(t) savePreset(t)
setSelectedPreset(t) setSelectedPreset(t)
}} }}
disabled={state.rules.length === 0}
className="btn-outlined"
style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
title={selectedPreset title={selectedPreset
? `Änderungen in "${selectedPreset}" speichern` ? `Änderungen in "${selectedPreset}" speichern`
: 'Aktuelle Regeln als neue Kombination speichern'} : 'Aktuelle Regeln als neue Kombination speichern'} />
> </div>
<Icon name="save" size={14} /> <BarButton icon="delete"
<span>{selectedPreset ? 'Speichern' : 'Als Kombination speichern…'}</span>
</button>
<button
onClick={() => { onClick={() => {
if (!selectedPreset) return if (!selectedPreset) return
if (!window.confirm(`Kombination "${selectedPreset}" dauerhaft loeschen?`)) return if (!window.confirm(`Kombination "${selectedPreset}" dauerhaft loeschen?`)) return
@@ -528,11 +525,7 @@ export default function OverridesApp() {
setSelectedPreset('') setSelectedPreset('')
}} }}
disabled={!selectedPreset} disabled={!selectedPreset}
className="btn-icon-danger" title="Gewaehlte Kombination dauerhaft loeschen" />
title="Gewaehlte Kombination dauerhaft loeschen"
>
<Icon name="delete" size={14} />
</button>
</div> </div>
</div> </div>
@@ -551,10 +544,9 @@ export default function OverridesApp() {
> >
<Icon name="add" size={16} /> <Icon name="add" size={16} />
</button> </button>
<select <BarCombo stretch
value={selectedTemplate} value={selectedTemplate}
onChange={(e) => { onChange={(v) => {
const v = e.target.value
if (!v) { setSelectedTemplate(''); return } if (!v) { setSelectedTemplate(''); return }
if (v === '__delete__') { if (v === '__delete__') {
if (!selectedTemplate) return if (!selectedTemplate) return
@@ -566,9 +558,7 @@ export default function OverridesApp() {
addFromTemplate(v) addFromTemplate(v)
setSelectedTemplate(v) setSelectedTemplate(v)
}} }}
style={{ flex: 1, minWidth: 0 }} title="Regel aus Vorlage einfuegen">
title="Regel aus Vorlage einfuegen"
>
<option value="">+ Aus Vorlage</option> <option value="">+ Aus Vorlage</option>
{(state.ruleTemplates || []).map(t => ( {(state.ruleTemplates || []).map(t => (
<option key={t.name} value={t.name}>{t.name}</option> <option key={t.name} value={t.name}>{t.name}</option>
@@ -579,7 +569,7 @@ export default function OverridesApp() {
<option value="__delete__">🗑 "{selectedTemplate}" loeschen</option> <option value="__delete__">🗑 "{selectedTemplate}" loeschen</option>
</> </>
)} )}
</select> </BarCombo>
</div> </div>
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic', lineHeight: 1.4 }}> <div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic', lineHeight: 1.4 }}>
+3 -7
View File
@@ -1,12 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react' import { useEffect } from 'react'
import ProjectSettingsDialog from './components/ProjectSettingsDialog' import ProjectSettingsDialog from './components/ProjectSettingsDialog'
import { notifyReady } from './lib/rhinoBridge' import { notifyReady, send as bridgeSend } 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
}
export default function ProjectSettingsApp() { export default function ProjectSettingsApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {} const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
+35 -30
View File
@@ -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 { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarToggle, BAR_H } from './components/BarControls'
import { onMessage, notifyReady } from './lib/rhinoBridge' import { onMessage, notifyReady } from './lib/rhinoBridge'
function send(type, payload = {}) { function send(type, payload = {}) {
@@ -7,6 +10,18 @@ function send(type, payload = {}) {
document.title = 'RHINOMSG::' + JSON.stringify({ 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 }) { function Field({ label, hint, children }) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}>
@@ -35,14 +50,10 @@ function Radio({ value, options, onChange }) {
return ( return (
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{options.map(o => ( {options.map(o => (
<button key={o.value} <BarToggle key={o.value} label={o.label}
active={value === o.value}
onClick={() => onChange(o.value)} onClick={() => onChange(o.value)}
className={value === o.value ? 'btn-contained' : 'btn-outlined'} title={o.hint || ''} />
style={{ padding: '4px 10px', fontSize: 10 }}
title={o.hint || ''}
>
{o.label}
</button>
))} ))}
</div> </div>
) )
@@ -198,16 +209,11 @@ export default function SwisstopoApp() {
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }} onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }}
placeholder="Adresse oder Ortsname" placeholder="Adresse oder Ortsname"
style={{ flex: 1, fontSize: 11, padding: '5px 8px' }} style={{ ...pillInput, flex: 1 }}
/> />
<button <BarToggle label={searching ? '…' : 'Suchen'}
className="btn-outlined"
onClick={handleSearch} onClick={handleSearch}
disabled={searching || !searchText.trim()} disabled={searching || !searchText.trim()} />
style={{ padding: '4px 10px', fontSize: 11 }}
>
{searching ? '…' : 'Suchen'}
</button>
</Field> </Field>
<Field label="ODER LV95-KOORDS (E / N)" <Field label="ODER LV95-KOORDS (E / N)"
@@ -215,23 +221,23 @@ export default function SwisstopoApp() {
<input <input
placeholder="E" placeholder="E"
onChange={(e) => handleManualCoords(e.target.value, center?.n || '')} 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> <span style={{ color: 'var(--text-muted)' }}>/</span>
<input <input
placeholder="N" placeholder="N"
onChange={(e) => handleManualCoords(center?.e || '', e.target.value)} 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> </Field>
{center && ( {center && (
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 10px', padding: '8px 12px',
background: 'var(--accent-dim)', background: 'var(--accent-dim)',
border: '1px solid var(--accent-border)', border: '1px solid var(--accent-border)',
borderRadius: 'var(--r)', borderRadius: 999,
marginTop: 4, marginTop: 4,
}}> }}>
<Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} /> <Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} />
@@ -395,7 +401,7 @@ export default function SwisstopoApp() {
<input type="text" <input type="text"
value={terrainVolumeDepth} value={terrainVolumeDepth}
onChange={(e) => setTerrainVolumeDepth(e.target.value)} 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)' }}> <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
m unter tiefstem Punkt m unter tiefstem Punkt
</span> </span>
@@ -451,7 +457,7 @@ export default function SwisstopoApp() {
fontSize: 10, fontFamily: 'DM Mono, monospace', fontSize: 10, fontFamily: 'DM Mono, monospace',
background: 'var(--bg-input)', background: 'var(--bg-input)',
border: '1px solid var(--border)', border: '1px solid var(--border)',
borderRadius: 'var(--r)', borderRadius: 'var(--r-lg)',
padding: 8, padding: 8,
maxHeight: 140, maxHeight: 140,
overflowY: 'auto', overflowY: 'auto',
@@ -474,15 +480,14 @@ export default function SwisstopoApp() {
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}> <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'} {center ? `Tiles werden im Projekt-Ordner neben der .3dm gecacht (Fallback: ~/Library/Caches/Dossier/swisstopo/ wenn ungespeichert)` : 'Wähle zuerst einen Standort'}
</div> </div>
<button className="btn-text" onClick={() => send('CANCEL', {})} <BarToggle label={done ? 'Schliessen' : 'Abbrechen'}
disabled={running}> onClick={() => send('CANCEL', {})}
{done ? 'Schliessen' : 'Abbrechen'} disabled={running} />
</button> <BarToggle icon="download"
<button className="btn-contained" onClick={handleImport} label={running ? 'Importiere…' : 'Importieren'}
disabled={!center || running}> active
<Icon name="download" size={13} /> onClick={handleImport}
<span>{running ? 'Importiere…' : 'Importieren'}</span> disabled={!center || running} />
</button>
</div> </div>
</div> </div>
) )
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import SymbolPicker from './components/SymbolPicker' import SymbolPicker from './components/SymbolPicker'
import { notifyReady, onMessage, send } from './lib/rhinoBridge' import { notifyReady, onMessage, send } from './lib/rhinoBridge'
+5 -13
View File
@@ -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 { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { BarToggle } from './components/BarControls'
import { onMessage, notifyReady, send } from './lib/rhinoBridge' import { onMessage, notifyReady, send } from './lib/rhinoBridge'
const SYMBOL_GROUPS = [ const SYMBOL_GROUPS = [
@@ -719,19 +722,8 @@ export default function TextEditorApp() {
{/* Bottom Buttons */} {/* Bottom Buttons */}
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6 }}> <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
<Pill onClick={onCancel}>Abbrechen</Pill> <BarToggle label="Abbrechen" onClick={onCancel} />
<button onClick={onCommit} <BarToggle label="Einfügen" active 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>
</div> </div>
</div> </div>
) )
+43 -23
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react' import { useEffect } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { notifyReady, runRhinoCommand } from './lib/rhinoBridge' 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 ( return (
<button <button
onClick={() => runRhinoCommand(cmd)} onClick={() => runRhinoCommand(cmd)}
title={`${tip} (${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={{ style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 6,
padding: '6px 8px', padding: '5px 10px 5px 8px',
display: 'flex', alignItems: 'center', gap: 8, background: 'var(--bg-input)',
background: 'var(--bg-item)', border: '1px solid var(--border)', border: '1px solid var(--border-light)',
borderRadius: 'var(--r)', color: 'var(--text-primary)', borderRadius: 999,
cursor: 'pointer', textAlign: 'left', 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 }} /> <Icon name={icon} size={14} style={{ color: 'var(--accent)', flexShrink: 0 }} />
<span style={{ fontSize: 11, fontWeight: 500 }}>{label}</span> <span>{label}</span>
</button> </button>
) )
} }
function GroupLabel({ children }) { function PillGroup({ label, children }) {
return ( return (
<div style={{ <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase', <span style={{
letterSpacing: '0.05em', fontSize: 9, color: 'var(--text-muted)',
padding: '8px 4px 4px', letterSpacing: '0.08em', textTransform: 'uppercase',
borderTop: '1px solid var(--border)', fontWeight: 600,
}}>{children}</div> }}>
{label}
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
{children}
</div>
</div>
) )
} }
@@ -93,20 +114,19 @@ export default function WerkzeugeApp() {
return ( return (
<div style={{ <div style={{
width: '100%', height: '100%', width: '100%', height: '100%',
display: 'flex', flexDirection: 'column', gap: 0, display: 'flex', flexDirection: 'column', gap: 10,
padding: 6, padding: 10,
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontFamily: 'var(--font)', color: 'var(--text-primary)',
background: 'var(--bg-base)', background: 'var(--bg-base)',
boxSizing: 'border-box', boxSizing: 'border-box',
overflowY: 'auto', overflowX: 'hidden', overflowY: 'auto', overflowX: 'hidden',
}}> }}>
{groups.map(([title, items], gi) => ( {groups.map(([title, items]) => (
<div key={title} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}> <PillGroup key={title} label={title}>
<GroupLabel>{title}</GroupLabel>
{items.map(([icon, label, cmd, tip]) => ( {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> </div>
) )
+2
View File
@@ -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 { useState, useEffect, useMemo } from 'react'
import GeschossManager from './components/GeschossManager' import GeschossManager from './components/GeschossManager'
import { import {
+54 -60
View File
@@ -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 { useState, useMemo, useEffect } from 'react'
import Icon from './Icon' 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. // Erzeugt ein vollstaendiges Draft-Array fuer einen Preset.
// Layer die im Preset nicht enthalten sind werden auf Default (visible=true, // 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 [filter, setFilter] = useState('')
const [newName, setNewName] = 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(() => { useEffect(() => {
if (dirty) return if (dirty) return
if (selectedPreset === null) { if (selectedPreset === null) {
@@ -133,17 +146,17 @@ export default function AusschnittLayerDialog({
padding: '12px 16px', padding: '12px 16px',
borderBottom: '1px solid var(--border)', 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 }}> <span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
{snapName} {snapName}
</span> </span>
{dirty && ( {dirty && (
<span style={{ fontSize: 10, color: 'var(--warn)', <span style={{ fontSize: 10, color: 'var(--warn)',
padding: '2px 6px', borderRadius: 'var(--r)', padding: '2px 6px', borderRadius: 999,
background: 'var(--warn-dim)' }} background: 'var(--warn-dim)' }}
title="Ungespeicherte Änderungen"></span> 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> </div>
)} )}
@@ -156,11 +169,9 @@ export default function AusschnittLayerDialog({
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="label-xs" style={{ flexShrink: 0 }}>Kombination</span> <span className="label-xs" style={{ flexShrink: 0 }}>Kombination</span>
<select <BarCombo stretch
value={selectedPreset || ''} value={selectedPreset || ''}
onChange={(ev) => pickPreset(ev.target.value || null)} onChange={(v) => pickPreset(v || null)}>
style={{ flex: 1, fontSize: 11 }}
>
<option value=""> Aktueller Zustand </option> <option value=""> Aktueller Zustand </option>
{presets.length > 0 && <option disabled></option>} {presets.length > 0 && <option disabled></option>}
{presets.map(p => ( {presets.map(p => (
@@ -168,31 +179,20 @@ export default function AusschnittLayerDialog({
{p.name} ({p.layers?.length || 0} Ebenen) {p.name} ({p.layers?.length || 0} Ebenen)
</option> </option>
))} ))}
</select> </BarCombo>
{selectedPreset && ( {selectedPreset && (
<button <BarButton icon="delete"
className="btn-icon-sm"
onClick={deleteSelected} onClick={deleteSelected}
title="Diese Kombination löschen" title="Diese Kombination löschen" />
style={{ color: 'var(--danger)' }}
>
<Icon name="delete" size={13} />
</button>
)} )}
</div> </div>
{selectedPreset && ( {selectedPreset && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<button <BarToggle icon="save" label="Speichern"
className="btn-contained" active={dirty}
onClick={savePresetChanges}
disabled={!dirty} disabled={!dirty}
style={{ fontSize: 10, padding: '3px 10px', onClick={savePresetChanges}
opacity: dirty ? 1 : 0.5 }} title={dirty ? `Änderungen in "${selectedPreset}" speichern` : 'Keine Änderungen'} />
title={dirty ? `Änderungen in "${selectedPreset}" speichern` : 'Keine Änderungen'}
>
<Icon name="save" size={12} />
<span>Speichern</span>
</button>
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}> <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
Änderungen werden NICHT automatisch gespeichert. Änderungen werden NICHT automatisch gespeichert.
</span> </span>
@@ -204,17 +204,12 @@ export default function AusschnittLayerDialog({
onChange={(ev) => setNewName(ev.target.value)} onChange={(ev) => setNewName(ev.target.value)}
onKeyDown={(ev) => { if (ev.key === 'Enter') saveAsNew() }} onKeyDown={(ev) => { if (ev.key === 'Enter') saveAsNew() }}
placeholder="Aktuelle Auswahl als neue Kombination speichern…" placeholder="Aktuelle Auswahl als neue Kombination speichern…"
style={{ flex: 1, fontSize: 10 }} style={{ ...pillInput, flex: 1 }}
/> />
<button <BarToggle icon="add" label="Neu"
className="btn-outlined"
onClick={saveAsNew} onClick={saveAsNew}
disabled={!newName.trim()} disabled={!newName.trim()}
title="Aktuelle Auswahl unter diesem Namen speichern" title="Aktuelle Auswahl unter diesem Namen speichern" />
style={{ fontSize: 10, padding: '3px 8px' }}
>
<Icon name="add" size={12} /> Neu
</button>
</div> </div>
</div> </div>
@@ -228,17 +223,17 @@ export default function AusschnittLayerDialog({
value={filter} value={filter}
onChange={(ev) => setFilter(ev.target.value)} onChange={(ev) => setFilter(ev.target.value)}
placeholder="Filter..." 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"> <BarButton icon="visibility"
<Icon name="visibility" size={12} /> onClick={() => setAll('visible', true)}
</button> title="Alle (gefiltert) sichtbar" />
<button className="btn-icon-xs" onClick={() => setAll('visible', false)} title="Alle (gefiltert) ausblenden"> <BarButton icon="visibility_off"
<Icon name="visibility_off" size={12} /> onClick={() => setAll('visible', false)}
</button> title="Alle (gefiltert) ausblenden" />
<button className="btn-icon-xs" onClick={() => setAll('locked', false)} title="Alle (gefiltert) entsperren"> <BarButton icon="lock_open"
<Icon name="lock_open" size={12} /> onClick={() => setAll('locked', false)}
</button> title="Alle (gefiltert) entsperren" />
</div> </div>
{/* Layer-Liste */} {/* Layer-Liste */}
@@ -254,7 +249,7 @@ export default function AusschnittLayerDialog({
key={l.id} key={l.id}
style={{ style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 8,
padding: '4px 14px', padding: '3px 14px',
borderBottom: '1px solid var(--border-light)', borderBottom: '1px solid var(--border-light)',
background: 'var(--bg-item)', background: 'var(--bg-item)',
opacity: l.visible ? 1 : 0.5, opacity: l.visible ? 1 : 0.5,
@@ -272,16 +267,16 @@ export default function AusschnittLayerDialog({
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
fontFamily: 'var(--font-mono)', fontFamily: 'var(--font-mono)',
}} title={l.fullPath}>{l.fullPath || l.name}</span> }} title={l.fullPath}>{l.fullPath || l.name}</span>
<button <BarButton
className={`btn-icon-xs ${l.visible ? 'is-on' : ''}`} icon={l.visible ? 'visibility' : 'visibility_off'}
active={l.visible}
onClick={() => toggle(l.id, 'visible')} onClick={() => toggle(l.id, 'visible')}
title={l.visible ? 'Ausblenden' : 'Einblenden'} title={l.visible ? 'Ausblenden' : 'Einblenden'} />
><Icon name={l.visible ? 'visibility' : 'visibility_off'} size={12} /></button> <BarButton
<button icon={l.locked ? 'lock' : 'lock_open'}
className={`btn-icon-xs ${l.locked ? 'is-on' : ''}`} active={l.locked}
onClick={() => toggle(l.id, 'locked')} onClick={() => toggle(l.id, 'locked')}
title={l.locked ? 'Entsperren' : 'Sperren'} title={l.locked ? 'Entsperren' : 'Sperren'} />
><Icon name={l.locked ? 'lock' : 'lock_open'} size={12} /></button>
</div> </div>
)) ))
)} )}
@@ -297,12 +292,11 @@ export default function AusschnittLayerDialog({
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}> <div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
{draft.filter(l => l.visible).length} / {draft.length} sichtbar {draft.filter(l => l.visible).length} / {draft.length} sichtbar
</div> </div>
<button className="btn-text" onClick={onClose}>Schliessen</button> <BarToggle label="Schliessen" onClick={onClose} />
<button className="btn-contained" onClick={applyToDoc} <BarToggle icon="check" label="Auf Doc anwenden"
title="Aktuelle Auswahl auf das Dokument anwenden"> active
<Icon name="check" size={12} /> onClick={applyToDoc}
<span>Auf Doc anwenden</span> title="Aktuelle Auswahl auf das Dokument anwenden" />
</button>
</div> </div>
</div> </div>
</div> </div>
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import Icon from './Icon' import Icon from './Icon'
// Gemeinsame Toolbar-Primitiven für Panels im Oberleiste-Stil: // Gemeinsame Toolbar-Primitiven für Panels im Oberleiste-Stil:
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
export default function BottomBar({ onApply, dirty }) { export default function BottomBar({ onApply, dirty }) {
return ( return (
<div style={{ <div style={{
+13 -10
View File
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState } from 'react' import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
import { BarToggle, BarCombo } from './BarControls'
export default function ConfirmDeleteEbene({ ebene, otherEbenen, onConfirm, onCancel }) { export default function ConfirmDeleteEbene({ ebene, otherEbenen, onConfirm, onCancel }) {
const [target, setTarget] = useState(otherEbenen[0]?.code ?? '_delete') 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 }}> <div style={{ padding: '10px 18px 14px', display: 'flex', flexDirection: 'column', gap: 6 }}>
<span className="label-xs">Inhalte auf der Ebene</span> <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 => ( {otherEbenen.map(e => (
<option key={e.code} value={e.code}> Verschieben nach {e.code}_{e.name}</option> <option key={e.code} value={e.code}> Verschieben nach {e.code}_{e.name}</option>
))} ))}
<option value="_delete"> Inhalte ebenfalls löschen</option> <option value="_delete"> Inhalte ebenfalls löschen</option>
</select> </BarCombo>
</div> </div>
<div style={{ <div style={{
display: 'flex', gap: 4, padding: '10px 14px', display: 'flex', gap: 6, padding: '10px 14px',
justifyContent: 'flex-end', justifyContent: 'flex-end',
borderTop: '1px solid var(--border-light)', borderTop: '1px solid var(--border-light)',
background: 'var(--bg-section)', background: 'var(--bg-section)',
}}> }}>
<button className="btn-text" onClick={onCancel}>Abbrechen</button> <BarToggle label="Abbrechen" onClick={onCancel} />
<button <BarToggle
className="btn-contained" label="Löschen"
style={isDelete ? { background: 'var(--danger)' } : undefined} active
onClick={() => onConfirm(isDelete ? null : target)} onClick={() => onConfirm(isDelete ? null : target)}
> />
Löschen
</button>
</div> </div>
</div> </div>
</div> </div>
+2
View File
@@ -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 { useEffect, useRef, useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
+2
View File
@@ -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 { useState, useRef, useMemo, useEffect } from 'react'
import Icon from './Icon' import Icon from './Icon'
import ConfirmDeleteEbene from './ConfirmDeleteEbene' import ConfirmDeleteEbene from './ConfirmDeleteEbene'
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState } from 'react' import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
import { BarCombo } from './BarControls' import { BarCombo } from './BarControls'
+57 -27
View File
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState } from 'react' import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls'
export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, onClose, embedded = false }) { export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState(zeichnungsebenen.map(z => ({ ...z }))) const [draft, setDraft] = useState(zeichnungsebenen.map(z => ({ ...z })))
@@ -46,14 +49,16 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
.filter(z => z.isGeschoss) .filter(z => z.isGeschoss)
.reduce((s, z) => s + (z.hoehe ?? 0), 0) .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 = { const col = {
move: { width: 28, flexShrink: 0 }, move: { width: BAR_H * 2 + 6, flexShrink: 0 },
geschoss:{ width: 24, flexShrink: 0 }, geschoss:{ width: 24, flexShrink: 0 },
name: { flex: 1, minWidth: 60 }, name: { flex: 1, minWidth: 60 },
okff: { width: 50, flexShrink: 0 }, okff: { width: 50, flexShrink: 0 },
hoehe: { width: 64, flexShrink: 0 }, hoehe: { width: 64, flexShrink: 0 },
schnitt: { width: 64, flexShrink: 0 }, schnitt: { width: 64, flexShrink: 0 },
del: { width: 22, flexShrink: 0 }, del: { width: BAR_H, flexShrink: 0 },
} }
const wrapperStyle = embedded ? { const wrapperStyle = embedded ? {
@@ -81,20 +86,34 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
maxHeight: 'calc(100vh - 80px)', maxHeight: 'calc(100vh - 80px)',
overflow: 'hidden', 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 ( return (
<div style={wrapperStyle}> <div style={wrapperStyle}>
<div style={innerStyle}> <div style={innerStyle}>
{/* Toolbar Add-Buttons + Bau-Gesamthoehe. Kein Title-Header mehr;
das Satelliten-Fenster bringt seinen eigenen mit. */}
<div style={{ <div style={{
display: 'flex', alignItems: 'center', gap: 8, display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0, padding: '8px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0,
}}> }}>
<span style={{ flex: 1, fontWeight: 600, fontSize: 12, color: 'var(--text-primary)' }}> <BarToggle icon="add" label="Geschoss" onClick={() => add(true)} />
Zeichnungsebenen <BarToggle icon="add" label="Zeichnung" onClick={() => add(false)} />
</span> <div style={{ flex: 1 }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}> <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
Gebäude {gesamthoehe.toFixed(2)} m Gebäude {gesamthoehe.toFixed(2)} m
</span> </span>
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
</div> </div>
<div style={{ <div style={{
@@ -119,13 +138,15 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
borderBottom: '1px solid var(--border-light)', borderBottom: '1px solid var(--border-light)',
background: i % 2 === 0 ? 'var(--bg-item)' : 'var(--bg-dialog)', background: i % 2 === 0 ? 'var(--bg-item)' : 'var(--bg-dialog)',
}}> }}>
<div style={{ ...col.move, display: 'flex', flexDirection: 'column', gap: 0 }}> <div style={{ ...col.move, display: 'flex', flexDirection: 'row', gap: 2 }}>
<button className="btn-step" onClick={() => move(i, -1)} disabled={i === 0}> <BarButton icon="arrow_drop_up"
<Icon name="arrow_drop_up" size={14} /> onClick={() => move(i, -1)}
</button> disabled={i === 0}
<button className="btn-step" onClick={() => move(i, 1)} disabled={i === draft.length - 1}> title="Nach oben" />
<Icon name="arrow_drop_down" size={14} /> <BarButton icon="arrow_drop_down"
</button> onClick={() => move(i, 1)}
disabled={i === draft.length - 1}
title="Nach unten" />
</div> </div>
<div style={col.geschoss}> <div style={col.geschoss}>
@@ -141,7 +162,19 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
<input <input
value={z.name} value={z.name}
onChange={ev => update(i, 'name', ev.target.value)} 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' }}> <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" <input type="number" step="0.05" min="0.5" max="20"
value={z.hoehe ?? 3.0} value={z.hoehe ?? 3.0}
onChange={ev => update(i, 'hoehe', parseFloat(ev.target.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> <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" <input type="number" step="0.05" min="0.1"
value={z.schnitthoehe ?? 1.0} value={z.schnitthoehe ?? 1.0}
onChange={ev => update(i, 'schnitthoehe', parseFloat(ev.target.value) || 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> <span style={{ color: 'var(--text-muted)', fontSize: 10 }}>m</span>
</> </>
@@ -179,9 +212,10 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
</div> </div>
<div style={col.del}> <div style={col.del}>
<button className="btn-icon-sm" onClick={() => remove(i)} title="Löschen"> <BarButton icon="close"
<Icon name="close" size={14} /> onClick={() => remove(i)}
</button> disabled={draft.length <= 1}
title="Löschen" />
</div> </div>
</div> </div>
))} ))}
@@ -192,13 +226,9 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
padding: '10px 14px', borderTop: '1px solid var(--border)', padding: '10px 14px', borderTop: '1px solid var(--border)',
background: 'var(--bg-section)', flexShrink: 0, 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 }} /> <div style={{ flex: 1 }} />
<button className="btn-text" onClick={onClose}>Abbrechen</button> <BarToggle label="Abbrechen" onClick={onClose} />
<button className="btn-contained" onClick={() => onSave(draft)}>Übernehmen</button> <BarToggle label="Übernehmen" active onClick={() => onSave(draft)} />
</div> </div>
</div> </div>
</div> </div>
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState } from 'react' import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
import ContextMenu from './ContextMenu' import ContextMenu from './ContextMenu'
+41 -25
View File
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState } from 'react' import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls'
/** Vertikales Feld-Layout: Label oben, Input darunter — passt in schmale Panels. */ /** Vertikales Feld-Layout: Label oben, Input darunter — passt in schmale Panels. */
function Field({ label, hint, children }) { 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 }) { export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState({ ...geschoss }) const [draft, setDraft] = useState({ ...geschoss })
const set = (patch) => setDraft({ ...draft, ...patch }) const set = (patch) => setDraft({ ...draft, ...patch })
@@ -95,14 +111,14 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
padding: '10px 12px', padding: '10px 12px',
borderBottom: '1px solid var(--border)', 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={{ <span style={{
flex: 1, fontWeight: 600, fontSize: 11, flex: 1, fontWeight: 600, fontSize: 11,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}> }}>
{geschoss.name} {geschoss.name}
</span> </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> </div>
{/* Body */} {/* Body */}
@@ -111,7 +127,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
<input <input
value={draft.name} value={draft.name}
onChange={(ev) => set({ name: ev.target.value })} 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> </Field>
@@ -145,7 +161,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
type="number" step="0.5" min="0.5" type="number" step="0.5" min="0.5"
value={depthBack} value={depthBack}
onChange={(ev) => set({ depthBack: parseFloat(ev.target.value) || 8.0 })} 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> </Field>
@@ -155,7 +171,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
type="number" step="0.1" type="number" step="0.1"
value={heightMin} value={heightMin}
onChange={(ev) => set({ heightMin: parseFloat(ev.target.value) })} 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>
<Field label="HÖHE OBEN (m)"> <Field label="HÖHE OBEN (m)">
@@ -163,31 +179,31 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
type="number" step="0.1" type="number" step="0.1"
value={heightMax} value={heightMax}
onChange={(ev) => set({ heightMax: parseFloat(ev.target.value) })} 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> </Field>
</div> </div>
<Field label="BLICKRICHTUNG" <Field label="BLICKRICHTUNG"
hint="Wechselt zwischen den beiden Seiten der Schnittlinie"> hint="Wechselt zwischen den beiden Seiten der Schnittlinie">
<button className={dirSign >= 0 ? 'btn-contained' : 'btn-outlined'} <BarToggle label="← Seite A"
onClick={() => set({ dirSign: 1 })} active={dirSign >= 0}
style={{ flex: 1, fontSize: 11 }}> Seite A</button> onClick={() => set({ dirSign: 1 })} />
<button className={dirSign < 0 ? 'btn-contained' : 'btn-outlined'} <BarToggle label="Seite B →"
onClick={() => set({ dirSign: -1 })} active={dirSign < 0}
style={{ flex: 1, fontSize: 11 }}>Seite B </button> onClick={() => set({ dirSign: -1 })} />
</Field> </Field>
<Field label="PROJEKTION" <Field label="PROJEKTION"
hint={projection === 'perspective' hint={projection === 'perspective'
? 'Schnittperspektive — perspektivische Section mit gleichem Clipping. Cutaway-Visualisierung.' ? 'Schnittperspektive — perspektivische Section mit gleichem Clipping. Cutaway-Visualisierung.'
: 'Klassischer Schnitt — Parallelprojektion, masstabsgetreu.'}> : 'Klassischer Schnitt — Parallelprojektion, masstabsgetreu.'}>
<button className={projection === 'parallel' ? 'btn-contained' : 'btn-outlined'} <BarToggle label="Parallel"
onClick={() => set({ projection: 'parallel' })} active={projection === 'parallel'}
style={{ flex: 1, fontSize: 11 }}>Parallel</button> onClick={() => set({ projection: 'parallel' })} />
<button className={projection === 'perspective' ? 'btn-contained' : 'btn-outlined'} <BarToggle label="Perspektive"
onClick={() => set({ projection: 'perspective' })} active={projection === 'perspective'}
style={{ flex: 1, fontSize: 11 }}>Perspektive</button> onClick={() => set({ projection: 'perspective' })} />
</Field> </Field>
{projection === 'perspective' && ( {projection === 'perspective' && (
@@ -197,7 +213,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
type="number" step="0.1" type="number" step="0.1"
value={cameraHeight} value={cameraHeight}
onChange={(ev) => set({ cameraHeight: parseFloat(ev.target.value) })} 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> </Field>
)} )}
@@ -213,7 +229,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
type="number" step="0.05" min="0.5" max="30" type="number" step="0.05" min="0.5" max="30"
value={hoehe} value={hoehe}
onChange={(ev) => set({ hoehe: parseFloat(ev.target.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> </Field>
@@ -222,7 +238,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
type="number" step="0.05" min="0.1" type="number" step="0.05" min="0.1"
value={schnitt} value={schnitt}
onChange={(ev) => set({ schnitthoehe: parseFloat(ev.target.value) || 1.0 })} 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> </Field>
@@ -250,7 +266,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
type="number" step="0.01" type="number" step="0.01"
value={draft.projectZeroMum ?? 0} value={draft.projectZeroMum ?? 0}
onChange={(ev) => set({ projectZeroMum: parseFloat(ev.target.value) || 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)' }} fontFamily: 'var(--font-mono)' }}
/> />
</Field> </Field>
@@ -264,8 +280,8 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
background: 'var(--bg-section)', background: 'var(--bg-section)',
}}> }}>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<button className="btn-text" onClick={onClose}>Abbrechen</button> <BarToggle label="Abbrechen" onClick={onClose} />
<button className="btn-contained" onClick={() => { <BarToggle label="Übernehmen" active onClick={() => {
// Numerische Felder NIEMALS als undefined/null rausgehen lassen // Numerische Felder NIEMALS als undefined/null rausgehen lassen
// sonst crasht der Plugin spaeter beim float()-Cast. Defaults // sonst crasht der Plugin spaeter beim float()-Cast. Defaults
// entsprechen den Werten die das UI auch ohne User-Input zeigt. // 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) onSave(out)
}}>Übernehmen</button> }} />
</div> </div>
</div> </div>
</Wrapper> </Wrapper>
+2
View File
@@ -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 }) { export default function Icon({ name, size = 18, fill = 0, weight = 400, style }) {
return ( return (
<span <span
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useMemo } from 'react' import { useState, useMemo } from 'react'
import Icon from './Icon' import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls' import { BarToggle, BarButton, BAR_H } from './BarControls'
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import Icon from './Icon' import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls' import { BarToggle, BarButton, BAR_H } from './BarControls'
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState } from 'react' import { useState } from 'react'
import Icon from './Icon' import Icon from './Icon'
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useMemo } from 'react' import { useState, useMemo } from 'react'
import Icon from './Icon' import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls' import { BarToggle, BarButton, BAR_H } from './BarControls'
+11 -2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
/** /**
* rhinoBridge.js Kommunikation React Rhino/Python * rhinoBridge.js Kommunikation React Rhino/Python
* Lange Payloads werden in Chunks <700 Zeichen aufgeteilt (document.title-Limit). * Lange Payloads werden in Chunks <700 Zeichen aufgeteilt (document.title-Limit).
@@ -241,8 +243,15 @@ export function saveOeffStyle(name, settings) {
send('SAVE_OEFF_STYLE', { name, settings }) send('SAVE_OEFF_STYLE', { name, settings })
} }
export function deleteOeffStyle(id) { send('DELETE_OEFF_STYLE', { id }) } export function deleteOeffStyle(id) { send('DELETE_OEFF_STYLE', { id }) }
export function setSectionStyle(enabled, source, color, pattern, scale, rotation) { export function setSectionStyle(enabled, source, color, pattern, scale, rotation,
send('SET_SECTION_STYLE', { enabled, source, color, pattern, scale, rotation }) opts = {}) {
send('SET_SECTION_STYLE', {
enabled, source, color, pattern, scale, rotation,
boundaryVisible: opts.boundaryVisible,
boundaryWidthScale: opts.boundaryWidthScale,
boundaryColor: opts.boundaryColor,
backgroundColor: opts.backgroundColor, // null = transparent (Viewport), Hex = SolidColor
})
} }
export function openAbout() { send('OPEN_ABOUT', {}) } export function openAbout() { send('OPEN_ABOUT', {}) }
export function createText() { send('CREATE_TEXT', {}) } export function createText() { send('CREATE_TEXT', {}) }
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
+2
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Karim Gabriele Varano
""" """
export_icons.py export_icons.py
Laedt alle Material-Symbols-Outlined Icons herunter die unsere WERKZEUGE- Laedt alle Material-Symbols-Outlined Icons herunter die unsere WERKZEUGE-
+2
View File
@@ -1,3 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import pkg from './package.json' with { type: 'json' } import pkg from './package.json' with { type: 'json' }