forked from 1b-agent/opencadstudio-upstream
Merge pull request #1798 from Pictus08/feat/autocad-pgp-ucs-follow
Import AutoCAD PGP aliases and support UCSFOLLOW workflows. The UCS conflict keeps main's click-origin flow (axis preview, Enter accepts the placed axes) and takes the PR's prompt and option keywords.
This commit is contained in:
commit
87c44cee78
39 files changed
+809
-29
No files matched your search
@@ -0,0 +1,40 @@
|
|||||||
|
# Importing AutoCAD command aliases (.pgp)
|
||||||
|
|
||||||
|
1. Run `ALIASEDIT` in OpenCADStudio.
|
||||||
|
2. Choose **Import PGP…** and select an AutoCAD or OpenCAD `.pgp` file.
|
||||||
|
3. Review the working rows. Supported imported aliases override matching
|
||||||
|
existing aliases; other existing aliases are retained.
|
||||||
|
4. Choose **Apply** to activate and save the rows, or close and discard them.
|
||||||
|
|
||||||
|
There is no automatic discovery or loading of AutoCAD profiles. Importing only
|
||||||
|
changes the editor's working rows; it never changes the active table until
|
||||||
|
Apply. The source PGP is not modified.
|
||||||
|
|
||||||
|
## Format and compatibility
|
||||||
|
|
||||||
|
- UTF-8 (with or without BOM) and legacy Windows-1252 are accepted.
|
||||||
|
- `;` comments, whitespace padding and case-insensitive names are supported.
|
||||||
|
- The last definition of an alias wins, including user overrides at the end.
|
||||||
|
- Common Spanish command targets are translated, for example `COPIA` to
|
||||||
|
`COPY`, `EMPALME` to `FILLET` and `SCP` to `UCS`. Alias keys are not translated.
|
||||||
|
- Only registered command targets are imported. Unsupported commands and
|
||||||
|
unmapped translations are counted in the editor's summary and do not replace
|
||||||
|
existing aliases. Full diagnostics are logged, rather than flooding the
|
||||||
|
command line with errors. This does not implement missing AutoCAD/Civil 3D
|
||||||
|
commands or add support for every AutoCAD language.
|
||||||
|
- External-program definitions are ignored and never executed.
|
||||||
|
|
||||||
|
## Synthetic example
|
||||||
|
|
||||||
|
This minimal example is authored for testing, not copied from an Autodesk PGP:
|
||||||
|
|
||||||
|
```text
|
||||||
|
; Last definition wins; imported aliases remain drafts until Apply.
|
||||||
|
E, *BORRA
|
||||||
|
E, *EMPALME
|
||||||
|
CC, *COPIA
|
||||||
|
SQ, *SCP
|
||||||
|
```
|
||||||
|
|
||||||
|
After importing, `E` resolves to `FILLET`, `CC` to `COPY`, and `SQ` to `UCS`.
|
||||||
|
No proprietary PGP or user profile is included with this feature.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# UCS input and UCSFOLLOW
|
||||||
|
|
||||||
|
## Direct point input
|
||||||
|
|
||||||
|
After running `UCS`, a click or typed coordinate can immediately supply the
|
||||||
|
new origin, followed by a point on the positive X axis and a point in the
|
||||||
|
positive XY plane. Explicit `3P` input continues to work. `Origin` / `O` still
|
||||||
|
changes only the origin with one point.
|
||||||
|
|
||||||
|
The interactive option prompt also accepts Spanish keywords, including
|
||||||
|
`Universal`, `Vista`, `Cara`, `Objeto`, `Origen`, `Guardar`, `Eliminar`,
|
||||||
|
and `3p` / `3 puntos`, as well as international English option keywords.
|
||||||
|
|
||||||
|
## UCSFOLLOW
|
||||||
|
|
||||||
|
- `UCSFOLLOW 0`: keep the camera orientation when the UCS changes.
|
||||||
|
- `UCSFOLLOW 1`: align the view immediately with the current UCS XY plane and
|
||||||
|
follow later UCS changes. Re-entering `1` realigns after a manual orbit.
|
||||||
|
- Bare `UCSFOLLOW` prompts for the value; Enter keeps the current setting.
|
||||||
|
- `SETVAR UCSFOLLOW 0` and `SETVAR UCSFOLLOW 1` work too.
|
||||||
|
|
||||||
|
The value belongs to the model pane / floating viewport and uses the drawing's
|
||||||
|
existing VPORT/VIEWPORT fields, not a global application preference. Paper
|
||||||
|
space itself does not follow the UCS: enter a model viewport first. BEDIT's
|
||||||
|
special block-reframing path is intentionally left unchanged.
|
||||||
|
|
||||||
|
The immediate alignment on entering `1` is deliberate: users can define their
|
||||||
|
UCS first, then enable following without another UCS change.
|
||||||
|
|
||||||
|
A user-defined alias such as `UF,*UCSFOLLOW` can invoke this command. It is not
|
||||||
|
installed automatically. A PGP imported before this command was implemented
|
||||||
|
must be imported again (or edited manually) to add its previously skipped
|
||||||
|
alias; activating it still requires Apply.
|
||||||
@@ -6599,3 +6599,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6587,3 +6587,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6584,3 +6584,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6585,3 +6585,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6587,3 +6587,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -4964,7 +4964,7 @@ view =
|
|||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: not yet implemented.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: not yet implemented.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ viewport(s) in layout "__ocs_fmt_1__":
|
.viewport-s-in-layout = __ocs_fmt_0__ viewport(s) in layout "__ocs_fmt_1__":
|
||||||
|
|
||||||
.ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name = UCS option [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:
|
.ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name = UCS specify new origin or [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:
|
||||||
.ucs-specify-new-origin = UCS specify new origin:
|
.ucs-specify-new-origin = UCS specify new origin:
|
||||||
.ucs-specify-point-on-positive-x-axis = UCS specify point on positive X axis:
|
.ucs-specify-point-on-positive-x-axis = UCS specify point on positive X axis:
|
||||||
.ucs-specify-point-in-positive-xy-plane = UCS specify point in positive XY plane:
|
.ucs-specify-point-in-positive-xy-plane = UCS specify point in positive XY plane:
|
||||||
@@ -6586,3 +6586,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -4965,7 +4965,7 @@ view =
|
|||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: aún no implementado.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: aún no implementado.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ ventanas gráficas en la presentación «__ocs_fmt_1__»:
|
.viewport-s-in-layout = __ocs_fmt_0__ ventanas gráficas en la presentación «__ocs_fmt_1__»:
|
||||||
|
|
||||||
.ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name = UCS { common.specify }: { action.options } [{ common.world }/{ ribbon-tab.view }/{ draw.value-3-point }/Z/X/Y/{ common.origin }/{ common.save }/{ common.delete }] ({ common.name-title-case }):
|
.ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name = UCS Precisa el nuevo origen o [Cara/Objeto/Universal/Vista/3p/Z/X/Y/Origen/Guardar/Eliminar] o nombre:
|
||||||
.ucs-specify-new-origin = UCS { common.specify }: { common.new-origin }:
|
.ucs-specify-new-origin = UCS { common.specify }: { common.new-origin }:
|
||||||
.ucs-specify-point-on-positive-x-axis = UCS { common.specify }: X · { common.positive-direction } · { common.point }:
|
.ucs-specify-point-on-positive-x-axis = UCS { common.specify }: X · { common.positive-direction } · { common.point }:
|
||||||
.ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }:
|
.ucs-specify-point-in-positive-xy-plane = UCS { common.specify }: XY · { common.positive-direction } · { common.point }:
|
||||||
@@ -6587,3 +6587,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Importar PGP…
|
||||||
|
.filter = Alias de comandos (*.pgp)
|
||||||
|
.empty = PGP: no se encontraron alias válidos. No se modificó el borrador.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ atajos cargados para revisar; __ocs_fmt_1__ comandos no compatibles omitidos. No se activó ningún cambio. Pulsa Aplicar para guardar.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: introduce 0 (conservar vista) o 1 (seguir SCP) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activa una ventana de modelo; no se aplica al espacio papel.
|
||||||
|
.invalid = UCSFOLLOW: solo se permite 0 o 1.
|
||||||
@@ -6584,3 +6584,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6587,3 +6587,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6575,3 +6575,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6571,3 +6571,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Előzetes összterület = { $total }
|
Előzetes összterület = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6586,3 +6586,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6575,3 +6575,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6586,3 +6586,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6587,3 +6587,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6585,3 +6585,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6586,3 +6586,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6587,3 +6587,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6588,3 +6588,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6508,3 +6508,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6570,3 +6570,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
@@ -6582,3 +6582,13 @@ measurement =
|
|||||||
.area-preview =
|
.area-preview =
|
||||||
{ $current }
|
{ $current }
|
||||||
Preview total area = { $total }
|
Preview total area = { $total }
|
||||||
|
pgp-import =
|
||||||
|
.button = Import PGP…
|
||||||
|
.filter = Command aliases (*.pgp)
|
||||||
|
.empty = PGP: no valid aliases found. The working rows were not changed.
|
||||||
|
.summary = PGP: __ocs_fmt_0__ alias(es) loaded for review; __ocs_fmt_1__ unsupported command(s) skipped. No changes activated. Apply to save.
|
||||||
|
|
||||||
|
ucs-follow =
|
||||||
|
.prompt = UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <__ocs_fmt_0__>:
|
||||||
|
.paper-space = UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.
|
||||||
|
.invalid = UCSFOLLOW: only 0 or 1 is allowed.
|
||||||
+53
-17
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
use super::OpenCADStudio;
|
use super::OpenCADStudio;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
mod pgp;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -144,23 +145,7 @@ fn migrate_aliases(stored: &mut FxHashMap<String, String>, from_version: u32) ->
|
|||||||
/// lines and `;` comments; tolerates a leading `*` on the command and arbitrary
|
/// lines and `;` comments; tolerates a leading `*` on the command and arbitrary
|
||||||
/// whitespace padding.
|
/// whitespace padding.
|
||||||
fn parse_pgp(body: &str) -> FxHashMap<String, String> {
|
fn parse_pgp(body: &str) -> FxHashMap<String, String> {
|
||||||
let mut map = FxHashMap::default();
|
pgp::parse(body).into_iter().collect()
|
||||||
for line in body.lines() {
|
|
||||||
let line = line.trim();
|
|
||||||
if line.is_empty() || line.starts_with(';') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some((alias, cmd)) = line.split_once(',') else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let alias = alias.trim().to_uppercase();
|
|
||||||
let cmd = cmd.trim().trim_start_matches('*').trim().to_uppercase();
|
|
||||||
if alias.is_empty() || cmd.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
map.insert(alias, cmd);
|
|
||||||
}
|
|
||||||
map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize an `alias → command` map to `.pgp` text with a header and aligned
|
/// Serialize an `alias → command` map to `.pgp` text with a header and aligned
|
||||||
@@ -300,6 +285,41 @@ pub(super) fn save_map(map: &FxHashMap<String, String>) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
|
/// Merge into the working buffer only. Apply persists; closing can discard.
|
||||||
|
pub(super) fn import_alias_pgp(&mut self, bytes: &[u8]) {
|
||||||
|
let parsed = pgp::parse(&pgp::decode(bytes));
|
||||||
|
let known = crate::command::all_registered_command_names();
|
||||||
|
let mut rows: FxHashMap<String, String> = self.alias_editor_rows.iter()
|
||||||
|
.filter(|(a, c)| !a.is_empty() && !c.is_empty()).cloned().collect();
|
||||||
|
let mut imported = 0;
|
||||||
|
let mut unsupported = std::collections::BTreeSet::new();
|
||||||
|
for (alias, target) in parsed {
|
||||||
|
let command = pgp::canonical(&target);
|
||||||
|
if known.iter().any(|name| name.eq_ignore_ascii_case(&command)) {
|
||||||
|
rows.insert(alias, command);
|
||||||
|
imported += 1;
|
||||||
|
} else {
|
||||||
|
unsupported.insert(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if imported > 0 {
|
||||||
|
self.alias_editor_rows = rows.into_iter().collect();
|
||||||
|
self.alias_editor_rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
self.alias_pending_add = false;
|
||||||
|
self.alias_reset_confirm = false;
|
||||||
|
self.alias_close_confirm = false;
|
||||||
|
}
|
||||||
|
self.alias_import_report = if imported == 0 && unsupported.is_empty() {
|
||||||
|
crate::t!("PGP: no valid aliases found. The working rows were not changed.").into_owned()
|
||||||
|
} else {
|
||||||
|
crate::tf!("PGP: {} alias(es) loaded for review; {} unsupported command(s) skipped. No changes activated. Apply to save.", imported, unsupported.len()).into_owned()
|
||||||
|
};
|
||||||
|
self.command_line.push_info(&self.alias_import_report);
|
||||||
|
// Unsupported AutoCAD/Civil 3D commands are an expected compatibility
|
||||||
|
// limitation, not import errors. Keep full diagnostics out of the UI.
|
||||||
|
log::info!("PGP import: {imported} aliases; unsupported targets: {unsupported:?}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Rewrite the leading command token through the alias table, leaving any
|
/// Rewrite the leading command token through the alias table, leaving any
|
||||||
/// arguments after the first whitespace untouched. Returns `None` when the
|
/// arguments after the first whitespace untouched. Returns `None` when the
|
||||||
/// verb is not an alias, so the caller passes the original string through
|
/// verb is not an alias, so the caller passes the original string through
|
||||||
@@ -418,6 +438,22 @@ impl OpenCADStudio {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn import_merges_supported_aliases_without_activating_them() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let original = app.command_aliases.clone();
|
||||||
|
app.alias_editor_rows = vec![("KEEP".into(), "LINE".into()), ("E".into(), "ERASE".into())];
|
||||||
|
app.import_alias_pgp(b"E,*BORRA\nE,*EMPALME\nSQ,*SCP\nCIVIL,*AECCUNSUPPORTED\nNOTEPAD,START NOTEPAD,1,,\n");
|
||||||
|
assert!(app.alias_editor_rows.contains(&("E".into(), "FILLET".into())));
|
||||||
|
assert!(app.alias_editor_rows.contains(&("SQ".into(), "UCS".into())));
|
||||||
|
assert!(app.alias_editor_rows.contains(&("KEEP".into(), "LINE".into())));
|
||||||
|
assert!(!app.alias_editor_rows.iter().any(|(a, _)| a == "CIVIL" || a == "NOTEPAD"));
|
||||||
|
assert_eq!(app.command_aliases, original, "import must not activate or persist aliases");
|
||||||
|
let staged = app.alias_editor_rows.clone();
|
||||||
|
app.import_alias_pgp(b"; no aliases\n");
|
||||||
|
assert_eq!(app.alias_editor_rows, staged);
|
||||||
|
}
|
||||||
|
|
||||||
fn map(pairs: &[(&str, &str)]) -> FxHashMap<String, String> {
|
fn map(pairs: &[(&str, &str)]) -> FxHashMap<String, String> {
|
||||||
pairs
|
pairs
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
//! AutoCAD PGP interoperability. Never interpret external-command definitions.
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
/// UTF-8 (including BOM), or the Windows-1252 encoding used by legacy acad.pgp.
|
||||||
|
pub(super) fn decode(bytes: &[u8]) -> String {
|
||||||
|
if let Ok(text) = std::str::from_utf8(bytes) {
|
||||||
|
return text.trim_start_matches('\u{feff}').to_owned();
|
||||||
|
}
|
||||||
|
const HIGH: [char; 32] = [
|
||||||
|
'€', '\u{81}', '‚', 'ƒ', '„', '…', '†', '‡', 'ˆ', '‰', 'Š', '‹', 'Œ', '\u{8d}', 'Ž', '\u{8f}',
|
||||||
|
'\u{90}', '‘', '’', '“', '”', '•', '–', '—', '˜', '™', 'š', '›', 'œ', '\u{9d}', 'ž', 'Ÿ',
|
||||||
|
];
|
||||||
|
bytes.iter().map(|&b| match b {
|
||||||
|
0x80..=0x9f => HIGH[(b - 0x80) as usize],
|
||||||
|
_ => char::from(b),
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Last definition wins, as in AutoCAD's user-defined aliases section.
|
||||||
|
/// Accept OpenCAD's optional star too, but reject all multi-field shell rows.
|
||||||
|
pub(super) fn parse(body: &str) -> BTreeMap<String, String> {
|
||||||
|
let mut aliases = BTreeMap::new();
|
||||||
|
for line in body.trim_start_matches('\u{feff}').lines() {
|
||||||
|
let line = line.split(';').next().unwrap_or("").trim();
|
||||||
|
let Some((alias, target)) = line.split_once(',') else { continue };
|
||||||
|
let alias = alias.trim().to_uppercase();
|
||||||
|
let target = target.trim().strip_prefix('*').unwrap_or(target.trim()).trim().to_uppercase();
|
||||||
|
if alias.is_empty() || target.is_empty() || target.contains(',')
|
||||||
|
|| alias.chars().any(char::is_whitespace) || target.chars().any(char::is_whitespace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
aliases.insert(alias, target);
|
||||||
|
}
|
||||||
|
aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translate command names, never alias keys (e.g. the user's E means FILLET).
|
||||||
|
pub(super) fn canonical(target: &str) -> String {
|
||||||
|
let target = target.trim_start_matches(['_', '.']);
|
||||||
|
let (prefix, name) = target.strip_prefix('-').map_or(("", target), |s| ("-", s));
|
||||||
|
let name = match name {
|
||||||
|
"LINEA" => "LINE", "POL" | "POLILINEA" => "PLINE", "3DPOL" => "3DPOLY",
|
||||||
|
"CIRCULO" => "CIRCLE", "ARCO" => "ARC", "RECTANG" => "RECTANG",
|
||||||
|
"ELIPSE" => "ELLIPSE", "POLIGONO" => "POLYGON", "ARANDELA" => "DONUT",
|
||||||
|
"PUNTO" => "POINT", "COPIA" => "COPY", "DESPLAZA" => "MOVE",
|
||||||
|
"DESFASE" => "OFFSET", "GIRA" => "ROTATE", "SIMETRIA" => "MIRROR",
|
||||||
|
"ESCALA" => "SCALE", "ESTIRA" => "STRETCH", "RECORTA" => "TRIM",
|
||||||
|
"ALARGA" => "EXTEND", "EMPALME" => "FILLET", "CHAFLAN" => "CHAMFER",
|
||||||
|
"BORRA" => "ERASE", "DESCOMP" => "EXPLODE", "JUNTAR" | "UNIR" => "JOIN",
|
||||||
|
"PARTE" => "BREAK", "PARTIRENPUNTO" => "BREAKATPOINT", "MATRIZ" => "ARRAY",
|
||||||
|
"ALINEAR" => "ALIGN", "LONGITUD" => "LENGTHEN", "EDITPOL" => "PEDIT",
|
||||||
|
"SOMBREA" => "HATCH", "EDITSOMB" => "HATCHEDIT", "CONTORNO" => "BOUNDARY",
|
||||||
|
"CAPA" => "LAYER", "CAMBCAPAMULT" => "LAYMCH", "IGUALARPROP" => "MATCHPROP",
|
||||||
|
"PROPIEDADES" => "PROPERTIES", "CAMBIA" => "CHANGE", "TIPOLIN" => "LINETYPE",
|
||||||
|
"GROSORLIN" => "LWEIGHT", "ESCALATL" => "LTSCALE",
|
||||||
|
"TEXTO" => "TEXT", "TEXTOM" => "MTEXT", "EDITARTEXTO" => "TEXTEDIT",
|
||||||
|
"EDITTXTM" => "MTEDIT", "ESTILO" => "STYLE", "BUSCAR" => "FIND",
|
||||||
|
"BLOQUE" => "BLOCK", "BLOQUEDISC" => "WBLOCK", "MODBLQ" => "BEDIT",
|
||||||
|
"ATRDEF" => "ATTDEF", "ATREDIT" => "ATTEDIT", "ATREXT" => "ATTEXT",
|
||||||
|
"ACOALINEADA" => "DIMALIGNED", "ACOANGULO" => "DIMANGULAR",
|
||||||
|
"ACOLINEAL" => "DIMLINEAR", "ACORADIO" => "DIMRADIUS",
|
||||||
|
"ACODIAMETRO" => "DIMDIAMETER", "ACOCOORDENADA" => "DIMORDINATE",
|
||||||
|
"ACOLINEABASE" => "DIMBASELINE", "ACOCONTINUA" => "DIMCONTINUE",
|
||||||
|
"ACOESTIL" => "DIMSTYLE", "ACOEDIC" => "DIMEDIT", "ACOTEDIC" => "DIMTEDIT",
|
||||||
|
"DIRECTRIZ" => "LEADER", "DIRECTRIZR" => "QLEADER",
|
||||||
|
"SCP" => "UCS", "ADMINSCP" => "UCSMAN", "VISTA" => "VIEW",
|
||||||
|
"VISTADIN" => "DVIEW", "3DORBITA" => "3DORBIT", "3DCARA" => "3DFACE",
|
||||||
|
"ENCUADRE" => "PAN", "REDIBUJA" => "REDRAW", "REDIBT" => "REDRAWALL",
|
||||||
|
"REGENERA" => "REGEN", "REGENT" => "REGENALL", "OCULTA" => "HIDE",
|
||||||
|
"ABRE" => "OPEN", "NUEVO" => "NEW", "GUARDAR" => "QSAVE",
|
||||||
|
"GUARDARCOMO" => "SAVEAS", "TRAZAR" => "PLOT", "IMPORTAR" => "IMPORT",
|
||||||
|
"EXPORTAR" => "EXPORT", "EXPORTARPDF" => "EXPORTPDF", "OPCIONES" => "OPTIONS",
|
||||||
|
"UNIDADES" => "UNITS", "PARAMSDIB" => "DSETTINGS", "DIST" => "DIST",
|
||||||
|
"LIST" => "LIST", "EXTRUSION" => "EXTRUDE", "REVOLUCION" => "REVOLVE",
|
||||||
|
"INTERSEC" => "INTERSECT", "DIFERENCIA" => "SUBTRACT", "REGION" => "REGION",
|
||||||
|
"ORDENAOBJETOS" => "DRAWORDER", "IMAGEN" => "IMAGE",
|
||||||
|
"ENLAZARIMG" => "IMAGEATTACH", "DELIMITARIMG" => "IMAGECLIP",
|
||||||
|
"AJUSTARIMG" => "IMAGEADJUST", "REFX" => "XREF",
|
||||||
|
"ESPACIOM" => "MSPACE", "ESPACIOP" => "PSPACE", "VMULT" => "MVIEW",
|
||||||
|
"REFENT" => "OSNAP", "PEGAESP" => "PASTESPEC", "CERPROP" => "PROPERTIESCLOSE",
|
||||||
|
"PREVISUALIZAR" => "PREVIEW", "LIMPIA" => "PURGE", "RENOMBRA" => "RENAME",
|
||||||
|
"MODIVAR" => "SETVAR", "MODOSOMBRA" => "SHADEMODE", "SECCION" => "SECTION",
|
||||||
|
"CORTE" => "SLICE", "FORZCURSOR" => "SNAP", "ORTOGRAFIA" => "SPELL",
|
||||||
|
"EDITSPLINE" => "SPLINEDIT", "TOLERANCIA" => "TOLERANCE", "TOROIDE" => "TORUS",
|
||||||
|
"PTOVISTA" => "VPOINT", "CUÑA" => "WEDGE", "LINEAX" => "XLINE",
|
||||||
|
"ENLAZARX" => "XATTACH", "UNIRX" => "XBIND", "DELIMITARX" => "XCLIP",
|
||||||
|
"TABLA" => "TABLE", "ESTILOTABLA" => "TABLESTYLE", "MARCAREV" => "REVCLOUD",
|
||||||
|
"CERRARBLOQUE" => "BCLOSE", "EDITARBLOQUE" => "BEDIT", "GUARDARBLOQUE" => "BSAVE",
|
||||||
|
"ACOARCO" => "DIMARC", "ACORECODO" => "DIMJOGGED", "DEGRADADO" => "GRADIENT",
|
||||||
|
"RECUPDIBUJO" => "RECOVER", "CALCURAPIDA" => "QUICKCALC", "ESTADOVISBLOQUE" => "BVSTATE",
|
||||||
|
"SIMETRIA3D" => "MIRROR3D", "DESPLAZA3D" => "3DMOVE", "GIRA3D" => "3DROTATE",
|
||||||
|
"ESCALA3D" => "3DSCALE", "ALINEAR3D" => "3DALIGN", "CAMARA" => "CAMERA",
|
||||||
|
"CILINDRO" => "CYLINDER", "REFEXTERNAS" => "EXTERNALREFERENCES", "GEOPLANA" => "FLATSHOT",
|
||||||
|
"POLISOLIDO" => "POLYSOLID", "PIRAMIDE" => "PYRAMID", "PLANOSECCION" => "SECTIONPLANE",
|
||||||
|
"ESTILVISUAL" => "VISUALSTYLES", "ESTVISACTUAL" => "VSCURRENT", "EDITATRINSITU" => "ATTIPEDIT",
|
||||||
|
"EXTRACDAT" => "DATAEXTRACTION", "ESTADOCAPAS" => "LAYERSTATE", "LUZPUNTUAL" => "POINTLIGHT",
|
||||||
|
"DIRECTRIZM" => "MLEADER", "ESTILDIRECTRIZM" => "MLEADERSTYLE", "EDITARDIRECTRIZM" => "MLEADEREDIT",
|
||||||
|
"ALINDIRECTRIZM" => "MLEADERALIGN", "COLECDIRECTRIZM" => "MLEADERCOLLECT",
|
||||||
|
"MEDIRGEOM" => "MEASUREGEOM", "PARAMETROS" => "PARAMETERS", "TIPOPUNTO" => "PTYPE",
|
||||||
|
"ALINEARTEXTO" => "TEXTALIGN", "ENLAZARNUBEPUNTOS" => "POINTCLOUDATTACH",
|
||||||
|
"RECORTANUBEPUNTOS" => "POINTCLOUDCLIP", "QUITA" => "REMOVE",
|
||||||
|
"ACOCENTRO" => "DIMCENTER", "ACOREMPLAZAR" => "DIMOVERRIDE", "GRADUA" => "MEASURE",
|
||||||
|
"LINEAM" => "MLINE", "GRUPO" => "GROUP", "FILTRO" => "FILTER",
|
||||||
|
"UNION" => "UNION", "INSERTOBJ" => "INSERTOBJ",
|
||||||
|
_ => name,
|
||||||
|
};
|
||||||
|
format!("{prefix}{name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn user_overrides_and_external_commands() {
|
||||||
|
let rows = parse("\u{feff}; header\r\nB,*BORRA\r\nNOTEPAD, START NOTEPAD,1,*File:,\nB,*BUSCAR ; custom\nE,*EMPALME\nBAD,*LINE,extra\nL,LINE\n");
|
||||||
|
assert_eq!(rows.len(), 3);
|
||||||
|
assert_eq!(canonical(&rows["B"]), "FIND");
|
||||||
|
assert_eq!(canonical(&rows["E"]), "FILLET");
|
||||||
|
assert_eq!(rows["L"], "LINE");
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn localized_targets_and_prefixes() {
|
||||||
|
for (input, expected) in [("_LINE", "LINE"), ("._-CAPA", "-LAYER"),
|
||||||
|
("COPIA", "COPY"), ("SCP", "UCS"), ("AECCUNKNOWN", "AECCUNKNOWN")] {
|
||||||
|
assert_eq!(canonical(input), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn legacy_encoding_and_utf8_bom() {
|
||||||
|
assert_eq!(decode(b"; par\xe1metros\nL,*LINEA"), "; parámetros\nL,*LINEA");
|
||||||
|
assert_eq!(decode("\u{feff}L,*LINEA".as_bytes()), "L,*LINEA");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1770,7 +1770,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plan_snap(&mut self, i: usize, r_ucs: glam::Mat4) {
|
pub(in crate::app) fn plan_snap(&mut self, i: usize, r_ucs: glam::Mat4) {
|
||||||
let eye_dir = r_ucs.transform_vector3(
|
let eye_dir = r_ucs.transform_vector3(
|
||||||
crate::scene::CubeRegion::Face(crate::scene::pipeline::viewcube::FACE_TOP)
|
crate::scene::CubeRegion::Face(crate::scene::pipeline::viewcube::FACE_TOP)
|
||||||
.snap_direction(),
|
.snap_direction(),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ mod plotvars;
|
|||||||
mod point_cloud;
|
mod point_cloud;
|
||||||
pub(crate) mod sheet_set;
|
pub(crate) mod sheet_set;
|
||||||
mod styleprops;
|
mod styleprops;
|
||||||
|
mod ucsfollow;
|
||||||
mod view;
|
mod view;
|
||||||
mod view_presets;
|
mod view_presets;
|
||||||
mod xclip;
|
mod xclip;
|
||||||
@@ -323,6 +324,9 @@ impl OpenCADStudio {
|
|||||||
/// `finish_dispatch`), or `None` to defer to the next — equivalent to one
|
/// `finish_dispatch`), or `None` to defer to the next — equivalent to one
|
||||||
/// sequential `match` over all arms.
|
/// sequential `match` over all arms.
|
||||||
fn dispatch_families(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
fn dispatch_families(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
|
if let Some(t) = self.dispatch_ucsfollow(cmd, i) {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
if let Some(t) = self.dispatch_fileops(cmd, i) {
|
if let Some(t) = self.dispatch_fileops(cmd, i) {
|
||||||
return Some(t);
|
return Some(t);
|
||||||
}
|
}
|
||||||
@@ -504,6 +508,12 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
// Drafting-aid / display toggles + customization entry points wired in the
|
// Drafting-aid / display toggles + customization entry points wired in the
|
||||||
// dispatch families (no interactive command module of their own).
|
// dispatch families (no interactive command module of their own).
|
||||||
"ALIASEDIT",
|
"ALIASEDIT",
|
||||||
|
// Implemented command-line block forms must also validate in ALIASEDIT.
|
||||||
|
"-BLOCK",
|
||||||
|
"-BMAKE",
|
||||||
|
"-WBLOCK",
|
||||||
|
"-WB",
|
||||||
|
"-ATTEDIT",
|
||||||
"CLEANSCREEN",
|
"CLEANSCREEN",
|
||||||
"CUI",
|
"CUI",
|
||||||
"DSETTINGS",
|
"DSETTINGS",
|
||||||
|
|||||||
@@ -1304,6 +1304,7 @@ impl OpenCADStudio {
|
|||||||
if name.is_empty() || name == "?" {
|
if name.is_empty() || name == "?" {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&super::plotvars::setvar_listing());
|
.push_info(&super::plotvars::setvar_listing());
|
||||||
|
self.command_line.push_info("SETVAR (view): UCSFOLLOW");
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&super::blockvars::setvar_listing());
|
.push_info(&super::blockvars::setvar_listing());
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
//! UCSFOLLOW is a drawing/viewport variable, not an application preference.
|
||||||
|
use crate::app::{Message, OpenCADStudio};
|
||||||
|
use iced::Task;
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["UCSFOLLOW"] });
|
||||||
|
|
||||||
|
impl OpenCADStudio {
|
||||||
|
pub(super) fn dispatch_ucsfollow(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
|
let rest = cmd.strip_prefix("SETVAR ").unwrap_or(cmd).trim();
|
||||||
|
let mut parts = rest.splitn(2, char::is_whitespace);
|
||||||
|
if parts.next()? != "UCSFOLLOW" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value = parts.next().map(str::trim).filter(|v| !v.is_empty());
|
||||||
|
let current = self.tabs[i].scene.ucs_follow();
|
||||||
|
match value {
|
||||||
|
None => {
|
||||||
|
self.command_line.push_output(crate::tf!(
|
||||||
|
"UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <{}>:",
|
||||||
|
u8::from(current)
|
||||||
|
).as_ref());
|
||||||
|
self.pending_setvar = Some("UCSFOLLOW".into());
|
||||||
|
}
|
||||||
|
Some("0" | "1") => {
|
||||||
|
let next = value == Some("1");
|
||||||
|
if next && self.tabs[i].scene.current_layout != "Model"
|
||||||
|
&& self.tabs[i].scene.active_viewport.is_none() {
|
||||||
|
self.command_line.push_error(crate::t!("UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS.").as_ref());
|
||||||
|
} else {
|
||||||
|
if current != next {
|
||||||
|
self.push_undo_snapshot(i, "UCSFOLLOW");
|
||||||
|
self.tabs[i].scene.set_ucs_follow(next);
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
|
// Enabling UCSFOLLOW must align an already-defined UCS
|
||||||
|
// immediately, not only follow subsequent UCS edits. Also
|
||||||
|
// realign when the value was already 1 (e.g. after orbit).
|
||||||
|
if next && self.tabs[i].active_block_edit.is_none() {
|
||||||
|
if current == next {
|
||||||
|
self.push_undo_snapshot(i, "UCSFOLLOW");
|
||||||
|
}
|
||||||
|
self.tabs[i].sync_ucs_to_scene();
|
||||||
|
let rotation = self.tabs[i].scene.viewcube_ucs_mat();
|
||||||
|
self.plan_snap(i, rotation);
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
|
self.command_line.push_output(&format!("UCSFOLLOW = {}", u8::from(next)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
self.command_line.push_error(crate::t!("UCSFOLLOW: only 0 or 1 is allowed.").as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Task::none())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn app() -> OpenCADStudio {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_is_registered_prompted_and_validated() {
|
||||||
|
let mut app = app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
assert!(!app.tabs[i].scene.ucs_follow());
|
||||||
|
let _ = app.run_command_line("UCSFOLLOW");
|
||||||
|
assert_eq!(app.pending_setvar.as_deref(), Some("UCSFOLLOW"));
|
||||||
|
app.pending_setvar = None;
|
||||||
|
let _ = app.run_command_line("SETVAR UCSFOLLOW 1");
|
||||||
|
assert!(app.tabs[i].scene.ucs_follow());
|
||||||
|
let _ = app.run_command_line("UCSFOLLOW 2");
|
||||||
|
assert!(app.tabs[i].scene.ucs_follow());
|
||||||
|
let _ = app.run_command_line("UCSFOLLOW 0");
|
||||||
|
assert!(!app.tabs[i].scene.ucs_follow());
|
||||||
|
assert!(crate::command::all_registered_command_names().contains(&"UCSFOLLOW"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ucs_changes_align_view_only_when_enabled_and_aliases_work() {
|
||||||
|
let mut app = app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
// Test-only table: never writes or imports into the user's profile.
|
||||||
|
app.command_aliases.insert("UF".into(), "UCSFOLLOW".into());
|
||||||
|
app.command_aliases.insert("SQ".into(), "UCS".into());
|
||||||
|
let before = app.tabs[i].scene.camera.borrow().clone();
|
||||||
|
let _ = app.run_command_line("SQ Z 30");
|
||||||
|
let unchanged = app.tabs[i].scene.camera.borrow().clone();
|
||||||
|
assert_eq!(before.rotation, unchanged.rotation);
|
||||||
|
let _ = app.run_command_line("UF 1");
|
||||||
|
assert!(app.tabs[i].scene.ucs_follow());
|
||||||
|
let _ = app.run_command_line("SQ Z 30");
|
||||||
|
let followed = app.tabs[i].scene.camera.borrow().clone();
|
||||||
|
assert_ne!(unchanged.rotation, followed.rotation);
|
||||||
|
let _ = app.run_command_line("UF 0");
|
||||||
|
let _ = app.run_command_line("SQ Z 30");
|
||||||
|
assert_eq!(followed.rotation, app.tabs[i].scene.camera.borrow().rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn floating_viewport_setting_does_not_leak_into_model_or_paper() {
|
||||||
|
let mut app = app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let first = app.tabs[i].scene.add_entity(codec::EntityType::Viewport(
|
||||||
|
codec::entities::Viewport::new()));
|
||||||
|
let second = app.tabs[i].scene.add_entity(codec::EntityType::Viewport(
|
||||||
|
codec::entities::Viewport::new()));
|
||||||
|
app.tabs[i].scene.active_viewport = Some(first);
|
||||||
|
let _ = app.run_command_line("UCSFOLLOW 1");
|
||||||
|
assert!(app.tabs[i].scene.ucs_follow());
|
||||||
|
app.tabs[i].scene.active_viewport = Some(second);
|
||||||
|
assert!(!app.tabs[i].scene.ucs_follow());
|
||||||
|
app.tabs[i].scene.active_viewport = None;
|
||||||
|
assert!(!app.tabs[i].scene.ucs_follow());
|
||||||
|
app.tabs[i].scene.current_layout = "Layout1".into();
|
||||||
|
let _ = app.run_command_line("UCSFOLLOW 1");
|
||||||
|
assert!(!app.tabs[i].scene.ucs_follow());
|
||||||
|
app.tabs[i].scene.active_viewport = Some(first);
|
||||||
|
assert!(app.tabs[i].scene.ucs_follow());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enabling_after_defining_ucs_aligns_immediately() {
|
||||||
|
let mut app = app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let before = app.tabs[i].scene.camera.borrow().rotation;
|
||||||
|
let _ = app.run_command_line("UCS Z 30");
|
||||||
|
assert_eq!(before, app.tabs[i].scene.camera.borrow().rotation);
|
||||||
|
let _ = app.run_command_line("UCSFOLLOW 1");
|
||||||
|
let aligned = app.tabs[i].scene.camera.borrow().rotation;
|
||||||
|
assert_ne!(before, aligned, "enabling must align the existing UCS immediately");
|
||||||
|
// The same orientation must be obtained through the explicit PLAN path.
|
||||||
|
let _ = app.run_command_line("PLAN CURRENT");
|
||||||
|
assert_eq!(aligned, app.tabs[i].scene.camera.borrow().rotation);
|
||||||
|
let _ = app.run_command_line("UCSFOLLOW 0");
|
||||||
|
assert_eq!(aligned, app.tabs[i].scene.camera.borrow().rotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1459,6 +1459,8 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// True while the "unsaved changes will be discarded" confirmation
|
/// True while the "unsaved changes will be discarded" confirmation
|
||||||
/// overlays the editor: the user tried to close with un-applied rows.
|
/// overlays the editor: the user tried to close with un-applied rows.
|
||||||
alias_close_confirm: bool,
|
alias_close_confirm: bool,
|
||||||
|
/// Summary of the most recent PGP import; never activates the working rows.
|
||||||
|
alias_import_report: String,
|
||||||
|
|
||||||
// ── Named Parameters ──────────────────────────────────────────────────
|
// ── Named Parameters ──────────────────────────────────────────────────
|
||||||
/// Working buffer for the PARAMETERS modal. Unlike `alias_editor_rows`,
|
/// Working buffer for the PARAMETERS modal. Unlike `alias_editor_rows`,
|
||||||
@@ -3747,6 +3749,9 @@ pub enum Message {
|
|||||||
// ── Command Alias Editor (ALIASEDIT) ────────────────────────────────
|
// ── Command Alias Editor (ALIASEDIT) ────────────────────────────────
|
||||||
/// Open the command-alias editor modal, seeding rows from the alias table.
|
/// Open the command-alias editor modal, seeding rows from the alias table.
|
||||||
AliasEditorOpen,
|
AliasEditorOpen,
|
||||||
|
/// Pick an AutoCAD/OpenCAD PGP file, then merge supported aliases for review.
|
||||||
|
AliasEditorImport,
|
||||||
|
AliasEditorImported(Option<Result<Vec<u8>, String>>),
|
||||||
/// Live edit of the alias or command in row `idx`.
|
/// Live edit of the alias or command in row `idx`.
|
||||||
AliasEditorInput {
|
AliasEditorInput {
|
||||||
idx: usize,
|
idx: usize,
|
||||||
@@ -4967,6 +4972,7 @@ impl OpenCADStudio {
|
|||||||
alias_pending_add: false,
|
alias_pending_add: false,
|
||||||
alias_reset_confirm: false,
|
alias_reset_confirm: false,
|
||||||
alias_close_confirm: false,
|
alias_close_confirm: false,
|
||||||
|
alias_import_report: String::new(),
|
||||||
named_parameter_editor_rows: Vec::new(),
|
named_parameter_editor_rows: Vec::new(),
|
||||||
// Layout Manager
|
// Layout Manager
|
||||||
layout_manager_selected: "Model".to_string(),
|
layout_manager_selected: "Model".to_string(),
|
||||||
|
|||||||
@@ -8566,6 +8566,7 @@ impl OpenCADStudio {
|
|||||||
.collect();
|
.collect();
|
||||||
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
self.alias_editor_rows = rows;
|
self.alias_editor_rows = rows;
|
||||||
|
self.alias_import_report.clear();
|
||||||
self.alias_pending_add = false;
|
self.alias_pending_add = false;
|
||||||
self.alias_reset_confirm = false;
|
self.alias_reset_confirm = false;
|
||||||
self.alias_close_confirm = false;
|
self.alias_close_confirm = false;
|
||||||
@@ -8587,6 +8588,29 @@ impl OpenCADStudio {
|
|||||||
// check button does, so no half-visible "ghost" row appears.
|
// check button does, so no half-visible "ghost" row appears.
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::AliasEditorImport => Task::perform(
|
||||||
|
async {
|
||||||
|
let handle = rfd::AsyncFileDialog::new()
|
||||||
|
.add_filter(crate::t!("Command aliases (*.pgp)").as_ref(), &["pgp"])
|
||||||
|
.pick_file().await?;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{ Some(std::fs::read(handle.path()).map_err(|e| e.to_string())) }
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{ Some(Ok(handle.read().await)) }
|
||||||
|
},
|
||||||
|
Message::AliasEditorImported,
|
||||||
|
),
|
||||||
|
Message::AliasEditorImported(result) => {
|
||||||
|
// A picker can finish after the modal was dismissed.
|
||||||
|
if self.active_modal == Some(super::ModalKind::Aliases) {
|
||||||
|
match result {
|
||||||
|
Some(Ok(bytes)) => self.import_alias_pgp(&bytes),
|
||||||
|
Some(Err(error)) => self.command_line.push_error(&format!("PGP: {error}")),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::AliasEditorAdd => {
|
Message::AliasEditorAdd => {
|
||||||
if self.alias_pending_add {
|
if self.alias_pending_add {
|
||||||
// One draft at a time: keep the existing top draft.
|
// One draft at a time: keep the existing top draft.
|
||||||
|
|||||||
@@ -3506,12 +3506,17 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if !same_basis(self.tabs[i].active_ucs.as_ref(), persisted.as_ref()) {
|
let changed = !same_basis(self.tabs[i].active_ucs.as_ref(), persisted.as_ref());
|
||||||
|
if changed {
|
||||||
self.push_undo_snapshot(i, label);
|
self.push_undo_snapshot(i, label);
|
||||||
self.tabs[i].persist_active_ucs();
|
self.tabs[i].persist_active_ucs();
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
self.tabs[i].sync_ucs_to_scene();
|
self.tabs[i].sync_ucs_to_scene();
|
||||||
|
if changed && self.tabs[i].scene.ucs_follow() {
|
||||||
|
let rotation = self.tabs[i].scene.viewcube_ucs_mat();
|
||||||
|
self.plan_snap(i, rotation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Per-pane Model viewport (pane_grid) ───────────────────────────────
|
// ── Per-pane Model viewport (pane_grid) ───────────────────────────────
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ impl OpenCADStudio {
|
|||||||
sized_flow(ex, 480, 520, |flow| {
|
sized_flow(ex, 480, 520, |flow| {
|
||||||
crate::ui::window::alias_editor::view_window(
|
crate::ui::window::alias_editor::view_window(
|
||||||
&self.alias_editor_rows,
|
&self.alias_editor_rows,
|
||||||
|
&self.alias_import_report,
|
||||||
self.alias_pending_add,
|
self.alias_pending_add,
|
||||||
self.alias_reset_confirm,
|
self.alias_reset_confirm,
|
||||||
&duplicate_set,
|
&duplicate_set,
|
||||||
|
|||||||
@@ -3237,6 +3237,14 @@ pub fn all_registered_command_names() -> Vec<&'static str> {
|
|||||||
mod constraint_registry_tests {
|
mod constraint_registry_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_block_alias_targets_are_registered() {
|
||||||
|
let names = all_registered_command_names();
|
||||||
|
for target in ["-BLOCK", "-WBLOCK", "-ATTEDIT"] {
|
||||||
|
assert!(names.contains(&target), "{target} must validate in ALIASEDIT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensures every constraint command remains discoverable by autocomplete
|
/// Ensures every constraint command remains discoverable by autocomplete
|
||||||
/// and command listings.
|
/// and command listings.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1137,6 +1137,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Columns" => Some(("properties", "columns")),
|
"Columns" => Some(("properties", "columns")),
|
||||||
"Command" => Some(("command", "command")),
|
"Command" => Some(("command", "command")),
|
||||||
"Command Aliases" => Some(("command", "command-aliases")),
|
"Command Aliases" => Some(("command", "command-aliases")),
|
||||||
|
"Import PGP…" => Some(("pgp-import", "button")),
|
||||||
|
"Command aliases (*.pgp)" => Some(("pgp-import", "filter")),
|
||||||
|
"PGP: no valid aliases found. The working rows were not changed." => Some(("pgp-import", "empty")),
|
||||||
|
"PGP: {} alias(es) loaded for review; {} unsupported command(s) skipped. No changes activated. Apply to save." => Some(("pgp-import", "summary")),
|
||||||
"Command cancelled because the active drawing space changed." => Some(("common", "command-cancelled-because-the-active-drawing-s")),
|
"Command cancelled because the active drawing space changed." => Some(("common", "command-cancelled-because-the-active-drawing-s")),
|
||||||
"Command history navigation" => Some(("common", "command-history-navigation")),
|
"Command history navigation" => Some(("common", "command-history-navigation")),
|
||||||
"Commit date" => Some(("ui", "commit-date")),
|
"Commit date" => Some(("ui", "commit-date")),
|
||||||
@@ -4903,7 +4907,11 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"2Point" => Some(("model", "value-2point")),
|
"2Point" => Some(("model", "value-2point")),
|
||||||
"Cube" => Some(("model", "cube")),
|
"Cube" => Some(("model", "cube")),
|
||||||
"UCS option [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:" => Some(("view", "ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name")),
|
"UCS option [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:" => Some(("view", "ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name")),
|
||||||
|
"UCS specify new origin or [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:" => Some(("view", "ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name")),
|
||||||
"UCS specify new origin:" => Some(("view", "ucs-specify-new-origin")),
|
"UCS specify new origin:" => Some(("view", "ucs-specify-new-origin")),
|
||||||
|
"UCSFOLLOW: enter 0 (keep view) or 1 (follow UCS) <{}>:" => Some(("ucs-follow", "prompt")),
|
||||||
|
"UCSFOLLOW: activate a model viewport; paper space cannot follow the UCS." => Some(("ucs-follow", "paper-space")),
|
||||||
|
"UCSFOLLOW: only 0 or 1 is allowed." => Some(("ucs-follow", "invalid")),
|
||||||
"UCS specify point on positive X axis:" => Some(("view", "ucs-specify-point-on-positive-x-axis")),
|
"UCS specify point on positive X axis:" => Some(("view", "ucs-specify-point-on-positive-x-axis")),
|
||||||
"UCS specify point in positive XY plane:" => Some(("view", "ucs-specify-point-in-positive-xy-plane")),
|
"UCS specify point in positive XY plane:" => Some(("view", "ucs-specify-point-in-positive-xy-plane")),
|
||||||
"Extrusion" => Some(("common", "extrusion")),
|
"Extrusion" => Some(("common", "extrusion")),
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ impl CadCommand for UcsCommand {
|
|||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match self.option.as_deref() {
|
match self.option.as_deref() {
|
||||||
None => {
|
None => {
|
||||||
t!("UCS option [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:")
|
t!("UCS specify new origin or [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:")
|
||||||
.into_owned()
|
.into_owned()
|
||||||
}
|
}
|
||||||
Some("Z") => t!("UCS rotation angle about Z (degrees):").into_owned(),
|
Some("Z") => t!("UCS rotation angle about Z (degrees):").into_owned(),
|
||||||
@@ -126,6 +126,20 @@ impl CadCommand for UcsCommand {
|
|||||||
return Some(CmdResult::Dispatch("UCS LIST".into()));
|
return Some(CmdResult::Dispatch("UCS LIST".into()));
|
||||||
}
|
}
|
||||||
let up = t.to_uppercase();
|
let up = t.to_uppercase();
|
||||||
|
// Accept the Spanish labels shown by the localized prompt as
|
||||||
|
// well as international option keywords. Only normalize the
|
||||||
|
// option token, never the value/name supplied in the next step.
|
||||||
|
let up = match up.trim_start_matches('_') {
|
||||||
|
"UNIVERSAL" | "U" => "WORLD",
|
||||||
|
"VISTA" => "VIEW",
|
||||||
|
"CARA" => "FACE",
|
||||||
|
"OBJETO" => "OBJECT",
|
||||||
|
"ORIGEN" => "ORIGIN",
|
||||||
|
"GUARDAR" | "G" => "SAVE",
|
||||||
|
"ELIMINAR" | "E" => "DELETE",
|
||||||
|
"3" | "3PUNTOS" | "3 PUNTOS" => "3POINT",
|
||||||
|
value => value,
|
||||||
|
}.to_owned();
|
||||||
if Self::is_zero_arg(&up) {
|
if Self::is_zero_arg(&up) {
|
||||||
Some(CmdResult::Dispatch(format!("UCS {up}")))
|
Some(CmdResult::Dispatch(format!("UCS {up}")))
|
||||||
} else if Self::takes_value(&up) || Self::takes_point(&up) {
|
} else if Self::takes_value(&up) || Self::takes_point(&up) {
|
||||||
@@ -168,8 +182,7 @@ impl CadCommand for UcsCommand {
|
|||||||
points[2].z,
|
points[2].z,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// A clicked point only makes sense for the Origin option; otherwise a
|
// Other point-taking options retain their explicit behavior.
|
||||||
// stray click is ignored (keep waiting for the typed keyword / value).
|
|
||||||
if matches!(self.option.as_deref(), Some(o) if Self::takes_point(o)) {
|
if matches!(self.option.as_deref(), Some(o) if Self::takes_point(o)) {
|
||||||
self.option.take();
|
self.option.take();
|
||||||
return CmdResult::Dispatch(format!("UCS ORIGINW {},{},{}", pt.x, pt.y, pt.z));
|
return CmdResult::Dispatch(format!("UCS ORIGINW {},{},{}", pt.x, pt.y, pt.z));
|
||||||
@@ -204,3 +217,51 @@ impl CadCommand for UcsCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bare_ucs_accepts_origin_then_x_then_xy_without_option() {
|
||||||
|
let mut cmd = UcsCommand::new();
|
||||||
|
assert!(matches!(cmd.on_point(DVec3::new(10.0, 20.0, 0.0)), CmdResult::NeedPoint));
|
||||||
|
assert_eq!(cmd.points.len(), 1);
|
||||||
|
assert!(matches!(cmd.on_point(DVec3::new(11.0, 20.0, 0.0)), CmdResult::NeedPoint));
|
||||||
|
match cmd.on_point(DVec3::new(10.0, 21.0, 0.0)) {
|
||||||
|
CmdResult::Dispatch(command) => assert_eq!(
|
||||||
|
command, "UCS 3POINTW 10,20,0|11,20,0|10,21,0"
|
||||||
|
),
|
||||||
|
_ => panic!("three clicks must dispatch the new UCS"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_coordinate_is_delegated_to_point_pipeline() {
|
||||||
|
let mut cmd = UcsCommand::new();
|
||||||
|
assert!(cmd.on_text_input("10,20,0").is_none());
|
||||||
|
assert!(matches!(cmd.on_point(DVec3::new(10.0, 20.0, 0.0)), CmdResult::NeedPoint));
|
||||||
|
assert_eq!(cmd.points.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_origin_still_changes_only_origin() {
|
||||||
|
let mut cmd = UcsCommand::new();
|
||||||
|
assert!(matches!(cmd.on_text_input("O"), Some(CmdResult::NeedPoint)));
|
||||||
|
match cmd.on_point(DVec3::new(10.0, 20.0, 0.0)) {
|
||||||
|
CmdResult::Dispatch(command) => assert_eq!(command, "UCS ORIGINW 10,20,0"),
|
||||||
|
_ => panic!("explicit Origin must dispatch after one point"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spanish_and_international_options_work() {
|
||||||
|
for option in ["Universal", "U", "_World"] {
|
||||||
|
let mut cmd = UcsCommand::new();
|
||||||
|
assert!(matches!(cmd.on_text_input(option), Some(CmdResult::Dispatch(s)) if s == "UCS WORLD"));
|
||||||
|
}
|
||||||
|
let mut cmd = UcsCommand::new();
|
||||||
|
assert!(matches!(cmd.on_text_input("Guardar"), Some(CmdResult::NeedPoint)));
|
||||||
|
assert!(matches!(cmd.on_text_input("MiPlano"), Some(CmdResult::Dispatch(s)) if s == "UCS SAVE MiPlano"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1073,6 +1073,51 @@ impl Scene {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Index of the current model pane's record, independent of record ordering.
|
||||||
|
fn ucs_follow_vport_index(&self) -> Option<usize> {
|
||||||
|
let rect = self.model_tiles.borrow().get(self.active_model_tile.get()).map(|t| t.rect);
|
||||||
|
let bounds = rect.map(Self::tile_rect_to_vport);
|
||||||
|
self.document.vports.iter().enumerate().find_map(|(index, entry)| {
|
||||||
|
(is_active_vport_name(&entry.name)
|
||||||
|
&& bounds.map_or(true, |(ll, ur)| Self::vport_rect_matches(entry, ll, ur)))
|
||||||
|
.then_some(index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the persisted per-viewport UCSFOLLOW bit. Paper space itself never follows.
|
||||||
|
pub(crate) fn ucs_follow(&self) -> bool {
|
||||||
|
if let Some(handle) = self.active_viewport {
|
||||||
|
return matches!(self.document.get_entity(handle),
|
||||||
|
Some(codec::EntityType::Viewport(vp)) if vp.status.ucs_follow);
|
||||||
|
}
|
||||||
|
if self.current_layout != "Model" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.ucs_follow_vport_index()
|
||||||
|
.and_then(|index| self.document.vports.iter().nth(index))
|
||||||
|
.is_some_and(|vp| vp.ucsfollow)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_ucs_follow(&mut self, enabled: bool) {
|
||||||
|
if let Some(handle) = self.active_viewport {
|
||||||
|
if let Some(codec::EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) {
|
||||||
|
vp.status.ucs_follow = enabled;
|
||||||
|
}
|
||||||
|
} else if self.current_layout == "Model" {
|
||||||
|
// Materialize current panes first; saving them preserves the bit by
|
||||||
|
// matching their rectangles, even when the active pane is reordered.
|
||||||
|
self.save_model_tiles_to_vports();
|
||||||
|
if self.ucs_follow_vport_index().is_none() {
|
||||||
|
self.document.vports.add_or_replace(codec::tables::VPort::new("*Active"));
|
||||||
|
}
|
||||||
|
if let Some(index) = self.ucs_follow_vport_index() {
|
||||||
|
if let Some(vp) = self.document.vports.iter_mut().nth(index) {
|
||||||
|
vp.ucsfollow = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Persist model tiles as duplicate `*Active` VPort entries.
|
/// Persist model tiles as duplicate `*Active` VPort entries.
|
||||||
fn save_model_tiles_to_vports(&mut self) {
|
fn save_model_tiles_to_vports(&mut self) {
|
||||||
// Stash the live camera into the active tile so the about-to-write
|
// Stash the live camera into the active tile so the about-to-write
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ fn danger_input_style(
|
|||||||
/// Build the alias editor content. `rows` is the live working buffer.
|
/// Build the alias editor content. `rows` is the live working buffer.
|
||||||
pub fn view_window<'a>(
|
pub fn view_window<'a>(
|
||||||
rows: &'a [(String, String)],
|
rows: &'a [(String, String)],
|
||||||
|
import_report: &'a str,
|
||||||
pending_add: bool,
|
pending_add: bool,
|
||||||
reset_confirm: bool,
|
reset_confirm: bool,
|
||||||
duplicate_aliases: &rustc_hash::FxHashSet<String>,
|
duplicate_aliases: &rustc_hash::FxHashSet<String>,
|
||||||
@@ -220,7 +221,10 @@ pub fn view_window<'a>(
|
|||||||
.style(button::secondary)]
|
.style(button::secondary)]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
row![add],
|
row![add, button(text(t!("Import PGP…")).size(12))
|
||||||
|
.on_press_maybe((!pending_add).then_some(Message::AliasEditorImport))
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(button::secondary)].spacing(8),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
// Apply — primary action; commits the rows to ocad.pgp and stays open.
|
// Apply — primary action; commits the rows to ocad.pgp and stays open.
|
||||||
@@ -265,21 +269,22 @@ pub fn view_window<'a>(
|
|||||||
column![
|
column![
|
||||||
title,
|
title,
|
||||||
hint,
|
hint,
|
||||||
|
text(import_report).size(11).style(muted_style),
|
||||||
Space::new().height(6),
|
Space::new().height(6),
|
||||||
head,
|
head,
|
||||||
scrollable(container(list).padding(gutter)).height(sizing.height),
|
scrollable(container(list).padding(gutter)).height(sizing.height),
|
||||||
Space::new().height(6),
|
Space::new().height(6),
|
||||||
conflict_banner,
|
scrollable(conflict_banner).height(Length::Fit.max(72)),
|
||||||
Space::new().height(4),
|
Space::new().height(4),
|
||||||
row![
|
row![
|
||||||
stats,
|
|
||||||
add_area,
|
add_area,
|
||||||
reset_area,
|
reset_area,
|
||||||
Space::new().width(sizing.width),
|
|
||||||
apply_area
|
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
|
row![stats, Space::new().width(Length::Fill), apply_area]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center),
|
||||||
]
|
]
|
||||||
.spacing(6)
|
.spacing(6)
|
||||||
.width(sizing.width)
|
.width(sizing.width)
|
||||||
|
|||||||
Reference in new issue
Block a user