From 04bc128cb5578477f394f9133ce11c46f53561c6 Mon Sep 17 00:00:00 2001 From: japbcoelho <319161831+japbcoelho@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:44:13 -0300 Subject: [PATCH] feat: add standalone Conics ipelet --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.yml | 1 + .github/workflows/validate.yml | 2 +- CONTRIBUTING.md | 6 + NOTICE.md | 2 + README.md | 8 +- README.pt-BR.md | 8 +- circles/README.md | 2 +- circles/README.pt-BR.md | 2 +- conics/CHANGELOG.md | 73 + conics/README.md | 205 + conics/README.pt-BR.md | 205 + conics/VERSION | 1 + conics/conics.lua | 7182 +++++++++++++++++ .../docs/images/conics-advanced-workflows.png | Bin 0 -> 86198 bytes .../images/conics-five-point-live-preview.png | Bin 0 -> 73023 bytes conics/docs/images/conics-overview.png | Bin 0 -> 138492 bytes conics/docs/images/conics-overview.svg | 805 ++ conics/docs/images/conics-parabolas.png | Bin 0 -> 106443 bytes conics/docs/images/conics-property-guides.png | Bin 0 -> 90070 bytes conics/examples/README.md | 10 + conics/examples/conics-feature-gallery.ipe | 4446 ++++++++++ conics/examples/conics-overview.ipe | 392 + conics/tests/conics_runtime.lua | 196 + conics/tests/test_advanced.py | 518 ++ conics/tests/test_geometry.py | 892 ++ conics/tests/test_package.py | 119 + conics/tests/test_regressions.py | 75 + conics/tests/test_release_contract.py | 70 + scripts/package.sh | 31 +- scripts/validate.sh | 24 +- 31 files changed, 15266 insertions(+), 10 deletions(-) create mode 100644 conics/CHANGELOG.md create mode 100644 conics/README.md create mode 100644 conics/README.pt-BR.md create mode 100644 conics/VERSION create mode 100644 conics/conics.lua create mode 100644 conics/docs/images/conics-advanced-workflows.png create mode 100644 conics/docs/images/conics-five-point-live-preview.png create mode 100644 conics/docs/images/conics-overview.png create mode 100644 conics/docs/images/conics-overview.svg create mode 100644 conics/docs/images/conics-parabolas.png create mode 100644 conics/docs/images/conics-property-guides.png create mode 100644 conics/examples/README.md create mode 100644 conics/examples/conics-feature-gallery.ipe create mode 100644 conics/examples/conics-overview.ipe create mode 100644 conics/tests/conics_runtime.lua create mode 100644 conics/tests/test_advanced.py create mode 100644 conics/tests/test_geometry.py create mode 100644 conics/tests/test_package.py create mode 100644 conics/tests/test_regressions.py create mode 100644 conics/tests/test_release_contract.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6ceb408..fbea5c3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,6 +10,7 @@ body: label: Ipelet options: - Circles + - Conics validations: required: true - type: input diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 35c20ab..1b771c7 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -10,6 +10,7 @@ body: label: Ipelet options: - Circles + - Conics - Repository-wide validations: required: true diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 2c1c972..0ccb3ae 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -9,7 +9,7 @@ permissions: contents: read jobs: - circles: + ipelets: runs-on: ubuntu-latest timeout-minutes: 10 steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45998aa..88ff519 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,12 @@ luac5.4 -p circles/circles.lua python3 -m unittest discover -s circles/tests -v ``` +For a focused Conics check: + +```bash +./scripts/validate.sh conics +``` + ## Visual changes When a change affects generated geometry or dialogs: diff --git a/NOTICE.md b/NOTICE.md index 30f8b93..ec71b06 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -7,3 +7,5 @@ This is an independent community repository and is not an official Ipe distribut Ipe is the extensible drawing editor created by Otfried Cheong. Ipe is distributed separately under the GNU General Public License, version 3 or any later version. See the [official Ipe website](https://ipe.otfried.org/) and the [Ipe source repository](https://github.com/otfried/ipe) for the editor's source, documentation, copyright notices, and license terms. The ipelets in this repository use Ipe's documented Lua extension interface. Ipe itself is not bundled in this repository or in its release archives. + +The ellipse-from-foci and parabola construction formulas in Conics are adapted from Ipe's GPL-licensed `goodies.lua`. The adapted implementation remains available under GPL-3.0-or-later, with the Ipe project acknowledged above. diff --git a/README.md b/README.md index 7628df3..a3182ab 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,14 @@ A curated collection of independent extensions for the [Ipe drawing editor](http [![Circles: preview every tangent-circle candidate and click to create one](circles/docs/images/circles-overview.svg)](circles/) +[![Conics: construct, classify, inspect, and annotate editable conics](conics/docs/images/conics-overview.svg)](conics/) + ## Available ipelets | Ipelet | Version | Description | | --- | --- | --- | | [Circles](circles/) | 1.0.0 | Circle construction, tangencies, inversion, radical geometry, center marking, and live previews. | +| [Conics](conics/) | 1.1.0 | Exact and fitted conics, dual and mixed constructions, intersections, arcs, analytic features, inspection, and metadata repair. | ## Quick installation @@ -20,16 +23,19 @@ On Linux, install an ipelet with the repository helper: ./scripts/install.sh circles ``` +Replace `circles` with `conics` to install Conics. + The helper detects the Ipe Flatpak installation and otherwise uses the standard `~/.ipe/ipelets` directory. Restart Ipe after installation. Manual installation and platform-specific details are documented inside each ipelet directory. -Prebuilt archives are available from the [latest GitHub release](https://github.com/japbcoelho/ipelets/releases/latest). +Prebuilt archives for each ipelet are available on the [GitHub releases page](https://github.com/japbcoelho/ipelets/releases). ## Repository layout ```text . ├── circles/ Circles source, documentation, examples, and tests +├── conics/ Conics source, documentation, examples, and tests ├── scripts/ Installation, validation, and packaging helpers ├── .github/ Continuous integration and contribution templates └── LICENSE Repository license diff --git a/README.pt-BR.md b/README.pt-BR.md index 32ef5c2..7418fdc 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -6,11 +6,14 @@ Uma coleção organizada de extensões independentes para o [editor de desenhos [![Circles: visualize todas as circunferências tangentes candidatas e clique para criar uma](circles/docs/images/circles-overview.svg)](circles/README.pt-BR.md) +[![Conics: construa, classifique, inspecione e anote cônicas editáveis](conics/docs/images/conics-overview.svg)](conics/README.pt-BR.md) + ## Ipelets disponíveis | Ipelet | Versão | Descrição | | --- | --- | --- | | [Circles](circles/README.pt-BR.md) | 1.0.0 | Construções de circunferências, tangências, inversão, geometria radical, marcação de centros e pré-visualização ao vivo. | +| [Conics](conics/README.pt-BR.md) | 1.1.0 | Cônicas exatas e ajustadas, construções duais e mistas, interseções, arcos, elementos analíticos, inspeção e reparo de metadados. | ## Instalação rápida @@ -20,16 +23,19 @@ No Linux, instale um Ipelet com o utilitário do repositório: ./scripts/install.sh circles ``` +Substitua `circles` por `conics` para instalar o Conics. + O utilitário detecta a instalação Flatpak do Ipe e, nos demais casos, utiliza a pasta padrão `~/.ipe/ipelets`. Reinicie o Ipe depois da instalação. As instruções manuais e os detalhes específicos de cada plataforma ficam documentados dentro da pasta de cada Ipelet. -Os pacotes prontos estão disponíveis na [versão mais recente do GitHub](https://github.com/japbcoelho/ipelets/releases/latest). +Os pacotes prontos de cada Ipelet estão disponíveis na [página de versões do GitHub](https://github.com/japbcoelho/ipelets/releases). ## Organização do repositório ```text . ├── circles/ Código, documentação, exemplos e testes do Circles +├── conics/ Código, documentação, exemplos e testes do Conics ├── scripts/ Utilitários de instalação, validação e empacotamento ├── .github/ Integração contínua e modelos de contribuição └── LICENSE Licença do repositório diff --git a/circles/README.md b/circles/README.md index b828137..bc5a5ab 100644 --- a/circles/README.md +++ b/circles/README.md @@ -47,7 +47,7 @@ The ipelet uses documented Ipe Lua objects and does not require a separate build ## Installation -Download the self-contained package from the [latest GitHub release](https://github.com/japbcoelho/ipelets/releases/latest), or use one of the methods below. +Download the self-contained package from the [Circles 1.0.0 release](https://github.com/japbcoelho/ipelets/releases/tag/v1.0.0), or use one of the methods below. ### Repository helper on Linux diff --git a/circles/README.pt-BR.md b/circles/README.pt-BR.md index 1ccbe78..105d762 100644 --- a/circles/README.pt-BR.md +++ b/circles/README.pt-BR.md @@ -47,7 +47,7 @@ O Ipelet utiliza objetos documentados da API Lua do Ipe e não exige compilaçã ## Instalação -Baixe o pacote autocontido na [versão mais recente do GitHub](https://github.com/japbcoelho/ipelets/releases/latest) ou utilize um dos métodos abaixo. +Baixe o pacote autocontido na [versão 1.0.0 de Circles](https://github.com/japbcoelho/ipelets/releases/tag/v1.0.0) ou utilize um dos métodos abaixo. ### Utilitário do repositório no Linux diff --git a/conics/CHANGELOG.md b/conics/CHANGELOG.md new file mode 100644 index 0000000..9156b0f --- /dev/null +++ b/conics/CHANGELOG.md @@ -0,0 +1,73 @@ +# Changelog + +All notable changes to Conics are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow semantic versioning. + +## [Unreleased] + +## [1.1.0] - 2026-08-29 + +### Added + +- Least-squares conic fitting from 6–512 sample marks, with expected-kind, conditioning, and residual diagnostics. +- Dual construction from five tangent lines and mixed systems containing five weighted point/tangent conditions. +- Focus–directrix construction from an explicit eccentricity, covering ellipses, parabolas, and hyperbolas through one workflow. +- Native ellipse construction from a center and two perpendicular semiaxis endpoints. +- Parabola construction from vertex and focus, plus hyperbola construction from two asymptotes and one point. +- Explicit intersecting-line, parallel-line, double-line, single-line, point, and empty degenerate conic loci. +- Tangents from an arbitrary point, chord of contact, pole of a line, and focal chord operations. +- Robust conic–conic intersections with zero through four finite real points and coincident-conic reporting. +- Exact/adaptive conic arcs and transactional fit-and-replace for a selected path. +- Latus recta, auxiliary circles, director circles, general/canonical equations, parameter labels, focal radius, semilatus rectum, and ellipse area. +- Canonical `create_ellipse` and `create_parabola` API names while preserving the 1.0 aliases. + +### Changed + +- Renamed the public menu entries to `Construct: ellipse` and `Construct: parabola` because each dialog now provides multiple constructions. +- Expanded every construction and feature dialog with contextual controls, exact selection guidance, persistent successful settings, and live preview. +- Extended metadata roles and fingerprints to cover conic arcs, degenerate loci, poles, chords, equations, and circular guides. +- Revalidation now uses the many-point least-squares fitter instead of choosing only five path samples. +- Conic–conic elimination now uses a normalized coordinate frame and both coordinate orientations for improved scale and tangency stability. +- Finite polar and contact-chord segments are anchored near their defining geometry instead of an arbitrary coordinate-axis intercept. + +### Compatibility + +- Lua API version remains `1`; all version 1.0 creator names and action aliases remain accepted. +- Existing `conics:v1` and supported legacy Geometry metadata remain readable. +- The installed runtime remains one standalone `conics.lua` file and does not depend on Geometry, MCP, a personal stylesheet, the network, or an external Lua package. + +### Verified + +- Added behavior tests for every new constructor and feature, noisy fitting, degenerate classifications, numerical scale stress, transactional replacement, invalid input, and package sanitization. +- Added a 32-page editable acceptance gallery plus README media rendered from the live Ipe audit. + +## [1.0.0] - 2026-08-29 + +### Added + +- Four general conic constructions: Steiner ellipses, a conic through five points, focus-directrix-point, and the canonical midpoint ellipse of a quadrilateral. +- Dedicated ellipse-from-foci, hyperbola, and directrix-with-foci parabola workflows. +- Tangent, normal, polar, line-intersection, property-guide, inspection, and metadata-revalidation tools. +- Automatic live preview and a manual Preview button for every construction dialog. +- Classification and extraction of centers, vertices, axes, foci, directrices, eccentricity, and asymptotes. +- A versioned public Lua API and a strict, role-aware `conics:v1` metadata format. + +### Changed + +- Prepared Conics as a self-contained single-file ipelet with no Geometry runtime dependency. +- Replaced fragmented implicit rendering with native ellipses, exact quadratic parabola splines, and adaptive continuous cubic hyperbola branches. +- Added scale-aware tolerances, stable quadratic roots, scaled norms, a direct null-space solver for five-point fitting, and finite-output validation. +- Made explicit inputs authoritative and selection contracts strict and visible in the dialogs. +- Preserved active Ipe attributes safely, standardized creation on the active layer, and made grouping explicit. + +### Compatibility + +- Affine transformations preserve conic coefficients. +- Existing `geometry:conic` and `geometry:hyperbola` metadata remain readable. +- Structural path edits are detected as stale metadata and can be revalidated from the selected curve. + +### Verified + +- Added standalone behavioral, numerical, package, and release-contract suites for Lua 5.4. + +[Unreleased]: https://github.com/japbcoelho/ipelets/compare/conics-v1.1.0...HEAD +[1.1.0]: https://github.com/japbcoelho/ipelets/releases/tag/conics-v1.1.0 diff --git a/conics/README.md b/conics/README.md new file mode 100644 index 0000000..2798bf6 --- /dev/null +++ b/conics/README.md @@ -0,0 +1,205 @@ +# Conics for Ipe + +[Leia em português](README.pt-BR.md) + +![Conics overview with editable constructions and property guides](docs/images/conics-overview.svg) + +Conics is a standalone Lua ipelet for constructing, fitting, classifying, intersecting, trimming, inspecting, and annotating conics. It supports ellipses, circles, parabolas, hyperbolas, and explicit degenerate loci. It keeps exact Ipe geometry where possible, uses compact adaptive splines for open hyperbola branches, and makes every visible result editable and undoable. + +## Signature workflow: five points become a structured conic + +Select five marks and choose `Conic through five points`. Conics solves the homogeneous five-point system directly, rejects duplicate, ill-conditioned, and degenerate data, classifies the result, and chooses its best Ipe representation automatically: a native ellipse, an exact quadratic parabola spline, or continuous adaptive cubic hyperbola branches. + +![Live preview of a general conic through five selected points](docs/images/conics-five-point-live-preview.png) + +Select the result and open `Property guides` to create its meaningful geometry—center or vertex, axes, vertices, foci, directrices, and asymptotes—using native marks, lines, and optional labels. + +![Editable property guides extracted from a conic](docs/images/conics-property-guides.png) + +The directrix-and-foci workflow can create several exact parabolas in one operation while preserving one clean undo step. + +![Multiple exact parabolas generated from one directrix and several foci](docs/images/conics-parabolas.png) + +Version 1.1 also works from the dual side of conic geometry: five tangent lines determine a conic, mixed point-and-tangent conditions can be combined, and an exterior point produces both tangents and their chord of contact. Two selected conics can be intersected directly, and an existing curve can be trimmed to a connected conic arc or fitted and replaced in one undoable transaction. + +![Five-line envelopes, exterior tangents with the contact chord, and four intersections of two conics](docs/images/conics-advanced-workflows.png) + +## Highlights + +- One self-contained `conics.lua` runtime file. +- No companion Geometry ipelet, bridge, network access, or external Lua package required. +- Native exact ellipses and compact exact parabola splines. +- Continuous hyperbola branches subdivided by geometric error tolerance. +- Exact five-line dual construction and mixed point/tangent constraints. +- Least-squares fitting from 6–512 sample marks, with conditioning and residual checks. +- Poles, polars, exterior tangents, chords of contact, focal chords, and conic–conic intersections. +- Exact/adaptive conic arcs and transactional fit-and-replace for selected paths. +- Explicit line-pair, double-line, single-line, point, and empty degenerate loci. +- Latus recta, auxiliary/director circles, equations, area, and parameter labels. +- Strict selection, option, metadata, and finite-number validation. +- Automatic live preview plus a manual Preview button. +- Active-layer creation, safe use of the current Ipe attributes, grouping controls, and single-step undo/redo. +- Scale-aware calculations for very small and very large coordinates. +- Compatible reading of conic metadata written by the earlier Geometry implementation. + +## Tools + +The Ipe menu contains `Ipelets → Conics` with seven entries: + +| Menu entry | Included workflows | +| --- | --- | +| Construct: conic | Steiner ellipses; exact five-point construction; best fit from many points; five tangent lines; five mixed point/tangent conditions; focus–directrix with a point or numeric eccentricity; canonical midpoint ellipse; and explicit degenerate loci. | +| Construct: ellipse | Native ellipse from two foci and one point, or from a center and two perpendicular semiaxis endpoints. | +| Construct: hyperbola | Hyperbola from two foci and a point, center and semiaxes, equal semiaxes, or two asymptotes and one point. | +| Construct: parabola | One exact parabola for every focus paired with a directrix, or one parabola from its vertex and focus. | +| Features: conic | Tangent/normal, polar and pole, tangents from a point, chord of contact, focal chord, line and conic intersections, conic arcs, fit-and-replace, and editable property/equation guides. | +| Inspect: conic | Compute coefficients and standard properties without changing the document. | +| Metadata: revalidate selected conic | Refit and replace stale metadata after structural path editing. | + +## Requirements + +- Ipe with Lua 5.4 ipelet support. +- Designed and tested for Ipe 7.2.30 on Linux. + +The ipelet uses documented Ipe Lua objects and does not require a build step. + +## Installation + +Download the self-contained package from the [Conics 1.1.0 release](https://github.com/japbcoelho/ipelets/releases/tag/conics-v1.1.0), or use one of the methods below. + +### Repository helper on Linux + +From the repository root: + +```bash +./scripts/install.sh conics +``` + +Restart Ipe after the copy completes. + +### Manual installation + +Copy [`conics.lua`](conics.lua) into one of Ipe's user ipelet directories: + +- Linux, native installation: `~/.ipe/ipelets/` +- Linux, Flatpak installation: `~/.var/app/org.otfried.Ipe/.ipe/ipelets/` +- macOS: `~/.ipe/ipelets/` or `~/Library/Ipe/Ipelets/` +- Windows: `%USERPROFILE%\Ipelets\` + +Place the file directly in the ipelet directory, restart Ipe, and open the new `Conics` submenu under `Ipelets`. + +## Selection contracts + +The dialogs show these requirements in the interface: + +| Workflow | Required selection | +| --- | --- | +| Steiner ellipses | Exactly three marks. | +| Conic through five points | Exactly five marks. | +| Best-fit conic | From 6 to 512 marks. | +| Conic tangent to five lines | Exactly five segments. | +| Five mixed conditions | Five marks; or four marks plus one tangent segment; or three marks plus two tangent segments. Each tangent segment must pass through exactly one selected tangent-point mark. | +| Focus, directrix, and point | Two marks and one segment; the point on the conic must be primary, the other mark is the focus, and the segment is the directrix. | +| Focus, directrix, and eccentricity | One primary focus mark and one secondary directrix segment; enter `e<1`, `e=1`, or `e>1` for an ellipse, parabola, or hyperbola. | +| Canonical midpoint ellipse | Exactly four marks. | +| Ellipse from foci and point | Three marks; the point on the ellipse must be primary and the two secondary marks are the foci. | +| Ellipse from center and semiaxes | Three marks; the center must be primary and the two endpoints must define nonzero perpendicular semiaxes. | +| Hyperbola from foci and point | Three marks; the point on the hyperbola must be primary and the two secondary marks are the foci. | +| Hyperbola from parameters | One primary center mark and, optionally, one secondary segment supplying the transverse-axis direction. | +| Hyperbola from asymptotes and point | One primary point mark and two secondary segments defining intersecting asymptotes. | +| Parabola from directrix and foci | One primary directrix segment and one or more secondary focus marks. | +| Parabola from vertex and focus | Two marks; the vertex must be primary. | +| Degenerate loci | Two segments for a line pair, one segment for a double or single line, one mark for a point, and no selection for the empty locus. | +| Tangent, normal, or polar | One primary conic and one secondary mark. A tangent or normal point must lie on the conic. | +| Tangents from a point | One primary conic and one secondary mark; zero, one, or two real tangents are reported correctly. | +| Pole of a line | One primary conic and one secondary segment. A pole at infinity is reported without creating a false finite point. | +| Focal chord | One primary noncircular conic and one secondary mark defining the line through a selected focus. | +| Line intersections | One primary conic and one secondary segment. | +| Intersections of two conics | Exactly two conics; the first must be primary. Zero through four finite real intersections and coincident conics are distinguished. | +| Trim conic to arc | One primary conic and two secondary marks on the same connected arc. | +| Fit and replace selected path | Exactly one primary path. | +| Property guides, inspection, or revalidation | Exactly one primary conic or one group containing a single logical conic. | + +Explicit API inputs take precedence over document selection. When a workflow must read the selection, extra or incorrectly typed selected objects are rejected instead of silently ignored. + +## Extent, quality, and grouping + +- `extent` sets a symmetric open-curve extent. For a hyperbola it is measured along the transverse axis and must exceed the transverse semiaxis. +- General conic construction also accepts `bounds={left,bottom,right,top}`. `extent` and `bounds` are mutually exclusive. +- `padding` expands an automatically inferred open-curve extent. +- `tolerance` controls the maximum geometric approximation error used to subdivide a hyperbola branch. +- `max_segments` is a safety budget for each adaptive hyperbola branch; the legacy `samples` alias remains accepted as a segment budget with a minimum of four. +- `expected_kind` can constrain a least-squares fit to `ellipse`, `parabola`, or `hyperbola`; the fit is rejected when its residual or conditioning is unreliable. +- `arc_mode` selects the shorter, longer, clockwise, or counterclockwise ellipse arc. Parabola and hyperbola endpoints must belong to one connected branch. +- `group_output` chooses whether related branches, auxiliaries, multiple parabolas, or multiple feature objects are grouped. If they remain separate, only the first object is primary and the rest are secondary selections. + +## Analytic properties and equations + +`Property guides` can create the applicable center or vertex, axes, vertices, foci, directrices, asymptotes, latus recta, auxiliary circles, and director circle. It can also place editable LaTeX labels for the normalized general equation, a canonical equation, and parameters such as `a`, `b`, `c`, eccentricity, focal parameter, semilatus rectum, focal radius, and ellipse area. Controls that do not apply to the selected operation are disabled in the dialog. + +## Preview and document behavior + +Live preview is enabled by default and never mutates the document. It follows dialog options and the selected objects' geometry and affine matrices. The manual Preview button reports a useful status or error in Ipe's status area. Canceling or an exception always removes the preview overlay and timer. + +Objects are created on the active layer. Conics warns when that layer is invisible. Path, mark, and text attributes are derived from the current Ipe attributes, but incompatible path attributes such as fill, arrows, and decorations are filtered out. The ipelet does not require a personal stylesheet; every default symbolic name comes from Ipe's standard styles. + +## Metadata and editing + +Created curves use versioned `conics:v1` metadata with an object role, conic identifier, kind, source, coordinate space, coefficients, and a geometry fingerprint. Auxiliary axes, marks, labels, directrices, asymptotes, poles, chords, latus recta, auxiliary/director circles, equations, and degenerate loci have distinct roles and never masquerade as an ordinary conic curve. + +Moving, rotating, scaling, or shearing a conic is supported: inspection transforms its stored coefficients by the object's affine matrix. Editing the internal nodes of an approximate path changes the geometry without changing that matrix, so Conics detects a stale fingerprint. Use `Metadata: revalidate selected conic` to refit the selected path; the command refuses a repair when the edited shape is not reliably conic. + +## Examples + +- [`conics-overview.ipe`](examples/conics-overview.ipe) is the editable presentation source for the five-point workflow, basic property extraction, focus-directrix construction, hyperbola branches, and multiple parabolas. +- [`conics-feature-gallery.ipe`](examples/conics-feature-gallery.ipe) is a 32-page editable gallery generated by the live acceptance audit. It covers every construction family, the advanced feature workflows, degenerate loci, previews, fitting, inspection, and metadata revalidation. +- Every mathematical diagram in the committed media comes from a real Ipe session. The presentation layouts use only those Ipe renders, and both example documents carry every style they need. + +## Public API + +`_G.CONICS` exposes API version `1`. The creators accept either selection-driven input or structured tables. For feature operations, prefer separate `definition` and `feature_input` blocks: + +```lua +local result = CONICS.create_conic_features(model, { + operation = "conic_intersections", + definition = { coefficients = { 1, 0, 1, 0, 0, -100 } }, + feature_input = { + second_coefficients = { 1, 0, 1, -12, 0, -64 }, + }, + marks = false, +}) +``` + +This returns a computed result with two points and does not mutate the document. The canonical creator names are `create_conic`, `create_ellipse`, `create_hyperbola`, `create_parabola`, and `create_conic_features`; the 1.0 aliases `create_ellipse_from_foci` and `create_parabolas` remain available. Public creators consistently report `created`, `status`, `operation`, `element_count`, `object_count`, `metadata`, and `result`. + +## Testing + +From the repository root: + +```bash +./scripts/validate.sh conics +``` + +The portable suite loads only `conics.lua` in a minimal Ipe-compatible Lua runtime. It covers construction contracts, numerical regressions, metadata migration and corruption, previews, transactions, package contents, and the absence of local development dependencies. + +## Troubleshooting + +### Conics does not appear + +Confirm that `conics.lua` is directly inside an Ipe user ipelet directory, not inside a nested `conics/` folder, and restart Ipe completely. + +### A selection is rejected + +Check the `Required selection` line in the dialog. Point inputs must be `mark/*` references, and a line input must be one open path containing exactly one segment. Extra selected text, paths, or references are intentionally rejected. + +### The object was created but is not visible + +Conics uses the active layer. Make that layer visible or activate a visible layer before creating the result. + +### Inspection says the metadata is stale + +The curve's internal shape was edited. Use the revalidation command if the edited path is still a conic, or reconstruct it from its defining inputs. + +## License and attribution + +Copyright (C) 2026 japbcoelho. Conics is licensed under the GNU General Public License, version 3 or any later version. The ellipse-from-foci and parabola formulas are adapted from Ipe's GPL-licensed `goodies.lua`. See [LICENSE](../LICENSE) and [NOTICE.md](../NOTICE.md). diff --git a/conics/README.pt-BR.md b/conics/README.pt-BR.md new file mode 100644 index 0000000..0b7e5a8 --- /dev/null +++ b/conics/README.pt-BR.md @@ -0,0 +1,205 @@ +# Conics para Ipe + +[Read in English](README.md) + +![Visão geral de construções editáveis e guias de propriedades do Conics](docs/images/conics-overview.svg) + +Conics é um Ipelet Lua autônomo para construir, ajustar, classificar, intersectar, recortar, inspecionar e anotar cônicas. Ele contempla elipses, circunferências, parábolas, hipérboles e lugares geométricos degenerados explícitos. Preserva a geometria exata do Ipe quando possível, utiliza splines adaptativas compactas nos ramos abertos de hipérboles e mantém todo resultado visível editável e compatível com desfazer. + +## Fluxo principal: cinco pontos viram uma cônica estruturada + +Selecione cinco marcas e escolha `Conic through five points`. O Conics resolve diretamente o sistema homogêneo dos cinco pontos, rejeita dados duplicados, mal condicionados ou degenerados, classifica o resultado e escolhe automaticamente a melhor representação no Ipe: elipse nativa, spline quadrática exata de parábola ou ramos contínuos e adaptativos de hipérbole em splines cúbicas. + +![Pré-visualização ao vivo de uma cônica geral por cinco pontos selecionados](docs/images/conics-five-point-live-preview.png) + +Selecione o resultado e abra `Property guides` para criar sua geometria significativa: centro ou vértice, eixos, vértices, focos, diretrizes e assíntotas, usando marcas, retas e rótulos opcionais nativos. + +![Guias editáveis extraídos das propriedades de uma cônica](docs/images/conics-property-guides.png) + +O fluxo de diretriz e focos pode criar várias parábolas exatas em uma única operação, preservando uma única etapa limpa de desfazer. + +![Várias parábolas exatas geradas por uma diretriz e diversos focos](docs/images/conics-parabolas.png) + +A versão 1.1 também trabalha com o lado dual da geometria das cônicas: cinco retas tangentes determinam uma cônica, condições mistas de pontos e tangentes podem ser combinadas, e um ponto exterior produz as duas tangentes e sua corda de contato. Duas cônicas selecionadas podem ser intersectadas diretamente, enquanto uma curva existente pode ser recortada em um arco conexo ou ajustada e substituída em uma única transação de desfazer. + +![Envoltória de cinco retas, tangentes por ponto exterior com corda de contato e quatro interseções entre duas cônicas](docs/images/conics-advanced-workflows.png) + +## Destaques + +- Um único arquivo autônomo de execução: `conics.lua`. +- Nenhuma dependência do Ipelet Geometry, de bridge auxiliar, da rede ou de pacotes Lua externos. +- Elipses nativas exatas e splines compactas e exatas de parábolas. +- Ramos contínuos de hipérboles subdivididos por tolerância de erro geométrico. +- Construção dual exata por cinco retas e condições mistas de pontos e tangentes. +- Ajuste por mínimos quadrados a partir de 6–512 marcas de amostra, com verificação de condicionamento e resíduo. +- Polos, polares, tangentes por ponto exterior, cordas de contato, cordas focais e interseções entre cônicas. +- Arcos de cônicas exatos ou adaptativos e ajuste com substituição transacional de caminhos selecionados. +- Lugares degenerados explícitos: par de retas, reta dupla, reta única, ponto e conjunto vazio. +- Lados retos, circunferências auxiliares ou diretora, equações, área e rótulos de parâmetros. +- Validação estrita de seleção, opções, metadados e números finitos. +- Pré-visualização automática e botão manual Preview. +- Criação na camada ativa, uso seguro dos atributos atuais do Ipe, controles de agrupamento e desfazer/refazer em uma única etapa. +- Cálculos sensíveis à escala para coordenadas muito pequenas ou muito grandes. +- Leitura compatível dos metadados de cônicas escritos pela implementação anterior no Geometry. + +## Ferramentas + +O menu `Ipelets → Conics` contém sete entradas: + +| Entrada do menu | Operações incluídas | +| --- | --- | +| Construct: conic | Elipses de Steiner; construção exata por cinco pontos; melhor ajuste por muitos pontos; cinco retas tangentes; cinco condições mistas de pontos e tangentes; foco–diretriz com ponto ou excentricidade numérica; elipse canônica pelos pontos médios; e lugares degenerados explícitos. | +| Construct: ellipse | Elipse nativa por dois focos e um ponto ou por um centro e dois extremos perpendiculares dos semieixos. | +| Construct: hyperbola | Hipérbole por dois focos e um ponto, centro e semieixos, semieixos iguais ou duas assíntotas e um ponto. | +| Construct: parabola | Uma parábola exata para cada foco combinado com uma diretriz ou uma parábola pelo vértice e foco. | +| Features: conic | Tangente/normal, polar e polo, tangentes por um ponto, corda de contato, corda focal, interseções com reta ou cônica, arcos de cônicas, ajuste com substituição e guias editáveis de propriedades e equações. | +| Inspect: conic | Calcula coeficientes e propriedades padrão sem alterar o documento. | +| Metadata: revalidate selected conic | Reajusta e substitui metadados obsoletos depois de uma edição estrutural do caminho. | + +## Requisitos + +- Ipe com suporte a Ipelets em Lua 5.4. +- Projetado e testado para o Ipe 7.2.30 no Linux. + +O Ipelet utiliza objetos documentados da API Lua do Ipe e não exige compilação. + +## Instalação + +Baixe o pacote autocontido na [versão 1.1.0 de Conics](https://github.com/japbcoelho/ipelets/releases/tag/conics-v1.1.0) ou utilize um dos métodos abaixo. + +### Utilitário do repositório no Linux + +Na raiz do repositório: + +```bash +./scripts/install.sh conics +``` + +Reinicie o Ipe depois que a cópia terminar. + +### Instalação manual + +Copie [`conics.lua`](conics.lua) para uma das pastas de Ipelets do usuário: + +- Linux, instalação nativa: `~/.ipe/ipelets/` +- Linux, instalação Flatpak: `~/.var/app/org.otfried.Ipe/.ipe/ipelets/` +- macOS: `~/.ipe/ipelets/` ou `~/Library/Ipe/Ipelets/` +- Windows: `%USERPROFILE%\Ipelets\` + +Coloque o arquivo diretamente na pasta de Ipelets, reinicie o Ipe e abra o novo submenu `Conics` em `Ipelets`. + +## Contratos de seleção + +As janelas mostram estes requisitos diretamente na interface: + +| Fluxo | Seleção necessária | +| --- | --- | +| Elipses de Steiner | Exatamente três marcas. | +| Cônica por cinco pontos | Exatamente cinco marcas. | +| Cônica por melhor ajuste | De 6 a 512 marcas. | +| Cônica tangente a cinco retas | Exatamente cinco segmentos. | +| Cinco condições mistas | Cinco marcas; ou quatro marcas e um segmento tangente; ou três marcas e dois segmentos tangentes. Cada segmento tangente deve passar por exatamente uma marca selecionada no ponto de tangência. | +| Foco, diretriz e ponto | Duas marcas e um segmento; o ponto da cônica deve ser primário, a outra marca é o foco e o segmento é a diretriz. | +| Foco, diretriz e excentricidade | Uma marca primária no foco e um segmento secundário na diretriz; informe `e<1`, `e=1` ou `e>1` para obter elipse, parábola ou hipérbole. | +| Elipse canônica pelos pontos médios | Exatamente quatro marcas. | +| Elipse por focos e ponto | Três marcas; o ponto da elipse deve ser primário e as duas marcas secundárias são os focos. | +| Elipse por centro e semieixos | Três marcas; o centro deve ser primário e os dois extremos devem definir semieixos não nulos e perpendiculares. | +| Hipérbole por focos e ponto | Três marcas; o ponto da hipérbole deve ser primário e as duas marcas secundárias são os focos. | +| Hipérbole por parâmetros | Uma marca primária para o centro e, opcionalmente, um segmento secundário que fornece a direção do eixo transverso. | +| Hipérbole por assíntotas e ponto | Uma marca primária no ponto e dois segmentos secundários que definem assíntotas concorrentes. | +| Parábola por diretriz e focos | Um segmento primário para a diretriz e uma ou mais marcas secundárias para os focos. | +| Parábola por vértice e foco | Duas marcas; o vértice deve ser primário. | +| Lugares degenerados | Dois segmentos para um par de retas, um segmento para reta dupla ou única, uma marca para um ponto e nenhuma seleção para o conjunto vazio. | +| Tangente, normal ou polar | Uma cônica primária e uma marca secundária. O ponto de tangente ou normal deve pertencer à cônica. | +| Tangentes por um ponto | Uma cônica primária e uma marca secundária; o resultado distingue corretamente zero, uma ou duas tangentes reais. | +| Polo de uma reta | Uma cônica primária e um segmento secundário. Um polo no infinito é informado sem criar um ponto finito falso. | +| Corda focal | Uma cônica não circular primária e uma marca secundária que define a reta por um foco escolhido. | +| Interseções com reta | Uma cônica primária e um segmento secundário. | +| Interseções de duas cônicas | Exatamente duas cônicas; a primeira deve ser primária. O resultado distingue de zero a quatro interseções reais finitas e cônicas coincidentes. | +| Recortar cônica em arco | Uma cônica primária e duas marcas secundárias no mesmo arco conexo. | +| Ajustar e substituir caminho | Exatamente um caminho primário. | +| Guias de propriedades, inspeção ou revalidação | Exatamente uma cônica primária ou um grupo que contenha uma única cônica lógica. | + +Entradas explícitas da API têm precedência sobre a seleção do documento. Quando um fluxo precisa ler a seleção, objetos extras ou de tipo incorreto são rejeitados em vez de ignorados silenciosamente. + +## Extensão, qualidade e agrupamento + +- `extent` define uma extensão simétrica da curva aberta. Em hipérboles, ela é medida no eixo transverso e deve ser maior que o semieixo transverso. +- A construção geral também aceita `bounds={left,bottom,right,top}`. `extent` e `bounds` são mutuamente exclusivos. +- `padding` amplia uma extensão de curva aberta calculada automaticamente. +- `tolerance` controla o erro geométrico máximo usado para subdividir um ramo de hipérbole. +- `max_segments` é o limite de segurança para cada ramo adaptativo de hipérbole; o alias legado `samples` continua aceito como limite de segmentos, com mínimo de quatro. +- `expected_kind` pode restringir um ajuste por mínimos quadrados a `ellipse`, `parabola` ou `hyperbola`; o ajuste é recusado quando o resíduo ou o condicionamento não é confiável. +- `arc_mode` escolhe o arco menor, maior, horário ou anti-horário de uma elipse. As extremidades de uma parábola ou hipérbole devem pertencer ao mesmo ramo conexo. +- `group_output` escolhe se ramos relacionados, auxiliares, múltiplas parábolas ou vários elementos serão agrupados. Quando permanecem separados, somente o primeiro objeto recebe seleção primária e os demais recebem seleção secundária. + +## Propriedades analíticas e equações + +`Property guides` pode criar centro ou vértice, eixos, vértices, focos, diretrizes, assíntotas, lados retos, circunferências auxiliares e circunferência diretora, quando forem aplicáveis. Também pode inserir rótulos LaTeX editáveis para a equação geral normalizada, uma equação canônica e parâmetros como `a`, `b`, `c`, excentricidade, parâmetro focal, semilado reto, raio focal e área da elipse. Os controles que não pertencem à operação escolhida ficam desabilitados na janela. + +## Pré-visualização e comportamento no documento + +A pré-visualização ao vivo fica ativada por padrão e nunca altera o documento. Ela acompanha as opções da janela, a geometria dos objetos selecionados e suas matrizes afins. O botão manual Preview informa um estado ou erro útil na área de status do Ipe. Cancelar ou ocorrer uma exceção sempre encerra o timer e remove a sobreposição da prévia. + +Os objetos são criados na camada ativa. O Conics avisa quando essa camada está invisível. Os atributos de caminhos, marcas e textos partem dos atributos atuais do Ipe, mas atributos incompatíveis com caminhos de cônicas, como preenchimento, setas e decorações, são filtrados. O Ipelet não exige stylesheet pessoal; todos os nomes simbólicos padrão pertencem aos estilos básicos do Ipe. + +## Metadados e edição + +As curvas criadas utilizam metadados versionados `conics:v1`, com papel do objeto, identificador da cônica, tipo, origem, sistema de coordenadas, coeficientes e impressão digital da geometria. Eixos, marcas, rótulos, diretrizes, assíntotas, polos, cordas, lados retos, circunferências auxiliares ou diretora, equações e lugares degenerados têm papéis próprios e nunca são confundidos com uma curva cônica comum. + +Mover, girar, redimensionar ou cisalhar uma cônica é suportado: a inspeção transforma os coeficientes armazenados pela matriz afim do objeto. Editar os nós internos de um caminho aproximado altera a geometria sem mudar essa matriz, então o Conics detecta uma impressão digital obsoleta. Use `Metadata: revalidate selected conic` para reajustar o caminho selecionado; o comando recusa o reparo quando a forma editada não é uma cônica confiável. + +## Exemplos + +- [`conics-overview.ipe`](examples/conics-overview.ipe) é a fonte editável da apresentação do fluxo de cinco pontos, da extração básica de propriedades, da construção por foco e diretriz, dos ramos de hipérbole e das múltiplas parábolas. +- [`conics-feature-gallery.ipe`](examples/conics-feature-gallery.ipe) é uma galeria editável de 32 páginas gerada pela auditoria viva de aceitação. Ela cobre todas as famílias de construção, os fluxos avançados de elementos, lugares degenerados, previews, ajuste, inspeção e revalidação de metadados. +- Todo diagrama matemático da mídia versionada vem de uma sessão real do Ipe. Os layouts de apresentação usam somente essas renderizações do Ipe, e os dois documentos de exemplo carregam internamente todos os estilos necessários. + +## API pública + +`_G.CONICS` expõe a versão de API `1`. Os criadores aceitam entradas guiadas por seleção ou tabelas estruturadas. Nas operações de elementos, prefira blocos separados para `definition` e `feature_input`: + +```lua +local result = CONICS.create_conic_features(model, { + operation = "conic_intersections", + definition = { coefficients = { 1, 0, 1, 0, 0, -100 } }, + feature_input = { + second_coefficients = { 1, 0, 1, -12, 0, -64 }, + }, + marks = false, +}) +``` + +Esse exemplo devolve um resultado calculado com dois pontos e não altera o documento. Os nomes canônicos dos criadores são `create_conic`, `create_ellipse`, `create_hyperbola`, `create_parabola` e `create_conic_features`; os aliases da versão 1.0, `create_ellipse_from_foci` e `create_parabolas`, continuam disponíveis. Todos os criadores públicos informam consistentemente `created`, `status`, `operation`, `element_count`, `object_count`, `metadata` e `result`. + +## Testes + +Na raiz do repositório: + +```bash +./scripts/validate.sh conics +``` + +A suíte portátil carrega apenas `conics.lua` em um ambiente Lua mínimo e compatível com o Ipe. Ela cobre contratos de construção, regressões numéricas, migração e corrupção de metadados, pré-visualizações, transações, conteúdo do pacote e ausência de dependências locais de desenvolvimento. + +## Solução de problemas + +### Conics não aparece + +Confirme que `conics.lua` está diretamente dentro de uma pasta de Ipelets do usuário, e não dentro de outra pasta chamada `conics/`, e reinicie completamente o Ipe. + +### Uma seleção é rejeitada + +Confira a linha `Required selection` na janela. Entradas de pontos devem ser referências `mark/*`, e uma entrada de reta deve ser um caminho aberto com exatamente um segmento. Textos, caminhos ou referências adicionais são rejeitados intencionalmente. + +### O objeto foi criado, mas não está visível + +O Conics usa a camada ativa. Torne essa camada visível ou ative uma camada visível antes de criar o resultado. + +### A inspeção informa que os metadados estão obsoletos + +A forma interna da curva foi editada. Use o comando de revalidação se o caminho alterado ainda for uma cônica ou reconstrua a curva a partir de suas entradas definidoras. + +## Licença e atribuição + +Copyright (C) 2026 japbcoelho. Conics é licenciado sob a GNU General Public License, versão 3 ou qualquer versão posterior. As fórmulas de elipse por focos e de parábola foram adaptadas do `goodies.lua` do Ipe, licenciado sob GPL. Consulte [LICENSE](../LICENSE) e [NOTICE.md](../NOTICE.md). diff --git a/conics/VERSION b/conics/VERSION new file mode 100644 index 0000000..9084fa2 --- /dev/null +++ b/conics/VERSION @@ -0,0 +1 @@ +1.1.0 diff --git a/conics/conics.lua b/conics/conics.lua new file mode 100644 index 0000000..19b5557 --- /dev/null +++ b/conics/conics.lua @@ -0,0 +1,7182 @@ +---------------------------------------------------------------------- +-- Conics +-- SPDX-License-Identifier: GPL-3.0-or-later +-- Copyright (C) 2026 japbcoelho +-- +-- Standalone conic construction tools for Ipe 7.2. +-- Ellipse-from-foci and parabola construction formulas are adapted +-- from Ipe's GPL-licensed goodies.lua. +---------------------------------------------------------------------- + +label = "Conics" + +about = [[ +Conics 1.1.0 + +Standalone construction, inspection, and feature tools for ellipses, +parabolas, hyperbolas, circles, and general or degenerate conics. + +Ellipse-from-foci and parabola formulas are adapted from Ipe's +GPL-licensed goodies.lua. + +License: GPL-3.0-or-later. +]] + +local _G = _G +local ipe = ipe +local ipeui = ipeui +local error = _G.error +local ipairs = _G.ipairs +local math = _G.math +local pairs = _G.pairs +local pcall = _G.pcall +local rawget = _G.rawget +local select = _G.select +local setmetatable = _G.setmetatable +local string = _G.string +local table = _G.table +local tonumber = _G.tonumber +local tostring = _G.tostring +local type = _G.type +local unpack = table.unpack + +local V = ipe.Vector +local API_VERSION = 1 +local VERSION = "1.1.0" +local MACHINE_EPSILON = 2.220446049250313e-16 +local MIN_NORMAL = 2.2250738585072014e-308 +local LOG_TWO = 0.6931471805599453 +local previous_api = rawget(_G, "CONICS") +local PERSISTED_DIALOG_STATE = type(previous_api) == "table" + and type(previous_api.dialog_state) == "table" + and previous_api.dialog_state or {} + +---------------------------------------------------------------------- +-- Pure numeric and analytic geometry +---------------------------------------------------------------------- + +local M = {} +local Advanced = {} + +local function finite_number(value) + return type(value) == "number" + and value == value + and value ~= math.huge + and value ~= -math.huge +end + +local function options_table(options) + if options == nil then return {} end + if type(options) ~= "table" then error("options must be a table") end + return options +end + +local function number_value(value, fallback) + if value == nil or value == "" then return fallback end + if type(value) == "number" then return value end + local ok, converted = pcall(tonumber, value) + if ok and converted ~= nil then return converted end + return fallback +end + +local function finite_number_option(value, fallback, name) + if value == nil or value == "" then value = fallback end + local converted = number_value(value, nil) + if not finite_number(converted) then + error((name or "value") .. " must be a finite number") + end + return converted +end + +local function positive_number_option(value, fallback, name) + local converted = finite_number_option(value, fallback, name) + if converted <= 0 then error((name or "value") .. " must be positive") end + return converted +end + +local function nonnegative_number_option(value, fallback, name) + local converted = finite_number_option(value, fallback, name) + if converted < 0 then error((name or "value") .. " must be nonnegative") end + return converted +end + +local function positive_integer_option(value, fallback, name, maximum, minimum) + local converted = finite_number_option(value, fallback, name) + minimum = minimum or 1 + if converted < minimum or converted ~= math.floor(converted) then + error((name or "value") .. " must be an integer greater than or equal to " + .. tostring(minimum)) + end + if maximum and converted > maximum then + error((name or "value") .. " must be at most " .. tostring(maximum)) + end + return converted +end + +local function bool_value(value, fallback) + if value == nil then return fallback end + if value == false or value == 0 or value == "0" or value == "false" then return false end + if value == true or value == 1 or value == "1" or value == "true" then return true end + error("boolean option must be true or false") +end + +function M.aliased_value(options, primary, legacy, context) + local primary_value, legacy_value = options[primary], options[legacy] + if primary_value ~= nil and legacy_value ~= nil then + error((context or "options") .. " cannot contain both '" .. primary + .. "' and its legacy alias '" .. legacy .. "'") + end + if primary_value ~= nil then return primary_value end + return legacy_value +end + +local function normalized_name(value) + return tostring(value or ""):lower():gsub("[%s%-]+", "_") +end + +local function clean_error_message(message) + message = tostring(message or "") + local cleaned = message:match("^%[string [^%]]+%]:%d+:%s*(.*)$") + or message:match("^[^:]+:%d+:%s*(.*)$") + return cleaned and cleaned ~= "" and cleaned or message +end + +local function format_number(value, precision) + precision = precision or 6 + if not finite_number(value) then return tostring(value) end + local text = string.format("%." .. tostring(precision) .. "g", value) + return text == "-0" and "0" or text +end + +local function scaled_tolerance(scale_value, multiplier) + return (multiplier or 128) * MACHINE_EPSILON + * math.max(math.abs(scale_value or 0), MIN_NORMAL) +end + +local function near_zero(value, scale_value, multiplier) + return math.abs(value) <= scaled_tolerance(scale_value, multiplier) +end + +local function hypot(x, y) + x, y = math.abs(x), math.abs(y) + local scale_value = math.max(x, y) + if scale_value == math.huge then return math.huge end + if scale_value == 0 then return 0 end + return scale_value * math.sqrt((x / scale_value) ^ 2 + (y / scale_value) ^ 2) +end + +local function add(a, b) return V(a.x + b.x, a.y + b.y) end +local function sub(a, b) return V(a.x - b.x, a.y - b.y) end +local function scale(vector, factor) return V(vector.x * factor, vector.y * factor) end +local function lerp(a, b, t) return add(scale(a, 1 - t), scale(b, t)) end +local function dot(a, b) return a.x * b.x + a.y * b.y end +local function cross(a, b) return a.x * b.y - a.y * b.x end +local function length(vector) return hypot(vector.x, vector.y) end +local function distance(a, b) return hypot(a.x - b.x, a.y - b.y) end +local function midpoint(a, b) return V(a.x * 0.5 + b.x * 0.5, a.y * 0.5 + b.y * 0.5) end +local function perpendicular(vector) return V(-vector.y, vector.x) end + +local function vector_scale(...) + local result = 0 + for index = 1, select("#", ...) do + local vector = select(index, ...) + if vector then result = math.max(result, math.abs(vector.x), math.abs(vector.y)) end + end + return result +end + +local function unit(vector, context) + local magnitude = length(vector) + if not finite_number(magnitude) or near_zero(magnitude, vector_scale(vector), 4096) then + error((context or "direction") .. " must be nonzero") + end + return scale(vector, 1 / magnitude) +end + +local function coordinate_value(point, key, fallback_key) + local ok, value = pcall(function() return point[key] end) + if ok and value ~= nil then return value end + if fallback_key ~= nil then + local fallback_ok, fallback = pcall(function() return point[fallback_key] end) + if fallback_ok then return fallback end + end + return nil +end + +local function point_from_table(point, name) + name = name or "point" + if point == nil then error(name .. " must be a point table or vector") end + local x = number_value(coordinate_value(point, "x", 1), nil) + local y = number_value(coordinate_value(point, "y", 2), nil) + if x == nil or y == nil then error(name .. " must contain x/y numbers") end + if not finite_number(x) or not finite_number(y) then + error(name .. " must contain finite x/y numbers") + end + return V(x, y) +end + +local function point_record(point) + if not finite_number(point.x) or not finite_number(point.y) then + error("derived point is not finite") + end + return { x = point.x, y = point.y } +end + +local function points_from_table(points, expected, name) + name = name or "points" + if type(points) ~= "table" then error(name .. " must be a point list") end + if expected and #points ~= expected then + error("exactly " .. tostring(expected) .. " points are required") + end + local result = {} + for index, point in ipairs(points) do + result[index] = point_from_table(point, name .. "[" .. tostring(index) .. "]") + end + return result +end + +local function line_from_points(p1, p2, name) + name = name or "line" + p1 = point_from_table(p1, name .. ".p1") + p2 = point_from_table(p2, name .. ".p2") + local direction = sub(p2, p1) + local magnitude = length(direction) + if not finite_number(magnitude) + or near_zero(magnitude, vector_scale(direction), 4096) then + error(name .. " endpoints must be distinct") + end + local a, b = -direction.y / magnitude, direction.x / magnitude + local c = -(a * p1.x + b * p1.y) + if not finite_number(c) then error(name .. " equation is not finite") end + return { + p1 = p1, p2 = p2, point = p1, direction = scale(direction, 1 / magnitude), + a = a, b = b, c = c, + } +end + +local function line_from_equation(a, b, c, preferred_point) + a = finite_number_option(a, nil, "line.a") + b = finite_number_option(b, nil, "line.b") + c = finite_number_option(c, 0, "line.c") + local norm = hypot(a, b) + if not finite_number(norm) or norm == 0 then error("line equation has no direction") end + a, b, c = a / norm, b / norm, c / norm + local point + if preferred_point then + local preferred = point_from_table(preferred_point, "line.point") + local offset = a * preferred.x + b * preferred.y + c + point = V(preferred.x - a * offset, preferred.y - b * offset) + elseif math.abs(a) >= math.abs(b) then + point = V(-c / a, 0) + else + point = V(0, -c / b) + end + if not finite_number(point.x) or not finite_number(point.y) then + error("line equation does not have a finite representable point") + end + local direction = V(-b, a) + return { + point = point, direction = direction, p1 = point, p2 = add(point, direction), + a = a, b = b, c = c, + } +end + +local function line_from_table(line, name) + name = name or "line" + if type(line) ~= "table" then error(name .. " must be a line table") end + local coefficient_a = rawget(line, "coefficient_a") or rawget(line, "A") + local coefficient_b = rawget(line, "coefficient_b") or rawget(line, "B") + local coefficient_c = rawget(line, "coefficient_c") or rawget(line, "C") + if coefficient_a == nil and type(rawget(line, "a")) == "number" + and type(rawget(line, "b")) == "number" + and rawget(line, "p1") == nil and rawget(line, 1) == nil then + coefficient_a, coefficient_b, coefficient_c = line.a, line.b, line.c + end + if coefficient_a ~= nil or coefficient_b ~= nil then + return line_from_equation(coefficient_a, coefficient_b, coefficient_c, line.point) + end + local p1 = line.p1 or line.start or line.a or line[1] + local p2 = line.p2 or line.finish or line.b or line[2] + if type(line.points) == "table" then + p1, p2 = p1 or line.points[1], p2 or line.points[2] + end + return line_from_points(p1, p2, name) +end + +local function line_record(line) + return { + point = point_record(line.point or line.p1), + direction = point_record(unit(line.direction or sub(line.p2, line.p1), "line direction")), + equation = { a = line.a, b = line.b, c = line.c }, + } +end + +function M.bounds_from_table(bounds) + if type(bounds) ~= "table" then error("bounds must be a table") end + local first = bounds.p1 or bounds.min or bounds[1] + local second = bounds.p2 or bounds.max or bounds[2] + local left = bounds.left or bounds.min_x or bounds.xmin + local right = bounds.right or bounds.max_x or bounds.xmax + local bottom = bounds.bottom or bounds.min_y or bounds.ymin + local top = bounds.top or bounds.max_y or bounds.ymax + if first ~= nil or second ~= nil then + if first == nil or second == nil then error("bounds require both corner points") end + first, second = point_from_table(first, "bounds.p1"), point_from_table(second, "bounds.p2") + left, right = math.min(first.x, second.x), math.max(first.x, second.x) + bottom, top = math.min(first.y, second.y), math.max(first.y, second.y) + else + left = finite_number_option(left, nil, "bounds.left") + right = finite_number_option(right, nil, "bounds.right") + bottom = finite_number_option(bottom, nil, "bounds.bottom") + top = finite_number_option(top, nil, "bounds.top") + end + if not left or not right or not bottom or not top or left >= right or bottom >= top then + error("bounds must have positive width and height") + end + return { + left = left, right = right, bottom = bottom, top = top, + corners = { V(left, bottom), V(right, bottom), V(right, top), V(left, top) }, + } +end + +local function validate_keys(options, allowed, context) + for key, _ in pairs(options) do + if not allowed[key] then + error((context or "options") .. " contains unsupported field '" .. tostring(key) .. "'") + end + end +end + +local function normalize_coefficients(coefficients) + if type(coefficients) ~= "table" or #coefficients ~= 6 then + error("exactly six conic coefficients are required") + end + local result, scale_value = {}, 0 + for index = 1, 6 do + local value = number_value(coefficients[index], nil) + if not finite_number(value) then error("conic coefficients must be finite numbers") end + result[index] = value + scale_value = math.max(scale_value, math.abs(value)) + end + if scale_value == 0 then error("conic coefficients must not all be zero") end + for index = 1, 6 do result[index] = result[index] / scale_value end + if coefficients.eccentricity ~= nil then result.eccentricity = coefficients.eccentricity end + return result +end + +local function conic_matrix_determinant(coefficients) + local a, b, c, d, e, f = unpack(normalize_coefficients(coefficients)) + return a * c * f + b * d * e * 0.25 + - a * e * e * 0.25 - c * d * d * 0.25 - b * b * f * 0.25 +end + +local function is_degenerate_conic(coefficients) + coefficients = normalize_coefficients(coefficients) + local determinant = conic_matrix_determinant(coefficients) + local term_scale = math.max(MIN_NORMAL, + math.abs(coefficients[1] * coefficients[3] * coefficients[6]) + + math.abs(coefficients[2] * coefficients[4] * coefficients[5] * 0.25) + + math.abs(coefficients[1] * coefficients[5] * coefficients[5] * 0.25) + + math.abs(coefficients[3] * coefficients[4] * coefficients[4] * 0.25) + + math.abs(coefficients[2] * coefficients[2] * coefficients[6] * 0.25) + ) + return near_zero(determinant, term_scale, 8192), determinant +end + +local function conic_terms(point) + point = point_from_table(point, "point") + return { point.x * point.x, point.x * point.y, point.y * point.y, point.x, point.y, 1 } +end + +local function evaluate_conic(coefficients, point) + coefficients = normalize_coefficients(coefficients) + local terms = conic_terms(point) + local value, magnitude = 0, 0 + for index = 1, 6 do + local term = coefficients[index] * terms[index] + if not finite_number(term) then error("conic evaluation exceeds numeric range") end + value, magnitude = value + term, magnitude + math.abs(term) + end + if not finite_number(value) or not finite_number(magnitude) then + error("conic evaluation exceeds numeric range") + end + return value, magnitude +end + +function Advanced.conic_matrix(coefficients) + local a, b, c, d, e, f = unpack(normalize_coefficients(coefficients)) + return { + { a, 0.5 * b, 0.5 * d }, + { 0.5 * b, c, 0.5 * e }, + { 0.5 * d, 0.5 * e, f }, + } +end + +function Advanced.matrix3_determinant(matrix) + return matrix[1][1] * (matrix[2][2] * matrix[3][3] - matrix[2][3] * matrix[3][2]) + - matrix[1][2] * (matrix[2][1] * matrix[3][3] - matrix[2][3] * matrix[3][1]) + + matrix[1][3] * (matrix[2][1] * matrix[3][2] - matrix[2][2] * matrix[3][1]) +end + +function Advanced.matrix3_adjugate(matrix) + return { + { + matrix[2][2] * matrix[3][3] - matrix[2][3] * matrix[3][2], + matrix[1][3] * matrix[3][2] - matrix[1][2] * matrix[3][3], + matrix[1][2] * matrix[2][3] - matrix[1][3] * matrix[2][2], + }, + { + matrix[2][3] * matrix[3][1] - matrix[2][1] * matrix[3][3], + matrix[1][1] * matrix[3][3] - matrix[1][3] * matrix[3][1], + matrix[1][3] * matrix[2][1] - matrix[1][1] * matrix[2][3], + }, + { + matrix[2][1] * matrix[3][2] - matrix[2][2] * matrix[3][1], + matrix[1][2] * matrix[3][1] - matrix[1][1] * matrix[3][2], + matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1], + }, + } +end + +function Advanced.matrix3_vector(matrix, vector) + return { + matrix[1][1] * vector[1] + matrix[1][2] * vector[2] + matrix[1][3] * vector[3], + matrix[2][1] * vector[1] + matrix[2][2] * vector[2] + matrix[2][3] * vector[3], + matrix[3][1] * vector[1] + matrix[3][2] * vector[2] + matrix[3][3] * vector[3], + } +end + +function Advanced.coefficients_from_symmetric_matrix(matrix) + return normalize_coefficients({ + matrix[1][1], + matrix[1][2] + matrix[2][1], + matrix[2][2], + matrix[1][3] + matrix[3][1], + matrix[2][3] + matrix[3][2], + matrix[3][3], + }) +end + +function Advanced.normalized_line_coordinates(line) + line = line_from_table(line, "line") + return { line.a, line.b, line.c }, line +end + +M.finite_number = finite_number +M.options_table = options_table +M.number_value = number_value +M.finite_number_option = finite_number_option +M.positive_number_option = positive_number_option +M.nonnegative_number_option = nonnegative_number_option +M.positive_integer_option = positive_integer_option +M.bool_value = bool_value +M.normalized_name = normalized_name +M.clean_error_message = clean_error_message +M.format_number = format_number +M.scaled_tolerance = scaled_tolerance +M.near_zero = near_zero +M.hypot = hypot +M.add = add +M.sub = sub +M.scale = scale +M.lerp = lerp +M.dot = dot +M.cross = cross +M.length = length +M.distance = distance +M.midpoint = midpoint +M.perpendicular = perpendicular +M.unit = unit +M.vector_scale = vector_scale +M.point_from_table = point_from_table +M.point_record = point_record +M.points_from_table = points_from_table +M.line_from_equation = line_from_equation +M.line_from_table = line_from_table +M.line_record = line_record +M.validate_keys = validate_keys +M.normalize_conic_coefficients = normalize_coefficients +M.conic_matrix_determinant = conic_matrix_determinant +M.is_degenerate_conic = is_degenerate_conic +M.evaluate_conic = evaluate_conic +M.conic_matrix = Advanced.conic_matrix +M.matrix3_determinant = Advanced.matrix3_determinant +M.matrix3_adjugate = Advanced.matrix3_adjugate +M.matrix3_vector = Advanced.matrix3_vector +M.coefficients_from_symmetric_matrix = Advanced.coefficients_from_symmetric_matrix + +local function jacobi_null_vector(rows) + local row_count, column_count = #rows, #rows[1] + local values, vectors = {}, {} + for row = 1, row_count do + values[row] = {} + for column = 1, column_count do values[row][column] = rows[row][column] end + end + for row = 1, column_count do + vectors[row] = {} + for column = 1, column_count do + vectors[row][column] = row == column and 1 or 0 + end + end + + for _ = 1, 160 do + local changed = false + for first = 1, column_count - 1 do + for second = first + 1, column_count do + local alpha, beta, gamma = 0, 0, 0 + for row = 1, row_count do + local left, right = values[row][first], values[row][second] + alpha = alpha + left * left + beta = beta + right * right + gamma = gamma + left * right + end + local pair_scale = math.sqrt(math.max(alpha * beta, 0)) + if math.abs(gamma) > scaled_tolerance(math.max(pair_scale, MIN_NORMAL), 256) then + local tau = (beta - alpha) / (2 * gamma) + local tangent + if tau >= 0 then + tangent = 1 / (tau + hypot(1, tau)) + else + tangent = -1 / (-tau + hypot(1, tau)) + end + local cosine = 1 / math.sqrt(1 + tangent * tangent) + local sine = cosine * tangent + for row = 1, row_count do + local left, right = values[row][first], values[row][second] + values[row][first] = cosine * left - sine * right + values[row][second] = sine * left + cosine * right + end + for row = 1, column_count do + local left, right = vectors[row][first], vectors[row][second] + vectors[row][first] = cosine * left - sine * right + vectors[row][second] = sine * left + cosine * right + end + changed = true + end + end + end + if not changed then break end + end + + local singular_values, order = {}, {} + for column = 1, column_count do + local norm_squared = 0 + for row = 1, row_count do + norm_squared = norm_squared + values[row][column] * values[row][column] + end + singular_values[column] = math.sqrt(math.max(norm_squared, 0)) + order[column] = column + end + table.sort(order, function(left, right) + return singular_values[left] < singular_values[right] + end) + local null_vector = {} + for row = 1, column_count do null_vector[row] = vectors[row][order[1]] end + return null_vector, singular_values[order[1]], singular_values[order[2]], + singular_values[order[#order]] +end + +local function conic_coefficients_from_five_points(input_points) + local points = points_from_table(input_points, 5, "points") + local center = V(0, 0) + for _, point in ipairs(points) do center = add(center, scale(point, 0.2)) end + local coordinate_scale = 0 + for _, point in ipairs(points) do + coordinate_scale = math.max( + coordinate_scale, + math.abs(point.x - center.x), + math.abs(point.y - center.y) + ) + end + if coordinate_scale == 0 or not finite_number(coordinate_scale) then + error("five points do not determine a stable conic") + end + for first = 1, 4 do + for second = first + 1, 5 do + if near_zero(distance(points[first], points[second]), coordinate_scale, 2048) then + error("five distinct points are required") + end + end + end + + local rows = {} + for index, point in ipairs(points) do + local x = (point.x - center.x) / coordinate_scale + local y = (point.y - center.y) / coordinate_scale + rows[index] = { x * x, x * y, y * y, x, y, 1 } + end + local normalized, smallest, second_smallest, largest = jacobi_null_vector(rows) + if largest == 0 or second_smallest <= scaled_tolerance(largest, 32768) then + error("five points do not determine a unique stable conic") + end + if smallest > scaled_tolerance(largest, 32768) then + error("five-point conic has no reliable homogeneous nullspace") + end + normalized = normalize_coefficients(normalized) + -- At this point the sample coordinates are centered and scaled to order one. + -- Test the determinant in that conditioned coordinate system: doing it only + -- after translating back can either hide a pair of lines in round-off or + -- reject an otherwise valid conic merely because its coordinates are large. + if near_zero(conic_matrix_determinant(normalized), 1, 8192) then + error("five points define a degenerate conic") + end + local maximum_residual = 0 + for sample = 1, 5 do + local residual = 0 + for column = 1, 6 do + residual = residual + rows[sample][column] * normalized[column] + end + maximum_residual = math.max(maximum_residual, math.abs(residual)) + end + if maximum_residual > 1e-7 then + error("five-point conic is numerically ill-conditioned") + end + + local a, b, c, d, e, f = unpack(normalized) + local coordinate_scale2 = coordinate_scale * coordinate_scale + if coordinate_scale2 == 0 or not finite_number(coordinate_scale2) then + error("five-point coordinate scale exceeds numeric range") + end + local qx, qy = center.x / coordinate_scale, center.y / coordinate_scale + local coefficients = normalize_coefficients({ + a / coordinate_scale2, + b / coordinate_scale2, + c / coordinate_scale2, + (d - 2 * a * qx - b * qy) / coordinate_scale, + (e - b * qx - 2 * c * qy) / coordinate_scale, + f - d * qx - e * qy + a * qx * qx + b * qx * qy + c * qy * qy, + }) + if is_degenerate_conic(coefficients) then + error("five points define a degenerate conic") + end + return coefficients +end + +local function eigenvectors_2x2(a, b_half, c) + local trace = 0.5 * (a + c) + local difference = 0.5 * (a - c) + local root = hypot(difference, b_half) + local lambda1, lambda2 = trace + root, trace - root + local direction1 + if math.abs(b_half) > scaled_tolerance(math.max(math.abs(a), math.abs(c)), 1024) then + direction1 = unit(V(b_half, lambda1 - a), "conic axis") + elseif a >= c then + direction1 = V(1, 0) + else + direction1 = V(0, 1) + end + return lambda1, lambda2, direction1, perpendicular(direction1) +end + +local function classify_conic(coefficients) + coefficients = normalize_coefficients(coefficients) + if is_degenerate_conic(coefficients) then + return { kind = "degenerate", classification = "degenerate", degenerate = true, + coefficients = coefficients } + end + local a, b, c = coefficients[1], coefficients[2], coefficients[3] + local discriminant = b * b - 4 * a * c + local discriminant_scale = math.abs(b * b) + math.abs(4 * a * c) + local tolerance = scaled_tolerance(discriminant_scale, 4096) + local kind + if discriminant > tolerance then + kind = "hyperbola" + elseif discriminant < -tolerance then + local circular_scale = math.max(math.abs(a), math.abs(c), MIN_NORMAL) + kind = math.abs(a - c) <= scaled_tolerance(circular_scale, 4096) + and math.abs(b) <= scaled_tolerance(circular_scale, 4096) + and "circle" or "ellipse" + else + kind = "parabola" + end + return { + kind = kind, + classification = kind, + degenerate = false, + discriminant = discriminant, + coefficients = coefficients, + } +end + +local function point_records(points) + local result = {} + for index, point in ipairs(points or {}) do result[index] = point_record(point) end + return result +end + +function Advanced.degenerate_conic_properties(coefficients) + coefficients = normalize_coefficients(coefficients) + local a, b, c, d, e, f = unpack(coefficients) + local quadratic_scale = math.max(math.abs(a), math.abs(b), math.abs(c), MIN_NORMAL) + local linear_scale = math.max(math.abs(d), math.abs(e), MIN_NORMAL) + local h_determinant = a * c - 0.25 * b * b + local h_scale = math.abs(a * c) + math.abs(0.25 * b * b) + local result = { + kind = "degenerate", + classification = "degenerate", + degenerate = true, + coefficients = coefficients, + lines = {}, + points = {}, + } + + if not near_zero(h_determinant, math.max(h_scale, MIN_NORMAL), 8192) then + local center = V( + (b * e - 2 * c * d) / (4 * h_determinant), + (b * d - 2 * a * e) / (4 * h_determinant) + ) + local lambda1, lambda2, direction1, direction2 = eigenvectors_2x2(a, 0.5 * b, c) + local value, magnitude = evaluate_conic(coefficients, center) + result.center = center + if lambda1 * lambda2 < 0 + and math.abs(value) <= scaled_tolerance(math.max(magnitude, MIN_NORMAL), 32768) then + local positive_value, positive_direction, negative_value, negative_direction + if lambda1 > 0 then + positive_value, positive_direction = lambda1, direction1 + negative_value, negative_direction = lambda2, direction2 + else + positive_value, positive_direction = lambda2, direction2 + negative_value, negative_direction = lambda1, direction1 + end + local positive_root, negative_root = math.sqrt(positive_value), math.sqrt(-negative_value) + for _, sign in ipairs({ -1, 1 }) do + local normal = add( + scale(positive_direction, positive_root), + scale(negative_direction, sign * negative_root) + ) + result.lines[#result.lines + 1] = line_from_equation( + normal.x, normal.y, -dot(normal, center), center + ) + end + result.subtype = "intersecting_lines" + elseif lambda1 * lambda2 > 0 + and math.abs(value) <= scaled_tolerance(math.max(magnitude, MIN_NORMAL), 32768) then + result.subtype = "point" + result.point = center + result.points[1] = center + else + result.subtype = "empty" + end + return result + end + + if quadratic_scale > scaled_tolerance( + math.max(quadratic_scale, linear_scale, math.abs(f), MIN_NORMAL), 8192) then + local lambda1, lambda2, direction1, direction2 = eigenvectors_2x2(a, 0.5 * b, c) + local lambda, normal_direction + if math.abs(lambda1) >= math.abs(lambda2) then + lambda, normal_direction = lambda1, direction1 + else + lambda, normal_direction = lambda2, direction2 + end + local null_direction = perpendicular(normal_direction) + local linear_normal = d * normal_direction.x + e * normal_direction.y + local linear_null = d * null_direction.x + e * null_direction.y + if not near_zero(linear_null, math.max(linear_scale, MIN_NORMAL), 8192) then + result.subtype = "unstable" + result.message = "near-degenerate parabola cannot be factored reliably" + return result + end + local center_coordinate = -linear_normal / (2 * lambda) + local completed_constant = f - linear_normal * linear_normal / (4 * lambda) + local separation_squared = -completed_constant / lambda + local separation_scale = math.max( + math.abs(completed_constant / lambda), + math.abs(center_coordinate * center_coordinate), + MIN_NORMAL + ) + if separation_squared > scaled_tolerance(separation_scale, 8192) then + local separation = math.sqrt(separation_squared) + for _, sign in ipairs({ -1, 1 }) do + local coordinate = center_coordinate + sign * separation + result.lines[#result.lines + 1] = line_from_equation( + normal_direction.x, normal_direction.y, -coordinate + ) + end + result.subtype = "parallel_lines" + elseif math.abs(separation_squared) <= scaled_tolerance(separation_scale, 8192) then + result.lines[1] = line_from_equation( + normal_direction.x, normal_direction.y, -center_coordinate + ) + result.subtype = "double_line" + result.multiplicity = 2 + else + result.subtype = "empty" + end + return result + end + + if linear_scale > scaled_tolerance(math.max(linear_scale, math.abs(f), MIN_NORMAL), 8192) then + result.lines[1] = line_from_equation(d, e, f) + result.subtype = "single_line" + return result + end + result.subtype = "empty" + return result +end + +local function conic_properties(coefficients) + local classification = classify_conic(coefficients) + if classification.degenerate then + return Advanced.degenerate_conic_properties(classification.coefficients) + end + coefficients = classification.coefficients + local a, b, c, d, e, f = unpack(coefficients) + local result = { + kind = classification.kind, + classification = classification.kind, + coefficients = coefficients, + degenerate = false, + } + + if classification.kind ~= "parabola" then + local center_determinant = 4 * a * c - b * b + local center_scale = math.abs(4 * a * c) + math.abs(b * b) + if near_zero(center_determinant, center_scale, 4096) then + error("central conic has an unstable center") + end + local center = V( + (b * e - 2 * c * d) / center_determinant, + (b * d - 2 * a * e) / center_determinant + ) + local value_at_center = evaluate_conic(coefficients, center) + local lambda1, lambda2, direction1, direction2 = eigenvectors_2x2(a, b * 0.5, c) + result.center, result.center_record = center, point_record(center) + + if classification.kind == "ellipse" or classification.kind == "circle" then + local radius1_squared = -value_at_center / lambda1 + local radius2_squared = -value_at_center / lambda2 + if radius1_squared <= 0 or radius2_squared <= 0 + or not finite_number(radius1_squared) or not finite_number(radius2_squared) then + error("ellipse has no real nondegenerate locus") + end + local radius1, radius2 = math.sqrt(radius1_squared), math.sqrt(radius2_squared) + local major_radius, minor_radius, major_direction, minor_direction + if radius1 >= radius2 then + major_radius, minor_radius = radius1, radius2 + major_direction, minor_direction = direction1, direction2 + else + major_radius, minor_radius = radius2, radius1 + major_direction, minor_direction = direction2, direction1 + end + local focal_radius = math.sqrt(math.max( + 0, (major_radius - minor_radius) * (major_radius + minor_radius) + )) + local eccentricity = focal_radius / major_radius + result.major_radius, result.minor_radius = major_radius, minor_radius + result.major_direction, result.minor_direction = major_direction, minor_direction + result.axis1, result.axis2 = scale(major_direction, major_radius), + scale(minor_direction, minor_radius) + result.vertices = { + add(center, scale(major_direction, major_radius)), + sub(center, scale(major_direction, major_radius)), + } + result.co_vertices = { + add(center, scale(minor_direction, minor_radius)), + sub(center, scale(minor_direction, minor_radius)), + } + result.foci = { + add(center, scale(major_direction, focal_radius)), + sub(center, scale(major_direction, focal_radius)), + } + result.eccentricity = eccentricity + result.focal_radius = focal_radius + result.semi_latus_rectum = minor_radius * minor_radius / major_radius + result.area = math.pi * major_radius * minor_radius + result.auxiliary_circles = { + { center = center, radius = major_radius }, + } + if math.abs(major_radius - minor_radius) + > scaled_tolerance(math.max(major_radius, minor_radius), 4096) then + result.auxiliary_circles[2] = { center = center, radius = minor_radius } + end + result.director_circle = { + center = center, + radius = hypot(major_radius, minor_radius), + } + result.latus_recta = {} + if classification.kind ~= "circle" then + for _, focus in ipairs(result.foci) do + result.latus_recta[#result.latus_recta + 1] = { + line = { point = focus, direction = minor_direction }, + endpoints = { + add(focus, scale(minor_direction, result.semi_latus_rectum)), + sub(focus, scale(minor_direction, result.semi_latus_rectum)), + }, + } + end + end + result.directrices = {} + if eccentricity > scaled_tolerance(1, 1024) then + local directrix_distance = major_radius / eccentricity + result.directrices = { + { point = add(center, scale(major_direction, directrix_distance)), + direction = minor_direction }, + { point = sub(center, scale(major_direction, directrix_distance)), + direction = minor_direction }, + } + end + else + local candidate1, candidate2 = -value_at_center / lambda1, -value_at_center / lambda2 + local transverse_direction, conjugate_direction, a_squared, b_squared + if candidate1 > 0 and candidate2 < 0 then + transverse_direction, conjugate_direction = direction1, direction2 + a_squared, b_squared = candidate1, -candidate2 + elseif candidate2 > 0 and candidate1 < 0 then + transverse_direction, conjugate_direction = direction2, direction1 + a_squared, b_squared = candidate2, -candidate1 + else + error("hyperbola has no real nondegenerate locus") + end + local a_radius, b_radius = math.sqrt(a_squared), math.sqrt(b_squared) + local focal_radius = hypot(a_radius, b_radius) + local eccentricity = focal_radius / a_radius + result.a, result.b = a_radius, b_radius + result.focal_radius = focal_radius + result.u, result.v = transverse_direction, conjugate_direction + result.vertices = { + add(center, scale(transverse_direction, a_radius)), + sub(center, scale(transverse_direction, a_radius)), + } + result.foci = { + add(center, scale(transverse_direction, focal_radius)), + sub(center, scale(transverse_direction, focal_radius)), + } + result.eccentricity = eccentricity + result.semi_latus_rectum = b_radius * b_radius / a_radius + result.auxiliary_circles = { + { center = center, radius = a_radius }, + } + result.latus_recta = {} + for _, focus in ipairs(result.foci) do + result.latus_recta[#result.latus_recta + 1] = { + line = { point = focus, direction = conjugate_direction }, + endpoints = { + add(focus, scale(conjugate_direction, result.semi_latus_rectum)), + sub(focus, scale(conjugate_direction, result.semi_latus_rectum)), + }, + } + end + local director_radius_squared = a_radius * a_radius - b_radius * b_radius + if director_radius_squared > scaled_tolerance( + math.abs(a_radius * a_radius) + math.abs(b_radius * b_radius), 4096) then + result.director_circle = { + center = center, + radius = math.sqrt(director_radius_squared), + } + end + local directrix_distance = a_radius / eccentricity + result.directrices = { + { point = add(center, scale(transverse_direction, directrix_distance)), + direction = conjugate_direction }, + { point = sub(center, scale(transverse_direction, directrix_distance)), + direction = conjugate_direction }, + } + result.asymptotes = { + { point = center, + direction = add(scale(transverse_direction, a_radius), scale(conjugate_direction, b_radius)) }, + { point = center, + direction = sub(scale(transverse_direction, a_radius), scale(conjugate_direction, b_radius)) }, + } + end + else + local lambda1, lambda2, direction1, direction2 = eigenvectors_2x2(a, b * 0.5, c) + local lambda, normal_direction, axis_direction + if math.abs(lambda1) >= math.abs(lambda2) then + lambda, normal_direction, axis_direction = lambda1, direction1, direction2 + else + lambda, normal_direction, axis_direction = lambda2, direction2, direction1 + end + local linear_normal = d * normal_direction.x + e * normal_direction.y + local linear_axis = d * axis_direction.x + e * axis_direction.y + if near_zero(lambda, math.max(math.abs(a), math.abs(b), math.abs(c)), 4096) + or near_zero(linear_axis, hypot(d, e), 4096) then + error("parabola is numerically degenerate") + end + local normal_coordinate = -linear_normal / (2 * lambda) + local constant_after_square = f - linear_normal * linear_normal / (4 * lambda) + local axis_coordinate = -constant_after_square / linear_axis + local vertex = add(scale(normal_direction, normal_coordinate), + scale(axis_direction, axis_coordinate)) + local focal_parameter = -linear_axis / (4 * lambda) + if near_zero(focal_parameter, math.max(vector_scale(vertex), MIN_NORMAL), 4096) then + error("parabola focal parameter is numerically zero") + end + local focus = add(vertex, scale(axis_direction, focal_parameter)) + result.vertex, result.vertices = vertex, { vertex } + result.focus, result.foci = focus, { focus } + result.axis_direction, result.normal_direction = axis_direction, normal_direction + result.focal_parameter, result.eccentricity = focal_parameter, 1 + result.semi_latus_rectum = 2 * math.abs(focal_parameter) + result.latus_recta = { + { + line = { point = focus, direction = normal_direction }, + endpoints = { + add(focus, scale(normal_direction, 2 * math.abs(focal_parameter))), + sub(focus, scale(normal_direction, 2 * math.abs(focal_parameter))), + }, + }, + } + result.directrices = { + { point = sub(vertex, scale(axis_direction, focal_parameter)), + direction = normal_direction }, + } + end + result.vertex_records = point_records(result.vertices) + result.focus_records = point_records(result.foci) + return result +end + +M.conic_coefficients_from_five_points = conic_coefficients_from_five_points +M.classify_conic = classify_conic +M.degenerate_conic_properties = Advanced.degenerate_conic_properties +M.conic_properties = conic_properties + +local function ellipse_coefficients(ellipse) + local center = point_from_table(ellipse.center, "ellipse.center") + local axis1 = point_from_table(ellipse.axis1, "ellipse.axis1") + local axis2 = point_from_table(ellipse.axis2, "ellipse.axis2") + local radius1, radius2 = length(axis1), length(axis2) + if radius1 == 0 or radius2 == 0 then error("ellipse axes must be nonzero") end + local unit1, unit2 = scale(axis1, 1 / radius1), scale(axis2, 1 / radius2) + if math.abs(dot(unit1, unit2)) > 8192 * MACHINE_EPSILON then + error("ellipse axes must be perpendicular") + end + local inverse1, inverse2 = 1 / radius1, 1 / radius2 + local inverse1_squared, inverse2_squared = inverse1 * inverse1, inverse2 * inverse2 + if inverse1_squared == 0 or inverse2_squared == 0 then + error("ellipse world coefficients exceed numeric range") + end + local q11 = unit1.x * unit1.x * inverse1_squared + + unit2.x * unit2.x * inverse2_squared + local q12 = unit1.x * unit1.y * inverse1_squared + + unit2.x * unit2.y * inverse2_squared + local q22 = unit1.y * unit1.y * inverse1_squared + + unit2.y * unit2.y * inverse2_squared + return normalize_coefficients({ + q11, + 2 * q12, + q22, + -2 * (q11 * center.x + q12 * center.y), + -2 * (q12 * center.x + q22 * center.y), + q11 * center.x * center.x + 2 * q12 * center.x * center.y + + q22 * center.y * center.y - 1, + }) +end + +local function ellipse_from_center_quadratic(center, m11, m12, m22, scale_factor) + center = point_from_table(center, "center") + scale_factor = scale_factor or 1 + local coefficient_scale = math.max(math.abs(m11), math.abs(m12), math.abs(m22)) + if coefficient_scale == 0 then error("ellipse quadratic form must be positive definite") end + local determinant = m11 * m22 - m12 * m12 + if determinant <= scaled_tolerance(coefficient_scale * coefficient_scale, 4096) then + error("ellipse quadratic form must be positive definite") + end + local c11, c12, c22 = m22 / determinant, -m12 / determinant, m11 / determinant + local lambda1, lambda2, direction1, direction2 = eigenvectors_2x2(c11, c12, c22) + if lambda1 <= 0 or lambda2 <= 0 then error("ellipse axes must be positive") end + return { + center = center, + axis1 = scale(direction1, math.sqrt(lambda1) * scale_factor), + axis2 = scale(direction2, math.sqrt(lambda2) * scale_factor), + } +end + +local function solve_3x3(matrix, rhs) + local function determinant3(value) + return value[1][1] * (value[2][2] * value[3][3] - value[2][3] * value[3][2]) + - value[1][2] * (value[2][1] * value[3][3] - value[2][3] * value[3][1]) + + value[1][3] * (value[2][1] * value[3][2] - value[2][2] * value[3][1]) + end + local determinant = determinant3(matrix) + local scale_value = 0 + for row = 1, 3 do + for column = 1, 3 do + scale_value = math.max(scale_value, math.abs(matrix[row][column])) + end + end + if near_zero(determinant, scale_value ^ 3, 4096) then return nil end + local result = {} + for column = 1, 3 do + local replaced = {} + for row = 1, 3 do + replaced[row] = {} + for inner = 1, 3 do + replaced[row][inner] = inner == column and rhs[row] or matrix[row][inner] + end + end + result[column] = determinant3(replaced) / determinant + if not finite_number(result[column]) then return nil end + end + return result +end + +local function steiner_ellipses(a, b, c) + a, b, c = point_from_table(a, "a"), point_from_table(b, "b"), point_from_table(c, "c") + local center = V(a.x / 3 + b.x / 3 + c.x / 3, a.y / 3 + b.y / 3 + c.y / 3) + local points, coordinate_scale = { a, b, c }, 0 + for _, point in ipairs(points) do + coordinate_scale = math.max( + coordinate_scale, math.abs(point.x - center.x), math.abs(point.y - center.y) + ) + end + if coordinate_scale == 0 or not finite_number(coordinate_scale) then + error("Steiner ellipse is undefined for a degenerate triangle") + end + local matrix = {} + for index, point in ipairs(points) do + local x = (point.x - center.x) / coordinate_scale + local y = (point.y - center.y) / coordinate_scale + matrix[index] = { x * x, 2 * x * y, y * y } + end + local solution = solve_3x3(matrix, { 1, 1, 1 }) + if not solution then error("Steiner ellipse is undefined for a degenerate triangle") end + local circumellipse = ellipse_from_center_quadratic( + center, solution[1], solution[2], solution[3], coordinate_scale + ) + return { + circumellipse = circumellipse, + inellipse = { + center = center, + axis1 = scale(circumellipse.axis1, 0.5), + axis2 = scale(circumellipse.axis2, 0.5), + }, + } +end + +local function cyclically_ordered_points(input_points) + local points = points_from_table(input_points, 4, "points") + local center = V(0, 0) + for _, point in ipairs(points) do center = add(center, scale(point, 0.25)) end + table.sort(points, function(left, right) + return math.atan(left.y - center.y, left.x - center.x) + < math.atan(right.y - center.y, right.x - center.x) + end) + local frame_scale, edges = 0, {} + for index = 1, 4 do + local next_index = index == 4 and 1 or index + 1 + local edge = distance(points[index], points[next_index]) + edges[index] = edge + frame_scale = math.max(frame_scale, edge) + end + if frame_scale == 0 or not finite_number(frame_scale) then + error("quadrilateral vertices must be distinct") + end + for _, edge in ipairs(edges) do + if near_zero(edge, frame_scale, 4096) then + error("quadrilateral vertices must be distinct") + end + end + local area = 0 + for index = 1, 4 do + local next_index = index == 4 and 1 or index + 1 + area = area + cross( + scale(sub(points[index], center), 1 / frame_scale), + scale(sub(points[next_index], center), 1 / frame_scale) + ) + end + if near_zero(area, 1, 16384) then + error("quadrilateral is excessively ill-conditioned") + end + return points, center +end + +function M.ellipse_from_conjugate_diameters(center, u, v) + local coordinate_scale = math.max(vector_scale(u, v), MIN_NORMAL) + if not finite_number(coordinate_scale) then + error("quadrilateral side-midpoint frame exceeds numeric range") + end + local normalized_u, normalized_v = scale(u, 1 / coordinate_scale), + scale(v, 1 / coordinate_scale) + local determinant = cross(normalized_u, normalized_v) + if near_zero(determinant, 1, 16384) then + error("quadrilateral side-midpoint frame is degenerate") + end + local c11 = normalized_u.x * normalized_u.x + normalized_v.x * normalized_v.x + local c12 = normalized_u.x * normalized_u.y + normalized_v.x * normalized_v.y + local c22 = normalized_u.y * normalized_u.y + normalized_v.y * normalized_v.y + local lambda1, lambda2, direction1, direction2 = eigenvectors_2x2(c11, c12, c22) + if lambda1 <= 0 or lambda2 <= 0 then + error("quadrilateral side-midpoint frame is degenerate") + end + local radius1 = coordinate_scale * math.sqrt(lambda1) + local radius2 = coordinate_scale * math.sqrt(lambda2) + if not finite_number(radius1) or not finite_number(radius2) then + error("quadrilateral midpoint ellipse exceeds numeric range") + end + return { + center = center, + axis1 = scale(direction1, radius1), + axis2 = scale(direction2, radius2), + } +end + +local function quadrilateral_midpoint_ellipse(input_points) + local points, center = cyclically_ordered_points(input_points) + local u = sub(midpoint(points[1], points[2]), center) + local v = sub(midpoint(points[2], points[3]), center) + return M.ellipse_from_conjugate_diameters(center, u, v), points +end + +local function ellipse_from_foci_point(focus_a, focus_b, point) + focus_a = point_from_table(focus_a, "focus_a") + focus_b = point_from_table(focus_b, "focus_b") + point = point_from_table(point, "point") + local center = midpoint(focus_a, focus_b) + local focal_radius = distance(focus_a, focus_b) * 0.5 + local distance_a, distance_b = distance(point, focus_a), distance(point, focus_b) + local major_radius = distance_a * 0.5 + distance_b * 0.5 + local input_scale = math.max(focal_radius, major_radius, distance_a, distance_b, MIN_NORMAL) + if not finite_number(major_radius) + or major_radius <= focal_radius + scaled_tolerance(input_scale, 4096) then + error("third point must define a nondegenerate ellipse") + end + local ratio = focal_radius / major_radius + local minor_radius = major_radius * math.sqrt(math.max(0, 1 - ratio * ratio)) + if not finite_number(minor_radius) or near_zero(minor_radius, major_radius, 4096) then + error("third point must define a nondegenerate ellipse") + end + local major_direction = not near_zero(focal_radius, input_scale, 4096) + and unit(sub(focus_b, focus_a), "ellipse focal axis") + or unit(sub(point, center), "ellipse major axis") + return { + center = center, + axis1 = scale(major_direction, major_radius), + axis2 = scale(perpendicular(major_direction), minor_radius), + focus_a = focus_a, + focus_b = focus_b, + defining_point = point, + major_radius = major_radius, + minor_radius = minor_radius, + } +end + +local function line_signed_distance(line, point) + line, point = line_from_table(line, "line"), point_from_table(point, "point") + return line.a * point.x + line.b * point.y + line.c +end + +local function conic_coefficients_for_focus_directrix(focus, directrix, eccentricity) + focus = point_from_table(focus, "focus") + directrix = line_from_table(directrix, "directrix") + eccentricity = positive_number_option(eccentricity, 1, "eccentricity") + local a, b, c = directrix.a, directrix.b, directrix.c + local focus_distance = math.abs(a * focus.x + b * focus.y + c) + local input_scale = math.max( + distance(directrix.p1, directrix.p2), + distance(focus, directrix.p1), + distance(focus, directrix.p2), + MIN_NORMAL + ) + if near_zero(focus_distance, input_scale, 4096) then + error("focus must not lie on the directrix or numerically too close to it") + end + local eccentricity_squared = eccentricity * eccentricity + if not finite_number(eccentricity_squared) then error("eccentricity exceeds numeric range") end + local coefficients = normalize_coefficients({ + 1 - eccentricity_squared * a * a, + -2 * eccentricity_squared * a * b, + 1 - eccentricity_squared * b * b, + -2 * focus.x - 2 * eccentricity_squared * a * c, + -2 * focus.y - 2 * eccentricity_squared * b * c, + focus.x * focus.x + focus.y * focus.y - eccentricity_squared * c * c, + }) + coefficients.eccentricity = eccentricity + return coefficients +end + +local function focus_directrix_conic_coefficients(focus, directrix, point) + focus = point_from_table(focus, "focus") + directrix = line_from_table(directrix, "directrix") + point = point_from_table(point, "point") + local distance_to_line = math.abs(line_signed_distance(directrix, point)) + local distance_to_focus = distance(point, focus) + local input_scale = math.max( + distance_to_focus, + distance(directrix.p1, directrix.p2), + math.abs(line_signed_distance(directrix, focus)), + MIN_NORMAL + ) + if near_zero(distance_to_line, input_scale, 4096) then + error("point must not lie on or numerically too close to the directrix") + end + if near_zero(distance_to_focus, input_scale, 4096) then + error("focus and point must be distinct") + end + return conic_coefficients_for_focus_directrix( + focus, directrix, distance_to_focus / distance_to_line + ) +end + +local function stable_asinh(value) + local sign = value < 0 and -1 or 1 + value = math.abs(value) + if value < 1e-8 then return sign * value end + if value > 1e150 then return sign * (math.log(value) + LOG_TWO) end + return sign * math.log(value + hypot(value, 1)) +end + +function M.stable_acosh(value) + value = finite_number_option(value, nil, "acosh argument") + if value < 1 then error("acosh argument must be at least one") end + if value == 1 then return 0 end + if value > 1e150 then return math.log(value) + LOG_TWO end + return math.log(value + math.sqrt(value - 1) * math.sqrt(value + 1)) +end + +local function hyperbola_from_parameters(center, axis, a_radius, b_radius) + center = point_from_table(center, "center") + local direction = axis and line_from_table(axis, "axis").direction or V(1, 0) + local u = unit(direction, "hyperbola axis") + a_radius = positive_number_option(a_radius, 32, "a") + b_radius = positive_number_option(b_radius, a_radius * 0.6, "b") + return { + center = center, u = u, v = perpendicular(u), a = a_radius, b = b_radius, + } +end + +local function stable_distance_difference(point, focus_a, focus_b, center, u, focal_radius) + local distance_a, distance_b = distance(point, focus_a), distance(point, focus_b) + local projection = dot(sub(point, center), u) + local scale_value = math.max( + math.abs(projection), focal_radius, distance_a, distance_b, MIN_NORMAL + ) + local denominator = distance_a / scale_value + distance_b / scale_value + if denominator == 0 then return 0 end + return (4 * (focal_radius / scale_value) * (projection / scale_value) + / denominator) * scale_value +end + +local function hyperbola_from_foci_point(focus_a, focus_b, point) + focus_a = point_from_table(focus_a, "focus_a") + focus_b = point_from_table(focus_b, "focus_b") + point = point_from_table(point, "point") + local center = midpoint(focus_a, focus_b) + local focal_distance = distance(focus_a, focus_b) * 0.5 + local input_scale = math.max(focal_distance, distance(focus_a, focus_b), MIN_NORMAL) + if near_zero(focal_distance, input_scale, 4096) then error("hyperbola foci must be distinct") end + local u, v = unit(sub(focus_b, focus_a), "hyperbola focal axis"), nil + v = perpendicular(u) + local difference = stable_distance_difference( + point, focus_a, focus_b, center, u, focal_distance + ) + local a_radius = math.abs(difference) * 0.5 + if a_radius <= scaled_tolerance(focal_distance, 4096) + or a_radius >= focal_distance - scaled_tolerance(focal_distance, 4096) then + error("point must define a nondegenerate hyperbola branch") + end + local ratio = a_radius / focal_distance + local b_radius = focal_distance * math.sqrt(math.max(0, 1 - ratio * ratio)) + local relative = sub(point, center) + return { + center = center, + u = u, + v = v, + a = a_radius, + b = b_radius, + point_parameter = math.abs(stable_asinh(dot(relative, v) / b_radius)), + point_branch = dot(relative, u) < 0 and -1 or 1, + defining_point = point, + } +end + +local function hyperbola_coefficients(hyperbola) + local inverse_a, inverse_b = 1 / hyperbola.a, 1 / hyperbola.b + local inverse_a_squared, inverse_b_squared = inverse_a * inverse_a, inverse_b * inverse_b + if inverse_a_squared == 0 or inverse_b_squared == 0 then + error("hyperbola world coefficients exceed numeric range") + end + local u, v, center = hyperbola.u, hyperbola.v, hyperbola.center + local q11 = u.x * u.x * inverse_a_squared - v.x * v.x * inverse_b_squared + local q12 = u.x * u.y * inverse_a_squared - v.x * v.y * inverse_b_squared + local q22 = u.y * u.y * inverse_a_squared - v.y * v.y * inverse_b_squared + return normalize_coefficients({ + q11, + 2 * q12, + q22, + -2 * (q11 * center.x + q12 * center.y), + -2 * (q12 * center.x + q22 * center.y), + q11 * center.x * center.x + 2 * q12 * center.x * center.y + + q22 * center.y * center.y - 1, + }) +end + +local function hyperbola_values(parameter) + local exponential = math.exp(math.abs(parameter)) + if not finite_number(exponential) then error("hyperbola extent exceeds finite coordinate range") end + local inverse = 1 / exponential + local cosine_hyperbolic = 0.5 * (exponential + inverse) + local sine_hyperbolic = 0.5 * (exponential - inverse) + if parameter < 0 then sine_hyperbolic = -sine_hyperbolic end + return cosine_hyperbolic, sine_hyperbolic +end + +local function hyperbola_point(hyperbola, branch, parameter) + local cosine_hyperbolic, sine_hyperbolic = hyperbola_values(parameter) + local point = add( + hyperbola.center, + add( + scale(hyperbola.u, branch * hyperbola.a * cosine_hyperbolic), + scale(hyperbola.v, hyperbola.b * sine_hyperbolic) + ) + ) + if not finite_number(point.x) or not finite_number(point.y) then + error("hyperbola extent exceeds finite coordinate range") + end + return point +end + +local function hyperbola_derivative(hyperbola, branch, parameter) + local cosine_hyperbolic, sine_hyperbolic = hyperbola_values(parameter) + return add( + scale(hyperbola.u, branch * hyperbola.a * sine_hyperbolic), + scale(hyperbola.v, hyperbola.b * cosine_hyperbolic) + ) +end + +local function cubic_point(control, t) + local one_minus = 1 - t + return add( + add( + scale(control[1], one_minus ^ 3), + scale(control[2], 3 * one_minus * one_minus * t) + ), + add( + scale(control[3], 3 * one_minus * t * t), + scale(control[4], t ^ 3) + ) + ) +end + +local function hyperbola_cubic(hyperbola, branch, start_parameter, finish_parameter) + local start_point = hyperbola_point(hyperbola, branch, start_parameter) + local finish_point = hyperbola_point(hyperbola, branch, finish_parameter) + local interval = finish_parameter - start_parameter + return { + start_point, + add(start_point, scale(hyperbola_derivative(hyperbola, branch, start_parameter), interval / 3)), + sub(finish_point, scale(hyperbola_derivative(hyperbola, branch, finish_parameter), interval / 3)), + finish_point, + } +end + +local function adaptive_hyperbola_cubics(hyperbola, branch, t_max, tolerance, maximum_segments) + t_max = positive_number_option( + t_max, math.max(2.2, hyperbola.point_parameter or 0), "t_max" + ) + tolerance = positive_number_option(tolerance, 0.25, "tolerance") + maximum_segments = positive_integer_option(maximum_segments, 256, "max_segments", 4096) + local cubics = {} + local function subdivide(start_parameter, finish_parameter, depth) + if #cubics >= maximum_segments then error("hyperbola approximation exceeded max_segments") end + local control = hyperbola_cubic(hyperbola, branch, start_parameter, finish_parameter) + local interval, error_value = finish_parameter - start_parameter, 0 + for _, fraction in ipairs({ 0.25, 0.5, 0.75 }) do + local parameter = start_parameter + interval * fraction + error_value = math.max(error_value, distance( + hyperbola_point(hyperbola, branch, parameter), + cubic_point(control, fraction) + )) + end + if not finite_number(error_value) then + error("hyperbola approximation error is not finite") + end + if error_value <= tolerance then + cubics[#cubics + 1] = control + else + if depth >= 20 then + error("hyperbola approximation could not satisfy the requested tolerance") + end + local middle_parameter = start_parameter * 0.5 + finish_parameter * 0.5 + subdivide(start_parameter, middle_parameter, depth + 1) + subdivide(middle_parameter, finish_parameter, depth + 1) + end + end + subdivide(-t_max, t_max, 0) + return cubics, t_max +end + +local function parabola_spline(properties, negative_extent, positive_extent) + if properties.kind ~= "parabola" then error("parabola properties are required") end + negative_extent = positive_number_option(negative_extent, 96, "negative_extent") + positive_extent = positive_number_option(positive_extent, negative_extent, "positive_extent") + local start_parameter, finish_parameter = -negative_extent, positive_extent + local function point(parameter) + return add( + properties.vertex, + add( + scale(properties.normal_direction, parameter), + scale(properties.axis_direction, + parameter * parameter / (4 * properties.focal_parameter)) + ) + ) + end + local start_point = point(start_parameter) + local finish_point = point(finish_parameter) + local middle_point = point(start_parameter * 0.5 + finish_parameter * 0.5) + local control = sub(scale(middle_point, 2), scale(add(start_point, finish_point), 0.5)) + for _, point_value in ipairs({ start_point, control, finish_point }) do + if not finite_number(point_value.x) or not finite_number(point_value.y) then + error("parabola extent exceeds finite coordinate range") + end + end + return { start_point, control, finish_point } +end + +M.ellipse_coefficients = ellipse_coefficients +M.ellipse_from_center_quadratic = ellipse_from_center_quadratic +M.steiner_ellipses = steiner_ellipses +M.cyclically_ordered_points = cyclically_ordered_points +M.quadrilateral_midpoint_ellipse = quadrilateral_midpoint_ellipse +M.ellipse_from_foci_point = ellipse_from_foci_point +M.conic_coefficients_for_focus_directrix = conic_coefficients_for_focus_directrix +M.focus_directrix_conic_coefficients = focus_directrix_conic_coefficients +M.stable_asinh = stable_asinh +M.hyperbola_from_parameters = hyperbola_from_parameters +M.hyperbola_from_foci_point = hyperbola_from_foci_point +M.hyperbola_coefficients = hyperbola_coefficients +M.hyperbola_point = hyperbola_point +M.hyperbola_derivative = hyperbola_derivative +M.adaptive_hyperbola_cubics = adaptive_hyperbola_cubics +M.parabola_spline = parabola_spline + +local function conic_gradient(coefficients, point) + coefficients = normalize_coefficients(coefficients) + point = point_from_table(point, "point") + local a, b, c, d, e = unpack(coefficients) + local gradient = V( + 2 * a * point.x + b * point.y + d, + b * point.x + 2 * c * point.y + e + ) + if not finite_number(gradient.x) or not finite_number(gradient.y) then + error("conic gradient is not finite") + end + return gradient +end + +local function conic_tangent_normal(coefficients, point) + coefficients = normalize_coefficients(coefficients) + point = point_from_table(point, "point") + local value, magnitude = evaluate_conic(coefficients, point) + if math.abs(value) > scaled_tolerance(math.max(magnitude, MIN_NORMAL), 32768) then + error("point must lie on the conic") + end + local normal_direction = conic_gradient(coefficients, point) + local gradient_scale = math.max( + math.max(math.abs(coefficients[1]), math.abs(coefficients[2]), math.abs(coefficients[3])) + * math.max(math.abs(point.x), math.abs(point.y)), + math.abs(coefficients[4]), + math.abs(coefficients[5]), + MIN_NORMAL + ) + if near_zero(length(normal_direction), gradient_scale, 4096) then + error("conic tangent is undefined at a singular point") + end + return { + tangent = { point = point, direction = perpendicular(normal_direction) }, + normal = { point = point, direction = normal_direction }, + } +end + +local function conic_polar_line(coefficients, point) + coefficients = normalize_coefficients(coefficients) + point = point_from_table(point, "point") + local a, b, c, d, e, f = unpack(coefficients) + return line_from_equation( + a * point.x + 0.5 * b * point.y + 0.5 * d, + 0.5 * b * point.x + c * point.y + 0.5 * e, + 0.5 * d * point.x + 0.5 * e * point.y + f, + point + ) +end + +local function roots_are_close(left, right) + return math.abs(left - right) + <= 32 * math.sqrt(MACHINE_EPSILON) + * math.max(1, math.abs(left), math.abs(right)) +end + +local function conic_line_intersections(coefficients, input_line) + coefficients = normalize_coefficients(coefficients) + local line = line_from_table(input_line, "line") + local point, direction = line.point, unit(line.direction, "line direction") + local a, b, c, d, e, f = unpack(coefficients) + local px, py, dx, dy = point.x, point.y, direction.x, direction.y + local q2 = a * dx * dx + b * dx * dy + c * dy * dy + local q1 = 2 * a * px * dx + b * (px * dy + py * dx) + + 2 * c * py * dy + d * dx + e * dy + local q0 = a * px * px + b * px * py + c * py * py + d * px + e * py + f + if not finite_number(q2) or not finite_number(q1) or not finite_number(q0) then + error("line-intersection polynomial is not finite") + end + local q2_scale = math.abs(a * dx * dx) + math.abs(b * dx * dy) + math.abs(c * dy * dy) + local q1_scale = math.abs(2 * a * px * dx) + math.abs(b * (px * dy + py * dx)) + + math.abs(2 * c * py * dy) + math.abs(d * dx) + math.abs(e * dy) + local q0_scale = math.abs(a * px * px) + math.abs(b * px * py) + math.abs(c * py * py) + + math.abs(d * px) + math.abs(e * py) + math.abs(f) + local q2_is_zero = near_zero(q2, math.max(q2_scale, MIN_NORMAL), 8192) + local q1_is_zero = near_zero(q1, math.max(q1_scale, MIN_NORMAL), 8192) + local q0_is_zero = near_zero(q0, math.max(q0_scale, MIN_NORMAL), 8192) + local polynomial_scale = math.max(math.abs(q2), math.abs(q1), math.abs(q0), MIN_NORMAL) + q2, q1, q0 = q2 / polynomial_scale, q1 / polynomial_scale, q0 / polynomial_scale + local roots = {} + if q2_is_zero then + if q1_is_zero then + if q0_is_zero then + roots.infinite, roots.count = true, math.huge + return roots + end + roots.count = 0 + return roots + end + roots[1] = -q0 / q1 + else + local discriminant = q1 * q1 - 4 * q2 * q0 + local discriminant_scale = math.max( + math.abs(q1 * q1) + math.abs(4 * q2 * q0), + math.max(math.abs(q2), math.abs(q1), math.abs(q0)) ^ 2 + ) + local tolerance = scaled_tolerance(math.max(discriminant_scale, MIN_NORMAL), 16384) + if discriminant < -tolerance then + roots.count = 0 + return roots + end + discriminant = math.max(0, discriminant) + local square_root = math.sqrt(discriminant) + if square_root == 0 then + roots[1] = -q1 / (2 * q2) + else + local signed_root = q1 >= 0 and square_root or -square_root + local q = -0.5 * (q1 + signed_root) + if q == 0 then + roots[1] = -q1 / (2 * q2) + else + roots[1], roots[2] = q / q2, q0 / q + if roots_are_close(roots[1], roots[2]) then roots[2] = nil end + end + end + end + table.sort(roots) + local points = {} + for _, root in ipairs(roots) do + if not finite_number(root) then error("line-intersection root is not finite") end + local intersection = add(point, scale(direction, root)) + if not finite_number(intersection.x) or not finite_number(intersection.y) then + error("line intersection is not finite") + end + if #points == 0 or distance(points[#points], intersection) + > scaled_tolerance(math.max(vector_scale(points[#points], intersection), MIN_NORMAL), 8192) then + points[#points + 1] = intersection + end + end + points.count = #points + return points +end + +local function transformed_conic_coefficients(coefficients, matrix) + coefficients = normalize_coefficients(coefficients) + local values = { matrix:coeff() } + if #values == 1 and type(values[1]) == "table" then values = values[1] end + local ma, mc, mb, md, tx, ty = unpack(values) + for _, value in ipairs({ ma, mc, mb, md, tx, ty }) do + if not finite_number(value) then error("conic object matrix must contain finite numbers") end + end + local determinant = ma * md - mb * mc + local matrix_scale = math.max(math.abs(ma), math.abs(mc), math.abs(mb), math.abs(md)) + if matrix_scale == 0 + or near_zero(determinant, matrix_scale * matrix_scale, 8192) then + error("conic object matrix must be nonsingular") + end + local alpha, beta = md / determinant, -mb / determinant + local gamma = (mb * ty - md * tx) / determinant + local delta, epsilon = -mc / determinant, ma / determinant + local zeta = (mc * tx - ma * ty) / determinant + local a, b, c, d, e, f = unpack(coefficients) + return normalize_coefficients({ + a * alpha * alpha + b * alpha * delta + c * delta * delta, + 2 * a * alpha * beta + b * (alpha * epsilon + beta * delta) + + 2 * c * delta * epsilon, + a * beta * beta + b * beta * epsilon + c * epsilon * epsilon, + 2 * a * alpha * gamma + b * (alpha * zeta + gamma * delta) + + 2 * c * delta * zeta + d * alpha + e * delta, + 2 * a * beta * gamma + b * (beta * zeta + gamma * epsilon) + + 2 * c * epsilon * zeta + d * beta + e * epsilon, + a * gamma * gamma + b * gamma * zeta + c * zeta * zeta + + d * gamma + e * zeta + f, + }) +end + +function Advanced.flexible_points(input_points, minimum, maximum, name) + name = name or "points" + if type(input_points) ~= "table" then error(name .. " must be a point array") end + if #input_points < minimum then + error(name .. " must contain at least " .. tostring(minimum) .. " points") + end + if maximum and #input_points > maximum then + error(name .. " must contain at most " .. tostring(maximum) .. " points") + end + local points = {} + for index, point in ipairs(input_points) do + points[index] = point_from_table(point, name .. "[" .. tostring(index) .. "]") + end + return points +end + +function Advanced.point_normalization(points) + local center = V(0, 0) + for _, point in ipairs(points) do center = add(center, point) end + center = scale(center, 1 / #points) + local coordinate_scale = 0 + for _, point in ipairs(points) do + coordinate_scale = math.max( + coordinate_scale, + math.abs(point.x - center.x), + math.abs(point.y - center.y) + ) + end + if coordinate_scale == 0 or not finite_number(coordinate_scale) then + error("points do not span a finite two-dimensional scale") + end + return center, coordinate_scale +end + +function Advanced.denormalize_conic(coefficients, center, coordinate_scale) + return transformed_conic_coefficients( + coefficients, + ipe.Matrix(coordinate_scale, 0, 0, coordinate_scale, center.x, center.y) + ) +end + +function Advanced.conic_coefficients_from_points(input_points, raw_options) + local options = options_table(raw_options) + validate_keys(options, { + allow_degenerate = true, expected_kind = true, maximum_points = true, + }, "point-fit options") + local maximum_points = positive_integer_option( + options.maximum_points, 512, "maximum_points", 4096, 5 + ) + local input = Advanced.flexible_points(input_points, 5, maximum_points, "points") + local center, coordinate_scale = Advanced.point_normalization(input) + local points = {} + for _, point in ipairs(input) do + local duplicate = false + for _, existing in ipairs(points) do + if near_zero(distance(existing, point), coordinate_scale, 2048) then + duplicate = true + break + end + end + if not duplicate then points[#points + 1] = point end + end + if #points < 5 then error("point fit requires at least five distinct sample points") end + center, coordinate_scale = Advanced.point_normalization(points) + local rows = {} + for index, point in ipairs(points) do + local x = (point.x - center.x) / coordinate_scale + local y = (point.y - center.y) / coordinate_scale + rows[index] = { x * x, x * y, y * y, x, y, 1 } + end + local normalized, smallest, second_smallest, largest = jacobi_null_vector(rows) + if largest == 0 or second_smallest <= scaled_tolerance(largest, 32768) then + error("sample points do not determine a unique stable conic") + end + if #points == 5 and smallest > scaled_tolerance(largest, 32768) then + error("five sample points have no reliable homogeneous nullspace") + end + if #points > 5 and smallest >= second_smallest * 0.98 then + error("sample points do not identify one conic reliably") + end + normalized = normalize_coefficients(normalized) + local coefficients = Advanced.denormalize_conic(normalized, center, coordinate_scale) + local degenerate = is_degenerate_conic(coefficients) + if degenerate and not bool_value(options.allow_degenerate, false) then + error("sample points fit a degenerate conic") + end + local classification = classify_conic(coefficients) + local expected = normalized_name(options.expected_kind or "auto") + if expected ~= "auto" and expected ~= "any" then + local accepted = classification.kind == expected + or (expected == "ellipse" and classification.kind == "circle") + or (expected == "central" and (classification.kind == "ellipse" + or classification.kind == "circle" or classification.kind == "hyperbola")) + if not accepted then + error("best-fit conic is " .. classification.kind .. ", not " .. expected) + end + end + local residual_sum, maximum_residual = 0, 0 + for _, row in ipairs(rows) do + local value, magnitude = 0, 0 + for column = 1, 6 do + local term = normalized[column] * row[column] + value, magnitude = value + term, magnitude + math.abs(term) + end + local residual = math.abs(value) / math.max(magnitude, MIN_NORMAL) + residual_sum = residual_sum + residual * residual + maximum_residual = math.max(maximum_residual, residual) + end + return coefficients, { + input_count = #input, + sample_count = #points, + rms_residual = math.sqrt(residual_sum / #points), + maximum_residual = maximum_residual, + singular_ratio = smallest / math.max(second_smallest, MIN_NORMAL), + kind = classification.kind, + } +end + +function Advanced.line_intersection(left, right) + left, right = line_from_table(left, "left line"), line_from_table(right, "right line") + local determinant = left.a * right.b - right.a * left.b + if near_zero(determinant, 1, 8192) then return nil end + local point = V( + (left.b * right.c - right.b * left.c) / determinant, + (left.c * right.a - right.c * left.a) / determinant + ) + if not finite_number(point.x) or not finite_number(point.y) then return nil end + return point +end + +function Advanced.dual_conic_coefficients(coefficients) + local matrix = Advanced.conic_matrix(coefficients) + local determinant = Advanced.matrix3_determinant(matrix) + if near_zero(determinant, 1, 8192) then error("degenerate conic has no invertible dual") end + return Advanced.coefficients_from_symmetric_matrix(Advanced.matrix3_adjugate(matrix)) +end + +function Advanced.conic_coefficients_from_five_lines(input_lines, raw_options) + local options = options_table(raw_options) + validate_keys(options, { allow_degenerate = true }, "five-tangent options") + if type(input_lines) ~= "table" or #input_lines ~= 5 then + error("exactly five tangent lines are required") + end + local lines, intersections = {}, {} + for index, input in ipairs(input_lines) do + lines[index] = line_from_table(input, "lines[" .. tostring(index) .. "]") + end + for first = 1, 4 do + for second = first + 1, 5 do + local point = Advanced.line_intersection(lines[first], lines[second]) + if point then intersections[#intersections + 1] = point end + end + end + if #intersections < 2 then error("five tangent lines do not span a stable finite frame") end + local center, coordinate_scale = Advanced.point_normalization(intersections) + local rows = {} + for index, line in ipairs(lines) do + local values = { + line.a * coordinate_scale, + line.b * coordinate_scale, + line.a * center.x + line.b * center.y + line.c, + } + local norm = hypot(hypot(values[1], values[2]), values[3]) + if norm == 0 or not finite_number(norm) then error("tangent line normalization failed") end + for component = 1, 3 do values[component] = values[component] / norm end + rows[index] = { + values[1] * values[1], values[1] * values[2], values[2] * values[2], + values[1] * values[3], values[2] * values[3], values[3] * values[3], + } + end + local dual, smallest, second_smallest, largest = jacobi_null_vector(rows) + if largest == 0 or second_smallest <= scaled_tolerance(largest, 32768) + or smallest > scaled_tolerance(largest, 32768) then + error("five tangent lines do not determine a unique stable conic") + end + dual = normalize_coefficients(dual) + if is_degenerate_conic(dual) then error("five tangent lines determine a singular dual conic") end + local normalized = Advanced.coefficients_from_symmetric_matrix( + Advanced.matrix3_adjugate(Advanced.conic_matrix(dual)) + ) + local coefficients = Advanced.denormalize_conic(normalized, center, coordinate_scale) + if is_degenerate_conic(coefficients) and not bool_value(options.allow_degenerate, false) then + error("five tangent lines determine a degenerate conic") + end + return coefficients +end + +function Advanced.constraint_rows(constraints, center, coordinate_scale) + local rows, weight = {}, 0 + for index, constraint in ipairs(constraints) do + if type(constraint) ~= "table" then + error("constraints[" .. tostring(index) .. "] must be a table") + end + local kind = normalized_name(constraint.type or constraint.kind or "point") + local point = point_from_table(constraint.point or constraint, "constraints[" + .. tostring(index) .. "].point") + local normalized_point = V( + (point.x - center.x) / coordinate_scale, + (point.y - center.y) / coordinate_scale + ) + rows[#rows + 1] = conic_terms(normalized_point) + weight = weight + 1 + if kind == "tangent" or kind == "tangent_at_point" then + local line = line_from_table(constraint.line or constraint.tangent, + "constraints[" .. tostring(index) .. "].line") + local signed_distance = line.a * point.x + line.b * point.y + line.c + if math.abs(signed_distance) > scaled_tolerance( + math.max(vector_scale(point), math.abs(line.c), MIN_NORMAL), 16384) then + error("a tangent constraint line must pass through its point") + end + local line_a = line.a * coordinate_scale + local line_b = line.b * coordinate_scale + local x, y = normalized_point.x, normalized_point.y + rows[#rows + 1] = { + 2 * line_b * x, + line_b * y - line_a * x, + -2 * line_a * y, + line_b, + -line_a, + 0, + } + weight = weight + 1 + elseif kind ~= "point" then + error("unsupported conic constraint type: " .. kind) + end + end + return rows, weight +end + +function Advanced.conic_coefficients_from_constraints(constraints, raw_options) + local options = options_table(raw_options) + validate_keys(options, { allow_degenerate = true }, "constraint options") + if type(constraints) ~= "table" or #constraints == 0 then + error("constraints must be a nonempty array") + end + local points = {} + for index, constraint in ipairs(constraints) do + if type(constraint) ~= "table" then + error("constraints[" .. tostring(index) .. "] must be a table") + end + points[index] = point_from_table(constraint.point or constraint, + "constraints[" .. tostring(index) .. "].point") + end + local center, coordinate_scale = Advanced.point_normalization(points) + local rows, weight = Advanced.constraint_rows(constraints, center, coordinate_scale) + if weight ~= 5 or #rows ~= 5 then + error("mixed conic construction requires exactly five weighted conditions") + end + local normalized, smallest, second_smallest, largest = jacobi_null_vector(rows) + if largest == 0 or second_smallest <= scaled_tolerance(largest, 32768) + or smallest > scaled_tolerance(largest, 32768) then + error("the five conditions do not determine a unique stable conic") + end + normalized = normalize_coefficients(normalized) + local coefficients = Advanced.denormalize_conic(normalized, center, coordinate_scale) + if is_degenerate_conic(coefficients) and not bool_value(options.allow_degenerate, false) then + error("the five conditions determine a degenerate conic") + end + return coefficients +end + +function Advanced.ellipse_from_center_axes(center, first_endpoint, second_endpoint) + center = point_from_table(center, "center") + first_endpoint = point_from_table(first_endpoint, "first_endpoint") + second_endpoint = point_from_table(second_endpoint, "second_endpoint") + local first_axis, second_axis = sub(first_endpoint, center), sub(second_endpoint, center) + local first_length, second_length = length(first_axis), length(second_axis) + local input_scale = math.max(first_length, second_length, MIN_NORMAL) + if near_zero(first_length, input_scale, 4096) + or near_zero(second_length, input_scale, 4096) then + error("ellipse semiaxes must be nonzero") + end + if math.abs(dot(first_axis, second_axis)) > scaled_tolerance( + first_length * second_length, 16384) then + error("ellipse semiaxes must be perpendicular") + end + return { + center = center, + axis1 = first_axis, + axis2 = second_axis, + major_radius = math.max(first_length, second_length), + minor_radius = math.min(first_length, second_length), + } +end + +function Advanced.parabola_from_vertex_focus(vertex, focus) + vertex = point_from_table(vertex, "vertex") + focus = point_from_table(focus, "focus") + local axis = sub(focus, vertex) + local focal_distance = length(axis) + if near_zero(focal_distance, vector_scale(vertex, focus), 4096) then + error("parabola vertex and focus must be distinct") + end + local axis_direction = scale(axis, 1 / focal_distance) + local directrix_point = sub(vertex, scale(axis_direction, focal_distance)) + local directrix = line_from_equation( + axis_direction.x, axis_direction.y, -dot(axis_direction, directrix_point), + directrix_point + ) + return conic_coefficients_for_focus_directrix(focus, directrix, 1), directrix +end + +function Advanced.hyperbola_from_asymptotes_point(first_line, second_line, point) + first_line = line_from_table(first_line, "first asymptote") + second_line = line_from_table(second_line, "second asymptote") + point = point_from_table(point, "point") + local center = Advanced.line_intersection(first_line, second_line) + if not center then error("hyperbola asymptotes must intersect") end + local first_value = first_line.a * point.x + first_line.b * point.y + first_line.c + local second_value = second_line.a * point.x + second_line.b * point.y + second_line.c + local product = first_value * second_value + local input_scale = math.max( + math.abs(first_value * second_value), + distance(center, point) * distance(center, point), + MIN_NORMAL + ) + if near_zero(product, input_scale, 8192) then + error("the defining point must not lie on either asymptote") + end + local coefficients = normalize_coefficients({ + first_line.a * second_line.a, + first_line.a * second_line.b + first_line.b * second_line.a, + first_line.b * second_line.b, + first_line.a * second_line.c + first_line.c * second_line.a, + first_line.b * second_line.c + first_line.c * second_line.b, + first_line.c * second_line.c - product, + }) + if classify_conic(coefficients).kind ~= "hyperbola" then + error("asymptotes and point did not produce a stable hyperbola") + end + return coefficients, center +end + +function Advanced.degenerate_conic_from_lines(input_lines) + if type(input_lines) ~= "table" or (#input_lines ~= 1 and #input_lines ~= 2) then + error("one or two lines are required for a degenerate line conic") + end + local first = line_from_table(input_lines[1], "first line") + local second = #input_lines == 2 + and line_from_table(input_lines[2], "second line") or first + return normalize_coefficients({ + first.a * second.a, + first.a * second.b + first.b * second.a, + first.b * second.b, + first.a * second.c + first.c * second.a, + first.b * second.c + first.c * second.b, + first.c * second.c, + }) +end + +function Advanced.degenerate_point_conic(point) + point = point_from_table(point, "point") + return normalize_coefficients({ 1, 0, 1, -2 * point.x, -2 * point.y, + point.x * point.x + point.y * point.y }) +end + +function Advanced.polynomial_trim(coefficients) + local result, coefficient_scale = {}, 0 + for index = 1, #coefficients do + local value = finite_number_option(coefficients[index], 0, + "polynomial coefficient") + result[index] = value + coefficient_scale = math.max(coefficient_scale, math.abs(value)) + end + local tolerance = scaled_tolerance(math.max(coefficient_scale, MIN_NORMAL), 65536) + while #result > 1 and math.abs(result[#result]) <= tolerance do + result[#result] = nil + end + if #result == 0 then result[1] = 0 end + return result +end + +function Advanced.polynomial_add(left, right, right_scale) + right_scale = right_scale or 1 + local result = {} + for index = 1, math.max(#left, #right) do + result[index] = (left[index] or 0) + right_scale * (right[index] or 0) + end + return Advanced.polynomial_trim(result) +end + +function Advanced.polynomial_multiply(left, right) + local result = {} + for index = 1, #left + #right - 1 do result[index] = 0 end + for left_index, left_value in ipairs(left) do + for right_index, right_value in ipairs(right) do + result[left_index + right_index - 1] = result[left_index + right_index - 1] + + left_value * right_value + end + end + return Advanced.polynomial_trim(result) +end + +function Advanced.polynomial_value_scale(coefficients, value) + local result, magnitude = 0, 0 + for index = #coefficients, 1, -1 do + result = result * value + coefficients[index] + magnitude = magnitude * math.abs(value) + math.abs(coefficients[index]) + end + return result, magnitude +end + +function Advanced.polynomial_root_is_close(left, right) + return math.abs(left - right) <= 1e-9 * math.max(1, math.abs(left), math.abs(right)) +end + +function Advanced.polynomial_append_root(roots, root) + if not finite_number(root) then return end + for _, existing in ipairs(roots) do + if Advanced.polynomial_root_is_close(existing, root) then return end + end + roots[#roots + 1] = root +end + +function Advanced.polynomial_real_roots(input_coefficients) + local coefficients = Advanced.polynomial_trim(input_coefficients) + local degree = #coefficients - 1 + if degree == 0 then return {} end + if degree == 1 then + local root = -coefficients[1] / coefficients[2] + if not finite_number(root) then error("polynomial root is not finite") end + return { root } + end + if degree > 4 then error("real-root solver supports degree four or less") end + local derivative = {} + for index = 2, #coefficients do derivative[index - 1] = (index - 1) * coefficients[index] end + local critical = Advanced.polynomial_real_roots(derivative) + local leading = math.abs(coefficients[#coefficients]) + local root_bound = 1 + for index = 1, #coefficients - 1 do + root_bound = math.max(root_bound, 1 + math.abs(coefficients[index]) / leading) + end + if not finite_number(root_bound) then error("polynomial root bound is not finite") end + local partitions = { -root_bound } + for _, root in ipairs(critical) do + if root > -root_bound and root < root_bound then partitions[#partitions + 1] = root end + end + partitions[#partitions + 1] = root_bound + table.sort(partitions) + local roots = {} + for _, point in ipairs(partitions) do + local value, magnitude = Advanced.polynomial_value_scale(coefficients, point) + if finite_number(value) and finite_number(magnitude) + and math.abs(value) <= 1e-8 * math.max(magnitude, MIN_NORMAL) then + Advanced.polynomial_append_root(roots, point) + end + end + for index = 1, #partitions - 1 do + local left, right = partitions[index], partitions[index + 1] + local left_value = Advanced.polynomial_value_scale(coefficients, left) + local right_value = Advanced.polynomial_value_scale(coefficients, right) + if finite_number(left_value) and finite_number(right_value) + and left_value * right_value < 0 then + for _ = 1, 96 do + local middle = left * 0.5 + right * 0.5 + local middle_value = Advanced.polynomial_value_scale(coefficients, middle) + if not finite_number(middle_value) then + error("polynomial evaluation exceeded finite numeric range") + end + if middle_value == 0 then + left, right = middle, middle + break + elseif left_value * middle_value < 0 then + right, right_value = middle, middle_value + else + left, left_value = middle, middle_value + end + if math.abs(right - left) <= 1e-12 * math.max(1, math.abs(left), math.abs(right)) then + break + end + end + Advanced.polynomial_append_root(roots, left * 0.5 + right * 0.5) + end + end + table.sort(roots) + return roots +end + +function Advanced.quadratic_real_roots(quadratic, linear, constant) + local scale_value = math.max(math.abs(quadratic), math.abs(linear), + math.abs(constant), MIN_NORMAL) + if near_zero(quadratic, scale_value, 65536) then + if near_zero(linear, scale_value, 65536) then return {} end + return { -constant / linear } + end + local discriminant = linear * linear - 4 * quadratic * constant + local discriminant_scale = math.max( + math.abs(linear * linear) + math.abs(4 * quadratic * constant), + scale_value * scale_value + ) + local tolerance = scaled_tolerance(math.max(discriminant_scale, MIN_NORMAL), 131072) + if discriminant < -tolerance then return {} end + local square_root = math.sqrt(math.max(0, discriminant)) + if square_root == 0 then return { -linear / (2 * quadratic) } end + local signed_root = linear >= 0 and square_root or -square_root + local q = -0.5 * (linear + signed_root) + if q == 0 then return { -linear / (2 * quadratic) } end + local roots = { q / quadratic, constant / q } + if Advanced.polynomial_root_is_close(roots[1], roots[2]) then roots[2] = nil end + table.sort(roots) + return roots +end + +function Advanced.conic_resultant_in_x(left, right) + local left_values = { + { left[6], left[4], left[1] }, + { left[5], left[2] }, + { left[3] }, + } + local right_values = { + { right[6], right[4], right[1] }, + { right[5], right[2] }, + { right[3] }, + } + local function polynomial_is_zero(value) + value = Advanced.polynomial_trim(value) + return #value == 1 and near_zero(value[1], 1, 262144) + end + local left_degree, right_degree = 2, 2 + while left_degree > 0 and polynomial_is_zero(left_values[left_degree + 1]) do + left_degree = left_degree - 1 + end + while right_degree > 0 and polynomial_is_zero(right_values[right_degree + 1]) do + right_degree = right_degree - 1 + end + local left0, left1, left2 = left_values[1], left_values[2], left_values[3] + local right0, right1, right2 = right_values[1], right_values[2], right_values[3] + if left_degree == 0 then + local result = { 1 } + for _ = 1, right_degree do result = Advanced.polynomial_multiply(result, left0) end + return result + elseif right_degree == 0 then + local result = { 1 } + for _ = 1, left_degree do result = Advanced.polynomial_multiply(result, right0) end + return result + elseif left_degree == 1 and right_degree == 1 then + return Advanced.polynomial_add( + Advanced.polynomial_multiply(left1, right0), + Advanced.polynomial_multiply(left0, right1), -1 + ) + elseif left_degree == 2 and right_degree == 1 then + return Advanced.polynomial_add(Advanced.polynomial_add( + Advanced.polynomial_multiply(left2, + Advanced.polynomial_multiply(right0, right0)), + Advanced.polynomial_multiply(left1, + Advanced.polynomial_multiply(right1, right0)), -1 + ), Advanced.polynomial_multiply(left0, + Advanced.polynomial_multiply(right1, right1))) + elseif left_degree == 1 and right_degree == 2 then + return Advanced.polynomial_add(Advanced.polynomial_add( + Advanced.polynomial_multiply(right2, + Advanced.polynomial_multiply(left0, left0)), + Advanced.polynomial_multiply(right1, + Advanced.polynomial_multiply(left1, left0)), -1 + ), Advanced.polynomial_multiply(right0, + Advanced.polynomial_multiply(left1, left1))) + end + local first = Advanced.polynomial_add( + Advanced.polynomial_multiply(left2, right0), + Advanced.polynomial_multiply(left0, right2), -1 + ) + local second = Advanced.polynomial_add( + Advanced.polynomial_multiply(left2, right1), + Advanced.polynomial_multiply(left1, right2), -1 + ) + local third = Advanced.polynomial_add( + Advanced.polynomial_multiply(left1, right0), + Advanced.polynomial_multiply(left0, right1), -1 + ) + return Advanced.polynomial_add( + Advanced.polynomial_multiply(first, first), + Advanced.polynomial_multiply(second, third), -1 + ) +end + +function Advanced.coefficients_are_proportional(left, right) + left, right = normalize_coefficients(left), normalize_coefficients(right) + local same, opposite = 0, 0 + for index = 1, 6 do + same = math.max(same, math.abs(left[index] - right[index])) + opposite = math.max(opposite, math.abs(left[index] + right[index])) + end + return math.min(same, opposite) <= scaled_tolerance(1, 4096) +end + +function Advanced.refine_conic_intersection(left, right, input_point) + local point = point_from_table(input_point, "intersection candidate") + for _ = 1, 12 do + local left_value = evaluate_conic(left, point) + local right_value = evaluate_conic(right, point) + local left_gradient = conic_gradient(left, point) + local right_gradient = conic_gradient(right, point) + local determinant = cross(left_gradient, right_gradient) + local gradient_scale = length(left_gradient) * length(right_gradient) + if near_zero(determinant, math.max(gradient_scale, MIN_NORMAL), 65536) then break end + local delta = V( + (-left_value * right_gradient.y + right_value * left_gradient.y) / determinant, + (-right_value * left_gradient.x + left_value * right_gradient.x) / determinant + ) + if not finite_number(delta.x) or not finite_number(delta.y) then break end + point = add(point, delta) + if length(delta) <= 1e-12 * math.max(1, vector_scale(point)) then break end + end + return point +end + +function Advanced.conic_intersection_candidates_at_x(left, right, x) + local candidates = {} + local left0 = left[1] * x * x + left[4] * x + left[6] + local left1 = left[2] * x + left[5] + local right0 = right[1] * x * x + right[4] * x + right[6] + local right1 = right[2] * x + right[5] + local eliminator = right[3] * left1 - left[3] * right1 + local eliminated_constant = right[3] * left0 - left[3] * right0 + local elimination_scale = math.abs(right[3] * left1) + math.abs(left[3] * right1) + + math.abs(right[3] * left0) + math.abs(left[3] * right0) + if not near_zero(eliminator, math.max(elimination_scale, MIN_NORMAL), 131072) then + candidates[1] = -eliminated_constant / eliminator + else + for _, root in ipairs(Advanced.quadratic_real_roots(left[3], left1, left0)) do + Advanced.polynomial_append_root(candidates, root) + end + for _, root in ipairs(Advanced.quadratic_real_roots(right[3], right1, right0)) do + Advanced.polynomial_append_root(candidates, root) + end + end + return candidates +end + +function Advanced.conic_conic_intersections(left, right) + left, right = normalize_coefficients(left), normalize_coefficients(right) + if is_degenerate_conic(left) or is_degenerate_conic(right) then + error("conic-conic intersections require two nondegenerate conics") + end + if Advanced.coefficients_are_proportional(left, right) then + return { count = math.huge, infinite = true, coincident = true } + end + local left_properties, right_properties = conic_properties(left), conic_properties(right) + local left_origin = left_properties.center or left_properties.vertex + local right_origin = right_properties.center or right_properties.vertex + local origin = midpoint(left_origin, right_origin) + local coordinate_scale = distance(left_origin, right_origin) + for _, properties in ipairs({ left_properties, right_properties }) do + coordinate_scale = math.max( + coordinate_scale, + properties.major_radius or 0, + properties.minor_radius or 0, + properties.a or 0, + properties.b or 0, + math.abs(properties.focal_parameter or 0) * 4 + ) + end + if coordinate_scale == 0 or not finite_number(coordinate_scale) then + error("conics do not provide a stable finite intersection frame") + end + local inverse_frame = ipe.Matrix( + 1 / coordinate_scale, 0, 0, 1 / coordinate_scale, + -origin.x / coordinate_scale, -origin.y / coordinate_scale + ) + local normalized_left = transformed_conic_coefficients(left, inverse_frame) + local normalized_right = transformed_conic_coefficients(right, inverse_frame) + local points = {} + local indeterminate_passes = 0 + for pass = 1, 2 do + local pass_left, pass_right = normalized_left, normalized_right + if pass == 2 then + pass_left = { + normalized_left[3], normalized_left[2], normalized_left[1], + normalized_left[5], normalized_left[4], normalized_left[6], + } + pass_right = { + normalized_right[3], normalized_right[2], normalized_right[1], + normalized_right[5], normalized_right[4], normalized_right[6], + } + end + local resultant = Advanced.conic_resultant_in_x(pass_left, pass_right) + if #resultant == 1 and near_zero(resultant[1], 1, 262144) then + indeterminate_passes = indeterminate_passes + 1 + else + for _, x in ipairs(Advanced.polynomial_real_roots(resultant)) do + for _, y in ipairs(Advanced.conic_intersection_candidates_at_x( + pass_left, pass_right, x)) do + local candidate = pass == 1 and V(x, y) or V(y, x) + local normalized_point = Advanced.refine_conic_intersection( + normalized_left, normalized_right, candidate + ) + local point = add(origin, scale(normalized_point, coordinate_scale)) + local left_value, left_magnitude = evaluate_conic(left, point) + local right_value, right_magnitude = evaluate_conic(right, point) + if math.abs(left_value) <= 1e-7 * math.max(left_magnitude, MIN_NORMAL) + and math.abs(right_value) <= 1e-7 * math.max(right_magnitude, MIN_NORMAL) then + local duplicate = false + for _, existing in ipairs(points) do + if distance(existing, point) + <= 1e-7 * math.max(1, vector_scale(existing, point)) then + duplicate = true + break + end + end + if not duplicate then points[#points + 1] = point end + end + end + end + end + end + if indeterminate_passes == 2 then + error("conic intersection resultants are numerically indeterminate") + end + table.sort(points, function(first, second) + if first.x ~= second.x then return first.x < second.x end + return first.y < second.y + end) + points.count = #points + return points +end + +function Advanced.conic_pole(coefficients, input_line) + coefficients = normalize_coefficients(coefficients) + if is_degenerate_conic(coefficients) then error("a degenerate conic has no unique pole map") end + local line = line_from_table(input_line, "line") + local homogeneous = Advanced.matrix3_vector( + Advanced.matrix3_adjugate(Advanced.conic_matrix(coefficients)), + { line.a, line.b, line.c } + ) + local homogeneous_scale = math.max(math.abs(homogeneous[1]), math.abs(homogeneous[2]), + math.abs(homogeneous[3]), MIN_NORMAL) + if near_zero(homogeneous[3], homogeneous_scale, 32768) then + return { + finite = false, + at_infinity = true, + direction = unit(V(homogeneous[1], homogeneous[2]), "pole direction"), + } + end + local point = V(homogeneous[1] / homogeneous[3], homogeneous[2] / homogeneous[3]) + if not finite_number(point.x) or not finite_number(point.y) then + error("pole is not finitely representable") + end + return { finite = true, at_infinity = false, point = point } +end + +function Advanced.tangents_from_point(coefficients, input_point) + coefficients = normalize_coefficients(coefficients) + if is_degenerate_conic(coefficients) then + error("tangents from a point require a nondegenerate conic") + end + local point = point_from_table(input_point, "point") + local value, magnitude = evaluate_conic(coefficients, point) + local result = { point = point, tangents = {}, contact_points = {} } + if math.abs(value) <= scaled_tolerance(math.max(magnitude, MIN_NORMAL), 32768) then + local feature = conic_tangent_normal(coefficients, point) + result.tangents[1], result.contact_points[1] = feature.tangent, point + result.chord_of_contact, result.count = feature.tangent, 1 + return result + end + local a, b, c, d, e, f = unpack(coefficients) + local polar_a = a * point.x + 0.5 * b * point.y + 0.5 * d + local polar_b = 0.5 * b * point.x + c * point.y + 0.5 * e + local polar_c = 0.5 * d * point.x + 0.5 * e * point.y + f + local polar_scale = math.max(math.abs(polar_a), math.abs(polar_b), + math.abs(polar_c), MIN_NORMAL) + if near_zero(hypot(polar_a, polar_b), polar_scale, 32768) then + result.count = 0 + result.polar_at_infinity = true + return result + end + local polar = line_from_equation(polar_a, polar_b, polar_c, point) + local contacts = conic_line_intersections(coefficients, polar) + if contacts.infinite then error("polar unexpectedly lies in the conic") end + for _, contact in ipairs(contacts) do + local feature = conic_tangent_normal(coefficients, contact) + result.contact_points[#result.contact_points + 1] = contact + result.tangents[#result.tangents + 1] = feature.tangent + end + result.chord_of_contact = polar + result.count = #result.tangents + return result +end + +function Advanced.focal_chord(coefficients, input_point, focus_index) + coefficients = normalize_coefficients(coefficients) + local properties = conic_properties(coefficients) + if properties.degenerate or not properties.foci or #properties.foci == 0 then + error("this conic has no real finite focus") + end + local point = point_from_table(input_point, "point") + local index = focus_index and positive_integer_option(focus_index, nil, + "focus_index", #properties.foci) or nil + if not index then + index = 1 + for candidate = 2, #properties.foci do + if distance(point, properties.foci[candidate]) < distance(point, properties.foci[index]) then + index = candidate + end + end + end + local focus = properties.foci[index] + local line = line_from_points(focus, point, "focal chord") + local intersections = conic_line_intersections(coefficients, line) + if intersections.infinite or #intersections == 0 then + error("the focal line has no finite real chord") + end + return { + focus = focus, + focus_index = index, + line = line, + endpoints = intersections, + count = #intersections, + } +end + +function Advanced.general_equation_latex(coefficients) + coefficients = normalize_coefficients(coefficients) + local first_nonzero + for index = 1, 6 do + if not near_zero(coefficients[index], 1, 8192) then + first_nonzero = coefficients[index] + break + end + end + if first_nonzero and first_nonzero < 0 then + for index = 1, 6 do coefficients[index] = -coefficients[index] end + end + local variables = { "x^{2}", "xy", "y^{2}", "x", "y", "" } + local parts = {} + for index, coefficient in ipairs(coefficients) do + if not near_zero(coefficient, 1, 8192) then + local absolute = math.abs(coefficient) + local magnitude = variables[index] ~= "" + and math.abs(absolute - 1) <= scaled_tolerance(1, 8192) + and "" or format_number(absolute, 7) + local term = magnitude .. variables[index] + if #parts == 0 then + parts[1] = coefficient < 0 and ("-" .. term) or term + else + parts[#parts + 1] = (coefficient < 0 and "-" or "+") .. term + end + end + end + if #parts == 0 then parts[1] = "0" end + return "$" .. table.concat(parts) .. "=0$" +end + +function Advanced.shifted_variable_latex(variable, value) + if near_zero(value, math.max(math.abs(value), 1), 8192) then return variable end + return "(" .. variable .. (value < 0 and "+" or "-") + .. format_number(math.abs(value), 6) .. ")" +end + +function Advanced.local_coordinate_latex(direction, origin) + local parts = {} + for _, component in ipairs({ + { coefficient = direction.x, variable = "x", shift = origin.x }, + { coefficient = direction.y, variable = "y", shift = origin.y }, + }) do + local coefficient = component.coefficient + if not near_zero(coefficient, 1, 8192) then + local absolute = math.abs(coefficient) + local magnitude = math.abs(absolute - 1) <= scaled_tolerance(1, 8192) + and "" or format_number(absolute, 6) + local term = magnitude + .. Advanced.shifted_variable_latex(component.variable, component.shift) + if #parts == 0 then + parts[1] = coefficient < 0 and ("-" .. term) or term + else + parts[#parts + 1] = (coefficient < 0 and "-" or "+") .. term + end + end + end + return #parts > 0 and table.concat(parts) or "0" +end + +function Advanced.conic_equation_strings(coefficients) + coefficients = normalize_coefficients(coefficients) + local properties = conic_properties(coefficients) + local result = { + general = Advanced.general_equation_latex(coefficients), + canonical = nil, + parameters = nil, + } + if properties.degenerate then + result.canonical = "$\\text{degenerate: }" .. tostring(properties.subtype) .. "$" + return result + end + if properties.kind == "ellipse" or properties.kind == "circle" then + local x_coordinate = Advanced.local_coordinate_latex( + properties.major_direction, properties.center + ) + local y_coordinate = Advanced.local_coordinate_latex( + properties.minor_direction, properties.center + ) + result.canonical = "$\\frac{X^{2}}{" .. format_number( + properties.major_radius * properties.major_radius, 7 + ) .. "}+\\frac{Y^{2}}{" .. format_number( + properties.minor_radius * properties.minor_radius, 7 + ) .. "}=1,\\quad X=" .. x_coordinate .. ",\\;Y=" .. y_coordinate .. "$" + result.parameters = "$a=" .. format_number(properties.major_radius, 7) + .. ",\\;b=" .. format_number(properties.minor_radius, 7) + .. ",\\;c=" .. format_number(properties.focal_radius, 7) + .. ",\\;e=" .. format_number(properties.eccentricity, 7) + .. ",\\;\\mathcal{A}=" .. format_number(properties.area, 7) .. "$" + elseif properties.kind == "hyperbola" then + local x_coordinate = Advanced.local_coordinate_latex(properties.u, properties.center) + local y_coordinate = Advanced.local_coordinate_latex(properties.v, properties.center) + result.canonical = "$\\frac{X^{2}}{" .. format_number(properties.a ^ 2, 7) + .. "}-\\frac{Y^{2}}{" .. format_number(properties.b ^ 2, 7) + .. "}=1,\\quad X=" .. x_coordinate .. ",\\;Y=" .. y_coordinate .. "$" + result.parameters = "$a=" .. format_number(properties.a, 7) + .. ",\\;b=" .. format_number(properties.b, 7) + .. ",\\;c=" .. format_number(properties.focal_radius, 7) + .. ",\\;e=" .. format_number(properties.eccentricity, 7) .. "$" + elseif properties.kind == "parabola" then + local x_coordinate = Advanced.local_coordinate_latex( + properties.axis_direction, properties.vertex + ) + local y_coordinate = Advanced.local_coordinate_latex( + properties.normal_direction, properties.vertex + ) + result.canonical = "$Y^{2}=4pX,\\quad p=" + .. format_number(properties.focal_parameter, 7) + .. ",\\quad X=" .. x_coordinate .. ",\\;Y=" .. y_coordinate .. "$" + result.parameters = "$p=" .. format_number(properties.focal_parameter, 7) + .. ",\\;e=1,\\;\\ell=" .. format_number(properties.semi_latus_rectum, 7) + .. "$" + end + return result +end + +function Advanced.point_on_conic(coefficients, input_point, name) + local point = point_from_table(input_point, name or "point") + local value, magnitude = evaluate_conic(coefficients, point) + if math.abs(value) > scaled_tolerance(math.max(magnitude, MIN_NORMAL), 262144) then + error((name or "point") .. " must lie on the conic") + end + return point +end + +function Advanced.conic_arc_definition(coefficients, first_point, second_point, raw_mode) + coefficients = normalize_coefficients(coefficients) + local properties = conic_properties(coefficients) + if properties.degenerate then error("a degenerate conic has no regular arc") end + first_point = Advanced.point_on_conic(coefficients, first_point, "first trim point") + second_point = Advanced.point_on_conic(coefficients, second_point, "second trim point") + if distance(first_point, second_point) <= scaled_tolerance( + math.max(vector_scale(first_point, second_point), MIN_NORMAL), 32768) then + error("trim points must be distinct") + end + local mode = normalized_name(raw_mode or "shorter") + if properties.kind == "ellipse" or properties.kind == "circle" then + local function angle(point) + local relative = sub(point, properties.center) + return math.atan( + dot(relative, properties.minor_direction) / properties.minor_radius, + dot(relative, properties.major_direction) / properties.major_radius + ) + end + local first_angle, second_angle = angle(first_point), angle(second_point) + local two_pi = 2 * math.pi + local counterclockwise = (second_angle - first_angle) % two_pi + local clockwise = two_pi - counterclockwise + local orientation + if mode == "counterclockwise" or mode == "ccw" then + orientation = "counterclockwise" + elseif mode == "clockwise" or mode == "cw" then + orientation = "clockwise" + elseif mode == "shorter" or mode == "short" then + orientation = counterclockwise <= clockwise and "counterclockwise" or "clockwise" + elseif mode == "longer" or mode == "long" then + orientation = counterclockwise >= clockwise and "counterclockwise" or "clockwise" + else + error("unsupported ellipse arc mode: " .. mode) + end + local start_angle, finish_angle = first_angle, second_angle + local axis2 = properties.axis2 + if orientation == "clockwise" then + start_angle, finish_angle = -first_angle, -second_angle + axis2 = scale(axis2, -1) + end + while finish_angle <= start_angle do finish_angle = finish_angle + two_pi end + return { + kind = properties.kind, + properties = properties, + first_point = first_point, + second_point = second_point, + orientation = orientation, + start_parameter = start_angle, + finish_parameter = finish_angle, + axis1 = properties.axis1, + axis2 = axis2, + } + elseif properties.kind == "parabola" then + local first_parameter = dot(sub(first_point, properties.vertex), properties.normal_direction) + local second_parameter = dot(sub(second_point, properties.vertex), properties.normal_direction) + return { + kind = "parabola", + properties = properties, + first_point = first_point, + second_point = second_point, + start_parameter = first_parameter, + finish_parameter = second_parameter, + } + end + local first_branch = dot(sub(first_point, properties.center), properties.u) < 0 and -1 or 1 + local second_branch = dot(sub(second_point, properties.center), properties.u) < 0 and -1 or 1 + if first_branch ~= second_branch then + error("hyperbola trim points must lie on the same connected branch") + end + return { + kind = "hyperbola", + properties = properties, + first_point = first_point, + second_point = second_point, + branch = first_branch, + start_parameter = stable_asinh( + dot(sub(first_point, properties.center), properties.v) / properties.b + ), + finish_parameter = stable_asinh( + dot(sub(second_point, properties.center), properties.v) / properties.b + ), + } +end + +function Advanced.parabola_interval_control(properties, start_parameter, finish_parameter) + local function point(parameter) + return add(properties.vertex, add( + scale(properties.normal_direction, parameter), + scale(properties.axis_direction, + parameter * parameter / (4 * properties.focal_parameter)) + )) + end + local start_point, finish_point = point(start_parameter), point(finish_parameter) + local derivative = add( + properties.normal_direction, + scale(properties.axis_direction, + start_parameter / (2 * properties.focal_parameter)) + ) + local control = add(start_point, + scale(derivative, (finish_parameter - start_parameter) * 0.5)) + return { start_point, control, finish_point } +end + +function Advanced.adaptive_hyperbola_interval( + properties, branch, start_parameter, finish_parameter, tolerance, maximum_segments) + local hyperbola = { + center = properties.center, + u = properties.u, + v = properties.v, + a = properties.a, + b = properties.b, + } + tolerance = positive_number_option(tolerance, 0.25, "tolerance") + maximum_segments = positive_integer_option( + maximum_segments, 256, "max_segments", 4096, 1 + ) + local cubics = {} + local function subdivide(first, second, depth) + if #cubics >= maximum_segments then error("conic arc exceeded max_segments") end + local control = hyperbola_cubic(hyperbola, branch, first, second) + local interval, error_value = second - first, 0 + for _, fraction in ipairs({ 0.25, 0.5, 0.75 }) do + local parameter = first + interval * fraction + error_value = math.max(error_value, distance( + hyperbola_point(hyperbola, branch, parameter), + cubic_point(control, fraction) + )) + end + if error_value <= tolerance then + cubics[#cubics + 1] = control + elseif depth >= 20 then + error("conic arc could not satisfy the requested tolerance") + else + local middle = first * 0.5 + second * 0.5 + subdivide(first, middle, depth + 1) + subdivide(middle, second, depth + 1) + end + end + subdivide(start_parameter, finish_parameter, 0) + return cubics +end + +M.conic_coefficients_from_points = Advanced.conic_coefficients_from_points +M.conic_coefficients_from_five_lines = Advanced.conic_coefficients_from_five_lines +M.conic_coefficients_from_constraints = Advanced.conic_coefficients_from_constraints +M.ellipse_from_center_axes = Advanced.ellipse_from_center_axes +M.parabola_from_vertex_focus = Advanced.parabola_from_vertex_focus +M.hyperbola_from_asymptotes_point = Advanced.hyperbola_from_asymptotes_point +M.degenerate_conic_from_lines = Advanced.degenerate_conic_from_lines +M.degenerate_point_conic = Advanced.degenerate_point_conic +M.conic_conic_intersections = Advanced.conic_conic_intersections +M.conic_pole = Advanced.conic_pole +M.tangents_from_point = Advanced.tangents_from_point +M.focal_chord = Advanced.focal_chord +M.conic_equation_strings = Advanced.conic_equation_strings +M.conic_arc_definition = Advanced.conic_arc_definition + +local function line_segment_from_infinite(point, direction, requested_length) + local line_length = positive_number_option(requested_length, 192, "line_length") + local direction_unit = unit(direction, "line direction") + local half = scale(direction_unit, line_length * 0.5) + return { p1 = sub(point, half), p2 = add(point, half) } +end + +M.conic_gradient = conic_gradient +M.conic_tangent_normal = conic_tangent_normal +M.conic_polar_line = conic_polar_line +M.conic_line_intersections = conic_line_intersections +M.transformed_conic_coefficients = transformed_conic_coefficients +M.line_segment_from_infinite = line_segment_from_infinite + +---------------------------------------------------------------------- +-- Ipe selection, object creation, metadata, and transactions +---------------------------------------------------------------------- + +local R = {} + +local DEFAULT_PATH_ATTRIBUTES = { + stroke = "black", + pen = "normal", + dashstyle = "normal", + linecap = "normal", + linejoin = "miter", +} +local DEFAULT_MARK_ATTRIBUTES = { + stroke = "black", + fill = "white", + symbolsize = "normal", + markshape = "mark/disk(sx)", +} +local DEFAULT_TEXT_ATTRIBUTES = { + stroke = "black", + textsize = "normal", + horizontalalignment = "left", + verticalalignment = "baseline", +} +local PATH_ATTRIBUTE_FIELDS = { + "stroke", "pen", "dashstyle", "linecap", "linejoin", "strokeopacity", "opacity", +} +local MARK_ATTRIBUTE_FIELDS = { + "stroke", "fill", "symbolsize", "markshape", "strokeopacity", "fillopacity", "opacity", +} +local TEXT_ATTRIBUTE_FIELDS = { + "stroke", "textsize", "textstyle", "labelstyle", "horizontalalignment", + "verticalalignment", "opacity", +} + +local function clone_table(source) + local result = {} + for key, value in pairs(source or {}) do result[key] = value end + return result +end + +local function filtered_attributes(source, fallback, fields, overrides) + local result = clone_table(fallback) + if type(source) == "table" then + for _, field in ipairs(fields) do + if source[field] ~= nil then result[field] = source[field] end + end + end + for key, value in pairs(overrides or {}) do result[key] = value end + return result +end + +local function construction_styles(model) + local source = model and model.attributes or nil + local path = filtered_attributes(source, DEFAULT_PATH_ATTRIBUTES, PATH_ATTRIBUTE_FIELDS) + path.fill = nil + path.farrow, path.rarrow, path.decoration = nil, nil, nil + return { + path = path, + dashed = filtered_attributes(path, DEFAULT_PATH_ATTRIBUTES, PATH_ATTRIBUTE_FIELDS, + { dashstyle = "dashed" }), + dotted = filtered_attributes(path, DEFAULT_PATH_ATTRIBUTES, PATH_ATTRIBUTE_FIELDS, + { dashstyle = "dotted" }), + mark = filtered_attributes(source, DEFAULT_MARK_ATTRIBUTES, MARK_ATTRIBUTE_FIELDS), + text = filtered_attributes(source, DEFAULT_TEXT_ATTRIBUTES, TEXT_ATTRIBUTE_FIELDS), + markshape = type(source) == "table" and source.markshape or DEFAULT_MARK_ATTRIBUTES.markshape, + } +end + +local function object_type(object) + if not object then return nil end + local ok, value = pcall(function() return object:type() end) + if ok then return value end + return type(object) == "table" and (object.type_value or object.kind or object.type) or nil +end + +local function object_matrix(object) + local ok, matrix = pcall(function() return object:matrix() end) + if ok and matrix then return matrix end + if type(object) == "table" and object.matrix_value then return object.matrix_value end + return ipe.Matrix() +end + +local function path_shape(object) + if object_type(object) ~= "path" then return nil end + local ok, shape = pcall(function() return object:shape() end) + if ok and type(shape) == "table" then return shape end + return type(object) == "table" and (object.shape_value or object.data) or nil +end + +local function object_elements(object) + if object_type(object) ~= "group" then return nil end + local ok, elements = pcall(function() return object:elements() end) + if ok and type(elements) == "table" then return elements end + return type(object) == "table" and (object.elements_value or object.elements) or nil +end + +local function object_custom_value(object) + if not object then return "" end + local ok, value = pcall(function() return object:getCustom() end) + if ok and value ~= nil then + local text = tostring(value) + return text == "undefined" and "" or text + end + if type(object) == "table" and object.custom ~= nil then return tostring(object.custom) end + return "" +end + +local function set_object_custom_value(object, value) + local ok = pcall(function() object:setCustom(value) end) + if not ok and type(object) == "table" then object.custom = value end +end + +local function append_object_custom_value(object, value) + if value == nil or value == "" then return end + local existing = object_custom_value(object) + if existing ~= "" then value = existing .. ";" .. value end + set_object_custom_value(object, value) +end + +local function reference_position(object) + if object_type(object) ~= "reference" then return nil end + local ok_symbol, symbol = pcall(function() return object:symbol() end) + if not ok_symbol or type(symbol) ~= "string" or symbol:sub(1, 5) ~= "mark/" then + return nil + end + local ok_position, position = pcall(function() return object:position() end) + if not ok_position or not position then return nil end + local ok_transformed, transformed = pcall(function() return object_matrix(object) * position end) + return ok_transformed and transformed or position +end + +local function single_segment_from_path(object) + local shape = path_shape(object) + if not shape or #shape ~= 1 then return nil end + local curve = shape[1] + if curve.type ~= "curve" or curve.closed or #curve ~= 1 then return nil end + local segment = curve[1] + if segment.type ~= "segment" then return nil end + local matrix = object_matrix(object) + return matrix * segment[1], matrix * segment[2] +end + +local function selected_objects(model) + local page, result = model:page(), {} + for index, object, selected, layer in page:objects() do + if selected and page:visible(model.vno, index) then + result[#result + 1] = { + index = index, + object = object, + selection = selected, + layer = layer, + primary = selected == 1, + } + end + end + return result +end + +local function selection_inputs(model) + local summary = { + entries = selected_objects(model), + points = {}, + segments = {}, + conic_entries = {}, + invalid = {}, + primary_point = nil, + primary_segment = nil, + primary_entry = nil, + } + for _, entry in ipairs(summary.entries) do + if entry.primary then summary.primary_entry = entry end + local point = reference_position(entry.object) + if point then + local record = { point = point, entry = entry } + summary.points[#summary.points + 1] = record + if entry.primary then summary.primary_point = record end + else + local p1, p2 = single_segment_from_path(entry.object) + if p1 and p2 then + local record = { line = line_from_points(p1, p2, "selected segment"), entry = entry } + summary.segments[#summary.segments + 1] = record + if entry.primary then summary.primary_segment = record end + elseif object_type(entry.object) == "path" or object_type(entry.object) == "group" then + summary.conic_entries[#summary.conic_entries + 1] = entry + else + summary.invalid[#summary.invalid + 1] = entry + end + end + end + return summary +end + +local function require_exact_construction_selection(summary, point_count, segment_count, message) + if #summary.invalid ~= 0 or #summary.conic_entries ~= 0 + or #summary.points ~= point_count or #summary.segments ~= segment_count + or #summary.entries ~= point_count + segment_count then + error(message) + end +end + +local function primary_first_point_values(summary) + local result = {} + if summary.primary_point then result[#result + 1] = summary.primary_point.point end + for _, record in ipairs(summary.points) do + if record ~= summary.primary_point then result[#result + 1] = record.point end + end + return result +end + +local function active_layer(model) + return model:page():active(model.vno) +end + +local function points_option(options, expected) + if options.points == nil then return nil end + return points_from_table(options.points, expected, "points") +end + +local function segment_shape(p1, p2) + return { type = "curve", closed = false; { type = "segment"; p1, p2 } } +end + +function M.require_finite_point(point, context) + if not point or not finite_number(point.x) or not finite_number(point.y) then + error((context or "geometry") .. " contains a non-finite point") + end + return point +end + +local function make_segment(p1, p2, attributes) + M.require_finite_point(p1, "segment") + M.require_finite_point(p2, "segment") + return ipe.Path(clone_table(attributes), { segment_shape(p1, p2) }, false) +end + +local function make_ellipse(ellipse, attributes) + M.require_finite_point(ellipse.center, "ellipse") + M.require_finite_point(ellipse.axis1, "ellipse") + M.require_finite_point(ellipse.axis2, "ellipse") + return ipe.Path(clone_table(attributes), { + { + type = "ellipse"; + ipe.Matrix( + ellipse.axis1.x, ellipse.axis1.y, + ellipse.axis2.x, ellipse.axis2.y, + ellipse.center.x, ellipse.center.y + ), + }, + }, false) +end + +local function make_spline(control_points, attributes) + local spline = { type = "spline" } + for index, point in ipairs(control_points) do + spline[index] = M.require_finite_point(point, "spline") + end + return ipe.Path(clone_table(attributes), { + { type = "curve", closed = false; spline }, + }, false) +end + +local function make_cubic_curve(cubics, attributes) + local curve = { type = "curve", closed = false } + for _, control in ipairs(cubics) do + local spline = { type = "spline" } + for index, point in ipairs(control) do + spline[index] = M.require_finite_point(point, "hyperbola spline") + end + curve[#curve + 1] = spline + end + return ipe.Path(clone_table(attributes), { curve }, false) +end + +function R.make_conic_arc(definition, attributes, options) + options = options or {} + if definition.kind == "ellipse" or definition.kind == "circle" then + if type(ipe.Arc) ~= "function" then error("this Ipe runtime does not support exact arcs") end + local properties = definition.properties + local matrix = ipe.Matrix( + definition.axis1.x, definition.axis1.y, + definition.axis2.x, definition.axis2.y, + properties.center.x, properties.center.y + ) + local arc = ipe.Arc(matrix, definition.start_parameter, definition.finish_parameter) + return ipe.Path(clone_table(attributes), { + { type = "curve", closed = false; + { type = "arc", arc = arc; definition.first_point, definition.second_point } }, + }, false) + elseif definition.kind == "parabola" then + return make_spline(Advanced.parabola_interval_control( + definition.properties, definition.start_parameter, definition.finish_parameter + ), attributes) + end + return make_cubic_curve(Advanced.adaptive_hyperbola_interval( + definition.properties, + definition.branch, + definition.start_parameter, + definition.finish_parameter, + options.tolerance, + options.max_segments + ), attributes) +end + +local function make_mark(point, styles) + M.require_finite_point(point, "mark") + return ipe.Reference(clone_table(styles.mark), styles.markshape, point) +end + +local function make_text(text, point, styles) + M.require_finite_point(point, "label") + return ipe.Text(clone_table(styles.text), text, point) +end + +local function add_line_object(entries, line, line_length, attributes, role) + local segment = line_segment_from_infinite(line.point, line.direction, line_length) + entries[#entries + 1] = { + object = make_segment(segment.p1, segment.p2, attributes), + role = role or "guide", + } +end + +local function matrix_values(matrix) + local values = { matrix:coeff() } + if #values == 1 and type(values[1]) == "table" then values = values[1] end + return values +end + +local function serialize_shape_value(parts, value, depth) + depth = depth or 0 + if depth > 8 then return end + if type(value) == "number" then + parts[#parts + 1] = string.format("%.17g", value) + elseif type(value) == "string" or type(value) == "boolean" then + parts[#parts + 1] = tostring(value) + elseif type(value) == "table" then + if finite_number(value.x) and finite_number(value.y) then + parts[#parts + 1] = string.format("%.17g,%.17g", value.x, value.y) + else + for index, item in ipairs(value) do serialize_shape_value(parts, item, depth + 1) end + local keys = {} + for key, _ in pairs(value) do + if type(key) ~= "number" then keys[#keys + 1] = key end + end + table.sort(keys) + for _, key in ipairs(keys) do + parts[#parts + 1] = tostring(key) + serialize_shape_value(parts, value[key], depth + 1) + end + end + else + local ok, values = pcall(function() return matrix_values(value) end) + if ok and type(values) == "table" and #values >= 6 then + for index = 1, 6 do parts[#parts + 1] = string.format("%.17g", values[index]) end + else + local ok_arc, alpha, beta = pcall(function() return value:angles() end) + local ok_matrix, arc_matrix = pcall(function() return value:matrix() end) + if ok_arc and ok_matrix and arc_matrix then + parts[#parts + 1] = string.format("%.17g", alpha) + parts[#parts + 1] = string.format("%.17g", beta) + local arc_values = matrix_values(arc_matrix) + for index = 1, math.min(6, #arc_values) do + parts[#parts + 1] = string.format("%.17g", arc_values[index]) + end + end + end + end +end + +local function fnv1a(text) + local hash = 2166136261 + for index = 1, #text do + hash = ((hash ~ text:byte(index)) * 16777619) & 0xffffffff + end + return string.format("%08x", hash) +end + +local function shape_fingerprint(object) + local shape = path_shape(object) + local parts = {} + if shape then + serialize_shape_value(parts, shape, 0) + elseif object_type(object) == "reference" then + local ok_position, position = pcall(function() return object:position() end) + local ok_symbol, symbol = pcall(function() return object:symbol() end) + if not ok_position or not position or not ok_symbol then return nil end + parts[#parts + 1] = tostring(symbol) + serialize_shape_value(parts, position, 0) + serialize_shape_value(parts, object_matrix(object), 0) + else + return nil + end + return fnv1a(table.concat(parts, "|")) +end + +local function coefficients_text(coefficients) + coefficients = normalize_coefficients(coefficients) + local values = {} + for index = 1, 6 do values[index] = string.format("%.17g", coefficients[index]) end + return table.concat(values, ",") +end + +local CONIC_ID_COUNTER = 0 +local function next_conic_id() + CONIC_ID_COUNTER = CONIC_ID_COUNTER + 1 + return string.format("c%08x", CONIC_ID_COUNTER) +end + +local function metadata_string(fields) + local parts = { "conics:v1" } + local order = { + "role", "id", "kind", "source", "coordinate_space", "coefficients", + "fingerprint", "trusted", "count", + } + for _, key in ipairs(order) do + if fields[key] ~= nil then parts[#parts + 1] = key .. "=" .. tostring(fields[key]) end + end + return table.concat(parts, ";") +end + +local function curve_metadata(object, id, kind, coefficients, source, role) + local coordinate_space = "object" + if coefficients == nil then + coefficients = { 1, 0, 1, 0, 0, -1 } + coordinate_space = "ellipse_shape" + end + return metadata_string({ + role = role or "curve", + id = id, + kind = kind, + source = source, + coordinate_space = coordinate_space, + coefficients = coefficients_text(coefficients), + fingerprint = shape_fingerprint(object), + trusted = "true", + }) +end + +local function auxiliary_metadata(id, role, kind, source) + return metadata_string({ + role = role, + id = id, + kind = kind, + source = source, + trusted = "true", + }) +end + +local function split_metadata(custom) + local tokens = {} + for token in tostring(custom or ""):gmatch("[^;]+") do tokens[#tokens + 1] = token end + return tokens +end + +local function metadata_fields(tokens, namespace_index) + local fields = {} + for index = namespace_index + 1, #tokens do + if tokens[index]:find(":", 1, true) and not tokens[index]:find("=", 1, true) then break end + local key, value = tokens[index]:match("^([^=]+)=(.*)$") + if key then fields[key] = value end + end + return fields +end + +local function strict_metadata_fields(tokens, namespace_index) + local allowed = { + role = true, id = true, kind = true, source = true, + coordinate_space = true, coefficients = true, fingerprint = true, + trusted = true, count = true, + } + local fields = {} + for index = namespace_index + 1, #tokens do + local token = tokens[index] + if token:find(":", 1, true) and not token:find("=", 1, true) then break end + local key, value = token:match("^([^=]+)=(.*)$") + if not key then error("Conics metadata contains a malformed field") end + if not allowed[key] then error("Conics metadata contains an unknown field: " .. key) end + if fields[key] ~= nil then error("Conics metadata repeats the field: " .. key) end + fields[key] = value + end + return fields +end + +local function parse_coefficients_text(encoded) + if type(encoded) ~= "string" then error("Conics metadata is missing coefficients") end + local coefficients = {} + for value in encoded:gmatch("[^,]+") do coefficients[#coefficients + 1] = value end + if #coefficients ~= 6 then error("Conics metadata must contain exactly six coefficients") end + return normalize_coefficients(coefficients) +end + +local function parse_conic_metadata(object) + local custom, tokens = object_custom_value(object), nil + tokens = split_metadata(custom) + local roles = { + curve = true, branch = true, asymptote = true, guide = true, + mark = true, label = true, group = true, axis = true, + directrix = true, tangent = true, normal = true, polar = true, + intersection = true, center = true, vertex = true, focus = true, + pole = true, chord = true, latus_rectum = true, + auxiliary_circle = true, director_circle = true, equation = true, + degenerate = true, + } + local namespace_count = 0 + for _, token in ipairs(tokens) do + if token:match("^conics:") then + if token ~= "conics:v1" then + error("Unsupported Conics metadata version: " .. token) + end + namespace_count = namespace_count + 1 + end + end + if namespace_count > 1 then error("Conics metadata namespace is repeated") end + for index, token in ipairs(tokens) do + if token == "conics:v1" then + local fields = strict_metadata_fields(tokens, index) + if not fields.role or fields.role == "" then + error("Conics metadata is missing its object role") + end + if not roles[fields.role] then + error("Conics metadata has an unsupported object role: " .. fields.role) + end + if fields.role ~= "group" and (not fields.id or fields.id == "") then + error("Conics metadata is missing its conic identifier") + end + if not fields.kind or fields.kind == "" or not fields.source or fields.source == "" then + error("Conics metadata is missing kind or construction source") + end + if fields.trusted ~= "true" then + error("Conics metadata is not marked as trusted") + end + if fields.role == "group" then + local count = tonumber(fields.count) + if not finite_number(count) or count < 1 or count ~= math.floor(count) then + error("Conics group metadata must contain a positive integer count") + end + if fields.coefficients or fields.coordinate_space or fields.fingerprint then + error("Conics group metadata contains curve-only fields") + end + return nil, { status = "auxiliary", role = fields.role, fields = fields } + end + if fields.role ~= "curve" and fields.role ~= "branch" + and fields.role ~= "degenerate" then + if fields.coefficients or fields.coordinate_space or fields.fingerprint then + error("Conics auxiliary metadata contains curve-only fields") + end + return nil, { status = "auxiliary", role = fields.role, fields = fields } + end + if fields.kind ~= "circle" and fields.kind ~= "ellipse" + and fields.kind ~= "parabola" and fields.kind ~= "hyperbola" + and fields.kind ~= "degenerate" then + error("Conics metadata has an unsupported conic kind: " .. fields.kind) + end + if fields.coordinate_space ~= "object" + and fields.coordinate_space ~= "ellipse_shape" then + error("Conics metadata has an unsupported coordinate space") + end + if not fields.fingerprint or fields.fingerprint == "" then + error("Conics metadata is missing its geometry fingerprint") + end + local expected_fingerprint = fields.fingerprint + local current_fingerprint = shape_fingerprint(object) + if expected_fingerprint and current_fingerprint + and expected_fingerprint ~= current_fingerprint then + error("Conics metadata is stale because the path geometry was edited") + end + return parse_coefficients_text(fields.coefficients), { + status = "current", + fields = fields, + id = fields.id, + kind = fields.kind, + } + end + end + local legacy = false + for _, token in ipairs(tokens) do + if token == "geometry:conic" or token == "geometry:hyperbola" then legacy = true end + end + if legacy then + local encoded + for _, token in ipairs(tokens) do + encoded = encoded or token:match("^coefficients=(.*)$") + end + if not encoded then error("Legacy conic metadata is present but has no coefficients") end + return parse_coefficients_text(encoded), { + status = "legacy", + id = "legacy:" .. fnv1a(custom), + kind = "legacy", + } + end + return nil, { status = "absent" } +end + +local function ellipse_coefficients_from_object(object, parent_matrix) + local shape = path_shape(object) + if not shape or #shape ~= 1 or shape[1].type ~= "ellipse" or not shape[1][1] then return nil end + local matrix = (parent_matrix or ipe.Matrix()) * object_matrix(object) * shape[1][1] + local a, c, b, d, tx, ty = unpack(matrix_values(matrix)) + return ellipse_coefficients({ + center = V(tx, ty), + axis1 = V(a, c), + axis2 = V(b, d), + }) +end + +local function collect_conic_definitions(object, parent_matrix, definitions, errors) + parent_matrix = parent_matrix or ipe.Matrix() + definitions, errors = definitions or {}, errors or {} + local matrix = parent_matrix * object_matrix(object) + local ok_metadata, coefficients, information = pcall(parse_conic_metadata, object) + if not ok_metadata then + errors[#errors + 1] = clean_error_message(coefficients) + elseif coefficients then + local ok_transform, transformed + if information.fields and information.fields.coordinate_space == "ellipse_shape" then + ok_transform, transformed = pcall(ellipse_coefficients_from_object, object, parent_matrix) + else + ok_transform, transformed = pcall( + transformed_conic_coefficients, coefficients, matrix + ) + end + if ok_transform then + definitions[#definitions + 1] = { + id = information.id, + kind = information.kind, + coefficients = transformed, + metadata_status = information.status, + object = object, + } + else + errors[#errors + 1] = clean_error_message(transformed) + end + elseif object_type(object) == "path" and information.status == "absent" then + local ok_ellipse, ellipse_result = pcall( + ellipse_coefficients_from_object, object, parent_matrix + ) + if ok_ellipse and ellipse_result then + definitions[#definitions + 1] = { + id = "native:" .. tostring(object), + kind = classify_conic(ellipse_result).kind, + coefficients = ellipse_result, + metadata_status = "native", + object = object, + } + end + end + if object_type(object) == "group" then + for _, child in ipairs(object_elements(object) or {}) do + collect_conic_definitions(child, matrix, definitions, errors) + end + end + return definitions, errors +end + +local function primary_conic_definition(model) + local page, primary = model:page(), model:page():primarySelection() + if not primary then error("Select a conic as the primary object.") end + local object = page[primary] + if not object then error("Primary conic object is unavailable.") end + local definitions, errors = collect_conic_definitions(object) + if #errors > 0 then error(errors[1]) end + local unique = {} + for _, definition in ipairs(definitions) do + local existing = unique[definition.id] + if not existing then + unique[definition.id] = definition + else + for index = 1, 6 do + if math.abs(existing.coefficients[index] - definition.coefficients[index]) + > scaled_tolerance(1, 16384) then + error("Selected group contains inconsistent conic branches.") + end + end + end + end + local selected + for _, definition in pairs(unique) do + if selected then error("Selected group contains more than one distinct conic.") end + selected = definition + end + if not selected then + error("Primary selection is not a conic curve; auxiliary lines are not accepted.") + end + return selected +end + +local function warn_for_invisible_layer(model, layer) + if model._conics_preview then return end + local ok, visible = pcall(function() return model:page():visible(model.vno, layer) end) + if ok and visible == false then + model:warning( + "Active layer is invisible", + "You have just created an object in layer '" .. tostring(layer) .. "'.\n\n" + .. "This layer is currently not visible, so the new object is hidden." + ) + end +end + +local function register_creation(model, label_value, entries, layer, group_output, group_metadata) + if #entries == 0 then error("construction produced no visible elements") end + for _, entry in ipairs(entries) do + if entry.metadata then append_object_custom_value(entry.object, entry.metadata) end + end + local objects = {} + if group_output and #entries > 1 and type(ipe.Group) == "function" then + local children = {} + for _, entry in ipairs(entries) do children[#children + 1] = entry.object end + local group = ipe.Group(children) + if group_metadata then append_object_custom_value(group, group_metadata) end + objects[1] = group + else + for _, entry in ipairs(entries) do objects[#objects + 1] = entry.object end + end + warn_for_invisible_layer(model, layer) + local transaction = { + label = label_value, + pno = model.pno, + vno = model.vno, + object = objects[1], + objects = objects, + layer = layer, + } + transaction.undo = function(record, document) + local page = document[record.pno] + for _ = 1, #record.objects do page:remove(#page) end + end + transaction.redo = function(record, document) + local page = document[record.pno] + page:deselectAll() + for index, object in ipairs(record.objects) do + page:insert(nil, object, index == 1 and 1 or 2, record.layer) + end + end + model:register(transaction) + return objects +end + +function R.register_replacement(model, label_value, index, entries, group_metadata) + if #entries == 0 then error("replacement produced no visible conic") end + for _, entry in ipairs(entries) do + if entry.metadata then append_object_custom_value(entry.object, entry.metadata) end + end + local replacement + if #entries == 1 then + replacement = entries[1].object + elseif type(ipe.Group) == "function" then + local children = {} + for _, entry in ipairs(entries) do children[#children + 1] = entry.object end + replacement = ipe.Group(children) + if group_metadata then append_object_custom_value(replacement, group_metadata) end + else + error("multiple replacement branches require group support") + end + local original = model:page()[index] + if not original then error("replacement target is unavailable") end + local transaction = { + label = label_value, + pno = model.pno, + vno = model.vno, + index = index, + original = original, + replacement = replacement, + object = replacement, + objects = { replacement }, + } + transaction.undo = function(record, document) + local page = document[record.pno] + page:replace(record.index, record.original) + pcall(function() page:setSelect(record.index, 1) end) + end + transaction.redo = function(record, document) + local page = document[record.pno] + page:replace(record.index, record.replacement) + pcall(function() page:setSelect(record.index, 1) end) + end + model:register(transaction) + return { replacement } +end + +local function warn_and_return(model, title, message) + message = clean_error_message(message) + if model and type(model.warning) == "function" then model:warning(title, message) end + return { + created = false, + status = "error", + operation = nil, + element_count = 0, + object_count = model and model.page and #model:page() or 0, + metadata = nil, + result = nil, + error = message, + } +end + +local function creator_call(model, title, callback) + local ok, result = pcall(callback) + if not ok then return warn_and_return(model, title, result) end + return result +end + +local function success_result(model, operation, entries, objects, metadata, result) + return { + created = true, + status = "created", + operation = operation, + element_count = #entries, + object_count = #model:page(), + created_object_count = #objects, + metadata = metadata, + result = result or {}, + } +end + +R.clone_table = clone_table +R.construction_styles = construction_styles +R.object_type = object_type +R.object_matrix = object_matrix +R.path_shape = path_shape +R.object_elements = object_elements +R.object_custom_value = object_custom_value +R.set_object_custom_value = set_object_custom_value +R.append_object_custom_value = append_object_custom_value +R.reference_position = reference_position +R.single_segment_from_path = single_segment_from_path +R.selected_objects = selected_objects +R.selection_inputs = selection_inputs +R.primary_first_point_values = primary_first_point_values +R.active_layer = active_layer +R.points_option = points_option +R.make_segment = make_segment +R.make_ellipse = make_ellipse +R.make_spline = make_spline +R.make_cubic_curve = make_cubic_curve +R.make_mark = make_mark +R.make_text = make_text +R.add_line_object = add_line_object +R.shape_fingerprint = shape_fingerprint +R.curve_metadata = curve_metadata +R.auxiliary_metadata = auxiliary_metadata +R.parse_conic_metadata = parse_conic_metadata +R.collect_conic_definitions = collect_conic_definitions +R.primary_conic_definition = primary_conic_definition +R.register_creation = register_creation +R.warn_and_return = warn_and_return +R.creator_call = creator_call +R.success_result = success_result + +---------------------------------------------------------------------- +-- Conic rendering and construction workflows +---------------------------------------------------------------------- + +local CREATE_CONIC_ALLOWED = { + operation = true, construction = true, definition = true, points = true, + lines = true, constraints = true, + focus = true, directrix = true, line = true, point_on_conic = true, point = true, + eccentricity = true, expected_kind = true, maximum_points = true, + allow_degenerate = true, degenerate = true, subtype = true, line_length = true, + steiner = true, mode = true, padding = true, extent = true, tolerance = true, + samples = true, max_segments = true, branch = true, group_output = true, + group = true, bounds = true, +} +local HYPERBOLA_ALLOWED = { + operation = true, construction = true, definition = true, + focus_a = true, focus_b = true, point = true, center = true, + asymptote_a = true, asymptote_b = true, lines = true, + axis = true, line = true, a = true, b = true, radius = true, + branch = true, t_max = true, extent = true, tolerance = true, + samples = true, max_segments = true, asymptotes = true, + asymptote_length = true, group_output = true, group = true, +} +local ELLIPSE_ALLOWED = { + operation = true, construction = true, definition = true, + focus_a = true, focus_b = true, point = true, center = true, + first_endpoint = true, second_endpoint = true, points = true, +} +local PARABOLA_ALLOWED = { + operation = true, construction = true, + definition = true, directrix = true, line = true, focus = true, foci = true, + vertex = true, + extent = true, padding = true, group_output = true, group = true, +} +local DEFINITION_ALLOWED = { + conic = { + lines = true, constraints = true, + points = true, focus = true, directrix = true, line = true, + point_on_conic = true, point = true, eccentricity = true, + expected_kind = true, maximum_points = true, allow_degenerate = true, + degenerate = true, subtype = true, + }, + ellipse = { + focus_a = true, focus_b = true, point = true, center = true, + first_endpoint = true, second_endpoint = true, points = true, + }, + hyperbola = { + focus_a = true, focus_b = true, point = true, center = true, + asymptote_a = true, asymptote_b = true, lines = true, + axis = true, line = true, a = true, b = true, radius = true, + }, + parabola = { + directrix = true, line = true, focus = true, foci = true, + vertex = true, + }, +} + +function M.reject_nested_aliases(options, keys, context) + if options.definition == nil then return end + for _, key in ipairs(keys) do + if options[key] ~= nil then + error((context or "options") .. " cannot mix nested 'definition' with legacy field '" + .. key .. "'") + end + end +end + +local function definition_options(options, allowed, context) + if options.definition == nil then return options end + if type(options.definition) ~= "table" then error("definition must be a table") end + if allowed then + validate_keys(options.definition, allowed, context or "definition") + end + return options.definition +end + +local function serializable_line(line) + local direction = unit(line.direction or sub(line.p2, line.p1), "line direction") + return { + point = point_record(line.point or line.p1), + direction = point_record(direction), + } +end + +local function serializable_properties(properties) + local result = { + kind = properties.kind, + classification = properties.classification, + degenerate = properties.degenerate == true, + subtype = properties.subtype, + coefficients = properties.coefficients, + eccentricity = properties.eccentricity, + focal_radius = properties.focal_radius, + semi_latus_rectum = properties.semi_latus_rectum, + area = properties.area, + vertices = point_records(properties.vertices), + foci = point_records(properties.foci), + points = point_records(properties.points), + directrices = {}, + asymptotes = {}, + lines = {}, + latus_recta = {}, + auxiliary_circles = {}, + } + if properties.center then result.center = point_record(properties.center) end + if properties.vertex then result.vertex = point_record(properties.vertex) end + if properties.major_radius then + result.major_radius, result.minor_radius = properties.major_radius, properties.minor_radius + end + if properties.a then result.a, result.b = properties.a, properties.b end + if properties.t_max then result.t_max = properties.t_max end + if properties.render_extent then result.render_extent = properties.render_extent end + if properties.axis_direction then + result.axis_direction = point_record(properties.axis_direction) + result.normal_direction = point_record(properties.normal_direction) + result.focal_parameter = properties.focal_parameter + end + if properties.major_direction then + result.major_direction = point_record(properties.major_direction) + result.minor_direction = point_record(properties.minor_direction) + end + if properties.u then + result.transverse_direction = point_record(properties.u) + result.conjugate_direction = point_record(properties.v) + end + for index, line in ipairs(properties.directrices or {}) do + result.directrices[index] = serializable_line(line) + end + for index, line in ipairs(properties.asymptotes or {}) do + result.asymptotes[index] = serializable_line(line) + end + for index, line in ipairs(properties.lines or {}) do + result.lines[index] = serializable_line(line) + end + for index, record in ipairs(properties.latus_recta or {}) do + result.latus_recta[index] = { + line = serializable_line(record.line), + endpoints = point_records(record.endpoints), + } + end + for index, circle in ipairs(properties.auxiliary_circles or {}) do + result.auxiliary_circles[index] = { + center = point_record(circle.center), + radius = circle.radius, + } + end + if properties.director_circle then + result.director_circle = { + center = point_record(properties.director_circle.center), + radius = properties.director_circle.radius, + } + end + result.equations = Advanced.conic_equation_strings(properties.coefficients) + return result +end + +function M.serializable_ellipse_geometry(ellipse) + local radius1, radius2 = length(ellipse.axis1), length(ellipse.axis2) + if not finite_number(radius1) or not finite_number(radius2) + or radius1 <= 0 or radius2 <= 0 then + error("ellipse axes must be finite and nonzero") + end + local major_radius, minor_radius, major_axis + if radius1 >= radius2 then + major_radius, minor_radius, major_axis = radius1, radius2, ellipse.axis1 + else + major_radius, minor_radius, major_axis = radius2, radius1, ellipse.axis2 + end + local major_direction = unit(major_axis, "ellipse major axis") + local ratio = minor_radius / major_radius + local focal_radius = major_radius * math.sqrt(math.max(0, 1 - ratio * ratio)) + local center = point_from_table(ellipse.center, "ellipse.center") + return { + kind = math.abs(major_radius - minor_radius) + <= scaled_tolerance(major_radius, 4096) and "circle" or "ellipse", + classification = math.abs(major_radius - minor_radius) + <= scaled_tolerance(major_radius, 4096) and "circle" or "ellipse", + center = point_record(center), + major_radius = major_radius, + minor_radius = minor_radius, + eccentricity = focal_radius / major_radius, + vertices = { + point_record(add(center, scale(major_direction, major_radius))), + point_record(sub(center, scale(major_direction, major_radius))), + }, + foci = { + point_record(add(center, scale(major_direction, focal_radius))), + point_record(sub(center, scale(major_direction, focal_radius))), + }, + directrices = {}, + asymptotes = {}, + } +end + +local function conic_curve_extent(properties, options, source_points) + local requested = options.extent + if requested ~= nil and options.bounds ~= nil then + error("conic options cannot contain both 'extent' and 'bounds'") + end + if requested ~= nil then return positive_number_option(requested, nil, "extent") end + local bounds = options.bounds and M.bounds_from_table(options.bounds) or nil + local extent = bounds and 0 or 96 + local center = properties.vertex or properties.center or V(0, 0) + local direction = properties.normal_direction or properties.u or V(1, 0) + for _, point in ipairs(bounds and bounds.corners or {}) do + local projection = math.abs(dot(sub(point, center), direction)) + if not finite_number(projection) then error("bounds projection exceeds numeric range") end + extent = math.max(extent, projection) + end + for _, point in ipairs(source_points or {}) do + local projection = math.abs(dot(sub(point, center), direction)) + if not finite_number(projection) then error("source-point projection exceeds numeric range") end + extent = math.max(extent, projection) + end + local padding = nonnegative_number_option(options.padding, bounds and 0 or 24, "padding") + return extent + padding +end + +function M.hyperbola_parameter_from_extent(hyperbola, extent) + extent = positive_number_option(extent, nil, "extent") + if extent <= hyperbola.a then error("hyperbola extent must be greater than a") end + return M.stable_acosh(extent / hyperbola.a) +end + +function M.automatic_hyperbola_parameter( + hyperbola, coefficients, options, source_points) + if options.extent ~= nil and options.bounds ~= nil then + error("conic options cannot contain both 'extent' and 'bounds'") + end + if options.extent ~= nil then + return M.hyperbola_parameter_from_extent(hyperbola, options.extent) + end + local maximum_conjugate = 0 + if options.bounds ~= nil then + for _, point in ipairs(M.bounds_from_table(options.bounds).corners) do + local projection = math.abs(dot(sub(point, hyperbola.center), hyperbola.v)) + if not finite_number(projection) then error("bounds projection exceeds numeric range") end + maximum_conjugate = math.max(maximum_conjugate, projection) + end + end + for _, point in ipairs(source_points or {}) do + local value, magnitude = evaluate_conic(coefficients, point) + if math.abs(value) <= scaled_tolerance(math.max(magnitude, MIN_NORMAL), 32768) then + local projection = math.abs(dot(sub(point, hyperbola.center), hyperbola.v)) + if not finite_number(projection) then + error("source-point projection exceeds numeric range") + end + maximum_conjugate = math.max(maximum_conjugate, projection) + end + end + local default_padding = options.bounds ~= nil and 0 or 24 + local padding = nonnegative_number_option(options.padding, default_padding, "padding") + local parameter = stable_asinh((maximum_conjugate + padding) / hyperbola.b) + if parameter > 0 then + parameter = parameter + scaled_tolerance(parameter, 64) + end + return math.max(2.2, parameter) +end + +local function render_conic_entries(coefficients, options, styles, source, source_points) + local properties = conic_properties(coefficients) + local entries, id = {}, next_conic_id() + if properties.kind == "ellipse" or properties.kind == "circle" then + local ellipse = { + center = properties.center, + axis1 = properties.axis1, + axis2 = properties.axis2, + } + local object = make_ellipse(ellipse, styles.path) + entries[1] = { + object = object, + role = "curve", + metadata = curve_metadata(object, id, properties.kind, coefficients, source, "curve"), + } + elseif properties.kind == "parabola" then + local extent = conic_curve_extent(properties, options, source_points) + local object = make_spline(parabola_spline(properties, extent, extent), styles.path) + entries[1] = { + object = object, + role = "curve", + metadata = curve_metadata(object, id, "parabola", coefficients, source, "curve"), + } + properties.render_extent = extent + elseif properties.kind == "hyperbola" then + local legacy_segment_budget + if options.samples ~= nil then + legacy_segment_budget = positive_integer_option( + options.samples, 80, "samples", 4096, 4 + ) + end + local branch = normalized_name(options.branch or "both") + if branch ~= "both" and branch ~= "right" and branch ~= "positive" + and branch ~= "left" and branch ~= "negative" then + error("unsupported hyperbola branch: " .. branch) + end + local hyperbola = { + center = properties.center, + u = properties.u, + v = properties.v, + a = properties.a, + b = properties.b, + } + local t_max = M.automatic_hyperbola_parameter( + hyperbola, coefficients, options, source_points + ) + local tolerance = positive_number_option(options.tolerance, 0.25, "tolerance") + local maximum_segments = positive_integer_option( + options.max_segments or legacy_segment_budget, 256, "max_segments", 4096, 4 + ) + local rendered_t_max + local function append_branch(branch_value) + local cubics, used_t_max = adaptive_hyperbola_cubics( + hyperbola, branch_value, t_max, tolerance, maximum_segments + ) + rendered_t_max = used_t_max + local object = make_cubic_curve(cubics, styles.path) + entries[#entries + 1] = { + object = object, + role = "branch", + metadata = curve_metadata(object, id, "hyperbola", coefficients, source, "branch"), + } + end + if branch == "both" or branch == "right" or branch == "positive" then append_branch(1) end + if branch == "both" or branch == "left" or branch == "negative" then append_branch(-1) end + properties.t_max = rendered_t_max + elseif properties.kind == "degenerate" then + local allow_degenerate = options.allow_degenerate + if allow_degenerate == nil and type(options.definition) == "table" then + allow_degenerate = options.definition.allow_degenerate + end + if not bool_value(allow_degenerate, false) then + error("degenerate conics require allow_degenerate=true") + end + local line_length = positive_number_option(options.line_length, 192, "line_length") + for _, line in ipairs(properties.lines or {}) do + add_line_object(entries, line, line_length, styles.path, "degenerate") + entries[#entries].metadata = curve_metadata( + entries[#entries].object, id, "degenerate", coefficients, source, "degenerate" + ) + end + for _, point in ipairs(properties.points or {}) do + local object = make_mark(point, styles) + entries[#entries + 1] = { + object = object, + role = "degenerate", + metadata = curve_metadata( + object, id, "degenerate", coefficients, source, "degenerate" + ), + } + end + else + error("unsupported conic classification: " .. tostring(properties.kind)) + end + return entries, properties, id +end + +local function explicit_or_selected_points(model, options, expected, message) + local explicit = points_option(options, expected) + if explicit then return explicit end + local summary = selection_inputs(model) + require_exact_construction_selection(summary, expected, 0, message) + local values = {} + for index, record in ipairs(summary.points) do values[index] = record.point end + return values +end + +function R.explicit_or_selected_flexible_points(model, options, minimum, maximum, message) + if options.points ~= nil then + return Advanced.flexible_points(options.points, minimum, maximum, "points") + end + local summary = selection_inputs(model) + if #summary.invalid ~= 0 or #summary.segments ~= 0 or #summary.conic_entries ~= 0 + or #summary.points < minimum or #summary.points > maximum + or #summary.entries ~= #summary.points then + error(message) + end + local values = {} + for index, record in ipairs(summary.points) do values[index] = record.point end + return values +end + +function R.explicit_or_selected_lines(model, options, expected, message) + if options.lines ~= nil then + if type(options.lines) ~= "table" or #options.lines ~= expected then error(message) end + local lines = {} + for index, line in ipairs(options.lines) do + lines[index] = line_from_table(line, "lines[" .. tostring(index) .. "]") + end + return lines + end + local summary = selection_inputs(model) + require_exact_construction_selection(summary, 0, expected, message) + local lines = {} + for index, record in ipairs(summary.segments) do lines[index] = record.line end + return lines +end + +function R.selected_mixed_constraints(model, message) + local summary = selection_inputs(model) + local segment_count = #summary.segments + if #summary.invalid ~= 0 or #summary.conic_entries ~= 0 + or segment_count > 2 or #summary.points + segment_count ~= 5 + or #summary.entries ~= #summary.points + segment_count then + error(message) + end + local constraints, used = {}, {} + for index, record in ipairs(summary.points) do + constraints[index] = { type = "point", point = record.point } + end + for segment_index, segment in ipairs(summary.segments) do + local matches = {} + for point_index, record in ipairs(summary.points) do + local point = record.point + local signed_distance = math.abs( + segment.line.a * point.x + segment.line.b * point.y + segment.line.c + ) + local scale_value = math.max(1, vector_scale(point, segment.line.p1, segment.line.p2)) + if signed_distance <= 1e-7 * scale_value then matches[#matches + 1] = point_index end + end + if #matches ~= 1 then + error("Each tangent segment must pass through exactly one selected mark.") + end + local point_index = matches[1] + if used[point_index] then + error("Two tangent segments cannot reuse the same tangent-point mark.") + end + used[point_index] = true + constraints[point_index] = { + type = "tangent", + point = summary.points[point_index].point, + line = segment.line, + } + end + return constraints +end + +local function create_conic(model, raw_options) + return creator_call(model, "Cannot create conic", function() + local options = options_table(raw_options) + validate_keys(options, CREATE_CONIC_ALLOWED, "conic options") + M.reject_nested_aliases(options, + { "points", "lines", "constraints", "focus", "directrix", "line", + "point_on_conic", "point", "eccentricity", "expected_kind", + "maximum_points", "allow_degenerate", "degenerate", "subtype" }, + "conic options") + local definition = definition_options( + options, DEFINITION_ALLOWED.conic, "conic definition" + ) + local operation = normalized_name( + M.aliased_value(options, "operation", "construction", "conic options") or "steiner" + ) + local styles, entries, result = construction_styles(model), {}, {} + local source, group_default = operation, false + + if operation == "steiner" or operation == "steiner_ellipses" then + local points = explicit_or_selected_points( + model, definition, 3, "Select exactly three marks, or pass exactly three points." + ) + local ellipses = steiner_ellipses(points[1], points[2], points[3]) + local mode = normalized_name( + M.aliased_value(options, "steiner", "mode", "conic options") or "both" + ) + if mode == "circum" then mode = "circumellipse" end + if mode == "in" then mode = "inellipse" end + if mode ~= "both" and mode ~= "circumellipse" and mode ~= "inellipse" then + error("unsupported Steiner mode: " .. mode) + end + local records = {} + local function append_ellipse(ellipse, attributes, construction_kind) + local ok_coefficients, coefficients = pcall(ellipse_coefficients, ellipse) + if not ok_coefficients then coefficients = nil end + local properties = coefficients + and serializable_properties(conic_properties(coefficients)) + or M.serializable_ellipse_geometry(ellipse) + local object, id = make_ellipse(ellipse, attributes), next_conic_id() + entries[#entries + 1] = { + object = object, + role = "curve", + metadata = curve_metadata( + object, id, properties.kind, coefficients, construction_kind, "curve" + ), + } + records[#records + 1] = { + kind = properties.kind, + source = construction_kind, + coefficients = coefficients, + coefficients_available = coefficients ~= nil, + properties = properties, + } + end + if mode == "both" or mode == "circumellipse" then + append_ellipse(ellipses.circumellipse, styles.path, "steiner_circumellipse") + end + if mode == "both" or mode == "inellipse" then + append_ellipse(ellipses.inellipse, styles.dashed, "steiner_inellipse") + end + result = { + type = "steiner", + center = point_record(ellipses.circumellipse.center), + conics = records, + } + group_default = #entries > 1 + elseif operation == "five_points" then + local points = explicit_or_selected_points( + model, definition, 5, "Select exactly five marks, or pass exactly five points." + ) + local coefficients = Advanced.conic_coefficients_from_points(points, { + allow_degenerate = bool_value(definition.allow_degenerate, false), + }) + entries, result.properties, result.id = render_conic_entries( + coefficients, options, styles, "five_points", points + ) + result.type = "conic" + result.coefficients = coefficients + result.properties = serializable_properties(result.properties) + elseif operation == "fit_points" or operation == "best_fit" then + local maximum_points = positive_integer_option( + definition.maximum_points, 512, "maximum_points", 4096, 6 + ) + local points = R.explicit_or_selected_flexible_points( + model, definition, 6, maximum_points, + "Select between six and the configured maximum number of marks." + ) + local coefficients, diagnostics = Advanced.conic_coefficients_from_points(points, { + allow_degenerate = bool_value(definition.allow_degenerate, false), + expected_kind = definition.expected_kind, + maximum_points = maximum_points, + }) + entries, result.properties, result.id = render_conic_entries( + coefficients, options, styles, "fit_points", points + ) + result.type = "best-fit-conic" + result.coefficients = coefficients + result.fit = diagnostics + result.properties = serializable_properties(result.properties) + elseif operation == "five_tangents" or operation == "tangent_to_five_lines" then + local lines = R.explicit_or_selected_lines( + model, definition, 5, + "Select exactly five segments, or pass exactly five tangent lines." + ) + local coefficients = Advanced.conic_coefficients_from_five_lines(lines, { + allow_degenerate = bool_value(definition.allow_degenerate, false), + }) + entries, result.properties, result.id = render_conic_entries( + coefficients, options, styles, "five_tangents", nil + ) + result.type = "five-tangent-conic" + result.coefficients = coefficients + result.tangent_lines = {} + for index, line in ipairs(lines) do result.tangent_lines[index] = serializable_line(line) end + result.properties = serializable_properties(result.properties) + elseif operation == "five_conditions" or operation == "mixed_conditions" then + local constraints + if definition.constraints ~= nil then + if type(definition.constraints) ~= "table" then + error("constraints must be an array") + end + constraints = definition.constraints + else + constraints = R.selected_mixed_constraints( + model, + "Select five marks, four marks plus one tangent segment, or three marks plus two tangent segments." + ) + end + local coefficients = Advanced.conic_coefficients_from_constraints(constraints, { + allow_degenerate = bool_value(definition.allow_degenerate, false), + }) + local source_points = {} + for _, constraint in ipairs(constraints) do + source_points[#source_points + 1] = point_from_table( + constraint.point or constraint, "constraint point" + ) + end + entries, result.properties, result.id = render_conic_entries( + coefficients, options, styles, "five_conditions", source_points + ) + result.type = "mixed-condition-conic" + result.constraint_count = #constraints + result.coefficients = coefficients + result.properties = serializable_properties(result.properties) + elseif operation == "focus_directrix_point" or operation == "directrix_focus_point" then + local focus_value = definition.focus + local directrix_value = M.aliased_value( + definition, "directrix", "line", "conic definition" + ) + local point_value = M.aliased_value( + definition, "point_on_conic", "point", "conic definition" + ) + local focus, directrix, point + local has_explicit = focus_value ~= nil or directrix_value ~= nil or point_value ~= nil + if has_explicit then + if focus_value == nil or directrix_value == nil or point_value == nil then + error("focus, directrix, and point_on_conic are all required") + end + focus = point_from_table(focus_value, "focus") + directrix = line_from_table(directrix_value, "directrix") + point = point_from_table(point_value, "point_on_conic") + else + local summary = selection_inputs(model) + require_exact_construction_selection( + summary, 2, 1, + "Select exactly two marks and one segment, with the point on the conic as primary." + ) + if not summary.primary_point then + error("The primary selection must be the point on the conic.") + end + point = summary.primary_point.point + for _, record in ipairs(summary.points) do + if record ~= summary.primary_point then focus = record.point end + end + directrix = summary.segments[1].line + end + local coefficients = focus_directrix_conic_coefficients(focus, directrix, point) + local source_points = { focus, point, directrix.p1, directrix.p2 } + entries, result.properties, result.id = render_conic_entries( + coefficients, options, styles, "focus_directrix_point", source_points + ) + result.type = "focus-directrix" + result.eccentricity = coefficients.eccentricity + result.coefficients = coefficients + result.properties = serializable_properties(result.properties) + group_default = #entries > 1 + elseif operation == "focus_directrix_eccentricity" + or operation == "focus_directrix_e" then + local focus_value = definition.focus + local directrix_value = M.aliased_value( + definition, "directrix", "line", "conic definition" + ) + local focus, directrix + if focus_value ~= nil or directrix_value ~= nil then + if focus_value == nil or directrix_value == nil then + error("focus and directrix are both required") + end + focus = point_from_table(focus_value, "focus") + directrix = line_from_table(directrix_value, "directrix") + else + local summary = selection_inputs(model) + require_exact_construction_selection( + summary, 1, 1, + "Select exactly one primary focus mark and one secondary directrix segment." + ) + if not summary.primary_point then error("The primary selection must be the focus mark.") end + focus, directrix = summary.primary_point.point, summary.segments[1].line + end + local eccentricity = positive_number_option( + definition.eccentricity, nil, "eccentricity" + ) + local coefficients = conic_coefficients_for_focus_directrix( + focus, directrix, eccentricity + ) + entries, result.properties, result.id = render_conic_entries( + coefficients, options, styles, "focus_directrix_eccentricity", + { focus, directrix.p1, directrix.p2 } + ) + result.type = "focus-directrix-eccentricity" + result.eccentricity = eccentricity + result.coefficients = coefficients + result.properties = serializable_properties(result.properties) + group_default = #entries > 1 + elseif operation == "degenerate" or operation == "degenerate_locus" + or operation == "degenerate_line_pair" or operation == "degenerate_double_line" + or operation == "degenerate_single_line" or operation == "degenerate_point" + or operation == "degenerate_empty" then + local default_subtype = ({ + degenerate_line_pair = "lines", + degenerate_double_line = "double_line", + degenerate_single_line = "single_line", + degenerate_point = "point", + degenerate_empty = "empty", + })[operation] or "lines" + local subtype = normalized_name( + definition.degenerate or definition.subtype or default_subtype + ) + local coefficients + if subtype == "lines" or subtype == "line_pair" or subtype == "double_line" + or subtype == "single_line" then + local expected = (subtype == "single_line" or subtype == "double_line") and 1 or 2 + local lines = R.explicit_or_selected_lines( + model, definition, expected, + expected == 1 and "Select exactly one segment." + or "Select exactly two segments." + ) + if subtype == "single_line" then + coefficients = normalize_coefficients({ + 0, 0, 0, lines[1].a, lines[1].b, lines[1].c, + }) + else + coefficients = Advanced.degenerate_conic_from_lines(lines) + end + elseif subtype == "point" then + local point_definition = definition + if definition.point ~= nil then + if definition.points ~= nil then + error("degenerate point cannot contain both point and points") + end + point_definition = clone_table(definition) + point_definition.points = { definition.point } + end + local points = explicit_or_selected_points( + model, point_definition, 1, "Select exactly one mark, or pass one point." + ) + coefficients = Advanced.degenerate_point_conic(points[1]) + elseif subtype == "empty" then + if definition.points ~= nil or definition.point ~= nil or definition.lines ~= nil then + error("the empty degenerate locus takes no points or lines") + end + coefficients = { 0, 0, 0, 0, 0, 1 } + else + error("unsupported degenerate conic subtype: " .. subtype) + end + local render_options = clone_table(options) + render_options.allow_degenerate = true + entries, result.properties, result.id = render_conic_entries( + coefficients, render_options, styles, "degenerate_locus", nil + ) + result.type = "degenerate-conic" + result.coefficients = normalize_coefficients(coefficients) + result.properties = serializable_properties(result.properties) + elseif operation == "quadrilateral_ellipse" + or operation == "ellipse_through_side_midpoints" then + local points = explicit_or_selected_points( + model, definition, 4, "Select exactly four marks, or pass exactly four points." + ) + local ellipse = quadrilateral_midpoint_ellipse(points) + local ok_coefficients, coefficients = pcall(ellipse_coefficients, ellipse) + if not ok_coefficients then coefficients = nil end + local properties = coefficients + and serializable_properties(conic_properties(coefficients)) + or M.serializable_ellipse_geometry(ellipse) + local object, id = make_ellipse(ellipse, styles.path), next_conic_id() + entries[1] = { + object = object, + role = "curve", + metadata = curve_metadata( + object, id, properties.kind, coefficients, "quadrilateral_midpoint_ellipse", "curve" + ), + } + result = { + type = "quadrilateral-midpoint-ellipse", + mathematical_choice = "canonical conjugate-diameter central minimum-area member", + coefficients = coefficients, + coefficients_available = coefficients ~= nil, + properties = properties, + } + else + error("unsupported conic construction: " .. operation) + end + + if #entries == 0 then + local message = "The requested degenerate conic has an empty real locus." + if model.ui and type(model.ui.explain) == "function" then model.ui:explain(message) end + return { + created = false, status = "empty", operation = operation, + element_count = 0, object_count = #model:page(), metadata = nil, + result = result, message = message, + } + end + local group_value = M.aliased_value( + options, "group_output", "group", "conic options" + ) + local group_output = bool_value(group_value, group_default) + local group_metadata = metadata_string({ + role = "group", kind = "conic-result", source = source, + trusted = "true", count = #entries, + }) + local objects = register_creation( + model, "create conic", entries, active_layer(model), group_output, group_metadata + ) + return success_result(model, operation, entries, objects, group_metadata, result) + end) +end + +local function selected_foci_and_point(model, message) + local summary = selection_inputs(model) + require_exact_construction_selection(summary, 3, 0, message) + if not summary.primary_point then error("The primary selection must be the point on the conic.") end + local foci = {} + for _, record in ipairs(summary.points) do + if record ~= summary.primary_point then foci[#foci + 1] = record.point end + end + return foci[1], foci[2], summary.primary_point.point +end + +local function create_ellipse_from_foci(model, raw_options) + return creator_call(model, "Cannot create ellipse", function() + local options = options_table(raw_options) + validate_keys(options, ELLIPSE_ALLOWED, "ellipse options") + M.reject_nested_aliases( + options, + { "focus_a", "focus_b", "point", "center", "first_endpoint", + "second_endpoint", "points" }, + "ellipse options" + ) + local definition = definition_options( + options, DEFINITION_ALLOWED.ellipse, "ellipse definition" + ) + local operation = normalized_name( + M.aliased_value(options, "operation", "construction", "ellipse options") + or "foci_point" + ) + local ellipse, defining = nil, {} + if operation == "foci_point" or operation == "foci_and_point" then + local focus_a, focus_b, point + local has_explicit = definition.focus_a ~= nil + or definition.focus_b ~= nil or definition.point ~= nil + if has_explicit then + if definition.focus_a == nil or definition.focus_b == nil or definition.point == nil then + error("focus_a, focus_b, and point are all required") + end + focus_a = point_from_table(definition.focus_a, "focus_a") + focus_b = point_from_table(definition.focus_b, "focus_b") + point = point_from_table(definition.point, "point") + else + focus_a, focus_b, point = selected_foci_and_point( + model, + "Select exactly three marks, with the point on the ellipse as primary." + ) + end + ellipse = ellipse_from_foci_point(focus_a, focus_b, point) + defining = { + foci = { point_record(focus_a), point_record(focus_b) }, + defining_point = point_record(point), + } + elseif operation == "center_axes" or operation == "center_semiaxes" then + local center, first_endpoint, second_endpoint + if definition.points ~= nil then + if definition.center ~= nil or definition.first_endpoint ~= nil + or definition.second_endpoint ~= nil then + error("center_axes cannot combine points with named endpoint fields") + end + local points = points_from_table(definition.points, 3, "points") + center, first_endpoint, second_endpoint = points[1], points[2], points[3] + elseif definition.center ~= nil or definition.first_endpoint ~= nil + or definition.second_endpoint ~= nil then + if definition.center == nil or definition.first_endpoint == nil + or definition.second_endpoint == nil then + error("center, first_endpoint, and second_endpoint are all required") + end + center = point_from_table(definition.center, "center") + first_endpoint = point_from_table(definition.first_endpoint, "first_endpoint") + second_endpoint = point_from_table(definition.second_endpoint, "second_endpoint") + else + local summary = selection_inputs(model) + require_exact_construction_selection( + summary, 3, 0, + "Select exactly three marks, with the center as primary." + ) + if not summary.primary_point then error("The primary selection must be the center mark.") end + center = summary.primary_point.point + local endpoints = {} + for _, record in ipairs(summary.points) do + if record ~= summary.primary_point then endpoints[#endpoints + 1] = record.point end + end + first_endpoint, second_endpoint = endpoints[1], endpoints[2] + end + ellipse = Advanced.ellipse_from_center_axes(center, first_endpoint, second_endpoint) + defining = { + center = point_record(center), + semiaxis_endpoints = { + point_record(first_endpoint), point_record(second_endpoint), + }, + } + else + error("unsupported ellipse construction: " .. operation) + end + local styles, object, id = construction_styles(model), + nil, next_conic_id() + object = make_ellipse(ellipse, styles.path) + local ok_coefficients, coefficients = pcall(ellipse_coefficients, ellipse) + if not ok_coefficients then coefficients = nil end + local serialized = coefficients + and serializable_properties(conic_properties(coefficients)) + or M.serializable_ellipse_geometry(ellipse) + local metadata = curve_metadata( + object, id, serialized.kind, coefficients, operation, "curve" + ) + local entries = { { object = object, role = "curve", metadata = metadata } } + local objects = register_creation( + model, "create ellipse", entries, active_layer(model), false + ) + local properties = { + kind = serialized.kind, + center = point_record(ellipse.center), + major_radius = ellipse.major_radius, + minor_radius = ellipse.minor_radius, + coefficients = coefficients, + coefficients_available = coefficients ~= nil, + } + for key, value in pairs(defining) do properties[key] = value end + properties.properties = serialized + properties.conics = { + { + kind = serialized.kind, + coefficients = coefficients, + properties = properties.properties, + }, + } + return success_result( + model, operation, entries, objects, metadata, properties + ) + end) +end + +local function create_hyperbola(model, raw_options) + return creator_call(model, "Cannot create hyperbola", function() + local options = options_table(raw_options) + validate_keys(options, HYPERBOLA_ALLOWED, "hyperbola options") + M.reject_nested_aliases(options, + { "focus_a", "focus_b", "point", "center", "axis", "line", "a", "b", + "radius", "asymptote_a", "asymptote_b", "lines" }, + "hyperbola options") + local definition = definition_options( + options, DEFINITION_ALLOWED.hyperbola, "hyperbola definition" + ) + local operation = normalized_name( + M.aliased_value(options, "operation", "construction", "hyperbola options") + or "foci_point" + ) + local hyperbola, coefficients + if operation == "foci_point" or operation == "foci_and_point" then + local has_explicit = definition.focus_a ~= nil + or definition.focus_b ~= nil or definition.point ~= nil + local focus_a, focus_b, point + if has_explicit then + if definition.focus_a == nil or definition.focus_b == nil or definition.point == nil then + error("focus_a, focus_b, and point are all required") + end + focus_a = point_from_table(definition.focus_a, "focus_a") + focus_b = point_from_table(definition.focus_b, "focus_b") + point = point_from_table(definition.point, "point") + else + focus_a, focus_b, point = selected_foci_and_point( + model, + "Select exactly three marks, with the point on the hyperbola as primary." + ) + end + hyperbola = hyperbola_from_foci_point(focus_a, focus_b, point) + elseif operation == "parameters" or operation == "rectangular" then + local center, axis + local axis_value = M.aliased_value( + definition, "axis", "line", "hyperbola definition" + ) + if definition.center ~= nil or axis_value ~= nil then + if definition.center == nil then + error("center is required when an explicit axis or line is supplied") + end + center = point_from_table(definition.center, "center") + if axis_value ~= nil then axis = line_from_table(axis_value, "axis") end + else + local summary = selection_inputs(model) + local expected_segments = #summary.segments + if expected_segments > 1 then + error("Select one center mark and at most one axis segment.") + end + require_exact_construction_selection( + summary, 1, expected_segments, + "Select exactly one center mark and optionally one axis segment." + ) + if not summary.primary_point then error("The primary selection must be the center mark.") end + center = summary.primary_point.point + axis = summary.segments[1] and summary.segments[1].line or nil + end + local a_radius = positive_number_option( + M.aliased_value(definition, "a", "radius", "hyperbola definition"), 32, "a" + ) + local b_radius = operation == "rectangular" and a_radius + or positive_number_option(definition.b, a_radius * 0.6, "b") + hyperbola = hyperbola_from_parameters(center, axis, a_radius, b_radius) + elseif operation == "asymptotes_point" or operation == "asymptotes_and_point" then + local first_asymptote, second_asymptote, point + if definition.lines ~= nil then + if definition.asymptote_a ~= nil or definition.asymptote_b ~= nil then + error("asymptotes_point cannot combine lines with named asymptote fields") + end + if type(definition.lines) ~= "table" or #definition.lines ~= 2 then + error("lines must contain exactly two asymptotes") + end + first_asymptote = line_from_table(definition.lines[1], "first asymptote") + second_asymptote = line_from_table(definition.lines[2], "second asymptote") + if definition.point == nil then error("point is required with explicit asymptotes") end + point = point_from_table(definition.point, "point") + elseif definition.asymptote_a ~= nil or definition.asymptote_b ~= nil + or definition.point ~= nil then + if definition.asymptote_a == nil or definition.asymptote_b == nil + or definition.point == nil then + error("asymptote_a, asymptote_b, and point are all required") + end + first_asymptote = line_from_table(definition.asymptote_a, "first asymptote") + second_asymptote = line_from_table(definition.asymptote_b, "second asymptote") + point = point_from_table(definition.point, "point") + else + local summary = selection_inputs(model) + require_exact_construction_selection( + summary, 1, 2, + "Select one primary point mark and two secondary asymptote segments." + ) + if not summary.primary_point then + error("The primary selection must be the point on the hyperbola.") + end + first_asymptote, second_asymptote = summary.segments[1].line, + summary.segments[2].line + point = summary.primary_point.point + end + coefficients = Advanced.hyperbola_from_asymptotes_point( + first_asymptote, second_asymptote, point + ) + local properties = conic_properties(coefficients) + hyperbola = { + center = properties.center, + u = properties.u, + v = properties.v, + a = properties.a, + b = properties.b, + point_parameter = math.abs(stable_asinh( + dot(sub(point, properties.center), properties.v) / properties.b + )), + point_branch = dot(sub(point, properties.center), properties.u) < 0 and -1 or 1, + } + else + error("unsupported hyperbola construction: " .. operation) + end + + coefficients = coefficients or hyperbola_coefficients(hyperbola) + local styles, entries = construction_styles(model), {} + local branch = normalized_name(options.branch or "both") + if branch ~= "both" and branch ~= "right" and branch ~= "positive" + and branch ~= "left" and branch ~= "negative" and branch ~= "defining" then + error("unsupported hyperbola branch: " .. branch) + end + if branch == "defining" then branch = hyperbola.point_branch == -1 and "left" or "right" end + local tolerance = positive_number_option(options.tolerance, 0.25, "tolerance") + local legacy_segment_budget + if options.samples ~= nil then + legacy_segment_budget = positive_integer_option( + options.samples, 80, "samples", 4096, 4 + ) + end + local max_segments = positive_integer_option( + options.max_segments or legacy_segment_budget, 256, "max_segments", 4096, 4 + ) + local requested_t_max = options.t_max + if requested_t_max == nil and options.extent ~= nil then + requested_t_max = M.hyperbola_parameter_from_extent(hyperbola, options.extent) + end + local id, rendered_t_max = next_conic_id(), nil + local function append_branch(branch_value) + local cubics, t_max = adaptive_hyperbola_cubics( + hyperbola, branch_value, requested_t_max, tolerance, max_segments + ) + rendered_t_max = t_max + local object = make_cubic_curve(cubics, styles.path) + entries[#entries + 1] = { + object = object, + role = "branch", + metadata = curve_metadata( + object, id, "hyperbola", coefficients, operation, "branch" + ), + } + end + if branch == "both" or branch == "right" or branch == "positive" then append_branch(1) end + if branch == "both" or branch == "left" or branch == "negative" then append_branch(-1) end + local asymptotes = bool_value(options.asymptotes, true) + if asymptotes then + local asymptote_length = positive_number_option( + options.asymptote_length, 192, "asymptote_length" + ) + for _, line in ipairs({ + { point = hyperbola.center, + direction = add(scale(hyperbola.u, hyperbola.a), scale(hyperbola.v, hyperbola.b)) }, + { point = hyperbola.center, + direction = sub(scale(hyperbola.u, hyperbola.a), scale(hyperbola.v, hyperbola.b)) }, + }) do + add_line_object(entries, line, asymptote_length, styles.dashed, "asymptote") + entries[#entries].metadata = auxiliary_metadata( + id, "asymptote", "hyperbola", operation + ) + end + end + local group_value = M.aliased_value( + options, "group_output", "group", "hyperbola options" + ) + local group_output = bool_value(group_value, #entries > 1) + local group_metadata = metadata_string({ + role = "group", id = id, kind = "hyperbola", source = operation, + trusted = "true", count = #entries, + }) + local objects = register_creation( + model, "create hyperbola", entries, active_layer(model), group_output, group_metadata + ) + return success_result(model, operation, entries, objects, group_metadata, { + kind = "hyperbola", + center = point_record(hyperbola.center), + a = hyperbola.a, + b = hyperbola.b, + t_max = rendered_t_max, + coefficients = coefficients, + properties = serializable_properties(conic_properties(coefficients)), + branch = branch, + asymptotes = asymptotes, + }) + end) +end + +local function parabola_inputs(model, options) + M.reject_nested_aliases( + options, { "directrix", "line", "focus", "foci", "vertex" }, "parabola options" + ) + local definition = definition_options( + options, DEFINITION_ALLOWED.parabola, "parabola definition" + ) + local directrix_value = M.aliased_value( + definition, "directrix", "line", "parabola definition" + ) + if definition.focus ~= nil and definition.foci ~= nil then + error("parabola definition cannot contain both 'focus' and 'foci'") + end + local raw_foci = definition.foci + if raw_foci == nil and definition.focus ~= nil then raw_foci = { definition.focus } end + local has_explicit = directrix_value ~= nil or raw_foci ~= nil + if has_explicit then + if directrix_value == nil or raw_foci == nil then + error("directrix and at least one focus are required") + end + if type(raw_foci) ~= "table" or #raw_foci == 0 then + error("directrix and at least one focus are required") + end + if #raw_foci > 64 then error("at most 64 foci are supported") end + local foci = {} + for index, focus in ipairs(raw_foci) do + foci[index] = point_from_table(focus, "foci[" .. tostring(index) .. "]") + end + return line_from_table(directrix_value, "directrix"), foci + end + local summary = selection_inputs(model) + if #summary.invalid ~= 0 or #summary.conic_entries ~= 0 + or #summary.segments ~= 1 or #summary.points == 0 + or #summary.entries ~= #summary.points + 1 then + error("Select one primary segment and one or more secondary marks.") + end + if not summary.primary_segment then error("The primary selection must be the directrix segment.") end + if #summary.points > 64 then error("at most 64 foci are supported") end + local foci = {} + for index, record in ipairs(summary.points) do foci[index] = record.point end + return summary.primary_segment.line, foci +end + +local function create_parabolas(model, raw_options) + return creator_call(model, "Cannot create parabolas", function() + local options = options_table(raw_options) + validate_keys(options, PARABOLA_ALLOWED, "parabola options") + local operation = normalized_name( + M.aliased_value(options, "operation", "construction", "parabola options") + or "directrix_foci" + ) + local definitions = {} + if operation == "directrix_foci" or operation == "directrix_and_foci" then + local directrix, foci = parabola_inputs(model, options) + local default_extent = distance(directrix.p1, directrix.p2) * 0.5 + for _, focus in ipairs(foci) do + definitions[#definitions + 1] = { + focus = focus, + directrix = directrix, + coefficients = conic_coefficients_for_focus_directrix(focus, directrix, 1), + default_extent = default_extent, + source = "directrix_foci", + } + end + elseif operation == "vertex_focus" or operation == "vertex_and_focus" then + M.reject_nested_aliases( + options, { "directrix", "line", "focus", "foci", "vertex" }, + "parabola options" + ) + local definition = definition_options( + options, DEFINITION_ALLOWED.parabola, "parabola definition" + ) + local vertex, focus + if definition.vertex ~= nil or definition.focus ~= nil then + if definition.vertex == nil or definition.focus == nil then + error("vertex and focus are both required") + end + if definition.directrix ~= nil or definition.line ~= nil or definition.foci ~= nil then + error("vertex_focus cannot contain directrix or foci") + end + vertex = point_from_table(definition.vertex, "vertex") + focus = point_from_table(definition.focus, "focus") + else + local summary = selection_inputs(model) + require_exact_construction_selection( + summary, 2, 0, + "Select exactly two marks, with the vertex as primary." + ) + if not summary.primary_point then error("The primary selection must be the vertex mark.") end + vertex = summary.primary_point.point + for _, record in ipairs(summary.points) do + if record ~= summary.primary_point then focus = record.point end + end + end + local coefficients, directrix = Advanced.parabola_from_vertex_focus(vertex, focus) + definitions[1] = { + vertex = vertex, + focus = focus, + directrix = directrix, + coefficients = coefficients, + default_extent = math.max(48, 6 * distance(vertex, focus)), + source = "vertex_focus", + } + else + error("unsupported parabola construction: " .. operation) + end + local styles, entries, records = construction_styles(model), {}, {} + local requested_extent = options.extent + if requested_extent ~= nil then + requested_extent = positive_number_option(requested_extent, nil, "extent") + end + local padding = nonnegative_number_option(options.padding, 0, "padding") + for _, definition in ipairs(definitions) do + local focus, coefficients = definition.focus, definition.coefficients + local properties = conic_properties(coefficients) + local extent = requested_extent or (definition.default_extent + padding) + if extent <= 0 then error("parabola extent must be positive") end + local object, id = make_spline( + parabola_spline(properties, extent, extent), styles.path + ), next_conic_id() + entries[#entries + 1] = { + object = object, + role = "curve", + metadata = curve_metadata( + object, id, "parabola", coefficients, definition.source, "curve" + ), + } + records[#records + 1] = { + coefficients = coefficients, + properties = serializable_properties(properties), + focus = point_record(focus), + extent = extent, + } + end + local group_value = M.aliased_value( + options, "group_output", "group", "parabola options" + ) + local group_output = bool_value(group_value, #entries > 1) + local group_metadata = metadata_string({ + role = "group", kind = "parabolas", source = operation, + trusted = "true", count = #entries, + }) + local objects = register_creation( + model, + #entries == 1 and "create parabola" or "create parabolas", + entries, + active_layer(model), + group_output, + group_metadata + ) + return success_result(model, operation, entries, objects, group_metadata, { + kind = "parabolas", + parabola_count = #entries, + conics = records, + }) + end) +end + +R.create_conic = create_conic +R.create_ellipse_from_foci = create_ellipse_from_foci +R.create_ellipse = create_ellipse_from_foci +R.create_hyperbola = create_hyperbola +R.create_parabolas = create_parabolas +R.create_parabola = create_parabolas + +---------------------------------------------------------------------- +-- Conic inspection, features, and metadata revalidation +---------------------------------------------------------------------- + +local FEATURE_ALLOWED = { + operation = true, definition = true, feature_input = true, + coefficients = true, points = true, focus = true, directrix = true, + point_on_conic = true, point = true, line = true, + line_length = true, marks = true, labels = true, + chord = true, focus_index = true, second_coefficients = true, + arc_mode = true, replace_original = true, expected_kind = true, + maximum_points = true, tolerance = true, max_segments = true, + tangent = true, normal = true, create_guides = true, guides = true, + axes = true, vertices = true, foci = true, directrices = true, + asymptotes = true, latus_recta = true, auxiliary_circles = true, + director_circle = true, general_equation = true, canonical_equation = true, + parameters = true, label_position = true, + group_output = true, group = true, +} +local FEATURE_NESTED_ALLOWED = { + definition = { + coefficients = true, points = true, focus = true, + directrix = true, point_on_conic = true, + }, + input = { + point = true, points = true, line = true, second_coefficients = true, + focus_index = true, label_position = true, + }, +} + +local function conic_coefficients_from_definition(model, options) + M.reject_nested_aliases(options, + { "coefficients", "points", "focus", "directrix", "point_on_conic" }, + "conic feature options") + local definition = definition_options( + options, FEATURE_NESTED_ALLOWED.definition, "conic feature definition" + ) + local explicit = definition.coefficients ~= nil or definition.points ~= nil + or definition.focus ~= nil or definition.directrix ~= nil + or definition.point_on_conic ~= nil + if explicit then + if definition.coefficients ~= nil then + if definition.points ~= nil or definition.focus ~= nil + or definition.directrix ~= nil or definition.point_on_conic ~= nil then + error("conic definition cannot combine coefficients with another definition form") + end + return normalize_coefficients(definition.coefficients), false, "coefficients" + end + if definition.points ~= nil then + if definition.focus ~= nil or definition.directrix ~= nil + or definition.point_on_conic ~= nil then + error("conic definition cannot combine points with focus-directrix fields") + end + return conic_coefficients_from_five_points( + points_from_table(definition.points, 5, "definition.points") + ), false, "five_points" + end + if definition.focus == nil or definition.directrix == nil + or definition.point_on_conic == nil then + error("focus, directrix, and point_on_conic are all required in definition") + end + return focus_directrix_conic_coefficients( + definition.focus, definition.directrix, definition.point_on_conic + ), false, "focus_directrix_point" + end + local selected = primary_conic_definition(model) + return selected.coefficients, true, selected.metadata_status +end + +local function selected_feature_point(model, definition_from_selection) + local summary = selection_inputs(model) + local expected_conics = definition_from_selection and 1 or 0 + if #summary.invalid ~= 0 or #summary.points ~= 1 or #summary.segments ~= 0 + or #summary.conic_entries ~= expected_conics + or #summary.entries ~= 1 + expected_conics then + error(definition_from_selection + and "Select one primary conic and exactly one secondary mark." + or "Select exactly one mark for the feature input.") + end + if definition_from_selection + and (not summary.primary_entry + or summary.primary_entry ~= summary.conic_entries[1]) then + error("The conic must be the primary selection.") + end + return summary.points[1].point +end + +local function selected_feature_line(model, definition_from_selection) + local summary = selection_inputs(model) + local expected_conics = definition_from_selection and 1 or 0 + if #summary.invalid ~= 0 or #summary.points ~= 0 or #summary.segments ~= 1 + or #summary.conic_entries ~= expected_conics + or #summary.entries ~= 1 + expected_conics then + error(definition_from_selection + and "Select one primary conic and exactly one secondary segment." + or "Select exactly one segment for the feature input.") + end + if definition_from_selection + and (not summary.primary_entry + or summary.primary_entry ~= summary.conic_entries[1]) then + error("The conic must be the primary selection.") + end + return summary.segments[1].line +end + +function R.single_conic_from_entry(entry, context) + local definitions, errors = collect_conic_definitions(entry.object) + if #errors > 0 then error(errors[1]) end + local unique, selected = {}, nil + for _, definition in ipairs(definitions) do + local key = definition.id or coefficients_text(definition.coefficients) + if not unique[key] then unique[key] = definition end + end + for _, definition in pairs(unique) do + if selected then + error((context or "selection") .. " contains more than one distinct conic") + end + selected = definition + end + if not selected then error((context or "selection") .. " is not a conic") end + return selected +end + +function R.selected_second_conic(model) + local entries = selected_objects(model) + if #entries ~= 2 then error("Select exactly two conics, with the first conic as primary.") end + local primary, secondary + for _, entry in ipairs(entries) do + if entry.primary then primary = entry else secondary = entry end + end + if not primary or not secondary then + error("The first conic must be the primary selection.") + end + R.single_conic_from_entry(primary, "primary selection") + return R.single_conic_from_entry(secondary, "secondary selection") +end + +function R.selected_feature_points(model, definition_from_selection, expected) + local summary = selection_inputs(model) + local expected_conics = definition_from_selection and 1 or 0 + if #summary.invalid ~= 0 or #summary.points ~= expected or #summary.segments ~= 0 + or #summary.conic_entries ~= expected_conics + or #summary.entries ~= expected + expected_conics then + error(definition_from_selection + and ("Select one primary conic and exactly " .. tostring(expected) + .. " secondary marks.") + or ("Select exactly " .. tostring(expected) .. " marks for the feature input.")) + end + if definition_from_selection + and (not summary.primary_entry or summary.primary_entry ~= summary.conic_entries[1]) then + error("The conic must be the primary selection.") + end + local points = {} + for index, record in ipairs(summary.points) do points[index] = record.point end + return points +end + +local function feature_input_options(options) + if options.feature_input ~= nil and (options.point ~= nil or options.line ~= nil + or options.second_coefficients ~= nil or options.focus_index ~= nil + or options.label_position ~= nil) then + error("conic feature options cannot mix nested 'feature_input' with legacy input fields") + end + if options.feature_input == nil then return options end + if type(options.feature_input) ~= "table" then error("feature_input must be a table") end + validate_keys(options.feature_input, FEATURE_NESTED_ALLOWED.input, "feature input") + return options.feature_input +end + +local function computed_result(model, operation, status, result, message) + if message and model and model.ui and type(model.ui.explain) == "function" then + model.ui:explain(message) + end + return { + created = false, + status = status, + operation = operation, + element_count = 0, + object_count = #model:page(), + metadata = nil, + result = result, + message = message, + } +end + +local function add_property_guides(entries, properties, options, styles, id) + local marks = bool_value(options.marks, true) + local labels = bool_value(options.labels, false) + local line_length + local function guide_length() + if line_length == nil then + line_length = positive_number_option(options.line_length, 192, "line_length") + end + return line_length + end + local function add_point(point, label_text, role) + if marks then + local object = make_mark(point, styles) + entries[#entries + 1] = { + object = object, + role = role, + metadata = auxiliary_metadata(id, role, properties.kind, "properties"), + } + end + if labels then + local object = make_text("$" .. label_text .. "$", add(point, V(4, 4)), styles) + entries[#entries + 1] = { + object = object, + role = "label", + metadata = auxiliary_metadata(id, "label", properties.kind, "properties"), + } + end + end + + if properties.degenerate then + for _, line in ipairs(properties.lines or {}) do + add_line_object(entries, line, guide_length(), styles.path, "degenerate") + entries[#entries].metadata = auxiliary_metadata( + id, "degenerate", "degenerate", "properties" + ) + end + for index, point in ipairs(properties.points or {}) do + add_point(point, "D_" .. tostring(index), "degenerate") + end + return + end + + if properties.center then add_point(properties.center, "C", "center") end + if bool_value(options.vertices, true) then + for index, point in ipairs(properties.vertices or {}) do + add_point(point, "V_" .. tostring(index), "vertex") + end + end + -- A circle's two focal records coincide with its center. Drawing all + -- three marks and labels would stack C, F_1, and F_2 at the same point. + if bool_value(options.foci, true) and properties.kind ~= "circle" then + for index, point in ipairs(properties.foci or {}) do + add_point(point, "F_" .. tostring(index), "focus") + end + end + if bool_value(options.axes, true) then + if properties.center and properties.vertices and #properties.vertices == 2 then + entries[#entries + 1] = { + object = make_segment(properties.vertices[1], properties.vertices[2], styles.dotted), + role = "axis", + metadata = auxiliary_metadata(id, "axis", properties.kind, "properties"), + } + end + if properties.center and properties.co_vertices and #properties.co_vertices == 2 then + entries[#entries + 1] = { + object = make_segment(properties.co_vertices[1], properties.co_vertices[2], styles.dotted), + role = "axis", + metadata = auxiliary_metadata(id, "axis", properties.kind, "properties"), + } + elseif properties.kind == "hyperbola" then + local p1 = add(properties.center, scale(properties.v, properties.b)) + local p2 = sub(properties.center, scale(properties.v, properties.b)) + entries[#entries + 1] = { + object = make_segment(p1, p2, styles.dotted), + role = "axis", + metadata = auxiliary_metadata(id, "axis", properties.kind, "properties"), + } + elseif properties.kind == "parabola" then + add_line_object(entries, { + point = properties.vertex, + direction = properties.axis_direction, + }, guide_length(), styles.dotted, "axis") + entries[#entries].metadata = auxiliary_metadata( + id, "axis", properties.kind, "properties" + ) + end + end + if bool_value(options.directrices, true) then + for _, line in ipairs(properties.directrices or {}) do + add_line_object(entries, line, guide_length(), styles.dashed, "directrix") + entries[#entries].metadata = auxiliary_metadata( + id, "directrix", properties.kind, "properties" + ) + end + end + if bool_value(options.asymptotes, true) then + for _, line in ipairs(properties.asymptotes or {}) do + add_line_object(entries, line, guide_length(), styles.dashed, "asymptote") + entries[#entries].metadata = auxiliary_metadata( + id, "asymptote", properties.kind, "properties" + ) + end + end + if bool_value(options.latus_recta, false) then + for _, record in ipairs(properties.latus_recta or {}) do + entries[#entries + 1] = { + object = make_segment(record.endpoints[1], record.endpoints[2], styles.dotted), + role = "latus_rectum", + metadata = auxiliary_metadata( + id, "latus_rectum", properties.kind, "properties" + ), + } + end + end + if bool_value(options.auxiliary_circles, false) then + for _, circle in ipairs(properties.auxiliary_circles or {}) do + local object = make_ellipse({ + center = circle.center, + axis1 = V(circle.radius, 0), + axis2 = V(0, circle.radius), + }, styles.dotted) + entries[#entries + 1] = { + object = object, + role = "auxiliary_circle", + metadata = auxiliary_metadata( + id, "auxiliary_circle", properties.kind, "properties" + ), + } + end + end + if bool_value(options.director_circle, false) and properties.director_circle then + local circle = properties.director_circle + local object = make_ellipse({ + center = circle.center, + axis1 = V(circle.radius, 0), + axis2 = V(0, circle.radius), + }, styles.dashed) + entries[#entries + 1] = { + object = object, + role = "director_circle", + metadata = auxiliary_metadata( + id, "director_circle", properties.kind, "properties" + ), + } + end + local equations = Advanced.conic_equation_strings(properties.coefficients) + local equation_labels = {} + if bool_value(options.general_equation, false) then + equation_labels[#equation_labels + 1] = equations.general + end + if bool_value(options.canonical_equation, false) and equations.canonical then + equation_labels[#equation_labels + 1] = equations.canonical + end + if bool_value(options.parameters, false) and equations.parameters then + equation_labels[#equation_labels + 1] = equations.parameters + end + local label_position = options.label_position + and point_from_table(options.label_position, "label_position") + or add(properties.center or properties.vertex, V(16, 16)) + for index, text in ipairs(equation_labels) do + local object = make_text(text, add(label_position, V(0, -14 * (index - 1))), styles) + entries[#entries + 1] = { + object = object, + role = "equation", + metadata = auxiliary_metadata(id, "equation", properties.kind, "properties"), + } + end +end + +local function create_conic_features(model, raw_options) + return creator_call(model, "Cannot create conic features", function() + local options = options_table(raw_options) + validate_keys(options, FEATURE_ALLOWED, "conic feature options") + local operation = normalized_name(options.operation or "tangent_normal") + if operation == "fit_replace_path" or operation == "replace_with_conic" then + if options.definition ~= nil or options.coefficients ~= nil or options.points ~= nil + or options.focus ~= nil or options.directrix ~= nil + or options.point_on_conic ~= nil or options.feature_input ~= nil then + error("fit_replace_path uses only the single selected path as its definition") + end + local selected = selected_objects(model) + if #selected ~= 1 or not selected[1].primary + or object_type(selected[1].object) ~= "path" then + error("Select exactly one path as the primary object to fit and replace.") + end + local samples = R.path_world_samples(selected[1].object) + local maximum_points = positive_integer_option( + options.maximum_points, 512, "maximum_points", 4096, 5 + ) + if #samples > maximum_points then + error("selected path produced more samples than maximum_points") + end + local coefficients, diagnostics = Advanced.conic_coefficients_from_points(samples, { + allow_degenerate = false, + expected_kind = options.expected_kind, + maximum_points = maximum_points, + }) + if diagnostics.rms_residual > 5e-3 or diagnostics.maximum_residual > 2e-2 then + error("selected path is too far from a conic to replace reliably") + end + local styles = construction_styles(model) + local entries, properties, id = render_conic_entries( + coefficients, options, styles, "fit_replace_path", samples + ) + local metadata = metadata_string({ + role = "group", id = id, kind = properties.kind, + source = operation, trusted = "true", count = #entries, + }) + local objects = R.register_replacement( + model, "fit and replace path with conic", selected[1].index, entries, metadata + ) + return success_result(model, operation, entries, objects, metadata, { + type = "fitted-replacement", + coefficients = coefficients, + fit = diagnostics, + properties = serializable_properties(properties), + replaced_index = selected[1].index, + }) + end + local coefficients, definition_from_selection, definition_source = + conic_coefficients_from_definition(model, options) + local feature_input = feature_input_options(options) + local styles, entries, result = construction_styles(model), {}, nil + local id = next_conic_id() + local replacement_index + + if operation == "tangent" or operation == "normal" or operation == "tangent_normal" then + local point = feature_input.point + and point_from_table(feature_input.point, "feature_input.point") + or selected_feature_point(model, definition_from_selection) + local lines = conic_tangent_normal(coefficients, point) + local create_tangent = operation ~= "normal" + local create_normal = operation ~= "tangent" + if operation == "tangent_normal" then + create_tangent = bool_value(options.tangent, true) + create_normal = bool_value(options.normal, true) + if not create_tangent and not create_normal then + error("tangent_normal must create at least the tangent or the normal") + end + elseif operation == "tangent" and options.tangent ~= nil + and not bool_value(options.tangent, true) then + error("the tangent operation cannot disable its tangent") + elseif operation == "normal" and options.normal ~= nil + and not bool_value(options.normal, true) then + error("the normal operation cannot disable its normal") + end + local line_length = positive_number_option(options.line_length, 192, "line_length") + local line_result = {} + if create_tangent then + add_line_object(entries, lines.tangent, line_length, styles.path, "tangent") + entries[#entries].metadata = auxiliary_metadata( + id, "tangent", "conic-feature", definition_source + ) + line_result.tangent = serializable_line(lines.tangent) + end + if create_normal then + add_line_object(entries, lines.normal, line_length, styles.dashed, "normal") + entries[#entries].metadata = auxiliary_metadata( + id, "normal", "conic-feature", definition_source + ) + line_result.normal = serializable_line(lines.normal) + end + if bool_value(options.marks, true) then + entries[#entries + 1] = { + object = make_mark(point, styles), + role = "mark", + metadata = auxiliary_metadata(id, "mark", "conic-feature", definition_source), + } + end + result = { + type = "tangent-normal", + point = point_record(point), + lines = line_result, + coefficients = coefficients, + } + elseif operation == "polar" or operation == "pole_polar" then + local point = feature_input.point + and point_from_table(feature_input.point, "feature_input.point") + or selected_feature_point(model, definition_from_selection) + local polar = conic_polar_line(coefficients, point) + local line_length = positive_number_option(options.line_length, 192, "line_length") + add_line_object(entries, polar, line_length, styles.path, "polar") + entries[#entries].metadata = auxiliary_metadata( + id, "polar", "conic-feature", definition_source + ) + if bool_value(options.marks, true) then + entries[#entries + 1] = { + object = make_mark(point, styles), + role = "mark", + metadata = auxiliary_metadata(id, "mark", "conic-feature", definition_source), + } + end + result = { + type = "line", + line = serializable_line(polar), + point = point_record(point), + coefficients = coefficients, + } + elseif operation == "tangents_from_point" or operation == "external_tangents" then + local point = feature_input.point + and point_from_table(feature_input.point, "feature_input.point") + or selected_feature_point(model, definition_from_selection) + local tangent_result = Advanced.tangents_from_point(coefficients, point) + if tangent_result.count == 0 then + return computed_result(model, operation, "empty", { + type = "tangents-from-point", + tangent_count = 0, + point = point_record(point), + coefficients = coefficients, + }, "The point has no real tangent lines to this conic.") + end + local line_length = positive_number_option(options.line_length, 192, "line_length") + for _, line in ipairs(tangent_result.tangents) do + add_line_object(entries, line, line_length, styles.path, "tangent") + entries[#entries].metadata = auxiliary_metadata( + id, "tangent", "conic-feature", definition_source + ) + end + if tangent_result.count == 2 and bool_value(options.chord, true) then + add_line_object( + entries, tangent_result.chord_of_contact, line_length, styles.dashed, "chord" + ) + entries[#entries].metadata = auxiliary_metadata( + id, "chord", "conic-feature", definition_source + ) + end + if bool_value(options.marks, true) then + for _, contact in ipairs(tangent_result.contact_points) do + entries[#entries + 1] = { + object = make_mark(contact, styles), + role = "intersection", + metadata = auxiliary_metadata( + id, "intersection", "conic-feature", definition_source + ), + } + end + end + result = { + type = "tangents-from-point", + tangent_count = tangent_result.count, + point = point_record(point), + contact_points = point_records(tangent_result.contact_points), + tangents = {}, + chord_of_contact = tangent_result.count == 2 + and serializable_line(tangent_result.chord_of_contact) or nil, + coefficients = coefficients, + } + for index, line in ipairs(tangent_result.tangents) do + result.tangents[index] = serializable_line(line) + end + elseif operation == "pole" or operation == "pole_of_line" then + local line = feature_input.line + and line_from_table(feature_input.line, "feature_input.line") + or selected_feature_line(model, definition_from_selection) + local pole = Advanced.conic_pole(coefficients, line) + if not pole.finite then + return computed_result(model, operation, "at_infinity", { + type = "pole", + finite = false, + direction = point_record(pole.direction), + line = serializable_line(line), + coefficients = coefficients, + }, "The pole of this line is a point at infinity.") + end + if bool_value(options.marks, true) then + entries[#entries + 1] = { + object = make_mark(pole.point, styles), + role = "pole", + metadata = auxiliary_metadata(id, "pole", "conic-feature", definition_source), + } + end + if bool_value(options.labels, false) then + entries[#entries + 1] = { + object = make_text("$P$", add(pole.point, V(4, 4)), styles), + role = "label", + metadata = auxiliary_metadata(id, "label", "conic-feature", definition_source), + } + end + if #entries == 0 then + return computed_result(model, operation, "computed", { + type = "pole", finite = true, point = point_record(pole.point), + line = serializable_line(line), coefficients = coefficients, + }, "Pole computed without creating a mark or label.") + end + result = { + type = "pole", + finite = true, + point = point_record(pole.point), + line = serializable_line(line), + coefficients = coefficients, + } + elseif operation == "focal_chord" then + local point = feature_input.point + and point_from_table(feature_input.point, "feature_input.point") + or selected_feature_point(model, definition_from_selection) + local focus_index = feature_input.focus_index or options.focus_index + local chord = Advanced.focal_chord(coefficients, point, focus_index) + if #chord.endpoints == 2 then + entries[#entries + 1] = { + object = make_segment(chord.endpoints[1], chord.endpoints[2], styles.path), + role = "chord", + metadata = auxiliary_metadata(id, "chord", "conic-feature", definition_source), + } + end + if bool_value(options.marks, true) then + for _, endpoint in ipairs(chord.endpoints) do + entries[#entries + 1] = { + object = make_mark(endpoint, styles), + role = "intersection", + metadata = auxiliary_metadata( + id, "intersection", "conic-feature", definition_source + ), + } + end + end + if #entries == 0 then + return computed_result(model, operation, "computed", { + type = "focal-chord", focus = point_record(chord.focus), + endpoints = point_records(chord.endpoints), coefficients = coefficients, + }, "Focal chord computed without creating visible objects.") + end + result = { + type = "focal-chord", + focus = point_record(chord.focus), + focus_index = chord.focus_index, + endpoints = point_records(chord.endpoints), + coefficients = coefficients, + } + elseif operation == "line_intersections" or operation == "intersections" then + local line = feature_input.line + and line_from_table(feature_input.line, "feature_input.line") + or selected_feature_line(model, definition_from_selection) + local intersections = conic_line_intersections(coefficients, line) + if intersections.infinite then + return computed_result(model, operation, "infinite", { + type = "points", + intersection_count = math.huge, + infinite = true, + points = {}, + coefficients = coefficients, + }, "The line is contained in the conic; there are infinitely many intersections.") + end + if #intersections == 0 then + return computed_result(model, operation, "empty", { + type = "points", + intersection_count = 0, + points = {}, + coefficients = coefficients, + }, "The line has no real intersections with the conic.") + end + local records = point_records(intersections) + local marks = bool_value(options.marks, true) + if not marks then + return computed_result(model, operation, "computed", { + type = "points", + intersection_count = #intersections, + points = records, + coefficients = coefficients, + }, "Intersections computed without creating marks.") + end + for index, point in ipairs(intersections) do + entries[#entries + 1] = { + object = make_mark(point, styles), + role = "intersection", + metadata = auxiliary_metadata( + id, "intersection", "conic-feature", definition_source + ), + } + if bool_value(options.labels, false) then + entries[#entries + 1] = { + object = make_text("$X_" .. tostring(index) .. "$", add(point, V(4, 4)), styles), + role = "label", + metadata = auxiliary_metadata( + id, "label", "conic-feature", definition_source + ), + } + end + end + result = { + type = "points", + intersection_count = #intersections, + points = records, + coefficients = coefficients, + } + elseif operation == "conic_intersections" or operation == "conic_conic_intersections" then + local second_coefficients + if feature_input.second_coefficients ~= nil then + second_coefficients = normalize_coefficients(feature_input.second_coefficients) + elseif definition_from_selection then + second_coefficients = R.selected_second_conic(model).coefficients + else + error("feature_input.second_coefficients is required with an explicit first conic") + end + local intersections = Advanced.conic_conic_intersections( + coefficients, second_coefficients + ) + if intersections.infinite then + return computed_result(model, operation, "infinite", { + type = "points", intersection_count = math.huge, infinite = true, + coincident = true, points = {}, coefficients = coefficients, + second_coefficients = second_coefficients, + }, "The two conics coincide and have infinitely many intersections.") + end + if #intersections == 0 then + return computed_result(model, operation, "empty", { + type = "points", intersection_count = 0, points = {}, + coefficients = coefficients, second_coefficients = second_coefficients, + }, "The two conics have no real intersections.") + end + local records = point_records(intersections) + if not bool_value(options.marks, true) then + return computed_result(model, operation, "computed", { + type = "points", intersection_count = #intersections, + points = records, coefficients = coefficients, + second_coefficients = second_coefficients, + }, "Conic intersections computed without creating marks.") + end + for index, point in ipairs(intersections) do + entries[#entries + 1] = { + object = make_mark(point, styles), + role = "intersection", + metadata = auxiliary_metadata( + id, "intersection", "conic-feature", definition_source + ), + } + if bool_value(options.labels, false) then + entries[#entries + 1] = { + object = make_text("$X_" .. tostring(index) .. "$", add(point, V(4, 4)), styles), + role = "label", + metadata = auxiliary_metadata(id, "label", "conic-feature", definition_source), + } + end + end + result = { + type = "points", intersection_count = #intersections, + points = records, coefficients = coefficients, + second_coefficients = second_coefficients, + } + elseif operation == "conic_arc" or operation == "trim" or operation == "crop" then + local points + if feature_input.points ~= nil then + points = points_from_table(feature_input.points, 2, "feature_input.points") + else + points = R.selected_feature_points(model, definition_from_selection, 2) + end + local definition = Advanced.conic_arc_definition( + coefficients, points[1], points[2], options.arc_mode + ) + local object = R.make_conic_arc(definition, styles.path, options) + entries[1] = { + object = object, + role = "curve", + metadata = curve_metadata( + object, id, definition.kind, coefficients, "conic_arc", "curve" + ), + } + if bool_value(options.replace_original, true) then + if not definition_from_selection then + error("replace_original requires a selected primary conic") + end + replacement_index = model:page():primarySelection() + end + result = { + type = "conic-arc", + kind = definition.kind, + first_point = point_record(points[1]), + second_point = point_record(points[2]), + arc_mode = normalized_name(options.arc_mode or "shorter"), + coefficients = coefficients, + replaced = replacement_index ~= nil, + } + elseif operation == "properties" or operation == "guides" then + local properties = conic_properties(coefficients) + local create_guides = bool_value(M.aliased_value( + options, "create_guides", "guides", "conic feature options" + ), operation == "guides") + if not create_guides then + return computed_result(model, operation, "inspected", { + coefficients = coefficients, + properties = serializable_properties(properties), + }, "Conic properties computed.") + end + local guide_options = clone_table(options) + if feature_input.label_position ~= nil then + guide_options.label_position = feature_input.label_position + end + add_property_guides(entries, properties, guide_options, styles, id) + if #entries == 0 then + return computed_result(model, operation, "inspected", { + coefficients = coefficients, + properties = serializable_properties(properties), + }, "Conic properties computed; no guide was selected for creation.") + end + result = { + type = "properties", + coefficients = coefficients, + properties = serializable_properties(properties), + } + else + error("unsupported conic feature operation: " .. operation) + end + + local group_value = M.aliased_value( + options, "group_output", "group", "conic feature options" + ) + local group_output = bool_value(group_value, #entries > 1) + local metadata = metadata_string({ + role = "group", id = id, kind = "conic-features", + source = operation, trusted = "true", count = #entries, + }) + local objects + if replacement_index then + objects = R.register_replacement( + model, "trim conic to arc", replacement_index, entries, metadata + ) + else + objects = register_creation( + model, "create conic features", entries, active_layer(model), group_output, metadata + ) + end + return success_result(model, operation, entries, objects, metadata, result) + end) +end + +local function inspect_conic(model, raw_options) + return creator_call(model, "Cannot inspect conic", function() + local options = options_table(raw_options) + validate_keys(options, { + definition = true, coefficients = true, points = true, focus = true, + directrix = true, point_on_conic = true, + }, "inspect options") + local coefficients, _, source = conic_coefficients_from_definition(model, options) + local properties = serializable_properties(conic_properties(coefficients)) + local message + if properties.degenerate then + message = "Conic: degenerate (" .. tostring(properties.subtype or "unclassified") .. ")" + else + message = "Conic: " .. properties.kind + .. "; eccentricity = " .. format_number(properties.eccentricity, 8) + end + if model.ui and type(model.ui.explain) == "function" then model.ui:explain(message) end + return { + created = false, + status = "inspected", + operation = "inspect", + element_count = 0, + object_count = #model:page(), + metadata = nil, + result = { + source = source, + coefficients = coefficients, + properties = properties, + }, + message = message, + } + end) +end + +local function bezier_point(control, parameter) + local work = {} + for index, point in ipairs(control) do work[index] = point end + for level = #work - 1, 1, -1 do + for index = 1, level do + work[index] = lerp(work[index], work[index + 1], parameter) + end + end + return work[1] +end + +function R.path_world_samples(object, parent_matrix) + local shape = path_shape(object) + if not shape then return {} end + local matrix = (parent_matrix or ipe.Matrix()) * object_matrix(object) + local samples = {} + local function append(point) + local transformed = matrix * point + if finite_number(transformed.x) and finite_number(transformed.y) then + if #samples == 0 or distance(samples[#samples], transformed) + > scaled_tolerance(math.max(vector_scale(samples[#samples], transformed), MIN_NORMAL), 4096) then + samples[#samples + 1] = transformed + end + end + end + for _, component in ipairs(shape) do + if component.type == "ellipse" and component[1] then + for index = 0, 15 do + local angle = 2 * math.pi * index / 16 + append(component[1] * V(math.cos(angle), math.sin(angle))) + end + elseif component.type == "curve" then + for _, segment in ipairs(component) do + if segment.type == "segment" then + append(segment[1]) + append(segment[2]) + elseif segment.type == "spline" and #segment >= 3 then + local control = {} + for index = 1, #segment do control[index] = segment[index] end + for _, parameter in ipairs({ 0, 0.25, 0.5, 0.75, 1 }) do + append(bezier_point(control, parameter)) + end + elseif segment.type == "arc" and segment.arc then + local ok_angles, alpha, beta = pcall(function() return segment.arc:angles() end) + local ok_matrix, arc_matrix = pcall(function() return segment.arc:matrix() end) + if ok_angles and ok_matrix and arc_matrix then + for index = 0, 12 do + local angle = alpha + (beta - alpha) * index / 12 + append(arc_matrix * V(math.cos(angle), math.sin(angle))) + end + else + append(segment[1]) + append(segment[2]) + end + end + end + end + end + return samples +end + +local function raw_conics_fields(object) + local tokens = split_metadata(object_custom_value(object)) + for index, token in ipairs(tokens) do + if token == "conics:v1" then return metadata_fields(tokens, index) end + end + return nil +end + +local function without_conics_metadata(custom) + local tokens, result, skipping = split_metadata(custom), {}, false + for _, token in ipairs(tokens) do + local namespace = token:find(":", 1, true) and not token:find("=", 1, true) + if token:match("^conics:v%d+$") then + skipping = true + elseif skipping and namespace then + skipping = false + result[#result + 1] = token + elseif not skipping then + result[#result + 1] = token + end + end + return table.concat(result, ";") +end + +local function replace_conics_metadata(object, metadata) + local remaining = without_conics_metadata(object_custom_value(object)) + set_object_custom_value(object, remaining ~= "" and (remaining .. ";" .. metadata) or metadata) +end + +local function inverse_matrix(matrix) + local a, c, b, d, tx, ty = unpack(matrix_values(matrix)) + local determinant = a * d - b * c + local scale_value = math.max(math.abs(a), math.abs(b), math.abs(c), math.abs(d)) + if scale_value == 0 or near_zero(determinant, scale_value * scale_value, 8192) then + error("conic object matrix must be nonsingular") + end + return ipe.Matrix( + d / determinant, + -c / determinant, + -b / determinant, + a / determinant, + (b * ty - d * tx) / determinant, + (c * tx - a * ty) / determinant + ) +end + +function R.fitted_coefficients_from_samples(samples) + if #samples < 5 then error("at least five sampled curve points are required") end + local coefficients, diagnostics = Advanced.conic_coefficients_from_points(samples, { + allow_degenerate = false, + maximum_points = 4096, + }) + if diagnostics.rms_residual > 5e-3 or diagnostics.maximum_residual > 2e-2 then + error("edited path is too far from a conic to revalidate reliably") + end + return coefficients, diagnostics.rms_residual, diagnostics +end + +local function selected_revalidation_paths(model) + local page, primary = model:page(), model:page():primarySelection() + if not primary then error("Select a conic as the primary object.") end + local root = page[primary] + if not root then error("Primary conic object is unavailable.") end + local result = {} + local function visit(object, parent_matrix) + local matrix = parent_matrix * object_matrix(object) + if object_type(object) == "group" then + for _, child in ipairs(object_elements(object) or {}) do visit(child, matrix) end + elseif object_type(object) == "path" then + local fields = raw_conics_fields(object) + if not fields or fields.role == "curve" or fields.role == "branch" then + result[#result + 1] = { + object = object, + matrix = matrix, + fields = fields or {}, + samples = R.path_world_samples(object, parent_matrix), + } + end + end + end + visit(root, ipe.Matrix()) + if #result == 0 then error("Primary selection contains no conic curve to revalidate.") end + return result +end + +local function revalidate_metadata(model, raw_options) + return creator_call(model, "Cannot revalidate conic metadata", function() + local options = options_table(raw_options) + validate_keys(options, {}, "revalidation options") + local paths, groups = selected_revalidation_paths(model), {} + for _, record in ipairs(paths) do + local id = record.fields.id or next_conic_id() + if not groups[id] then groups[id] = {} end + groups[id][#groups[id] + 1] = record + end + local changes, diagnostics = {}, {} + for id, records in pairs(groups) do + local samples = {} + for _, record in ipairs(records) do + for _, point in ipairs(record.samples) do samples[#samples + 1] = point end + end + local coefficients, residual = R.fitted_coefficients_from_samples(samples) + local kind = classify_conic(coefficients).kind + diagnostics[#diagnostics + 1] = { + id = id, kind = kind, residual = residual, path_count = #records, + } + for _, record in ipairs(records) do + local local_coefficients = transformed_conic_coefficients( + coefficients, inverse_matrix(record.matrix) + ) + local source = record.fields.source or "revalidated" + local metadata = curve_metadata( + record.object, id, kind, local_coefficients, source, + record.fields.role == "branch" and "branch" or "curve" + ) + local old_custom = object_custom_value(record.object) + local remaining = without_conics_metadata(old_custom) + local new_custom = remaining ~= "" and (remaining .. ";" .. metadata) or metadata + changes[#changes + 1] = { + object = record.object, + old_custom = old_custom, + new_custom = new_custom, + } + end + end + local transaction = { + label = "revalidate conic metadata", + pno = model.pno, + vno = model.vno, + changes = changes, + } + transaction.undo = function(record) + for _, change in ipairs(record.changes) do + set_object_custom_value(change.object, change.old_custom) + end + end + transaction.redo = function(record) + for _, change in ipairs(record.changes) do + set_object_custom_value(change.object, change.new_custom) + end + end + model:register(transaction) + if model.ui and type(model.ui.explain) == "function" then + model.ui:explain("Conic metadata revalidated.") + end + return { + created = false, + status = "updated", + operation = "revalidate_metadata", + element_count = 0, + object_count = #model:page(), + metadata = "conics:v1", + result = { + updated_object_count = #changes, + conic_count = #diagnostics, + conics = diagnostics, + }, + } + end) +end + +R.create_conic_features = create_conic_features +R.inspect_conic = inspect_conic +R.revalidate_metadata = revalidate_metadata + +---------------------------------------------------------------------- +-- Live and manual previews +---------------------------------------------------------------------- + +local P = (function() +local exports = {} + +local function preview_object_position(object) + local ok, position = pcall(function() return object:position() end) + if ok and position then return position end + return type(object) == "table" and (object.position_value or object.position) or nil +end + +local function append_shapes(target, shapes) + for _, shape in ipairs(shapes or {}) do target[#target + 1] = shape end +end + +local function clone_preview_value(value) + if type(value) ~= "table" then return value end + local cloned = {} + for key, item in pairs(value) do + cloned[clone_preview_value(key)] = clone_preview_value(item) + end + return setmetatable(cloned, _G.getmetatable(value)) +end + +local function transform_preview_shapes(matrix, shapes) + local transformed = clone_preview_value(shapes or {}) + if type(_G.transformShape) == "function" then + _G.transformShape(matrix, transformed) + return transformed + end + for _, path in ipairs(transformed) do + if path.type == "ellipse" or path.type == "closedspline" then + for index = 1, #path do path[index] = matrix * path[index] end + else + for _, segment in ipairs(path) do + for index = 1, #segment do segment[index] = matrix * segment[index] end + if segment.type == "arc" and segment.arc then segment.arc = matrix * segment.arc end + end + end + end + return transformed +end + +local function point_preview_shapes(point) + local size = 3 + return { + { type = "curve", closed = false; + { type = "segment"; V(point.x - size, point.y), V(point.x + size, point.y) } }, + { type = "curve", closed = false; + { type = "segment"; V(point.x, point.y - size), V(point.x, point.y + size) } }, + } +end + +local function label_preview_shape(point) + local left, bottom = point.x + 4, point.y + 2 + local right, top = left + 12, bottom + 8 + return { + type = "curve", + closed = true, + { type = "segment"; V(left, bottom), V(right, bottom) }, + { type = "segment"; V(right, bottom), V(right, top) }, + { type = "segment"; V(right, top), V(left, top) }, + { type = "segment"; V(left, top), V(left, bottom) }, + } +end + +local function preview_bbox_shapes(object, matrix) + if type(ipe.Rect) ~= "function" then return {} end + local rect = ipe.Rect() + local ok = pcall(function() object:addToBBox(rect, ipe.Matrix(), false) end) + if not ok or rect:isEmpty() then return {} end + local bottom_left, top_right = rect:bottomLeft(), rect:topRight() + local points = { + bottom_left, + V(top_right.x, bottom_left.y), + top_right, + V(bottom_left.x, top_right.y), + } + for index, point in ipairs(points) do points[index] = matrix * point end + return { { + type = "curve", + closed = true, + { type = "segment"; points[1], points[2] }, + { type = "segment"; points[2], points[3] }, + { type = "segment"; points[3], points[4] }, + { type = "segment"; points[4], points[1] }, + } } +end + +local function object_preview_shapes(object, parent_matrix) + parent_matrix = parent_matrix or ipe.Matrix() + local kind, matrix = object_type(object), parent_matrix * object_matrix(object) + if kind == "path" then + local shape = path_shape(object) + return shape and transform_preview_shapes(matrix, shape) or {} + elseif kind == "group" then + local shapes = {} + for _, child in ipairs(object_elements(object) or {}) do + append_shapes(shapes, object_preview_shapes(child, matrix)) + end + return shapes + elseif kind == "reference" then + local position = preview_object_position(object) + return position and point_preview_shapes(matrix * position) or {} + elseif kind == "text" then + local position = preview_object_position(object) + return position and { label_preview_shape(matrix * position) } or {} + end + return preview_bbox_shapes(object, parent_matrix) +end + +local function default_preview_page() + local page = {} + function page:active() return "alpha" end + function page:visible() return true end + function page:primarySelection() return nil end + function page:objects() return function() return nil end end + setmetatable(page, { __len = function() return 0 end }) + return page +end + +local function preview_page(page) + if not page then return default_preview_page() end + local wrapper = {} + function wrapper:active(vno) + local ok, value = pcall(function() return page:active(vno) end) + return ok and value or "alpha" + end + function wrapper:visible(vno, index) + local ok, value = pcall(function() return page:visible(vno, index) end) + return not ok or value + end + function wrapper:primarySelection() + local ok, value = pcall(function() return page:primarySelection() end) + return ok and value or nil + end + function wrapper:objects() + local ok, iterator, state, initial = pcall(function() return page:objects() end) + if ok and iterator then return iterator, state, initial end + return function() return nil end + end + setmetatable(wrapper, { + __index = function(_, key) + if type(key) == "number" then + local ok, value = pcall(function() return page[key] end) + return ok and value or nil + end + return rawget(wrapper, key) or page[key] + end, + __len = function() + local ok, count = pcall(function() return #page end) + return ok and count or 0 + end, + }) + return wrapper +end + +local function preview_capture_model(model) + local captured = {} + local preview_model = { + pno = model and model.pno or 1, + vno = model and model.vno or 1, + attributes = model and model.attributes or {}, + captured_objects = captured, + _conics_preview = true, + } + function preview_model:page() + local ok, page = pcall(function() return model:page() end) + return ok and preview_page(page) or default_preview_page() + end + function preview_model:register(record) + if type(record.objects) == "table" then + for _, object in ipairs(record.objects) do captured[#captured + 1] = object end + elseif record.object then + captured[#captured + 1] = record.object + elseif record.redo then + local document = { [self.pno] = self:page() } + record:redo(document) + end + end + function preview_model:warning(title, message) + error(tostring(title or "Preview") .. ": " .. tostring(message or "failed")) + end + return preview_model +end + +local PREVIEW_CREATORS = { + conic = create_conic, + conic_construct = create_conic, + ellipse_from_foci = create_ellipse_from_foci, + ellipse = create_ellipse_from_foci, + hyperbola = create_hyperbola, + make_parabolas = create_parabolas, + parabolas = create_parabolas, + parabola = create_parabolas, + conic_features = create_conic_features, +} + +local function action_options(action, options) + action = normalized_name(action) + options = options_table(options) + local cloned = {} + for key, value in pairs(options) do cloned[key] = value end + return cloned +end + +local function preview_shape_data(model, action, options) + action = normalized_name(action) + local creator = PREVIEW_CREATORS[action] + if not creator then error("unsupported Conics preview action: " .. tostring(action)) end + local preview_model = preview_capture_model(model) + local result = creator(preview_model, action_options(action, options)) + if type(result) ~= "table" or result.created ~= true then + error(clean_error_message(result and (result.error or result.message) + or "preview produced no construction")) + end + local shapes = {} + for _, object in ipairs(preview_model.captured_objects) do + append_shapes(shapes, object_preview_shapes(object)) + end + if #shapes == 0 then error("preview produced no visible shapes") end + return { + action = action, + created = true, + shapes = shapes, + shape_count = #shapes, + captured_count = #preview_model.captured_objects, + metadata = result.metadata, + result = result, + } +end + +local function preview_shapes(model, action, options) + return preview_shape_data(model, action, options).shapes +end + +local PREVIEW_COLOR = { 0.1, 0.35, 0.95 } +local PREVIEW_TOOL = {} +PREVIEW_TOOL.__index = PREVIEW_TOOL + +function PREVIEW_TOOL:new(model) + local tool = { model = model, active = true } + setmetatable(tool, PREVIEW_TOOL) + model.ui:shapeTool(tool) + if tool.setColor then tool.setColor(unpack(PREVIEW_COLOR)) end + return tool +end + +function PREVIEW_TOOL:update(shapes) + if not self.active or not self.setShape then return end + self.setShape(shapes or {}) + if self.model.ui and self.model.ui.update then self.model.ui:update(false) end +end + +function PREVIEW_TOOL:finish() + if not self.active then return end + self.active = false + self.model.ui:finishTool() + if self.model.ui and self.model.ui.update then self.model.ui:update(false) end +end + +function PREVIEW_TOOL:mouseButton() return true end +function PREVIEW_TOOL:key(text) return text == "\027" end + +local function add_preview_controls(dialog, row, columns, initial_value) + dialog:add("live_preview", "checkbox", { label = "Live preview" }, row, 1, 1, columns or 2) + dialog:set("live_preview", initial_value ~= false) + return row + 1 +end + +local function signature_value(parts, prefix, value, depth) + depth = depth or 0 + if type(value) ~= "table" or depth > 4 then + parts[#parts + 1] = prefix .. "=" .. tostring(value) + return + end + for index, item in ipairs(value) do + signature_value(parts, prefix .. "[" .. tostring(index) .. "]", item, depth + 1) + end + local keys = {} + for key, _ in pairs(value) do if type(key) ~= "number" then keys[#keys + 1] = key end end + table.sort(keys) + for _, key in ipairs(keys) do + signature_value(parts, prefix .. "." .. tostring(key), value[key], depth + 1) + end +end + +local function selection_preview_signature(model) + local parts = {} + local ok, entries = pcall(selected_objects, model) + if not ok then return "" end + for _, entry in ipairs(entries) do + parts[#parts + 1] = tostring(entry.index) .. ":" .. tostring(entry.selection) + .. ":" .. tostring(object_type(entry.object)) + local ok_matrix, values = pcall(matrix_values, object_matrix(entry.object)) + if ok_matrix then + for index = 1, math.min(6, #values) do + parts[#parts + 1] = string.format("%.17g", values[index]) + end + end + parts[#parts + 1] = object_custom_value(entry.object) + parts[#parts + 1] = shape_fingerprint(entry.object) or "" + end + return table.concat(parts, "|") +end + +local function preview_signature(model, action, options) + local parts, keys = { tostring(action), selection_preview_signature(model) }, {} + for key, _ in pairs(options or {}) do keys[#keys + 1] = key end + table.sort(keys) + for _, key in ipairs(keys) do signature_value(parts, tostring(key), options[key]) end + return table.concat(parts, "|") +end + +local function start_dialog_preview(model, dialog, action, read_options) + if not model.ui or not model.ui.shapeTool then return nil end + local preview = { + active = true, + live = true, + last_signature = nil, + tool = PREVIEW_TOOL:new(model), + } + local function explain(message) + if model.ui and type(model.ui.explain) == "function" then model.ui:explain(message) end + end + local function update(force) + if not preview.active then return end + local ok_live, live = pcall(function() return dialog:get("live_preview") end) + local live_enabled = ok_live and live == true + if not force and not live_enabled then + if preview.live then preview.tool:update({}); preview.last_signature = nil end + preview.live = false + return + end + preview.live = live_enabled + local ok_options, options = pcall(read_options) + if not ok_options then + preview.tool:update({}) + explain("Conics preview: " .. clean_error_message(options)) + return + end + local signature = preview_signature(model, action, options) + if not force and signature == preview.last_signature then return end + preview.last_signature = signature + local ok_shapes, data = pcall(preview_shape_data, model, action, options) + if ok_shapes then + preview.tool:update(data.shapes) + if force then explain("Conics preview updated.") end + else + preview.tool:update({}) + explain("Conics preview: " .. clean_error_message(data)) + end + end + preview.update = update + preview.stop = function() + if not preview.active then return end + preview.active = false + if preview.timer then pcall(function() preview.timer:stop() end) end + if preview.tool then pcall(function() preview.tool:finish() end) end + end + preview.tick = function() update(false) end + if ipeui and ipeui.Timer then + preview.timer = ipeui.Timer(preview, "tick") + preview.timer:setInterval(150) + preview.timer:start() + end + update(false) + return preview +end + +exports.action_options = action_options +exports.preview_shape_data = preview_shape_data +exports.preview_shapes = preview_shapes +exports.object_preview_shapes = object_preview_shapes +exports.preview_signature = preview_signature +exports.add_preview_controls = add_preview_controls +exports.start_dialog_preview = start_dialog_preview +return exports +end)() + +---------------------------------------------------------------------- +-- Dialogs +---------------------------------------------------------------------- + +local D = (function() +local exports = {} +local state = PERSISTED_DIALOG_STATE + +for _, name in ipairs({ "conic", "ellipse", "hyperbola", "parabolas", "features" }) do + if type(state[name]) ~= "table" then state[name] = {} end +end + +local function value_index(values, value, fallback) + for index, item in ipairs(values) do + if item == value then return index end + end + return fallback or 1 +end + +local function operation_index(entries, operation, fallback) + for index, entry in ipairs(entries) do + if entry.operation == operation then return index end + end + return fallback or 1 +end + +local function operation_combo(entries, action) + local values = { action = action } + for index, entry in ipairs(entries) do values[index] = entry.label end + return values +end + +local function optional_input(dialog, name) + local value = dialog:get(name) + if value == nil or value == "" then return nil end + return value +end + +local function optional_bounds(dialog, name) + local value = optional_input(dialog, name) + if value == nil then return nil end + local values = {} + for item in tostring(value):gmatch("[^,%s]+") do values[#values + 1] = item end + if #values ~= 4 then + error("bounds must contain left, bottom, right, and top") + end + return { + left = values[1], bottom = values[2], right = values[3], top = values[4], + } +end + +local function set_enabled(dialog, enabled, ...) + for index = 1, select("#", ...) do + dialog:setEnabled(select(index, ...), enabled) + end +end + +local function remember(dialog_state, options, dialog, fields) + for _, field in ipairs(fields) do dialog_state[field] = options[field] end + dialog_state.live_preview = dialog:get("live_preview") == true +end + +local function successful_creation(result) + return type(result) == "table" and result.created == true +end + +local function execute_dialog(model, title, dialog, action, read_options, creator, on_success) + local preview + local ok_execute, accepted_or_error = pcall(function() + preview = P.start_dialog_preview(model, dialog, action, read_options) + dialog:addButton("cancel", "&Cancel", "reject") + dialog:addButton("preview", "&Preview", function() + if preview then preview.update(true) end + end) + dialog:addButton("ok", "&Create", "accept") + return dialog:execute() + end) + if preview then pcall(function() preview.stop() end) end + if not ok_execute then return warn_and_return(model, title, accepted_or_error) end + if not accepted_or_error then return false end + + local ok_options, options_or_error = pcall(read_options) + if not ok_options then return warn_and_return(model, title, options_or_error) end + local ok_result, result_or_error = pcall(creator, model, options_or_error) + if not ok_result then return warn_and_return(model, title, result_or_error) end + if successful_creation(result_or_error) and on_success then + local ok_remember, remember_error = pcall(on_success, options_or_error, dialog) + if not ok_remember and model.ui and type(model.ui.explain) == "function" then + model.ui:explain("The construction succeeded, but dialog preferences were not saved: " + .. clean_error_message(remember_error)) + end + end + return result_or_error +end + +local CONIC_OPERATIONS = { + { + label = "Steiner ellipses", operation = "steiner", + selection = "Exactly 3 marks", + help = "Creates the Steiner circumellipse, inellipse, or both.", + steiner = true, + }, + { + label = "Conic through five points", operation = "five_points", + selection = "Exactly 5 marks", + help = "Fits one stable, nondegenerate conic through the five marks.", + adaptive = true, allow_degenerate = true, + }, + { + label = "Best-fit conic", operation = "fit_points", + selection = "6 to 512 marks", + help = "Computes a least-squares conic from all selected sample marks.", + adaptive = true, fit = true, allow_degenerate = true, + }, + { + label = "Conic tangent to five lines", operation = "five_tangents", + selection = "Exactly 5 segments", + help = "Constructs the unique stable conic tangent to all five lines.", + adaptive = true, allow_degenerate = true, + }, + { + label = "Five mixed conditions", operation = "five_conditions", + selection = "5 marks; or 4 marks + 1 tangent; or 3 marks + 2 tangents", + help = "Each tangent segment must pass through exactly one selected tangent-point mark.", + adaptive = true, allow_degenerate = true, + }, + { + label = "Focus, directrix, and point", operation = "focus_directrix_point", + selection = "2 marks + 1 segment; primary: point on conic", + help = "The secondary mark is the focus and the segment is the directrix.", + adaptive = true, + }, + { + label = "Focus, directrix, and eccentricity", operation = "focus_directrix_eccentricity", + selection = "1 primary focus mark + 1 secondary directrix segment", + help = "Uses the numeric eccentricity: e<1 ellipse, e=1 parabola, e>1 hyperbola.", + adaptive = true, eccentricity = true, + }, + { + label = "Canonical midpoint ellipse", operation = "quadrilateral_ellipse", + selection = "Exactly 4 marks", + help = "Uses the canonical minimum-area central ellipse through the side midpoints.", + }, + { + label = "Degenerate pair of lines", operation = "degenerate_line_pair", + selection = "Exactly 2 segments", + help = "Creates the explicit degenerate conic formed by the two selected lines.", + line_length = true, + }, + { + label = "Degenerate double line", operation = "degenerate_double_line", + selection = "Exactly 1 segment", + help = "Creates a line with algebraic multiplicity two.", + line_length = true, + }, + { + label = "Degenerate single line", operation = "degenerate_single_line", + selection = "Exactly 1 segment", + help = "Creates a first-degree single-line conic locus.", + line_length = true, + }, + { + label = "Degenerate point", operation = "degenerate_point", + selection = "Exactly 1 mark", + help = "Creates a point locus represented as a degenerate conic.", + }, + { + label = "Empty degenerate locus", operation = "degenerate_empty", + selection = "No selection required", + help = "Computes an empty real conic locus without creating an object.", + }, +} +local STEINER_VALUES = { "both", "circumellipse", "inellipse" } +local STEINER_LABELS = { "Both", "Circumellipse", "Inellipse" } +local BRANCH_VALUES = { "both", "right", "left" } +local BRANCH_LABELS = { "Both branches", "Right branch", "Left branch" } +local EXPECTED_KIND_VALUES = { "auto", "ellipse", "parabola", "hyperbola" } +local EXPECTED_KIND_LABELS = { "Automatic", "Ellipse/circle", "Parabola", "Hyperbola" } + +local function update_conic_dialog(dialog) + local entry = CONIC_OPERATIONS[dialog:get("operation")] or CONIC_OPERATIONS[1] + dialog:set("selection_value", entry.selection) + dialog:set("help_value", entry.help) + set_enabled(dialog, entry.steiner == true, "steiner_label", "steiner") + set_enabled(dialog, entry.eccentricity == true, "eccentricity_label", "eccentricity") + set_enabled(dialog, entry.fit == true, "expected_kind_label", "expected_kind", + "maximum_points_label", "maximum_points") + set_enabled(dialog, entry.allow_degenerate == true, "allow_degenerate") + set_enabled(dialog, entry.line_length == true, "line_length_label", "line_length") + set_enabled(dialog, entry.adaptive == true, "branch_label", "branch", + "extent_label", "extent", "padding_label", "padding", + "bounds_label", "bounds", + "tolerance_label", "tolerance", "segments_label", "max_segments") +end + +local function conic_dialog(model) + local saved = state.conic + local dialog = ipeui.Dialog(model.ui:win(), "Construct conic") + dialog:add("operation_label", "label", { label = "Construction" }, 1, 1) + dialog:add("operation", "combo", operation_combo(CONIC_OPERATIONS, update_conic_dialog), 1, 2) + dialog:add("selection_label", "label", { label = "Required selection" }, 2, 1) + dialog:add("selection_value", "label", { label = "" }, 2, 2) + dialog:add("help_label", "label", { label = "How it works" }, 3, 1) + dialog:add("help_value", "label", { label = "" }, 3, 2) + dialog:add("steiner_label", "label", { label = "Steiner output" }, 4, 1) + dialog:add("steiner", "combo", STEINER_LABELS, 4, 2) + dialog:add("eccentricity_label", "label", { label = "Eccentricity e" }, 5, 1) + dialog:add("eccentricity", "input", {}, 5, 2) + dialog:add("expected_kind_label", "label", { label = "Expected fitted type" }, 6, 1) + dialog:add("expected_kind", "combo", EXPECTED_KIND_LABELS, 6, 2) + dialog:add("maximum_points_label", "label", { label = "Maximum sample marks" }, 7, 1) + dialog:add("maximum_points", "input", {}, 7, 2) + dialog:add("allow_degenerate", "checkbox", { + label = "Allow an explicitly degenerate result", + }, 8, 1, 1, 2) + dialog:add("branch_label", "label", { label = "Hyperbola branch" }, 9, 1) + dialog:add("branch", "combo", BRANCH_LABELS, 9, 2) + dialog:add("extent_label", "label", { label = "Open-curve extent (optional)" }, 10, 1) + dialog:add("extent", "input", {}, 10, 2) + dialog:add("bounds_label", "label", { + label = "Bounds: left, bottom, right, top (optional)", + }, 11, 1) + dialog:add("bounds", "input", {}, 11, 2) + dialog:add("padding_label", "label", { label = "Automatic extent padding" }, 12, 1) + dialog:add("padding", "input", {}, 12, 2) + dialog:add("tolerance_label", "label", { label = "Approximation tolerance" }, 13, 1) + dialog:add("tolerance", "input", {}, 13, 2) + dialog:add("segments_label", "label", { label = "Maximum curve segments" }, 14, 1) + dialog:add("max_segments", "input", {}, 14, 2) + dialog:add("line_length_label", "label", { label = "Degenerate line length" }, 15, 1) + dialog:add("line_length", "input", {}, 15, 2) + dialog:add("group_output", "checkbox", { label = "Group multiple outputs" }, 16, 1, 1, 2) + dialog:set("operation", operation_index(CONIC_OPERATIONS, saved.operation, 1)) + dialog:set("steiner", value_index(STEINER_VALUES, saved.mode, 1)) + dialog:set("eccentricity", saved.eccentricity or "1") + dialog:set("expected_kind", value_index(EXPECTED_KIND_VALUES, saved.expected_kind, 1)) + dialog:set("maximum_points", saved.maximum_points or "512") + dialog:set("allow_degenerate", saved.allow_degenerate == true) + dialog:set("branch", value_index(BRANCH_VALUES, saved.branch, 1)) + dialog:set("extent", saved.extent or "") + dialog:set("bounds", saved.bounds_text or "") + dialog:set("padding", saved.padding or "24") + dialog:set("tolerance", saved.tolerance or "0.25") + dialog:set("max_segments", saved.max_segments or "256") + dialog:set("line_length", saved.line_length or "192") + dialog:set("group_output", saved.group_output ~= false) + P.add_preview_controls(dialog, 17, 2, saved.live_preview) + update_conic_dialog(dialog) + local function read_options() + local entry = CONIC_OPERATIONS[dialog:get("operation")] or CONIC_OPERATIONS[1] + local options = { + operation = entry.operation, + group_output = dialog:get("group_output"), + } + if entry.steiner then options.mode = STEINER_VALUES[dialog:get("steiner")] end + if entry.eccentricity then options.eccentricity = optional_input(dialog, "eccentricity") end + if entry.fit then + options.expected_kind = EXPECTED_KIND_VALUES[dialog:get("expected_kind")] + options.maximum_points = optional_input(dialog, "maximum_points") + end + if entry.allow_degenerate then + options.allow_degenerate = dialog:get("allow_degenerate") + end + if entry.line_length then options.line_length = optional_input(dialog, "line_length") end + if entry.adaptive then + options.branch = BRANCH_VALUES[dialog:get("branch")] + options.extent = optional_input(dialog, "extent") + options.bounds = optional_bounds(dialog, "bounds") + options.padding = optional_input(dialog, "padding") + options.tolerance = optional_input(dialog, "tolerance") + options.max_segments = optional_input(dialog, "max_segments") + end + return options + end + return execute_dialog( + model, "Cannot create conic", dialog, "conic", read_options, create_conic, + function(options, successful_dialog) + remember(saved, options, dialog, + { "operation", "mode", "eccentricity", "expected_kind", "maximum_points", + "allow_degenerate", "branch", "extent", "padding", "tolerance", + "max_segments", "line_length", "group_output" }) + saved.bounds_text = successful_dialog:get("bounds") + end + ) +end + +local ELLIPSE_OPERATIONS = { + { + label = "Foci and point", operation = "foci_point", + selection = "Exactly 3 marks; primary: point on ellipse", + help = "The two secondary marks are the foci.", + }, + { + label = "Center and semiaxis endpoints", operation = "center_axes", + selection = "Exactly 3 marks; primary: center", + help = "The two secondary marks are perpendicular semiaxis endpoints.", + }, +} + +local function update_ellipse_dialog(dialog) + local entry = ELLIPSE_OPERATIONS[dialog:get("operation")] or ELLIPSE_OPERATIONS[1] + dialog:set("selection_value", entry.selection) + dialog:set("help_value", entry.help) +end + +local function ellipse_dialog(model) + local saved = state.ellipse + local dialog = ipeui.Dialog(model.ui:win(), "Construct ellipse") + dialog:add("operation_label", "label", { label = "Construction" }, 1, 1) + dialog:add("operation", "combo", operation_combo( + ELLIPSE_OPERATIONS, update_ellipse_dialog + ), 1, 2) + dialog:add("selection_label", "label", { label = "Required selection" }, 2, 1) + dialog:add("selection_value", "label", { label = "" }, 2, 2) + dialog:add("help_label", "label", { label = "How it works" }, 3, 1) + dialog:add("help_value", "label", { label = "" }, 3, 2) + dialog:add("output_label", "label", { label = "Output" }, 4, 1) + dialog:add("output_value", "label", { + label = "One native, editable Ipe ellipse.", + }, 4, 2) + dialog:set("operation", operation_index(ELLIPSE_OPERATIONS, saved.operation, 1)) + P.add_preview_controls(dialog, 5, 2, saved.live_preview) + update_ellipse_dialog(dialog) + local function read_options() + local entry = ELLIPSE_OPERATIONS[dialog:get("operation")] or ELLIPSE_OPERATIONS[1] + return { operation = entry.operation } + end + return execute_dialog( + model, "Cannot create ellipse", dialog, "ellipse_from_foci", read_options, + create_ellipse_from_foci, + function(options) remember(saved, options, dialog, { "operation" }) end + ) +end + +local HYPERBOLA_OPERATIONS = { + { + label = "Foci and point", operation = "foci_point", + selection = "Exactly 3 marks; primary: point on hyperbola", + help = "The two secondary marks are the foci.", + foci = true, + }, + { + label = "Center and semiaxes", operation = "parameters", + selection = "1 primary center mark; optional secondary axis segment", + help = "The segment supplies only the transverse-axis direction.", + parameters = true, + }, + { + label = "Rectangular hyperbola", operation = "rectangular", + selection = "1 primary center mark; optional secondary axis segment", + help = "Uses equal transverse and conjugate semiaxes.", + parameters = true, rectangular = true, + }, + { + label = "Asymptotes and point", operation = "asymptotes_point", + selection = "1 primary point mark + 2 secondary asymptote segments", + help = "The point chooses the scale and branch of the hyperbola.", + }, +} + +local function update_hyperbola_dialog(dialog) + local entry = HYPERBOLA_OPERATIONS[dialog:get("operation")] or HYPERBOLA_OPERATIONS[1] + dialog:set("selection_value", entry.selection) + dialog:set("help_value", entry.help) + set_enabled(dialog, entry.parameters == true, "a_label", "a") + set_enabled(dialog, entry.parameters == true and not entry.rectangular, "b_label", "b") + local asymptotes = dialog:get("asymptotes") == true + set_enabled(dialog, asymptotes, "asymptote_length_label", "asymptote_length") +end + +local function hyperbola_dialog(model) + local saved = state.hyperbola + local dialog = ipeui.Dialog(model.ui:win(), "Construct hyperbola") + dialog:add("operation_label", "label", { label = "Construction" }, 1, 1) + dialog:add("operation", "combo", operation_combo(HYPERBOLA_OPERATIONS, update_hyperbola_dialog), 1, 2) + dialog:add("selection_label", "label", { label = "Required selection" }, 2, 1) + dialog:add("selection_value", "label", { label = "" }, 2, 2) + dialog:add("help_label", "label", { label = "How it works" }, 3, 1) + dialog:add("help_value", "label", { label = "" }, 3, 2) + dialog:add("a_label", "label", { label = "Transverse semiaxis a" }, 4, 1) + dialog:add("a", "input", {}, 4, 2) + dialog:add("b_label", "label", { label = "Conjugate semiaxis b" }, 5, 1) + dialog:add("b", "input", {}, 5, 2) + dialog:add("branch_label", "label", { label = "Branches" }, 6, 1) + dialog:add("branch", "combo", BRANCH_LABELS, 6, 2) + dialog:add("extent_label", "label", { label = "Transverse extent (optional)" }, 7, 1) + dialog:add("extent", "input", {}, 7, 2) + dialog:add("tolerance_label", "label", { label = "Approximation tolerance" }, 8, 1) + dialog:add("tolerance", "input", {}, 8, 2) + dialog:add("segments_label", "label", { label = "Maximum curve segments" }, 9, 1) + dialog:add("max_segments", "input", {}, 9, 2) + dialog:add("asymptotes", "checkbox", { + label = "Create asymptotes", action = update_hyperbola_dialog, + }, 10, 1, 1, 2) + dialog:add("asymptote_length_label", "label", { label = "Asymptote length" }, 11, 1) + dialog:add("asymptote_length", "input", {}, 11, 2) + dialog:add("group_output", "checkbox", { label = "Group branches and auxiliaries" }, 12, 1, 1, 2) + dialog:set("operation", operation_index(HYPERBOLA_OPERATIONS, saved.operation, 1)) + dialog:set("a", saved.a or "32") + dialog:set("b", saved.b or "20") + dialog:set("branch", value_index(BRANCH_VALUES, saved.branch, 1)) + dialog:set("extent", saved.extent or "") + dialog:set("tolerance", saved.tolerance or "0.25") + dialog:set("max_segments", saved.max_segments or "256") + dialog:set("asymptotes", saved.asymptotes ~= false) + dialog:set("asymptote_length", saved.asymptote_length or "192") + dialog:set("group_output", saved.group_output ~= false) + P.add_preview_controls(dialog, 13, 2, saved.live_preview) + update_hyperbola_dialog(dialog) + local function read_options() + local entry = HYPERBOLA_OPERATIONS[dialog:get("operation")] or HYPERBOLA_OPERATIONS[1] + local options = { + operation = entry.operation, + branch = BRANCH_VALUES[dialog:get("branch")], + extent = optional_input(dialog, "extent"), + tolerance = optional_input(dialog, "tolerance"), + max_segments = optional_input(dialog, "max_segments"), + asymptotes = dialog:get("asymptotes"), + group_output = dialog:get("group_output"), + } + if options.asymptotes then + options.asymptote_length = optional_input(dialog, "asymptote_length") + end + if entry.parameters then + options.a = optional_input(dialog, "a") + if not entry.rectangular then options.b = optional_input(dialog, "b") end + end + return options + end + return execute_dialog( + model, "Cannot create hyperbola", dialog, "hyperbola", read_options, + create_hyperbola, + function(options) + remember(saved, options, dialog, + { "operation", "a", "b", "branch", "extent", "tolerance", + "max_segments", "asymptotes", "asymptote_length", "group_output" }) + end + ) +end + +local PARABOLA_OPERATIONS = { + { + label = "Directrix and foci", operation = "directrix_foci", + selection = "1 primary directrix segment + 1 or more secondary focus marks", + help = "Creates one parabola for each selected focus.", + }, + { + label = "Vertex and focus", operation = "vertex_focus", + selection = "Exactly 2 marks; primary: vertex", + help = "The secondary mark is the focus; the directrix is derived automatically.", + }, +} + +local function update_parabolas_dialog(dialog) + local entry = PARABOLA_OPERATIONS[dialog:get("operation")] or PARABOLA_OPERATIONS[1] + dialog:set("selection_value", entry.selection) + dialog:set("help_value", entry.help) +end + +local function parabolas_dialog(model) + local saved = state.parabolas + local dialog = ipeui.Dialog(model.ui:win(), "Construct parabola") + dialog:add("operation_label", "label", { label = "Construction" }, 1, 1) + dialog:add("operation", "combo", operation_combo( + PARABOLA_OPERATIONS, update_parabolas_dialog + ), 1, 2) + dialog:add("selection_label", "label", { label = "Required selection" }, 2, 1) + dialog:add("selection_value", "label", { label = "" }, 2, 2) + dialog:add("help_label", "label", { label = "How it works" }, 3, 1) + dialog:add("help_value", "label", { label = "" }, 3, 2) + dialog:add("extent_label", "label", { label = "Half-extent (optional)" }, 4, 1) + dialog:add("extent", "input", {}, 4, 2) + dialog:add("padding_label", "label", { label = "Automatic extent padding" }, 5, 1) + dialog:add("padding", "input", {}, 5, 2) + dialog:add("group_output", "checkbox", { label = "Group multiple parabolas" }, 6, 1, 1, 2) + dialog:set("operation", operation_index(PARABOLA_OPERATIONS, saved.operation, 1)) + dialog:set("extent", saved.extent or "") + dialog:set("padding", saved.padding or "0") + dialog:set("group_output", saved.group_output ~= false) + P.add_preview_controls(dialog, 7, 2, saved.live_preview) + update_parabolas_dialog(dialog) + local function read_options() + local entry = PARABOLA_OPERATIONS[dialog:get("operation")] or PARABOLA_OPERATIONS[1] + return { + operation = entry.operation, + extent = optional_input(dialog, "extent"), + padding = optional_input(dialog, "padding"), + group_output = dialog:get("group_output"), + } + end + return execute_dialog( + model, "Cannot create parabolas", dialog, "parabolas", read_options, + create_parabolas, + function(options) + remember(saved, options, dialog, { "operation", "extent", "padding", "group_output" }) + end + ) +end + +local FEATURE_OPERATIONS = { + { + label = "Tangent", operation = "tangent", + selection = "1 primary conic + 1 secondary mark on the conic", + point = true, line_length = true, marks = true, + }, + { + label = "Normal", operation = "normal", + selection = "1 primary conic + 1 secondary mark on the conic", + point = true, line_length = true, marks = true, + }, + { + label = "Tangent and normal", operation = "tangent_normal", + selection = "1 primary conic + 1 secondary mark on the conic", + point = true, line_length = true, marks = true, line_choices = true, + }, + { + label = "Polar line of point", operation = "polar", + selection = "1 primary conic + 1 secondary mark", + point = true, line_length = true, marks = true, + }, + { + label = "Tangents from point", operation = "tangents_from_point", + selection = "1 primary conic + 1 secondary mark", + point = true, line_length = true, marks = true, chord = true, + }, + { + label = "Pole of line", operation = "pole", + selection = "1 primary conic + 1 secondary segment", + marks = true, labels = true, + }, + { + label = "Focal chord", operation = "focal_chord", + selection = "1 primary conic + 1 secondary mark defining the focal line", + marks = true, + }, + { + label = "Intersections with line", operation = "line_intersections", + selection = "1 primary conic + 1 secondary segment", + marks = true, labels = true, + }, + { + label = "Intersections of two conics", operation = "conic_intersections", + selection = "Exactly 2 conics; primary: first conic", + marks = true, labels = true, + }, + { + label = "Trim conic to arc", operation = "conic_arc", + selection = "1 primary conic + 2 secondary marks on one connected arc", + arc = true, quality = true, + }, + { + label = "Fit and replace selected path", operation = "fit_replace_path", + selection = "Exactly 1 primary path", + fit = true, quality = true, + }, + { + label = "Property guides", operation = "guides", + selection = "Exactly 1 primary conic", + line_length = true, marks = true, labels = true, properties = true, + }, +} + +local ARC_MODE_VALUES = { "shorter", "longer", "counterclockwise", "clockwise" } +local ARC_MODE_LABELS = { "Shorter ellipse arc", "Longer ellipse arc", "Counterclockwise", "Clockwise" } + +local function update_features_dialog(dialog) + local entry = FEATURE_OPERATIONS[dialog:get("operation")] or FEATURE_OPERATIONS[1] + dialog:set("selection_value", entry.selection) + set_enabled(dialog, entry.line_length == true, "line_length_label", "line_length") + set_enabled(dialog, entry.marks == true, "marks") + set_enabled(dialog, entry.labels == true, "labels") + set_enabled(dialog, entry.line_choices == true, "tangent", "normal") + set_enabled(dialog, entry.chord == true, "chord") + set_enabled(dialog, entry.arc == true, "arc_mode_label", "arc_mode", "replace_original") + set_enabled(dialog, entry.fit == true, + "expected_kind_label", "expected_kind", "maximum_points_label", "maximum_points") + set_enabled(dialog, entry.quality == true, + "tolerance_label", "tolerance", "segments_label", "max_segments") + set_enabled(dialog, entry.properties == true, + "axes", "vertices", "foci", "directrices", "asymptotes", + "latus_recta", "auxiliary_circles", "director_circle", + "general_equation", "canonical_equation", "parameters") +end + +local function features_dialog(model) + local saved = state.features + local dialog = ipeui.Dialog(model.ui:win(), "Conic features") + dialog:add("operation_label", "label", { label = "Feature" }, 1, 1) + dialog:add("operation", "combo", operation_combo(FEATURE_OPERATIONS, update_features_dialog), 1, 2) + dialog:add("selection_label", "label", { label = "Required selection" }, 2, 1) + dialog:add("selection_value", "label", { label = "" }, 2, 2) + dialog:add("line_length_label", "label", { label = "Line length" }, 3, 1) + dialog:add("line_length", "input", {}, 3, 2) + dialog:add("tangent", "checkbox", { label = "Create tangent" }, 4, 1, 1, 2) + dialog:add("normal", "checkbox", { label = "Create normal" }, 5, 1, 1, 2) + dialog:add("chord", "checkbox", { label = "Create chord of contact" }, 6, 1, 1, 2) + dialog:add("marks", "checkbox", { label = "Create point marks" }, 7, 1) + dialog:add("labels", "checkbox", { label = "Create labels" }, 7, 2) + dialog:add("axes", "checkbox", { label = "Axes" }, 8, 1) + dialog:add("vertices", "checkbox", { label = "Vertices" }, 8, 2) + dialog:add("foci", "checkbox", { label = "Foci" }, 9, 1) + dialog:add("directrices", "checkbox", { label = "Directrices" }, 9, 2) + dialog:add("asymptotes", "checkbox", { label = "Asymptotes" }, 10, 1) + dialog:add("latus_recta", "checkbox", { label = "Latus recta" }, 10, 2) + dialog:add("auxiliary_circles", "checkbox", { label = "Auxiliary circles" }, 11, 1) + dialog:add("director_circle", "checkbox", { label = "Director circle" }, 11, 2) + dialog:add("general_equation", "checkbox", { label = "General equation label" }, 12, 1) + dialog:add("canonical_equation", "checkbox", { label = "Canonical equation label" }, 12, 2) + dialog:add("parameters", "checkbox", { label = "Parameter label (a, b, c, e, p, area)" }, 13, 1, 1, 2) + dialog:add("arc_mode_label", "label", { label = "Ellipse arc choice" }, 14, 1) + dialog:add("arc_mode", "combo", ARC_MODE_LABELS, 14, 2) + dialog:add("replace_original", "checkbox", { label = "Replace the selected conic" }, 15, 1, 1, 2) + dialog:add("expected_kind_label", "label", { label = "Expected fitted type" }, 16, 1) + dialog:add("expected_kind", "combo", EXPECTED_KIND_LABELS, 16, 2) + dialog:add("maximum_points_label", "label", { label = "Maximum sampled points" }, 17, 1) + dialog:add("maximum_points", "input", {}, 17, 2) + dialog:add("tolerance_label", "label", { label = "Approximation tolerance" }, 18, 1) + dialog:add("tolerance", "input", {}, 18, 2) + dialog:add("segments_label", "label", { label = "Maximum curve segments" }, 19, 1) + dialog:add("max_segments", "input", {}, 19, 2) + dialog:add("group_output", "checkbox", { label = "Group multiple outputs" }, 20, 1, 1, 2) + dialog:set("operation", operation_index(FEATURE_OPERATIONS, saved.operation, 1)) + dialog:set("line_length", saved.line_length or "192") + dialog:set("tangent", saved.tangent ~= false) + dialog:set("normal", saved.normal ~= false) + dialog:set("chord", saved.chord ~= false) + dialog:set("marks", saved.marks ~= false) + dialog:set("labels", saved.labels == true) + dialog:set("axes", saved.axes ~= false) + dialog:set("vertices", saved.vertices ~= false) + dialog:set("foci", saved.foci ~= false) + dialog:set("directrices", saved.directrices ~= false) + dialog:set("asymptotes", saved.asymptotes ~= false) + dialog:set("latus_recta", saved.latus_recta == true) + dialog:set("auxiliary_circles", saved.auxiliary_circles == true) + dialog:set("director_circle", saved.director_circle == true) + dialog:set("general_equation", saved.general_equation == true) + dialog:set("canonical_equation", saved.canonical_equation == true) + dialog:set("parameters", saved.parameters == true) + dialog:set("arc_mode", value_index(ARC_MODE_VALUES, saved.arc_mode, 1)) + dialog:set("replace_original", saved.replace_original ~= false) + dialog:set("expected_kind", value_index(EXPECTED_KIND_VALUES, saved.expected_kind, 1)) + dialog:set("maximum_points", saved.maximum_points or "512") + dialog:set("tolerance", saved.tolerance or "0.25") + dialog:set("max_segments", saved.max_segments or "256") + dialog:set("group_output", saved.group_output ~= false) + P.add_preview_controls(dialog, 21, 2, saved.live_preview) + update_features_dialog(dialog) + local function read_options() + local entry = FEATURE_OPERATIONS[dialog:get("operation")] or FEATURE_OPERATIONS[1] + local options = { + operation = entry.operation, + group_output = dialog:get("group_output"), + } + if entry.line_length then options.line_length = optional_input(dialog, "line_length") end + if entry.marks then options.marks = dialog:get("marks") end + if entry.labels then options.labels = dialog:get("labels") end + if entry.line_choices then + options.tangent = dialog:get("tangent") + options.normal = dialog:get("normal") + end + if entry.chord then options.chord = dialog:get("chord") end + if entry.arc then + options.arc_mode = ARC_MODE_VALUES[dialog:get("arc_mode")] + options.replace_original = dialog:get("replace_original") + end + if entry.fit then + options.expected_kind = EXPECTED_KIND_VALUES[dialog:get("expected_kind")] + options.maximum_points = optional_input(dialog, "maximum_points") + end + if entry.quality then + options.tolerance = optional_input(dialog, "tolerance") + options.max_segments = optional_input(dialog, "max_segments") + end + if entry.properties then + options.axes = dialog:get("axes") + options.vertices = dialog:get("vertices") + options.foci = dialog:get("foci") + options.directrices = dialog:get("directrices") + options.asymptotes = dialog:get("asymptotes") + options.latus_recta = dialog:get("latus_recta") + options.auxiliary_circles = dialog:get("auxiliary_circles") + options.director_circle = dialog:get("director_circle") + options.general_equation = dialog:get("general_equation") + options.canonical_equation = dialog:get("canonical_equation") + options.parameters = dialog:get("parameters") + end + return options + end + return execute_dialog( + model, "Cannot create conic features", dialog, "conic_features", read_options, + create_conic_features, + function(options) + remember(saved, options, dialog, + { "operation", "line_length", "tangent", "normal", "chord", "marks", "labels", + "axes", "vertices", "foci", "directrices", "asymptotes", + "latus_recta", "auxiliary_circles", "director_circle", + "general_equation", "canonical_equation", "parameters", + "arc_mode", "replace_original", "expected_kind", "maximum_points", + "tolerance", "max_segments", "group_output" }) + end + ) +end + +local function inspect_selected_conic(model) + return inspect_conic(model, {}) +end + +local function revalidate_selected_conic(model) + return revalidate_metadata(model, {}) +end + +exports.conic = conic_dialog +exports.ellipse_from_foci = ellipse_dialog +exports.ellipse = ellipse_dialog +exports.hyperbola = hyperbola_dialog +exports.parabolas = parabolas_dialog +exports.parabola = parabolas_dialog +exports.features = features_dialog +exports.inspect = inspect_selected_conic +exports.revalidate_metadata = revalidate_selected_conic +exports.state = state +return exports +end)() + +local CONICS_API = (function() + local api = {} + local public = { + "finite_number", "hypot", "point_from_table", "line_from_equation", + "line_from_table", "bounds_from_table", "normalize_conic_coefficients", + "conic_matrix_determinant", "is_degenerate_conic", "evaluate_conic", + "conic_coefficients_from_five_points", "classify_conic", "conic_properties", + "ellipse_coefficients", "steiner_ellipses", "quadrilateral_midpoint_ellipse", + "ellipse_from_foci_point", "conic_coefficients_for_focus_directrix", + "focus_directrix_conic_coefficients", "stable_asinh", "stable_acosh", + "hyperbola_from_parameters", "hyperbola_from_foci_point", + "hyperbola_coefficients", "hyperbola_point", "adaptive_hyperbola_cubics", + "parabola_spline", "conic_gradient", "conic_tangent_normal", + "conic_polar_line", "conic_line_intersections", + "transformed_conic_coefficients", "conic_coefficients_from_points", + "conic_coefficients_from_five_lines", "conic_coefficients_from_constraints", + "ellipse_from_center_axes", "parabola_from_vertex_focus", + "hyperbola_from_asymptotes_point", "degenerate_conic_from_lines", + "degenerate_point_conic", "conic_conic_intersections", "conic_pole", + "tangents_from_point", "focal_chord", "conic_equation_strings", + "conic_arc_definition", "parse_conic_metadata", + "create_conic", "create_ellipse_from_foci", "create_ellipse", "create_hyperbola", + "create_parabolas", "create_parabola", "create_conic_features", "inspect_conic", + "revalidate_metadata", "action_options", "preview_shape_data", + "preview_shapes", "preview_signature", + } + for _, name in ipairs(public) do + local value = M[name] or R[name] or P[name] + if type(value) ~= "function" then error("missing public Conics function: " .. name) end + api[name] = value + end + api.public_functions = public + return api +end)() +CONICS_API.api_version = API_VERSION +CONICS_API.version = VERSION +CONICS_API.dialog_state = D.state +CONICS_API.required_functions = { + "create_conic", "create_ellipse", "create_ellipse_from_foci", + "create_hyperbola", "create_parabola", "create_parabolas", + "create_conic_features", "inspect_conic", + "revalidate_metadata", "preview_shape_data", +} +function CONICS_API.is_compatible(required_version) + if required_version ~= nil and required_version ~= API_VERSION then return false end + for _, name in ipairs(CONICS_API.required_functions) do + if type(CONICS_API[name]) ~= "function" then return false end + end + return true +end + +_G.CONICS = CONICS_API +_G.CONICS_DIALOGS = { + conic = D.conic, + ellipse_from_foci = D.ellipse_from_foci, + ellipse = D.ellipse, + hyperbola = D.hyperbola, + parabolas = D.parabolas, + parabola = D.parabola, + features = D.features, + inspect = D.inspect, + revalidate_metadata = D.revalidate_metadata, +} + +methods = { + { label = "Construct: conic", run = D.conic }, + { label = "Construct: ellipse", run = D.ellipse }, + { label = "Construct: hyperbola", run = D.hyperbola }, + { label = "Construct: parabola", run = D.parabola }, + { label = "Features: conic", run = D.features }, + { label = "Inspect: conic", run = D.inspect }, + { label = "Metadata: revalidate selected conic", run = D.revalidate_metadata }, +} diff --git a/conics/docs/images/conics-advanced-workflows.png b/conics/docs/images/conics-advanced-workflows.png new file mode 100644 index 0000000000000000000000000000000000000000..c8e6ce7894f27f6ccc21892c1293fdcb71d118c7 GIT binary patch literal 86198 zcmeFZXIK+$yEcjy6%`Z_m1YA36jXW_5tSN5dR1vsLkpp#C<=%OC*0D%MuA$#z7*SFU9WB=aY-p8I_b0nFWd#-ZcS2?dl7#ch}b>h+qHa50X z+K(R?v9YlqV`DqGb?hMU$!{weFxvq(Hth!*#sOo?1SpJqPUz4>eErEmI{yk&cJ^lL0e@+qM|Nm{-{?8SHqQ&oW zsQT7)9cR6l?RtZBjLP;reDoq-?4@+10 zt8i=k(;sL83hNvA%o1E>q1|wa)693Y+@IvhKh)?ivM_Rmp}_NoeD%#{GN0Lb{f{?mNPPGT9gg zM<$G~_;(2If6m6XY854Y+qJ^{yp+|GJ`C`bO3H*Le*gZ!fynn(v-ex6>8h^Y_B1vB z;boCFsVaupiue=JbvKUl6DOPK>E^%odOFItDJPjRuZ2rM%U$?zPW|5-{7$ zq+w`DPEo4QrTkD4c0Gp2jbHcu`$(2`wf|t-bF#dnTr!DS60optA___o8|2k`@D}1e z;xwq==vm{PV{ECd7r0rY&n>DxUF)zse@3WY{o6#m`p)vWJaKz8uwA!Djka}-{tiuD zU*{B3_1>C~XnYhSDx78A*(pBAdok)^=>0P}1F{hji-Q@2A$Tw+pORsgQ9Op3NfoPO z5P#QZx{*%j?`>&uz2z3|=}fMF$!zhscrj59bE`>h0 zrqWayr)}G+{z!<%u8LCy5a1not;Uu zyyx;v@_KuJ8wiYTE{(fwbjcq+vglg$veq!A=|sr0+ZRxhcI6O85tBW}x}*Kp&EEW> z#g8A;=?y z{6RSVM#sAg7$Ml&WVoFIn)M$ul=Zu8lI_=IG>;_Y_(=PA?R5DfZ39+&`@T%N9XQTDL^4wVe|#U3|SexG*+#dbLBZD6#^X)fOw{`GY;=u-rj5lSu{*OVNeZu2)vfB3Pfl0? zwpSRB5#2P`m+>E(bwbgWmA9xN!sAeKd(}U}aUIh$$=1rNi^^c3rwfwXvu*oUd0D38 z=X`7yGEq;2B&=B^aC<%MO84t08eHdZS}$VN{db!df(2naoz>K6;e`d>fdQ!^+scJx z3JJc}H%^AyKVxC<^nK!$E)~#CK$|nV`OrB-b4`-G%)M-C+TBFI&iOOZ>WouZMTU_9 z9%NDY@XfD(;9{iSPc!Pb%=s@}x@*7a`wfi!)%c;r(gTd8_5lI|%!Qu=l%}r)ms@B} zLo#*-pyAfa>0f&!8NQpz6qui+ShHjGKU(aZLi_IwvtFL`nCtZ09oW^9CbBveotwDA zusJt`HyhFX8#e^>)1J$%IHS=`!wBW^zY$4=5uD8;pZX$#g2e>e# zATB9Eja8XdFS%piL5)#g|2dna3)w6Lr2FfUT`w<>;#{WJK$Pb9y0uhagT0!Uc$}0< z#z)jZ$4$H9;q9K^=|875*|+^g-KrB-uSdFmz2UauzWD$yL08I^u6uSj(H6E@SgkJQ zr}#1^O@XJGD*{nXh4?LHm9tKD-Lqc_N8xvu+1NESEvihMLavu0kiSagl z4-Hb(lwEfT-6^il44M!&aDpI~>*DFJDH0c5?f8}&`*nIc-c9ba4HXbcxA#>IuA?-{ z_;r!IyyHDM9C9lHrN#=X5S2*2mB20tCl5%~L&-OkE}U!uTyAG@q12IBf_De0s;{K7 zr%8_Upg%Xifd4KgrNKOtbYr@;GO#aO#)T#kbbF`LYLWLXzC+ptqnmY1a;t41Qvg+E zg$HlL@pgyMLT~`dk|e4uAA_^HQ+2U{oi>t>FpaM@A3X{*^;c8;==#;K>0lC!71#ab zi8E!FLbXue{gRY`Xw1!-JF4-Hj16MwbSRTuczxesgn7?0PPYg(&R zkixaMTU7^XR*No#Zk!|ufAhR#+V>Jl8Face6?lHT;)dIf`=()?IDHH)T{oEpyx*9x z_@bea(as=s0C4d7Nz?tn<(_^2=)rHKSR)#l)iZli<8_VO4*iLEyrky+0@LvZVVnf{ zQQdFE&i9Rt$auAOzukY5Z4#pol> zxHB%4V6)dhSow!p(uRQtdv;q8xCl~kEeI<=rXra4?8HUF~ z_7TX@RI=WFcR(t#!~e0T0A%KmY~(=qG6A!K78R@;iF-6y{&KQ_&Vy2vvcMp85gHlP zvls$4U;(+KI5m|qa!Sz9-_49xv)eUdXI;HavS-NIDqz&-2FWSO3CJz-QWtl!BHF1G zuf}k#0NwRn+3AvevjEus6aNdkkG*!{9)$ zKiFdJXI-2MJ{oj7Q|V?3;B)U=I+ZeQ9uu*?F`Q3TSLU*%<2W5fgpy>YOi`4b#W-kF zB|mFI#bSLnx3pbs?qpfn_)5s-Cb1IX*cL5U75>WL-$YRGEd(fG6t+N`>hwQwvtO#wb@u2|WMkSC!^&cuP#1HS1mC7cMo zswPQmb{2$BEj&`e3!~25aVp)8QD+S(PSnxwxmwmxWAA)>Sf!Fu2%-osv35M#Lnsui zJ*-kvps`VN&L>ePOGode5~5QdcYZH@$o0#JUA=dHxT3!!8i$hFy8|qhsccFNWL!W& z3r)wdZ^BbhwVRWol1mS?9u`>Bs56|zff^HzcpaW|IhfjD)QBy@iS&JLWP>tO`v>se zsdc=C-5v#4q6Z;Q>)tD+3hfOKd(XYVZjz)6i`Z&6WY}k%rxd|+c#6qc?_=RJ^TVxv zZI!<_1vn-^jF_2Bw|dN^iSC#%F_ zpbB_)OPu)!K)aW20lSB1y~jpT7g9ukxIMs`7XJ1y3e!ESZBKV+E=?2*5RuH+xHoEt zHAd@5={-+ZB%VLN0>>gXl@Agwv*yC70as z6}W9dR*=bYK-X+h55JxGKprjc$y(w$dlm)+a?@_1_wQ`fxLK>MFI^qEx7ggdTv8lE z!fkvNss$T)89vLx$h_EsC>0I|M&P2Tj^LvD!cO>x9g^l5PV*4odEfSKwJ1edNvisD z+|v4uN*@Si*yYy*I>>t1B+IB-E+SQN#*rjes^#1*qG;=E-8oiCEMM9h-awt@PD&sI z7&|ltgX$2 z1YuAMALhr`C;pO+5LEr0<*k%tZ3ZUJ1j3c_{ax z?jx?ZpX5&lQVPwulbYRR4!k+KQOAg0a~!D-oZSS2()NX)KXj7s?)>XuFw|md0|}~I z%pNnlAMz$aDk+#QFf;Qa`9^$)>8-P(`c<1lsMNE>l>v-1rlRo62+L*f1!iDI7R98a zt^iWq`97sKQTIRfWt&6wK{J&~lTwj8&r!$ztY*By__ch{~$%wvKbsG@}uZ7@aAzVVd<0V=lxcgZ~fsPcxcyjDR?%V4vEZ71q zC0GC?US?h(*`v1a6>^1E35008yKQw)BL}+Hd*GjTLa^O!_W4&M_wN6 z_})xnH;PJ#zI-f3-KPh6pGROkp|o3*Ue%;vt!s#;d*s3A5=DjF`)U)DdB{mJ-P=7t zMAu~nRoe4QG;jjRUPGjcAfd% z>hcZ)5`EQ@xpw$r)WaDeWR1ZOuL0zn*h59+nY%hSBpCf+UE|gXRqogCT)|F&Z$bkM zEF|SmDybX$%_OWy_Q)pR_B6)dy2*0u;xXe)^XsuE_9RNRm~3ik%^CbyN{7DB`z&$! z9w8grbGL5E5G8!{P2bwi3)2J~CPk@?y7~m}sJ^7w>ZhtEl_b9Mg9Jd%6=@u#$QCB=5BOt(tUSV+`KgG%6Yax35dKCaV z-MS%fLh}3;O+-ogx&5yZmC-eQSxG7KEDHNIK_#DEm`@(nN-YhME$4Pzs-#U+%VNMe zAG%mE;a1;W5dD2kf7T~awAI1i-$1qCzhz@qU~QKpwFB-EZE`dWyDwRf?fh{GwVecH zuQb^O?Uj(r=gw6fwX#XYFFwTg1R2;FTWe}P%@Uxg6yFbg6Jsqhyj9tY zyPlD5NE!bNw0TvTO^y+!RtUU{Y*LciN7o|1!x}I9uh-};+_2`jQxt(a8#!c@^|U9_ znDM~fT`%hpQzq$Eab>ih?K;VBX`RY(Si>u@!3X$?g{Xs%bM1$DTIaVj|Au)6goNOg zk;I`h=|$-@3EzVACf4(i|6rru%<&HiDKTpD^6Ev7P!y&UvAqdJ4eeQ$+uCh_)0^f# zrH5SpoRE;fh{`(pCY&LM=tR~;K8j9JfdBdO>)Ec`P!MVE8u-HW`iih{mK1i~N#Aeu z;9#Cl^wC7j%oT;v%}+4NPxIpNVc+Q4kq3Q)4*=gecA`HXKeP_b&D1+Pp}y1m?{1S< zTSAr5gdr{uSuY$5NjHrbOJ}J)GPxmT>|Ba>QyNk`bBTYs-4&{TiL|?69@oCHG_8JE z!<@+1OTS{oi0Gzws~ZDCwM<6N+ngj5bLzup)~88@qjyk9Kgd>+r6=HtBox zjW>SXq%w-h5Tv!e^Fr}-i)P0&G*H2Z2p4>D@8q2j4#nmIx(KV1S|AITe3RTUX;bAn zV~P?)8;ip?eqkO)@t^1Ao%yFdsI!S7?>T8ZkjT%?GgmU(R?H2cMx3OlvBO(j?D3h0 zOz@vF@s)e}=k)cjn{vIK&v}dz)&}C8qC`Kntp3`(BSU& zl+?7#!IwU)lUcX&17DQBW}+=I&m-;IK~!EKac`{l5O#X z5ka#q!QGE*FN3!Va#jkR5%gb4-|N;re`|ZdQEdl+|KQDK^7^k*Qa9#iwa0Rolq%gE z*aY~W0C{%@*;ap^u;hah1`0~ZB5P8+GrG26vvf(${f^a5ld;!HHNcD#Dh+_IvBdO0_A7f+tOQa*Vckj7+WzEDHvutK`ZNkxG z%N0qZBn+9KvP&wS$({^gTq6ma+2Bhp1{BheV2|Om5R=bC>Q-*#+d26@U~@f@r`0@! zol{me3D(2qr9FRxb~e_6kZiQW=ad+%j{ou)Dk(t{8yyktK2i~nJ0Z=YC*oqIu&GZM z29Bl>XvDsBv0Pr$qRHR24!fg1BHwN&cS-7YsPR%TiuFJ+iw@QEJj)HSfR3a$Mxtvi zJRgYFr>|z25q)+q^6?F1Jq;%D+?8~y_WQ@?i>^)}WCtUJRKpc#hP`k3i-~HI!;VJ< zHMk~WbZ(Sz{;>N5hV>EGaODmA;NC^!t_@Rh@w;E2hWkr$5ShO@P{Km6b%?ijMDohk z?0<>o>FcV=W;=gz@w5vv>1f=bN%x`Sp?6FBBb;iP8MsCekj(nWV=Lv{B5K4t7Sjr4 zp%((o$cXtA7Mu*@j<|dbi1vKv+q_XjN3G#mJrb`M(;R`#14#NV9Q@V_`l1%J>j0^E zRymHe*#N(Utgp;in{ELKY>bNH+(x1=5CY#1Y8j`%cW8_>(?U#uzB-C8AVr8FjKZ}{ z_}Jq?vTln?Y3rO+M2a+JL|3soD=Vz=5lCgPQ^Mrb6O&dBkot7Vbzn#O$k&S8-7Xk# zjsx`e#Ct8{S}X91C z1lQ5qWi=n!DgF*aW0}RP^}izj%iKV!2qfO7j@3ts7ElMOk)={|Fek1 zB>GWI+QxRTbi{+Z+q7byI=*u`d}@APr{+xlV~Q$&O|cO$MSqIw?dR)_?573qBeRUJ| z=4xk0v~a0G%M|nWx8CxRbq`QHU*2S!2*+u+PWa4-+QH1om^VcWX5>;1_EYouruiq2 zezT8gcL<_oE%abz>I_P1ko6LjT6pKO5U+w%r_>FY{hvqRPCtQ~uLDfy_sMSTn?lf~ z$EbGnu*YakQD@g1&%yuT^#wJ*-gi?Yw`}&(EFLA?Wa8$2`kuddMh(7L3V{fkNY}+* z^2N0Y{n-op-TQ5#DzGx zp!}cjl|)uqX6Bbbr-uZYPJO1eK9JZAnmjS3x)=5}rWbY8{lfon84uIvQRB0@f~wzJ zl$L$+H?sE{{{85KWuY7@9C9M$_Rb%^2|89J8k6N-=>-ICNa*8p@1@iOm1Sg_y3W5y zet=SL8Hg(Jm0*s?v78r0d4Zf8q#9Ju=%UruT2U-vElKNpdAL}T9DV#Jbf;}ZWl1z>r3mKRO!ZjJnfDn%=|jlWQJPpJ$%vp%|3@*M=n zF3&uI`ONNH1MqH#zVXR}r5Iji$7duKBxx(YTKA<#FoP*V#$(?hxaRs!*P*M$6EV=0 ziRXP%r5%$bvMuiXakGtiTmL{H#YlJCG1v1BaM;~|KdqFW9D`T6BJaP8oRv5hqLa1bLL&{>BF5k!9*W0L zDlCry6@jrr-BzWd(|IUckiqL_4y@bwcgJdq|5CsA+Aifq$HZ*Dk(k@`e?W>OWOe%y zSnF;0P`4Tp-nv!7F<$Am66j=Vyg8I+`L*bMNs_Z$l;F`JDSC_k6%>*_$KYu$mrZ)b z>iQxV*gu-|chh3q4NfHVavRdfZJsvD@hKKr?#{stqffwCMlrR4h+=NGl#zH&gnr|# zd_L3w)DJCdU~}yQ&Kw+(R~s~y%iMei-YtQvU8?l{=<97)6w6b_N{NnB|5$l`#yNVb zZFWS0_Fs;I{8A4mO#-zhpX!@;uhzCY+B_o2xc9!oLSk30h1o%Yta$`HQSqEwQO7uK z^SGx?>PDHR$C;^&GgHr3#wviy10|n9lcbm0KZ>~kr=D^f_82R3Y@Iw5e)sNOD#wY! z^KQ>RO5V7ATj7S`#C?W;p>i2`;JZnrkMbxGE??3qcyKjvbyAu9MZq8FnOIe-6#Co&@n?XbVi;Y+F~^DN1T$GOIc)$TIy%IICkU$H9~nr&xithVFY zn*LF{0ZNf_OLa`}&cDU^Rj9+yz!R6*a)7dHZmY=%=EDKIVh3BiVpX4ECo^zrK&tl7 z*3%3_{>i$Ei6=Ft`Gqzk*wCG;t+DjUpiSEN!?^G$%miQE-yRN&gg^LSjkW85U)+OV z7Up0ZjK0=tpu*_i05%n#!rROhPMfb^0e>dc_%J*U{kbdM`6J_v`1|M;0RF z$pbBB#>C&~?{quR$}MkvQQVzKU)m)mMAuH$Tp6()sB>!t;y(-_H9n;j%*w#~^16=~ zx6WOo6X6)+4vPG#82pR&&d!}RQYf%*&eYaUWP{$zxsQk}BJ_4z@Yi}<>)oSvg)U28i) z(a+t;>5`;OhYve&k5@`fhBw(ZpP&p@csrSvxyG?PQEwh{uhKq<@zhxv=BRtkzuO8L z{ip%7Azt%S!~eagrup3ywoToaFWcS#t(f^eWgZ{#oQL$@ti&Pn)6h#!u>>~<=mJ;tj{PN%%zpiO}WynjY zNm}a2=~~MS!;dymuPh67UYbuLe^r^224)}tI*j-*-o{8;phhbanv%bf#H)%y#-B6xJWc#YTH1*KciI*EyGy_vd)AuX>tJ z!|N8mPOpId9__uX3wQn(p`eX<0DHx4SKu-7-j(ei;`=EY+> z4X$PX=L0k*U&V@U-ABiAeA0tZy8-(8FEje*$9uM{-ePGAObI+af91G-@=fcCJ(%mm zxcOCW`>;CNWPuxlUTeR6n-oQv2;PXZsE-nYtgQZmcMv2nHlwxFpDiAYr&11M?lh4* zXAUuJ5u5P@2O}W!<*Ohw{3lCV$#t9W$pZXBXL!{DS)({ig#;9NKje^<)0}aV5$NpF zWGqiI!m7fS>n$;%kk{^5$nOg&e$#sEHC@3ysUTdt#7V<^OWBzEwHmC4r5vQa#k6aN zBxT_%r)ZP=5xO+!1gu-h;}x~?9O?qXrpBNCdPX>_WlQAKw=m9LUKC7~1SPq9r+w6^ z$HDKbOkw%8oMZbZG6x+hc8gt?qU#=&NX@m+tpie)w+=QzoxUX?@j3XnK5_~hYt)F= z9a$Me)@BY~{u2WgeQe~jBOzUXQMR_eDs<5y2`bvR!Ama1FT2UDOqMb44QHun{CM;z zY;yr0Xjm_(lhr`IR`IpOyuwGfT6sCv3-rE``mNH44C3yo$3-^&ES_^(yHhV|ll&zi zci^z~cs(gfbLT|J57)*b6F&dB**}lohJV0%KSSe`0=BWq_UQ}U91EBdc1^Q$IqjF&*#b9slv z=NxUuqb@4mc$~ea^fyqt_pB_T44lHn&4Y!EWO&Ql<$uKS<6ZXJV|lbj2KMOyJjq*`^?_^{4)P!8XHv!hEXQkF$t#vY?JWW~J55;Y#&X-$nODVs3?bOp2Zo zLVTgyyODi!Y3k2U>Ev5rn4$KbaX4?{pH1!S#~OBtBRB2pR*3F;?T>ZZKjIulPFFQF z-ol7+09ESe-o~tV>#H)k8<;%b3tzN#d_UhWYFA;=%Y8y?)6?f;hOC--?%wrPOWCy2 z8-yQUyJIr8QZlzR{qxg68iH>1z`RYGQ$P9`2ki%fTYhfpxv6h%{$l&|-Y9KaWqqoe z5*P~t1nbf#4kVSwLfYN&KOn^1QZckT678E$(lYrA6OeqMh!>Npm$cRECdnb7$-$s6 zs3F^v>exXW*j>t`y)AYRaBPB7#pzS0&mDfgRHSJI74W>oCrke0W+l=hc5XfM^M)da zX#uf@vM7P6^jxF3*DBKmmWbJms1X|;o--oxI)^nb_~@ghoTMg8sJ*}QS*KI1+uGC+ zN`b3Gsxk)_edzu(G$x?cA(ulfA1(}*KXpryyR3RGcqO6kZel^+aVS2`F~6b)Qi!2eGNRhX@P=N-)V94tl3*+VZ{~(vCSV3)SXRgIn2KJURNh3&0sI6JJwO6Qh{Dp zxuT|$V;A%M4f^D%<4S3b=!>?2HqQOL(5<3D|xX( z1AIpj85e%sn`-09ZKMcT09?lmJONpBcMULKO|cIzcS|uea-FF1U`?ybcQZX4V~SQn z-uUtGY^)EBH|rLKvft;I4pE@A&cd9j7+Gcd{WqWxNu$LADxl zb&Ow>@8K*>(zS^Rrj7SQfUUsyErlPR*P2?YU2S z16+VdWWkjSW}_-sXsK__rDFe-sK4M(~b3^a!;|s5((tTYFO(^d1+g1u6AMv^o*Wq#| zzq;AbTjL!YTWTgh0meaHX3@U7A#{zgO>xo2&~rIK3PksCC3f z4oT*hzj=+?-xsH?rs!DPR<`)%!SgE`A(yR(vkJG4g*dJCbxorS`xD?k27%`ML+W2u zRP@D}MT`7LZIsHA6oTGu)i~2>D2NLtn6-Ai(po+p1ofV(N-iP*-WeK+Hz5E%E>SY( zUW&B9It`?{v0HmH<})1azsC=QQ-z#lQ8js~H_0&`Qr*{tQW>eD)s8PF>MCQ-aeVd2 zcLJ&1CgVrc2<0sE>eqdjJYMYU>u30`qv_7?%N~{89y9?ZZ?VeDS8tlbb}hU<7FlHv zPoHqPh?h%d7K^cf?tZNuHI26+YvLeDAL2UFkku%K!X4dQDt@^g#_b$CUaoWdg1(i% z;Nk?<(OvI>79-AkE!Ol~(V`7?&Ig~S^VBk{DQIe=dQe_%_t%T!nw#>8UERnEAe7m4 zHhz$E`B>J`ID2g;f6td*A4jPy`OgE`c;9D(jsLBPU?=|~1HQ{)Q~Wg$&r(48oy*qp zM{E##e(rPmKXd+dpy~MxA-FVa!AzappJ7tEHE-ILEFu?``@hRwMG;Y}c5 zcRv8S=9kvvI$5#3AJZEezUGnr@#{Rdwtb)G{!)1Hn0V(Jd!Sjp$y5Qlc+h0i!5irw z>@B#xIzd-y2b{-G@?FlmUXdqL7cCTr3jPL7>7MOO(cBSh2!8U7sGJ8;-F55LOm>g6 z6N#XZiM9{pUt54o9~!haFJLXmBq%p`q$5-liY98EX2|}9XDgOfj9qV^uKsQYp_yL& zbS&f*)nwLJgK1kfy7*ucX4vx^g$hg;Vt(84Xp#U4trddEMHBH`R7ha$~-zp@MeY z7u%)9A``$T2VDfKgZ&0ZbCT3v;8ZtZsfv>+BC)Sxpy0`UD-`4;$E+-Vc{D_V#1itm_7f+zdyuUs&&qVeO#z?a zs}!mVG-sW%q8|_GWp$9Fq4wXd#p+#5!)}&|+?#7|dGn^MPKr_m@A(wc@U=YIzx6>V zWMy=$Ye)aZRfAM*#h|;n_Tzu%inz|@0R0^aQDR_5_@R)A?6RE*9&O>X8eHzIiJ*m# z5HTr*$4e@@K2H9V%>~wl4{fqmfZ!=b7wdK9nsT?xv(Hqoz9fTpqT;t6fnozi#8#HK zIl8%hg;abjVfp>L+PF4^MvKUxMMd&aJXB)x%Pz_4&pf%FxB3Ws^q0N)r6Yo2*TSs_ z#$C(d%o$4Vf(3x2bBIxk`bolJjV+F@dq&&Xg2P9dN~ethjg)|`2+EegVQcxu3+ zUO>U8X7E{Omz4E7={v-VCO%t&!<0d&za(m)KlbmDid=Sx{q?e=yFHDGOT*U0Ok(u% z4ZLfC%%MD)4nzxbiMN1w{7q~3IUbaQO$QZ*BjXwxjc7Frc^zii+P8#`4iQ&?a2>7Y zYnE=HOq~(pr6#7h$s~eO^|x2^ zw}E2sm+>ERIDi4p#sO%6xD$}N#3o$Mr+{Bqn^V#`+KCRHgT`RGdwyKU(OmW2*mAiqqdz} zG65(_C5%zjymq5FkzA;@%zkWLjXOTD?c1nEv)JbCVst%=kXxDyuJuFShi%6+Mn;_A zJat^f|L($TXW|)DCli);`Pd=Grb56*CntyL?vH1oRVu|UdN6lm#A~e1ks!h=+i`yZ zgX)h9Z3iMA%tI`~Qy9^Te_&`tZngG1(L_TRMbpKxJ+tL8U=L_}bOOzS80eJ8c2JVD7y=!fzG4zhTzb^G&&0aAYlY|*EOV+?LlCFYoR z?vV*~0-7C>9sU}$O#Kt`~9Lyj)YOYj)z0Q z<5vG$IyzBFY(qRKrgKi@gR2wrF0=3z!IIyxW7GUfk(YP8xC8PEkdbG=UNuo}!mn|X?Rl%t*ICN9eGWTQn;iaQA z%x-fl#jwrM_6N7KxyEfkh$wd}c~aW@t-Yx?sA3s=bK-Le=>506 zKyv;-6SeWDD6D+^m&TYFXcC_nxXWA0TR2&T(Mj9f2d&)v|IJq>zB>+(1XD-v-Z9W? zex*jRL%5b$W);lc{X29(j2(4zJ8=nln_d|&F+vFdkJ`X7L+nWioqM9x-AddGQrmpN7>+2HRu5KfKWwCt z`j>nDD(SK)M;^Fu{S>g*owFl)*$^Lo{QA`S>D7jiVEVjqCFawoK$qsRm2TZZ@wvC} z!5SW)6Yq()`TkvtA$L{$Bzx1(moL43|8QeitN)O{I4!|j>Vw)V-v){;6SX01<#{$s zO-~vbt*s9~N&mfyEl1>kj9mO5Rgn7s!qmqn`ROtNP{;RI2iU&Sx~{oou(6dHmKn0W z3LCvY#08AefMK~`71`Mad%mZfKf!j7;|N=NXrAaLt7n7NiuCZGa%|}tGF-p~Y?i=e z%`M})et@1Aeem1A7Fc#v3N^t?!Rho zM47FhH`E~q*=nB-s0Z}_{eJ(oHZNZLf9o6%Fv3#MrHbN5gW!A`VL<6OVtMU#^vEam zrFJg1d#YzP@OrIF4dH2`n!wD}v1<=mbNoEF7@}-!kY+`3OBx8c;=QkN*>V~i+o12^ zhb&Xi3!g`Cu%&m$^BonIJHeLYI~S%A5p|I564(Fmw*NWc^pc1SN>4Cs#br}C4w%+iAY%(tnKU_mEd0v37uHISOthQG!vpkjf zJ1r4d@pg_VUhCokwp$0qju`qL`!F1KYY71+lqhZk+n1H(n|pQ$k3aInE^a%btjmPe zfZf$dHnt7Gl=t5Jd`WO4lmt4hUs-=0djxpOk0;J}LA$q_S=#;5Whh5=rnJh5MouD)}wZ|0LR<;LfN1m<| zKudig0Qnz4AGfXZpV`qLH%L|T6%z$CDt4S`oDSFzBpRmg?Gd?mWAaNylhXXfn`#AF5RM#A78^?uWI%eE;G7yBH523wpQa zQ91?QJZB_EEj+M#41zT$jpXYLJ-fX>5CBX!2&(#v%S)->csz3T&hyz*nk^p$&+wm{ zH!n;#WX*u;3vDrLvkSgKb+-9^G9Y)kbYxn4WlUs5wBLY1b^9I8QXI9hCGMO_sSWKm zhQIT?ZdQ2FNFZZ791P#USL&kBI+%}r)lV@zx_^#`{P1WcbE-4_iF=Kv$>s3zVLCx} z?fV(cTo_wVV1Q21_z%dg0FuQo>(Wq2!EV3b8ha@t=Qi*ZMkt1l1KrNW?QN@tc%)X^ z`feUJPu3L@bVIi9Y5)O-N`|D$x0)=TGb_EQqhlZ@FP-lP3-T7wis8I`dy%&#>f&}c zdz!^fQw=VJObxDlf}h_y6ZAdX&aWWB&aevTJFY0x_qGPW zR4nUeg|?q8G`B}qzUC%CG7T0Jg)+M9h%5yp9$X6WF>#2zsw#(#tzdx0BD&{tH*vUt zSPKj!%|#-Y`*Z8;%k2QCmH#i_3PRkIRYB7=>>9)3&+d`1_69_ zMV}1^_>)ieT>yh0n3RF8;8}C>zUO<=_8Wr+l7Qa`I^D0!Gma5Zhm0Ho38#R(Z??C$ z1LFnb0Q1?$b?ifKPb3F@Svg>Y337CDREBn*=2rrU0DTrj)-FCAe7f5I9wh0ykf5C4 z_ne2N`8w@%CGh6XG#|KnW_~TMn**6tKc7poD;-~%P79_B)c{-#nt+Pl+@XLr%u}PQ zpNto*I#iK$d&cpWGL%cxW6POUBpaaYHyk+0Z?>T8R(5fnytQ$iw6JzStu^NOrm>&# z?C;lRk!X4MNq;fXk+orTWJ09WpzF{r`BS`yr(QVGwv+|W@x5AI?X4!leQ2Y!Bfj+~ zI5~OTcD14Ei-@4sY9cZz5taiq#iE5t(JO6wYS;I;ksC|Ekl6>99<;3*YaV;b#hFGP z$F6T@3!O*b>K~9YNZGTkwwkPkWZT-+`ZtGgv&-qH$_zhgM(*M(qaod2lv%(8U$dak zi8I)}B68kN+v{Rj$FLCjpxecqhQ{PE)N=mEP`sFHAK^Q>0IPmT$^=84xi4 z)efr|svG%T!kw}Cr)j=?Disoh%~7ZM=65*4bZ zFJkNBbsSj;Gd0?(s-yfoYvxqzw0XlkiI2q%M zZx9Ny^S&s0jcpOuBb~8loF@dQ70!tEkGDi8t?$kh7vFI$&jaYKoCGLohUGvIP3!59 zx^d1W}gj7P@?&56W z5ISQgL9UOmf%sQnnz51@MaihA^CAy*7J|ZnVvox{*<hR%0k!m5Uq#KMt|eI>z^q|3(|faxCrx}X6+iw@sF7EZ20y*nH=(U_LvP>En0asU$`W&u>)VkJgg zVmy=Z#ZC^44|{s1v+?|}H+rkKT0t5m{Y?D~)SF9FyE`HNkn z|Ak#U|Ak$To|VbDThN1-61wtt#Bz;+X^}1pawF^VF<@d4h?{aDxIY}IT_&Fy@C$&7 z3G|JITrnS0uSfy}_=uIF8&Ee=+(2I3TYzHsK@b@;6OQ&t)caGyVdLr?djAU6OT}21 z>u~GSXV=ea>*;-_It!nTpe6w$YNEkLIB`N&-go+UzHl-gY6wg5zoNQwzvjm}<%59A z>S7ErS@8V%hV5gol!%&}nMUQ79t#(F1xAkEN|L8Kl+WiNMg0xi<^T>1Fj09#<94E6 zq6I2RHi{bfuk5~F>XM|ln~BH2cxJ4Jkfm0*zyQi4Foqjldbz{Jt*`Hir1OD$LLHSu zDGw~mgBNhA^^@?tyjw{lr@UAEjKmAW>pZczE-z5hj5yrILr&8~NlmpqFbMXl$6Eqk zC-;1!E80#LX;ZcgY$yP4@jT0K;w2?xIyQE3xm^O?jZ5OFXzJ=D|l}bz+CUy9qt>{FW<&#w&XNM)LjXWwGQ!@?9q zoM=~NPx6eb|9iLprRvsqeL=S*E81V>qpd5S1N}}8UY5(Wfr@m-Sy z^|bkITa=tXYlSwJtveeryC0zfle_yH^m5a}fOGeXy4URQDf?-R+c3Cdd}Z^>)syLJ z-@550SxQSdmLki-Fb6!aEv~_p8aVySISuwB&qHG@)UVjDTR2N0^^VySe~ZG3&I0$= zpWLO*(*dzbM!6r}pG1#0wltnI$zIr4X@RF!>L)$O&a~y~3F#~-s2Z(*^w`nK>5lce z)orl_>nD6Ef8;R%cS;eqLnD5g*2J3IYA*Y@Q{H=W$l6yghI3W%;fG;p^r!vN`1)jO zB*1g&)#$!98*Zz75B9{;b<1rt2wkr$$&xoD|HFNc>u_?HXDJh&SoBeXe{;D)0MpEO zATJl7I|`u96joRz>Sib#yjet{tH>aaRDZs)IAenx2uurgeOX)2Q%6*LdLg~5+a2<% zoy!irl*yqkJ9^L5|$oq>I{)%4L;ee$$RClbz2XlHMn-@D>k$%{|xmM zS+1GLa;TPV&A*N6Bf*68HyZ4#6USzGQ+o(GefE7}?jq_1(Z$G3)7l07af~<;pJr z)a?RRNJn%_N~A8HY?=LBwjVH3X3$|o7>4S=%j1ps-rhf#&%cSamITJR-)t#KOEB`m z$eWUq_+mF=BgkM)`x2#|;ooX=h{c%C_fV$&)Aa|yE*KD@IGWK+UgReohh*t2!2dLl z2nFxVshbt4LU!CWGA14F?%U0L@zZI{%6Pfw#3ACp>aWHd zygW~=>_G~O5Uz{3?tPIGQC~F<+G*b14G~R+moNLPY)`!e{-1#VkpDhat5ixK2pPkc z*n4)?k!bA3>^~9oNOi9|C8mvc$DmqW@B^*4LJEFmNiOdAt5XDdyTJA8T)E^GBLi}m zJ>@Kl2&FgS)g=K)3>mm}8>>^mn`kJ?elNa18HHlCzL%vUtt|ahUa@WR4WyFZ9{hcL z5mwxdR*!cD2%I6+cKTRK`{zLppkUVs!g`ZS3Be9c~JA z@^QKW9bl;X0j>71l6;-;sBp3MA{!cP#^l6-t=r)7GS6*b{0w-b8iFT!{wCY+{HI3w zLn<&<3X8ENp~0mmG9W-#laxe~bHgXMiKKmrKK#Fz=yQ2MqQIcgV748C67Xm6ZSl7b zHJMDS5rgEBn|pI3M3!aw-d4oGkt3l1g0;1+0%!&gqF?AmgDQ&zB$!`;NiX=uF z(7)Lnu-VfMQi0Li47;8k1~%;Hy+Sbek{00RJp+t-s4d@}-5Ovxo?q2}@r9GAEdudM zDcxmYG(U)53xGT@PU|gqVD&Hrp;}-J(Az3qHm89<(%7r^Apj!=>+2i`50&3a7LC4u zAdFf)QZiiv$h=){s9hjM17?y{0(Y#RJozMYIUN|wIKX5~$hyGv{2U;(UvPhu%^z#G zwI<7Pf}XWHu;j{0qFfE|D6TNByDSj%+R;2M*UDfr_* zHsPB)F_(>v`h6o;BHJKsMSja^ZS%L=PFFJXV&K7agxpoxnl+&GGbb6dYXM5(1gct) zshrKdphjZc_*44f6C`tsdcSokW5h-iC>KH0_G_tICo22D*=_+7rCH7n z(Y4PX=D>eWY;{4YO;CVUD(l&)KYNzVkhEX&eTC+_pjBsRZoUEJ_Dn!} zj>N^u!U4J+eEy$W9=3bH3V>tiQR|`225FF5FV<$s5oZRlR6l#)(G`z>@bK+E#kuK} zW*Dcy&A7ErcA(_6PmyG^yL$V5kVf`Uw1DahfOqY1@5@8?!ud0yFH|gR^#X6o+3;J5$?|+yqOyRZ%s?W4`So+a}Oc zNgrQGi}#1?x}+?FAlR~0A#rQOt00UZ`J|}GJ)74-(LY~CnO7rPUpHpkBBv(bZe8%! zZmsCbR`aE|tgyFzxda*6;%htX!^5^)n40A`p3Lwe^;NrG>G|NF@#14wyV%`oR52UB*o7l z?QV%{jO_H&clW+?4{fa1hhAFSe)JLCR3ZCrvwxZCq2E>)Rpi83?O1;;`&)E0j!!n3VA)zA6M&uh3h zZCD+p6K0-t4SFV4J|FG+_uOJn(SmTScHi2c=#oJR)g1n@eg0#eWlU4~iR<*6^66h) zSeUoU&Q6V1)*Z;y`H{Y6-FIn8EGR-oqO>nCcBylo;dGIIP5Q{*x|)4*JPU6~D$%~e zdg{Wu)#l;D<+SY0Exm9yxQ~^d6(!d*mV7ZlYTR(%C0bEt&zU8qo9;5xf4`b6Si%ps zx>`S^>^nKWFIjyc#AmIsS#PlKxpBx?qj$>30snyJ8>Q|Z{qw)KvvI|R{%8?EXV$G% z=TvN>>1Sc3QFN5c{@!dEPCt03ntQ``X(s4WYg20!1#SH0=DRM*=BA4ao~J5Ji%Esc z?a_cn7s-X0Yh$JI9C7rfB$eaOm>CVEpLQ;5iBBvL>Th`drL?(h(Je)NjP^sldf9`J zM7gGxhsi<{5$>5zf*M(`P!F0zy$;v9rzrk<${Zq3`r6vu(qFX0qezrR_ zxq7o|>1)5nm67Qa0OI;yFPvg zOF7UI&T%bxerHGq_7ryc_#(Id&2||p56muSlQVsD-|Kag??g~1ZoUsldv`5%dj3nx z)zOdE;x1jqdWwg>+818>>h^6V=Sjrt-z(uy(`Q7==OyBe=x)&TF9peak3YJ1{c+I3 z5}Y4xRcB<^#zvNP*iLt|UzulWHF-Ap&%-uTQFc6W^kD1$pv{eAv?Vj&1HU+5X**%p zHA@0dE*;rB2MO%gG(F!JS;Xd5&@3M@!>R10q?zUWH>WXV-^aB}BmG>^moDCMBsKav zWu~_MOXhK*<@(2Ad(O0f$SW!t2{dh+<_LVWHP(|%UclvmzCQE29 zD5Q1nFdAQ2(4sPIPe6|vz2f+#=@OxysnS53nVZcrNs2*dSE-ikRB!GK`DW;7y6WY^ z)~DV1tIF$2Cy$Ze#Do+H ziA}YWt50&HZ2X-cBqqGb)j8SXWhPgAar|ulFR#IAOWLl<=jq-XaS=+!$yJZ$xuK*H zM=)@*3Pw$HfA5x2Ma=DHb+yUwv9#;E3p>`V+`^C2-3TUlsR;>hdi*HfC@6iUaS&ba zDxG1(1-w)#F{Z!FxFr1_k|hWk8~WTD3iZ5p+@O!!fI9JgWI+oq=m zt-tIbALqNXcUsG+%cVrj;&q@+b#npNXL(u8y=)A!whq;=+(c<(%jZM(eV%#`FZSZ^ zp1|6>RdSb9EMgiOf4IVbZgWMqs-Wt9T}NypLILjPFS*@KNIpZCr;Hyv56o{oa|*v@ zq+N#}ELEmYz8(zYpz3xSsobp*x`P~lh|TtHC(CNDf?oO2rz(S7S0CFw3)47}dG*Y+ zw*b?huZnU<9?PD6X;w0$G*GR#oXe>^9dp{nWo>qaY(hDsQDH8vmwjs&hy1D6-)EoX zyqS8)eN9fsO!E_rC1RrD14mqZEMhd_UR-~ZN3DL!P}|Q~uWh1a-*hi5tO50(&sGat zRoc}(9Wr&UT3({JHR_67?a|?#REmmy-Rfd~Lz^lym*%{eC~ea2;j<*M{iJ@q9e)oM z_{W|bdD_}yA6M{I&RIWYI=Mf)yXiT{nL)J7C!|tGrtf>R2&!y+|J!ag9{$7Pys@WK zY2Pi{@)HvAMV6B{cn`Mj47t2uXXt5XR=}COD@9g_;{ClJV_kyh^J-TFjcu8X`K)>m zb~};@<&RCit{GO#H|u^gSk9U4ZK*c1D(3RPnBk7mB44WX-*{LlooK1>gbp z`zTh2?|*)DEAOiJ+HB%qQ_q=C&%KUiT@iVIEM(xfI$Vo=DXmRj0wt+0vY#C9A63!{ zERz?bfaeiA&86cF$o4qLvtqxG`=`WA7N0+ls7iNMDskeRF*9rzGdk~H)49>*xJ^Oq zUQ)$->$vCgm@=s^8k`C-o&CvG{bTrx&HsAWq#C03WLh<#4<{)2N&AkXo?>%M@&(aN#=ooDvfPAmGPKk52Z;N2^Dpt{5EVPe0Mg3`6sg7n5XcABt*I>slB zR+JCU4bfP~{Gu3Bj*K`FU`%D4N&3vL1y&U<`B+%;7kg<+hp{@M!i7T6$xcY;TxPz9 znn=h&_|HOLCB%`p?sQ8zH+@EB#$1kT)cCN+$AAgJFV0ris-b}?so2M|t;Qy5S)qwhSVvJaZ8)|h5@h|d(S-%B+5WPC$kr+Ij~kaD8;0FU zxirLQz!&3Z6<(Hevf{ZOqp>TQV!^+`_RargIXLY_+f`0X5G>0nf8L0Vx@%JZ?*Fk| zEzKqCKJiNQlFW=Of2{WNiN4HD>wm>dJ&WcRHSLj@2cs?rG+Yg_=%iOBQ+GzL+_j^D zd)rlxBY`fxRp*Ab7V5FeA>#OSv+=Yf7l*U%kW)$j_M>+x7*h6Erzf*ik4$m}Z2^}g z$Bm7$n7q;H51P+TFIZ{o-Fc;Lxy;$cr!?hpTm!lFCPQ|o!dk

|6S=hT^W@b8EJ! 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 F{{=xuYGVKZ literal 0 HcmV?d00001 diff --git a/conics/docs/images/conics-five-point-live-preview.png b/conics/docs/images/conics-five-point-live-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..6ee71326fff87e088f793b4805947edeb5965744 GIT binary patch literal 73023 zcmYg&1z1&Uw>2IM6{Vz08jz(hMV~#QA3Y3)=ef$vrArcbO<2Pc$@<>Q`0+5jY=|lbpUa=RNWP|@b zGZ2%PLPBz-LPC1)gM@SjFTGzuLULe2LR!;BLgI-*Lc+05sFve{U)QzA?3WyoN}u~qR{Lz+MbV((C*$EBhP*L`0)Q}`N@gBQJ+C;FdqBch|IT66#)SOloCM|9Vl#r8=@ev`zhxcjeF38-!d$&80cf`H(3%O`qT->{v`l<2pwLQ~& zD7Z}aE3*6aRaI4-?qkT2hh$V$ zovsb#(9_d12=bWIky%+=$JxQoj zPy#|iF5v>EitKn2uNJ~(wq_c17@}(o{U^#Tb8>SpF7|t+G>*2W;$vg0;CAPmF~B|g zhV#?n`9Tw^plO-A_}+rQ_j=Yd3t$hVLV*s2CxVrdKkS^r=mkDHxaemwTsqK^=$vN}u4> zNmI$=p~!^mu9-Tw;}7?Ve|L3JOe_1Kg|$6;@Zfa6S5WIDdOi5!;vzSfjwdnc>sJ<+ zy#*ovCvVc_Vb$T<-k(QE1;+N9vf-jEX)N_5YAD_uhV@?%DF>Sq6*>)`)6>)PSqHGD z4h{}S_k89nUu*e%|Ni~f?R$l)x!KvYgl&O7AtGt~mj{FQpAi1Eao-p%($>}v!lp+c zYgJmOq@*-XyV?+{&^$doJuNZr)-O^ONwXO%Hd0el+uhxz!wwR|5e>#AMMFkIu(Pwn ztIc@J7pMEah?Ir~!9?jyS(@0`*pQGAipMOr3t}AcA@h~V$vO~3!oof-3-&x7@P%+E z$lHnU{L{EI+r&-y1o5M+>~OTmpq%7+axV_@or1a}bp-`X#E;tA$q3`?b)D;nC@6nS z+2H#j+#0Sgw`ZID(GiV}jaChpj<&WeiPDVp^i!o~3Z9+<2-Ay`-GchFwOCe@drt^| z&sx8mu03tR(TTBjp2$+nj*5z^tmK+Ath1V_fBpLPQkI;Bg@vmtkAKsb&P7hE>D=60 zis?(eCcl=}R{Ri=ppuKi!on0G|2=2E5D~lcqfLm0ae}X>a&+pYrX3N_W%zb|gvnkZ z!|l49xxSo%y=z0*+tG1$+;B}F-Y!##_gF$oDla!zv&PZV)^^XXmw$PF z-fnN9{V@SYCB-aUH>~vG?rx(^ZGHV!ui&*JZDjkuhv4M_rd(@V+fAYvYw+^Y&>)EJ zBx2SVwze+pH-!_fb=-{8E-+z<#ZEpQ9UcAiN0BPrj0smrvB&~oz z(n%@WwuOR1xGEjT)BtElam7j^7tV~&6qL~k&zGvw@45EdZP{x4J|G% z9ibzudW4a?G!LxWm{p~Y;e(%=gcK?aCHrympuvG zfJvpA@V0YtPa>KjgqZ*A=ZD)p zio)sg1_lO0Z_Cv4;7gEG`uh6PM8hzu<{&AQsnavZSPp!DgGnvT=dkv4_t(nG3j8)( zk@myO9k?~bT-Go1l*8G?Bqh6w4B7%pUg*=LJ-6L|gEM@@r1#5beWYNf#wiSua_9d2 z35VdkygVZ#BlF>0ZtK4<<>cg~q(V+h4BA3H&Q>$Gr|Sr?u*$ryuX0ohhX4GLrHM%P zx^jU#;#%hk8}It+xWS}1Ns1!2PFD{#J{qT{jnvJ(dj3xX*3 z+qa(+6TCDuE5CdnWi->VvDw2`$dpOOe(3w<%a?F!83E4=X9a~pxcs~SG%_|^q$=mY zUe=2%e2Rv~WI0|M92^WOLLgbta-1zvd~t`SOfF5N?qEOy&Wx(~l~3Q<;rgQOsSqkZ z+&tLC^I-yKriW`o6cgwOyQMCd&9aezT#Nms?y<2k_!UiZ?@Wn_jI#1@OAt=tX&**_ zUR`&0H{<{~FSwgm^I?Lpihhp?u%A8^rkH*6<_#jHFfT7eI*gwAT%m9WC@Cq?1My(ThJ`i2et6AnzO7rTtS>fs=1p1lGTN9w^ctLdAo)@xJ#sj>lmQ?&GidT}@o#;%fpw z-OZ4z&5D^M_r3P{wlGW~Jgtcs?AqGe`qQQO8PD_0yozbasL7sZtJz99uzsvT`uS&6 zw;-xr8RD_8RZC6#i;ZAyH8{+N>Avb;o*j1f^t>}OgQQSgT%3|ZN=4N={T?brRBYg< zPY7g6Dypc62sSphzg*ht>f#AT)x(uG^Tci&MbydeEEE(A5X>3U!5S6s(ADswJ0CxK zMBZv-#uf()Dk3Z#<2Nx?<0O+VPwL}?NRgx>f0FS1l@EgSOXQc5^80h&W1D9Ga&5hw z`ODhNX9vsLM&fmGgH2?T+)rlxrFxBxjWOO>DJdyIv6-p&Ku5rR#$nKu%~FJwnXqa& z+}mRz3J^(yq$P+D%Ne?fm=cp-$J|SEbMsYuS6NwEd3kvkmn!W7PX5S<2t{>ubq$T6 zd1v@YIP~?&5HzHuq}0@+DYCdIe&!?ju_AR;)YQ>YQOe57b6gDAK{vaHgk)P)5sJgj zrYqF*HXBZU`3#rMw9h2jgDSc+U4C_`J6<#d9}5jl*OGTP8+KP!Wuz`}NNG!Z8!C%=9n zd}QtIOZ!c0UG_5?b)R*_bADN6m0>e``4Y*yv2WG>N!RfUTH3k&(^s!v38yR4mR-`A znr}zT(olqwL6kvgyVgzj_A*jXH0v@2wCFDzkPE0c>s&%X1X$Ff&y4s{SSXvPJkO=A zpdduyFT7)0Rr}*d0P+Bv>iecy$fr8>?rQ1scqo1ZobTY2c23=+eN1{Fr~Y$FU^(~I zbh$S_C|(F=nYa@!dCAv{0`p06htiio3wffoJU3 zA4^NdUS3|bk>UcZg+h=3aB!%psUO|>2%9!uuw~Zzt+{!(_woLInk-H84!wSwbSAj~ zEi&m`*9lZn(kp;eL>jZto;~~9$=Gm%41r2Uhn>^33E2vnbnrZ1 zJ`0N6b7IJeo12?2EjCZj&Ys}0_?UV<^hIP2!hUMhrJ<#bV>S>`R*ri4?3;X6^9*`) z=S}>xqkoXj#6*=Ok&cyg+?uS4h=^b|Xe})-Us_mDR8g70ij>a+U`8gw1eLGi`}bzP zMJlg`we|Hei}Hd3D^=Ce$QLSHgrP5=>9WMaKI01IB_TlkC@no`4Iyx?OFHo+|DK-S z($>ZrE9rw4-cC{7Ut3ov<#i4x&MT%#NGfs<32J3gN^82}X$ zKJiBm!{svJ@nzRJnP_YCk3kk4c(&B99+Y5+s$X;h5b5bdN*Wqdh)w86ma>AhL_GkO zXC}GQGBbY>>*6W3UF`V%`!~GsNo;~avr_jhHbekNyi@`^1^q6q}`R5;E*A-HIHvmzRRO=9I z_Vx#WS^}Ih^YWw^qT`p6V$m#3UG2~j&{)6$pGiup2RL!Y>$=Y6i9t|mYU-PCs>S7H ze?LFRlMGe2274YYfXB%K9+jH;VibEvN7CZr`Yf>-)3nP5gX@RbUWfht{aIOA&=Niq zSVQI8K`PQpk&ST@i52+VT+s;dW8BM)izJELtwB$6*5O3NAZC%XVZR^1%06oC>LOej z#Ld#w-W~ni%D_hrZ*Ce1;*Lnp7zP@eXHPp*!iJL4+}4Ka z@d&b4^Ix(@;^|*o^kaK^w4@oLM@B|aNdyw0F^%m4l&qwp63<{jO+|%qANrNK%}uE; z(c+?4Vr1LasLh&0RN;a1mE!3v_#uvojXS3oD%vjTu-Pp*;+tmaR12T6u|?GOhIB3h z)I`tB-e2zJlnJMK+5vmAOPA@Rw|BD3QY_TX?pbVQ+Q_z0B3`GhgqEYZxjCraA6~vp zktHK12k`ePC3JmGlqR1=;&Ff;A_C=* zC{fzv0yRVg^Pw*{H#hVPwEj&{LR?N4qXT2LhRb;;@fYZ>Bm-&zt`6xk0Vo4hz5mPC z*B7t>ps9?AjMUVRjk+lKYyqm0A0Z`+h?Ez!jEtx}Lqp@YYJU6piRRUhtSso&6fz{u zEG#y5hzTfeD!|4}gO`T~f9O0c3KX{wA3ijnVKW5g@#~w(&Q*s&7xhJ#+=uqXiz>}` z^70tyq&7IBKC&7b8cIq-kfj5ysHwuoGG#~}`p)g`5T5%L9f@Ej`tT9=`dXE;L)Je& zI?^uEzZ>73A$wCifpRd@>GGYSfK`8qy4cYoxVOK5=z4jwJ71-luU&u>DEwsdZF_I; z_G)G_$K^hhCMe-Q^76WSdZ?%-0W<^fIAXLgGkgBxg+{3AY` zwrF$ZH`x8qvkodaZBG*c!I7MttVqibz}e<>vh`k3z+@CvNLUyNv;Lh(aiyFg7Z(@D zJWsecfQ$qH6ny@SR;R6_rgqxHyKQJ<5?;FiIY|wgje3uBMcN^tPr6OYh9L5WhM?;p zY$}AFfy-qg^@+(%kD^s=Z+3GQ*}-^tc)~>q2?@|13Lf+ciKhc%YGgxJpuo7iI^Xub zk4nt>ju0V6_6555Q7jR_?miDN(Gb|je|exk2OK)>JRfom9VWT*YF9_cc&QoYZf@D; z*5|J`hyK|zd@!R<{Y|;>ygY;2TU^YtpC#Ok@eCT?C33+q5-Ilsyk8Y#$`p!G+>CJv770Ru9^lwNIvYDdw6Xf=r0{W?|tqt%c z)WXnkOHT*HuRHt87g`juFELTAaydf{*H`Dip;SQ!jvWMtRMrNNG4~(iv$-8vU`(bCdt5+@nVFi}_?l$nn}o$%LFg)Q*k^H-p1+Sqh!Ny8@@;xVnMDQw7MH~9+(9GMn3}Kt5z7Bdf zSt+NkzP76B8rbN4|NQjSVa#LG^^z0C9SAxkq=4Esi_^VDqIaChvzrJl&BVG!X9^Uj z!eMR>)4p%{sIBAi@8-T3Q?Cjhh`oqiFKqd(@tlDnR6L!>y$NZ=Z``buI5>o_5cr`l zV&D5s|G9lTJUqNWvuc2S*1>;?Y$Wko``>JK$F)oI+lWqY`MrEQC$BKUE3VLa2;;&^ zJ)%MtS65eY@rNNI3bc{1f1S1_SBtaV{`VA-F<5WT6pkG(mHdtyn(gNJdFyk`4K9b{ zy}w@13)z_!a)!7DWK`C$U%t-hqZz}QAzG~_$Deo(Ng3ta`0NiZ^F6;#gEn^H`Uyf4L_~_N55%xZtK?jcEbTv zwtL?FM(hY)xQ^m}`!Oo4nf5(TotH9(qG7RZkMUUS zr)yozF3&rSiaA~rM6Wj;?RB(w^SiI;zIyc`Gi|?ibX3&`jYXoXwssVNZy60X7S_~i zJ{$hypqA>H;=4Yw(6H$0KC?O5On&^}!R1^hy^KInF5s?lVr)h=3>=)}qw%tG#rvC+ zW?^a&Z^FV0{b{1(f@Oy71JIU0Z{vJss<{Nv6WTVG8GR;P&x_qQKfilUiqT0)N%8R$ zSCc@Yp%8L0QB${_(n4Eg+!fP5s{lpcddZC!u9k)-;QMbjnNZWO9NHO|zsd_?$CDB=}L0aq}daEmE&8SY~zhqVdUGJH-~9yV*iV#zMJ z2r@D<&+AL4!P_#T0cZ$l46a=*rR3!YQiSevTAn%C*|8S!Q&1TG@(Co#J!fKNwOfy( z=Sj?~Gnvwysc)C-mupDoS!~4Jn)rf5abH_Y>u@<)CCPJXEQDa#N%a{Qm-E%dUB-se zamt~?cO1md=H~0?B)K)tyC0!pnPEUi)u}aXk7f1Z!fZNufEkA$zcuYJZ$wm6F9)Cj zpMXFrE92#}7;ZabVc}*#4O3s@`Jo&E$=DJ30(%0oY>{a{Su}+)A2A~(WqMQQ_=q_U z2?wWEZRs9kDZEeD~GG zOuP?bc2I^N8lCRsx2C3}4PGFHSr&HtC{p5T-4kmb)HhrXNysU}IxpM9cB|%%5CxqrAWP(J)nJewK0 zH85G-8ssFz#aUQc1KZ$400S)SpIX)K`tjP903+g0;&}p%{@b$g(l70X0v9_?K*{!O zxBy8H_!&6`dYi8-fZUK%IyyRl|KO1g5+=*a&)?d~IyrIXh);}&@IkX>;Uq6E2AX4c zZ?UsyqwQ8SgVryb3!Uq;=%%W%K+FauWkqEb74&NhORQvmx7LG=NH1bJ2Zy0AI$^?X z!V?;uQy*)VYBnQYqHP&s0r=NMt2gO==ObJBjBk6G-n-HI_}zz9t136;TNV)mjK1B* zGgWrf3=9kc0<(Xz!lA#Fh@!hZn$SW{@h6V++FVo8OSB#>oJCFn^5fyJ8;aPDj#=Sv ztCP_o%cSX*cPVO$VesLd2}Udfk1SK=S0i{Uh^WBW)sOjF9!782T5YkDMbj_y&3JPo zt2mlRhHDv~J`_AXAx|rsZ+M)gc-AvvE_iBxx!+qbS-dFW{{jQ~4s_V~~`vyDve*`X!7f3)p2>x-kUUN;`0?kx8sr}p;QVBH_ltCMflql3&x9D-NJ{@pt6 zaK}j%GGm|dfwo`+X|iidNyfw$r}*@3+OCF?M7i|hPh!Gzk=Mvjj3G7qI7==V!wnbp z^sFyaY`!IUeX$SSK42xgoxiUJTh&U8ZMYJE5EzT)Ki-~!gM>69CPqx&5BGP+{4SwH zm&L3hY%`qWar1%)zKAFO`#{i=`2>rkDbmuvcwsk)yzukqPss4ZVWR2sf%Jg8L17pN zc4+D4I}zK}JfWc>r3|`!URGQbXfYhxk}xqb2hWZxMt>VJlf(%oJG=8T7F3RAF`hoj zVOmzj0zkjJxK9)yIxz0K6inp8jtCG>KU&2M>gSDplxC{%=MUG3hXZhz$zEq>bIot~ zlD2yVFPoRo>D7u4mXkdZ$WjShs{@M?6}EPPoqdAW2ZQfF+{O(5N|=L8O0h|^4O_N> z+{(F~_b=u4`w&a1&#@mE=HpU=WlBz#9Xj~D&$J4DT6kAa{w*f;g>nTlqw84~HBiiV z5~YtX5DwL+!zEU0)m3V+YJ-7r#Ov_fK)Csziysr zKAw+`j3g4U|6Q8cdmPVg4#E;|*CR|KQ*43>?<7L=CnN$Fe-Bo}siZixv^_qzA7;qw ze0dnXsu&&|G3BVdoZ;vRZPVj>O~ZK_H5pSIM(1+yfkq}Kf)_jF8LQY~{6{hSQ$~po zzw1kkd2V+S{Q7aQTz5Mu@(`mAqhvUKGy%1^uSGu9#}f zvu8{{r~Sy9TUsC>o^w-<_4Zmpyg->mP6@=MCiUSW>;ba=^28JU3wm?wj};oX<87%F zTAua$gMV8YvARiYg)*2uq~B^zE+V~W&&TWdo!%!NJg%Om9ZFF3H2cv!q||Aswe%uP zj$(A!B>b-SJ(DiiR~M}JXcHVhk zR`SNbq&QzVMOFYd<<;eRfojpOqg!rG{-`>@$$h1i;3KLxU$Gxg}` zl}x`Yrbw$#t2IS7va$$K5GnUqI{+d2l)jjk-OkwB+0t^eC(%pha(kp8IV=pd-(@a) za-%sA6GRlPTIbyisdzNRjT)h$0lf3&($~a_($ddPN@b;`Xhh}=Bje+5)eED$jnB`{ zc*;r0m~YV6@z#{v_B8*XBO_zM-ACB9=UX+rjvJN0=&RR~v)_7Sb-|Gr3O-)USpvtE3!{?}c@6kR51}^Pia$Hz_qf6di-rinzzbBSC zc%GhtVX^?qpO^XOI96w}0~aSNeT&{+fmaDgd**eSne3># z5pqp$GkI4D!cjwFS}Yr_q7*He1mJCvDjhN^;|qGq>7YM<{(xTh;bq?d+0l{l{d;!< zHPkOIF6pUw?ExKDXJ@ZX*XjH8ee``~o^775RpMID=D4A1Wfh9(H&($yPB}TbL)<&k zdvjRrMLdVjfRxRpGg$sF0qp3dY73Dz1>IKGft}mEuuJ#(}$Z* zL>VSiOfS|8sZBuR%UG{0=VXy9&OB%s!&r{&j9%`FJztq?skz=K@#%BO_M~rOBur}? zj3F4`IKP+}?coYlQBvYRp44ghCX&|QdbCmA{Z2&}IDBAxgfn9$jHAa=JHF+yu5g?^ z#6{K`cux4c<1^?rc_7_0bb&@S1??=vb8J*p#>hG}*rj&M(i7XS5Lf#J-T)Ko*pk_wuf18nk)Ny&8i%bg}vV7G4=Y!GDT7To$lk6TW1 zd$&HE2QA&wL}2d5`F4X7-3uXP{u=j|Si!3m>wiYq&9w;x+?zGbN8&OqZ*-=;pSKGN zT)JbMULo6XO@#@ZCPO<7y$dtwq!lRan-2;5M zbLx2x2w}aO6Ahu`QFl3E_gAmrxyAL>Wl;A(iGaQfZR^*&dpMqjJ&S61kqErhruAVj?vFh^ z17nxI0|I);j~{Cl>NbKjalM~>Z66~I$?g$o1g>=g&`7uF6LH%v0EGhP2_O=)3ktST z1ux#CjTh<(0nY={qG%YgROTQs=8*h=e&EI-cJ_DXn}$L}pP$oiSPZrtHFtFjN!)T^ zWTG|7AtwWKrMEFa$<8-3!|72q^~0R$qU-`?_x9v!SkC5V@O-668tKeLFdl0_6C~AU z8_g_53`SHq&njCZaq&-$x|Qyydr-#%wSc9!0*zKrPY=ZWkpi7$E}J<88UCTX@v((A zlKN}=qkpPpc9`(ZTk5wH9t`-1r=RU!o*1t$q8+#NN+TTh*5xf#A&2orFNnVqfE^np9GXDQ5Xfj75-+ivN@WnR+QwDw)~4cUdNxnVgBq zdbx*ai6|I31&`Hu_@Oxw*8|e*!gNEY{JFkHZL&1Jdi`t!VRMa8}Y zC6O%nQ;Y2Cl0y9Bd)|nYzqXqaT}xew$4#g+IM`Qp`c7UEZ(QI*>d^wB0sqw(H3URLn&}39H@?W&is13otbiuj8MgApm+-K-EBufxUvV zqbq@%_4)JF-8K>{bMyWElJgfs%<@?grj80deR}1(HG6w|paIkA)X&viyK~wN=PoC^ z-rw9<4GKoA`L_~GK z-IjBh2re^NnS@u4XZTGCa;&j+>v-Vg$X0%Dkj&|4P_49X{N=lu>}oo#3CX2xw|w+B z_QHu-UHovBQOOuobh|hDerV9|&$w;Z`J=qa?i_6)jjRd;y&^9nW< z8SizMx7n!H<%|mEu)l5$0n>Wwh$hehAJwYz&QrAx`af~ zekaN($O;$WZ-D+CbR=LPz#n7aKlX8xpWDq5TnZ-JaqQ;c0I4`KmqFK(*KyxnENHl> zHtF@seT-UClqew~aT819#o~y3L&OBR6-WV#bp|(6e@5h$PX+m z@N_wr%kXQjoj<_&l3w1pudYiQ+$eU9ny(fHQi5}o^H}(`OHIE>{32)LSG-M|&GrMj)1If#XgO+`t`oe%>K6l`ql@zL{T3`JUp{;>HA{|LK5GX?!1KlA3rh>j-_lP(g1hLX~EdEMM|#Q%KwOh%Bs zTbtak5QJSMQZ}PK3ZFi^7g6Lswsy9XMv}*!jxq5fonGgg`A0%{Bncc?qz6MI6Hc3F zt+6S=Y(5*G;y!-%7E-xYn5-a#m+X2LTU)Ck8cN7)*=Xb58Li`4Wz-4Dd?{xu6rcaB z1xICZaq4VqLaY4#NS|!)(O$7?-)gXQN^8giwx#2@?d+F=(vtIYa~c{g3tBuJ*=%b| z%vBB|mv=Y{`uu&fwY=)d_7_7HR@Q>g`t8eguX1r*w1FO}z^yP#b0Bg5yV#;#=IUT% zw73i2u#j;32xgRRjx;^oLARM?94pQ19J}4!wrEnH`fJaE%_(vK4iGlXQr2X8Nk-7J zP&kx++yoSN;(i50WK&a+Go9XnV^s&r9&%d@BC1lbR*^_CvZQz-#K+PP< z*J5SF#KkqOmyDIAVcQ%AEf6p%_0=26Uk7`850Q~gEiBj~IlVT<9H|6)u?g}#lB5#0 zx9sjap%~d$LF*Ju#EVV3u)KT>)~9OgUJ%;e2}am}c_{-m)8NK}IIl^B=}&#vAsfWh zSZ)hG;)0-ky|n%l5PF>|`*v5D4XA>XUk*GlCeKpbD^wG;*NFFMBBx(4G-&|lv-#Oi zpzJz2#Pw@%CW6}L!I{|;$3O9>KU2<}aK)5G=~Zwj`G{rCOXRxBw?mah^Pm$e%c<=S ztVQ$C?Jq_#8l)N+_a=Gl4zvKdG&oJ$mB{Nh991?}9gm$x`uXU1Y`e`egy@M88~>A3 z{g75-uQ1I%1upi8-5M*WGC{oT{CpnUg%Kqn4P7>MxI*`xYjL!lnc3MLCWh&cJnx#8 zjVDY_g2@p4$-Slh0rNNx9YI7=adBK`b7eUr8$W(l2l3nuLcD2HeXTj{9nZ03Ica@) zrsH;Wbd>3!=i%%xDuk4OC@a_+QD=|+w zr%W9rMYisRD=ax0!UoE$+g`N4dDEh1y>OICeJs=zN9-x-C@H}YvRt!$l{8x7vcCk} znF}<#*o$}n`LL=4ETQTJXD1MjcK&mw)cP`$v_`tKc0L7=dmp}G#rPpSo zjMveEQ)e0s!*%D=Zgh+VnA9?w_4e(lJ0I^p0^FZExEdE9FL)hnkuK{tF{aCe8`63d zG#MaFCMYO4C?Y7>0QrotjR61VNOGQg5ajxrtRGZtsYP#Z?=R~NUOa<6&C6r@^M^YD zcf(CkE*Hi6(jL9rm>C?WwRw}UwUm((%G$>qBv4k=iM7 zB=KCvdDp*c|KP!c<^ojzrmTVjXa;(_yNN*IfP@Y^B|JVJOkz4ZIx@i#juv_D1Vv*O zCe@db;_2~G8N$eQ7ke+>c$~*sW#wg^uB4&nO?mpY7}{(QYDg>Y?#uWC8NzD6Qka?K z8TaGYw8KREqM^z1rQOckmoI+;>l_gtez`3b53Z$X9rsrjf;MxUZC|ZZAMSrh9JXJk zeL}?NyteQrP&jqy?TqvM;ge1_c6KZD{}YPgtEq>%V#2 zSN`5#7vOU#-e2E>9s%x|X8na)bm^nezU}ey899P%v-Wd~c>=5YD!t3IiwoZUC4M%O ztx30*7Pp3(^_?CCdHG?Flm4TEhBG(qSlKIR49@Qe3RI8o9&~-YHEXhZviEp9If1Xe zeHeO@VlxqcXpr)?Y6MRH-q!z}>f7AhoTZQnMHuRd5g28fzit89%tsv7x!AF2>(d8Q z2n)6@pk0Fri{|N8&DTv^?Aweaz!a8qwA^HN;^PN*o@bg$1^v6OklD&IXS68+{w%w86GC{$=9sf z`vdLKZZoF;6R?Xaf&~T!6xusFs=;u8Sbf4Jyy>#X^$U^{w!PWTV??4O!1%rv-^7koT2Zcw(ZKnwgHe zwBoDWOE6x3{bCbZ`_NzQLS8aXvG>jo81b7P$EM zJo?Q6(r$U&gguED?rq90b&iK?>7CJFvF9@F`)k!O)v{0_^*IvF3uPe$-`G`4;ZG&X2(IcL<*L8^F@Ujt(dWmXOQ0Bp-bpBzxzY>p2-UKjMZ z_I5x{*@)+yaXH^iO-1-0ix?Uaj8_K z(J9xgiCm;QIr|1?7;bzNrQnMwFE5AAMzg__h}0gC8@QsO;(mb~2rY%Xz%_Uv9>6@p zsy*b?^No_;>Y=Y&9QT^$ccXwxIp3~x1C%>Am)b_6Q|@s-vUJz;l|f$We4(5RR$CHP zx+pPTU*&!cBM$ArTv=UB^krYMRojI_fa#)l4|Gw7seTH#a3cect6%uSG5yWqR_eu|BrE{298=>J!D?m9!u* zkx5O)brC83QB(;_X1Yacf92kKXg*?z+Vvr530UugxI%Xjn=ca-1d zlgu!J(5C#3p@^8=rp*$}Nb2gYFF2ik|0T@`ailbUJLxP;?(uQ+IJEZF;8^OZ{A8uF zvX!-IzZVkW-#7xvis<<8{T7_~602$Zs|#ZgtCQW&R;iQouMfh*cnO0@u(8XJHl3i@ zS!izmz6Hq2Z9R{(N^>h64De%N&tLG`wXUsAAX2Oc8w3Rly>x_yzsEmm!_zNvo~*PP zP8u?013Rxpg9o5_n4)63EIFUd@7C*t(s^^z1FfOp5#1hjxu}?58o8Umo%nil+y%a+ z#?e7QaDN3X2VumP4R!hXp{kC(2yN}@n^7-5&$X;fO|I^)vM2_vvo?~d2j$`-k|1p@9E zzf1T-|L4ySWM6)Kdp0AbWHD8puJabE@5?uD0^{quyQhv@E(~`6u>)lCpe0$Q7N!@Cp zOf7p$)y%>A!zF@V!3Nvj8?l_)~uYIlFKenAggB4(LtmKAqN;D3euRbGdN~z zICxb(zlw>8X%EljJMYqYE(IcoRa@AN0BSzt+WBFPZ&V)y88AdewHpF&11{osamaHDkcv2`W-D z%p`PmNiO5K)!MJhgAN85|7NfROeCnpiiU=Y_L8MqBP4C11S)EBX6oxNXlQ;w0w}*| z<@Ody>yBX#=TaN_!TzN8_uB&4{BW3OThqr%9`*Z^T)7QWMQ>NBu#g?3r=EgniCRVK z7NH-+9P-r1oksEbg27MBNtTHIMc~z==yiZD)CV2vMhuAVRHAuG_$0GT$$fLJY!Nh) zHe``#C~G$+pf~NI~dAK;C4b;$Y>)M4NU;kBN*>^>F}_zm#lx; zfvFA1T$LMNcn2ud=*MzQ?Z1CJ!%hQFW58Ci!#bhnH%1X{isuSmkc>94!B9|w&vmIM z{so%P#34DmL91j+;nD8`(d?WY1Oj0UP{i=Jtb#s=YZor|J_+Z0maw)DX+LsuYTPP* z>N5R#XnrHb{%Zk}`Yzet{#;nlt|IT()We5+#~Y3#l_?C4o8wA@wx>0Y9!<;VH;Wx( zz@hv1XLzPGPl=|$D_;W6a&{?$v)VbL(N6^LR+Y%SlRnC#VG)QOr^?Zo8y)tk+^bHi z@nfkLGBaCkfQ|qGGSi85l>kfZAIcZ=PO%oplcrdt_9uO$K79Zt)s&S*_^3cLfNE6~ zLtLldVuzd}-D^tp8ykkb<}z+?h?f(dY-dWVS;)~wULUW$c$eMdaRlr~<`n(?8o!YH z3~0!$p_iht4}24iowl(*qXgN0ZJPN3Wu5E`5G3R3xBS}@!5``YfC40Y=nuxK>@r(u zSwI4U(W*Z`FuZckgiMQlOhv^pWZOV~6C)$bsp_G!^|%CnH;xxC%y<4a+Su5@cd_B;0U6#- zD=r~EH#dLxV}r`{3))hiS3Epa>d%lF%k1t-q<&h7Sy@}lh51b;+fmGSNmwo%*6Jcb~n9$qAf^`X*tiLt+TL`aw7l}%-lt?T2%B)P=$|5hD z_R33^zI`}bR~v5!xdYTci|5pl;t-!*+tf?6z}bM-#Yc>qU0C$c{4S4V*ApTEzQyDm zmhrDKFiaT`fQ-@vsiqV4JMaGC;n4aa41{PE#t?jTEKf=FMBszv;aeiF<#KLr7ySDq`Rwfk zhYZPmFe2hi)R^>YtDr2dP%T#=jo!8M#Q7F)Dp;wRF3(@_%l7XfOvDVV=EH!INE-No z95;qP^W(rIYriSVy)^S|Z^tA1XYesdNK?KqM%6At&*tB=J8HMz_at^fP+67K} zhq!(q{0)+z-gq+q|3~XKhURNy2N}8PwY~h$%g495bpHAIG0(2+-*>@(Dj)oxS#?zn zjrjlTU_F9-OFzwfOqkh_OUys?V=-=F{1-}~Rs`PuaB|GQmF<^_zw(g}7_lzTYJ zkmuk}U`Mg%{qJ9V-!>W-V^*f_BMTnTf8FT}Sp;o1>GIDp0!!7w8nt7u@IP-gfw$s? zw1O`jr3q|$TB`iXy{ITC7N({<|M&BF+F(kcaBKr6I!2Ud#>e44YVq>j%!u9`uGqZT z*VaSGt@&g0|9J)J;Av@JpR7j5syz?Pxs;c4M2r5MwT7uP-Ae#YFlPv}=Uky~JZSL# zDR8>SGU*{;NO(k%(`K$2Mk7y@{y{pRbIk29l@b&C4YffgiEqqErxH2k+qY*vvM>~~ zUjt*oa)yTA%S;;1)?WA`m`!>JefnSk65J-YmH764lLIioCct{w0y^goTDq*Ekhu7r+D!On$tCaY@Lp;0@7ak#+!_0jQw6 zqXVWzzz0E3M+XFPu*7)x$OtDp`;h)T00u(0G=7@r&&?onW_S_vyOvf~jt;(Z_;?{? zz6E2Vpa&k?*BV02LPJ0$8~lJI!G_;GF;N5le^a)jNm^GP?4UA`5+RvGAC#YE14ddP z_9n{ARb(!5zRPDpqmD@Vy=V&>SUoKhin*mFWPz<8A4Ss8MM)#YrxfqP%pw&NQ(^qI z2$C*&XEU%{44f2@L>`%G6ld1bTnHG=c*y60Gr)&s`Xk#gaZ`qh=@|J zw=JKrZe(b1D1tFe0NQv!!Qh)~o2CT=B0K^CD0-9ex$W)ipSMvf3tx?^^k2~=utD1n zJsr8v+3|LPA}uf_U?ebeb%aqjbaZrp*@S$~J0QUZ>T`vHf;3sQ_N%EW+2cqJChz0p zJN1jC6L}8dp#yMRZr^{x2Yx~z51>!#A$jWE_}7{X5@QBEaCQuuQcwf-bb?K*pstO|rT3B`<7{K%|JtY{5iSmu+ngc;C$nYvJ zExkAxl*#a7P%Zok7Po`muU|3lOA`(b41lu$4FRoYjmLR;&Uo7-Fv2c;Nvd>Pr3-Ky zN{Wjo(S>9d*4KHByKqsOVD+GVj1V`H$^DhbgyOc-c>g{M?%(BQ6)i0fFdcx6s+yze z;PBAn@^A#^q=9+(g!Mvlg8m9Ok<)HT5{7E{G6q+{bMWxdBaqEJa5PO}D^H?%DYExqT*33=VG zF$$M8oUipFQhYOS4*CvwDgjipW{e21faXheNh+a*LA$T0sVQmr&yt`|Pe5QGGk<7E z2q@O?930qpdtu4~=pda282$rMad1{*qYU^N2nCqnb3f=8yMhc-Ri2C{!u?HqdoaLn@9_y!k#4>rxPG@E$Ou8@Oi4^;Rcwub>%=*o6l2hXo ze>XSzVQ>TZqThxroJwClf0k8Ii4G5E1&#*>tDL@fYsvbuH)HufDWDw>Xq&Iw>v%!- zWd$q_Fvt$+YrZuk9wwh5CbdQv86w5^_x3;#2E*m4i7Jo><*#5*N@n!~bBBfiLdW(r zGpOAVB($T|LI2K0DA_5pqvPY^6h0ucJ*kLqXt;);24)E&7e*xCBp9EZo`&GFzlEtD zP%j~0z+D$c=1v#_p&ocL9yYcEm}`b{9twtole4o>cq=?dfg$T7*F%PAH<&L4`UXuf zLzBS?0z?&H`H_(^f@HJeH#>PSH=EIY~;6O5g zfj}8CF)&XzJaW=jRD>bqXLB|{t4c_80?8S0v%UYqY!hj9A#5*j#p${RHVw&T{Q+^K zPTLDSH>r;#(&f=r((q7P0bpvVt5ZqFSfB10xitg`ArS8F?p+mVQ$$oSww%!h6N*I> z+-Pd>KoFR1M7c-K#l^+Lqn<7g(}gt)n@pTQn(Lzzkp)71fTy(pt@y-Ah0F)kkt29= z3MwI&H7sOLnD=HM40U(AuNgC7gcMS58HNOnFEAVp`CI#!*ldIjc)prr2tJ* z<_+O**bClI=!Rh~AHX}P*1!h9QC=P;ohd3RYR8oT-;;VRRpI#epQz;I8CWmCjPU3T zg?%|Mm@kFkg*NVKPz%&^dmeron%_kc@JtO8NKFuh+qL_lE`u8aJOc<0KR-X9g(2NQ5g2vT z%LV5T1R7)Q4nj{aMI3VOX1-O!6fXhV0%@V37J?U}6T|vVwVs{L5yj z2R8Gq=wX2kkI0yb1wAhSmk<(vHDtmqx0<<{wCl}ND`}OgQj?YaY&Q8NBw!aY^+`Ri z9%ck#bU@Zb2oHx|ePLDC5-RtNtOHL~NNkz~ZO#d#?=7)jc!~k@J?~v8i~c@7U69rx zT5saYW$(NBIj1!tY_w<9yI>EV4P9FMNXsJ(hRdT(H!zL+ANPUH4%#YsHp77aIdGW3 znSu}N29e=7nkVx*B=#c6{fY|`64_C^n&nA4zPk;^?Ln3%Dz0F%J%(s zrwlKWWDc1^nTH}%qC}Z086ug>Op!8#l8B7SEK*da&|n@y5<;YeD1=OzQ_5M7-uFA4 z-#MSpIs2dQx8C;N&wif!zV7Q<*IMiP-SMeN$6zxPQ*3hb1R^|cXaDN&XSMyD$Cz-m zp&Se6y!uyEEGGnqNWZAk(VJuAfp~8MD(^g#XL-V>yGR2k#8_XQiH?u|4n3p~&S{7w zra}Qs`GP)hvP(sg%LVHn@vF#41kA$YuC5ZQXCQOH@qW(S1zs#Dj?mYIY%u#R!+NP6 zK8o}`-$LVW6R#@daxnFi{=y1RW zD0kW1wrm3h1%n@pYP!UpJr_sY(vhQNmKZ^@jk_%_BEtW|TVXK-tD6e7=)HRfVB^C` z9r_m|z=_h*#P}q=lP4#u7eb*+g)5j-Z*ltvp);RZ47lm{A2>>T>+7*mcSe^ z<2(SO&uoH^z+5QkrPyzfCymO)cEK0QVUI}`)PNAZBXD7t96NF1M~iI#OY6e!qSGcm zh72_#CQq9KcItrqkBpt}HdqL(yX-qb?w+(R;tK?s3RP(sv7+`rhUCZnx{6O03?LMI z_Q@(LCN~TkZ=H*bj~9wPq+qI5&s?{lYPrD{Qhji6@NxT^b?XPS$L0TRmHJ9Ap^dqAw6%@kbZnc+WAbNR*37+H)>bRBb|@5q33?Lqx4n zb{+eI>ajWNiBa+>vbYFV-MHph4<{buWxT@X$eb^0@o;-z4~>@%yWA`y?OHn z#yw#gtwtiu?fcOskv~5XZ}qDHbx_h?zu!o2>{DsM@Q!~|gy4#kT$u6tW~Ik)GZ z>IOhoeBIQ<_xnZcA)6B?5}U3?d1AfIPxtRtQ0Pf`R+*muiSSLr6=M%WybZmHo&`zU0wej9Jit1lY?_mXQ2Rt_C9isrvn6oN}8_ zWPJNgxHAk7+r0Ca*|zOA#84>f&$QoxR^lld#=Z&2*l`H9Cdnc>_O^c_4{BD6t-`H% z?F6aqa&qt84>Rz#xK1uPvFKZD46=aY^mV`MJt6_SFNB88+lR*q`o+bfM&uAPFZm+m z7CFa4oh!9CBSooS$Vv>yAQ3yYgcT5i5 zl;+*ORmlssSG!K0s*!L+1CA=4$Vk3VrNKi`yaVSm$68KO0CHZ=uSFUbF@VvO4Krxe zmE$h&cmATna$~twaPw(rXE8~Q=HZfr<+w4NK=7aIwkUuD9}@2BgzHC-9m~$nZppt$ zNkI|LvFA;~loSLguvY5r@4sDRZd&RdQu~R%CnQ7S*KF*qTfBj2902jFHSFknh}0A) z6y#E1NWgNi-g0w4&_o?Apf{7IpSmNw6Z@L7oEG35nZo z-@Rz9!1OB1ad*%1@I+Y}^*Nlz2uW|o++kHIkvm7gWDcGe-BeDV^E9RdXy(R%q50~hn3K|N7hv^+$DfCHXuWV3Yi5| zfeuI27vc4(0T7cw||8R78#&gfP@%r3dHBq3eF2W%#4n) zv**rD{wUo3A_>I zU!}-}25PPl%vi&>(UFnWwnaUy%RIN*2WKwO@!QIU-`?w&T5NUwpNCwNTeYh$c301y zMVr^=67uCA-tgoaIJiI|F2>9IeDNwXK}@6M+OlQNZ*|EJ$@LZiEhC&A2r+0M4udt0 za-r(cBl<$AHizO%LK%C&NRx%h2M!m{3leVd!Xp$JE8pELh4>1NV*6f*3qBWW75H{+}Ox=30PtJ^7W(2aP3{?|eIo z%UiLrdt4b|3p8#ZhccEa` zI&|o3e^nLWP|%)|jeY8$BSI3saCEKpM3fUQLpG-~$7b^C0%+{?*J=wq4N4~O9^DaI zFExGUiP7j=A4}GV2CD+2MKi`24Ss@+sb~`P1UqbW!ONSn8t@4a+rb&f5ZDS88i{yk zgUp{^WBwhf6LK%Y;RTAA)!8O-tWVTBcj~*@NlJ-mT_(1T%@gPnnzji$PVe_p&+5=N z5z%>s8fe@#G3wT>r*M{fa^hDFVBs#cNFb7193K7MFl9B|) z6e?QUiQk7mGkDhQ{v|Cg?t-c|?^TOdpAAxRhpOkFfPKLOF^Js-x(-w;c`v>d4;MY1|JGjoWGPhI_dCV_t}7!cW)e1lI|24rK7jMOzGnE`804OK z0<^WEP%1DIzGCAGbxv`R2-W|%fFEj7Xsu~(o+iEH#zlbt73kd~GXDk$9I{odWCX)4)(*TKmlR=lK8?xP{7m7F_)e1V7)b;*g?1SI#!jVxZ)L zdi#KT3Qm$eMK4dIL?7#%BfjxNSF6&JDV8VzZk;TvqHZ8Lvh@Qbkig-S!7#o?h-5y+ z4X-e)c&y2))!${c=Z>{x0~>==;O*T`9{`w0OG%+%6pqqJ&VLTqkiYUu_ezC;hX#1+ zLG6K$xLzPz{I)2kv5R|pdbFRNDg)4ncY;FS(tmYvnkvWwl?$KotKd4MFA&D8D>3~D z4veX(OZH~;FV%%cysfRzR!U)`I2-1lm?a`e%7g%O5; zNg#5%%l|?)*thydlr&j_g9cDIBFsst(o8kjox;U6b8DG*J}Tw zp&55wc!??zNDSo{T6cn|gQF__=mQ*rI_KjOK*ooJ`2x7lL)ixKhJ2Uj`rgJgIw|4S zFJP}O;KcXT! zewYpx#sdJa;GF$r@n=AC(#n9F&KKJ+O_!39gjshfxMjKU~v(x$~<%>^yUg#=9QAT@E3nGAs5u~4=`r3#q=TS#fi0!> zvr-xtmq1tW9hwHGh$xl+UJT+&r}9S1<>-Mi)B1;6H~M^YI<{{gL^ zF_#3@bXdQua?{^`YmaROEaDOxv2+~u2sv6xi#V6R_wA#aSzbZCMcA|QGY~8y8RkAT zBp{_cevV!`X~)|sG}`c9?b!hnE2{P(^g|nxZ@}Fx1h9g>X0Pc{X9t79V1T^G=$4u910cX`AI74`H zaGP)E?~h%(fgZBUxu)r40X?}M9=^D+P;qvcI?94#!v?IURd_(tQ(r}?mnSWX?$2}$%G^Xk-XS+3an@jg^0hDRuC1-^O5x*u&&88lqXKs1$fOULo@_lbaAQA z6RnYbE0^f`mV#G1#@t7;TwUdoU%u3E(DkTkzK%Ff`DbMa>R2GXlTYSQg>f_CH6L}$ z{X%Rfb6rJPOW6Xzs~?M#XjQ)+nFIc(Ysv->`^V4xxFIqsr_tCMTk2!6K9Ihkw-^0N zcse^f=hbiIhbW%_u+i#L{q)qShl49TDJki2rK2w%)3v6YZXsdf6 zmw0a#fxHvlA7ly9O^}G(xaHE&+IA-rH4Ie-&p@ z$yat6(#a4hG=}H=wp5K1Y0lWXf|C#Q5oRL8x^4sqpE+~J)!f_y1S4dv?;W3>%m+rT znrEh1{qCJ70vQ$0+%Z}98&56kuLFHEw6VcKjNEJVu@T^$+9E5kI*^EWe44F9OL*_z z{?AYKU&A5~c2&DoN1?rX+H3sA45!6bXhpB_72{EHCQ&$D?=9!MR{;}0PmUuIkUO=3 z(KP2EhKgF_@L__(^75st?YbZEwyY~>d704a6EOgQPd>4uzv$uU7_4gzvOUIci~-t- z!zx8`lWS16OZLmmg}PU_r?%c3%iIaT8xs^d%8M6)U%1sD15?1xNBoBR+}s?Tml#6o zv}4GEcM35c!ZZf_Et*KSpNG+dLbIKb`T+8`lP6CidwlZD3e-)&N3iC)T)!6{e+lu% z9d?#qBpMis^7d!6mu9YRAoyNsvOH2UNV{zkL`h04UD{KtZQ9RABiGBCto>&a>{RTkX;=7*77tBUv}&mGG$@SGC<+sDyg%-q3J{oYX=>6inQoR&AI>MCh2;rmRZDT;6EsN>aglw za;L41#jqId2t)+*Y&AdW&|tM#y^Ms3QOz0-Av%s$no{!w%+9xFHRWb;>kpU=DZ;*dTO0Khv%hh_6-i(LQq zGdH2ludJMMfLo+5PJA3xOUEq^2TXm7@@M4^;$pnQ8S3ZHwNHdOi3=A0v(HnQBb$c5 zD7c$%%DW!GFFAgh6|6Ima!c~BfSe%_R%31?42TvoDD{j21C{)Dp!{5^QDL}?7Su#f zz~F_YWer^F`Z|*2W5Jk4C~3^{1-sXW5Aug305{Bji~l`z4@3`s!_T0y;y;!!oR(ql z`S9UoT*>*u9Ww0No);9jDT3EoJ`$$TX!hC~%8{aIGyRwp*W_`QxG0|^dY%p3v4+9N5HTYo)&#z=l#WTw#O$nr_`t^=!l|fr5U%~O!or`IX_%-F;2^~ zag>-|LL(OqE5d|-K6o=9w85%sYB+p+eSZN+38|;Wk&M5GfixOdkYU3GH<)b~<|;J3 zdUZV_;&g!#MxyvA`@Mfev@+oG4g$xnZLi*c;yNSq0fHh0=YaU}rw+ww9P!Lf(lj?LcubDKzQ8$H^r44}XQX{N#~zhg{mE5>TriBi*D zv#Q*1RZBoRcNuhkExxI>HaZJd<=GPavIxc5;ESS}Z(FgdvW}eSQG*N!$TFUrua6J> zkC85RwVacVUbrX|JmXZlZGXOafSHru7CYqLQDaA$Q#_bB(&DpB4W`1e zv-P$`;08yv&sm_*30u0CF4z8!_M;{D>B=(^ImN_uxanao z4v+(_w9lU{Kcirh^7-z{e&fsJWJ+qPPBU}%{reDBgOQ0ss;+PvP%Y;aMn8C5s5;=_ z4Qrz9x4PQE^~mi~^q!!@L98d_`2F^eA6{G$@5Z9TsCZmlrt!fAM$JIk3F0W+CWE%K z{dxAIxf~^jjZU8ILnQ-i7tNek@ljFxa#jN9qnn1$maHIUzIXHiC`8@e-Bdg&I>5?j zENX+4Kx1~7ugJ}v1k{en8Iwys*tjTW(0nxHXGl@J_~von2QC~Y4Pk79lan}Dpc5B2 zD<2(OTuIX4r>3KudgK?jvYS*#gJ(A3%TrTsmD>@JvKH)zlssM=T3FF$7UXk5JpnxJD;1lQ!T zid1-j9aw{+R&=u8+T+NY zRnYELx$JWnM6UV&adI()fE{{`MOOQ}k21VxELfok)@+9Q5=?*sqb_P7DM&;a@U?Xk zIkh~hdGorm7VSn#ClIgz^6Vf+{gI|z7U8>{z`#<{Y?2Gt6ju)xYU#FG-4sK6t(>=tacUi;!75@Kl zQ0TIM!Wy)vNLc0dod-&sbbsDaz3aDpNrRi>&Yje?5+F}koB8xTY4*(dxAa!TVwDLEzp?J<)xjZ^`!?7?#l=bBem{Nq*Kz@W)(>#f#5^?XTV?xsF)CvyPG2kz4>Ee0Y-j9s z;Ft+o(K}&#_3YWRCuB%X))@7r`Tw0y1XWdex^gtQadbyqh+THz{204T7qDw+a&Tni z;B2c-lL_icovE7Rl95aFr3w@e^bdVx)DqZID-qofDFwEb#-*$UFC`GLs&-n)uq*vM zKOpzfSD9fg-BF>P4B5}Ce0zFuQ0*w%y)pc@tU<6oh_2ImpbzF2L?XHV2HBWx!|#Ss zXSB7r`F-ygF`PxAoOA6H-ZQNinffO~z3?>SkJ$n{WqkV6KE*+=544l?#%EibI%puK+eS~1P{7a7 zi0IY4MH+z)AqW#GMn*=w^M0)&4+D59Vi17`3Ph9n@bW&(Ke84T@hf}4&R9a%v2W8FJi$SF?bzHlqg z%gZY$kd%<1Zjg@+5sC*pG5Ok~maonuJ^q1Oc* z2{pAVSrIww_bW4R-D+ZBK;dii=MnS@{*H>@Bi+z^>qhuw8}ReT^*uc%LX3$^oTz9q zEWdkVGOz#pqJo1wvM~Y0GzHEQ(OVNb?EFK)!^0%0m_)e)c1Wb*h!0Z=Gdc#)W+LE} zQ;yJ}@8kuA!T0aj&<5Fdge>WqflBsUtogMqP-Cu_{T8?=|2}j+6O>HiRy{NI}hC9Pu&XaWBoU6wk%BS&hG`IC|5FzE>~ zvIZH8JhaovHGs@9y8JMzC`M}dB{SSL5(y6tJvSZ|&4NP|!z{QI-(QiWSNvTNNSu=C z-Y*8=7s}3l=q#tU2s6ZP4j~EVk{8juE6%^tk^isCZ}K>%X#r3D@#BN{-OKfwFX`+! zgHC%}Erv&IKxH5Si@8a`QE1k19jTX+p(vH| z0}%sNI3pBkg?j^HHrdR(clOn%IF-`<>}@`=kbluR^5MF+UD!LDHw7=}EZSFnPYrvB_2~BPz&Ov@Cey) zduJ!+@}>Oi;8f9FYLy8KkL1vqDck#5NvbubrT^ds%5|>I{UPAGu~1(P7v+FZ4{*$X zf1#U*e4XPdJ9_uiU#2s?S?^`E%vt)sY{OyKzY*(!Oi&eH-VQ`~Lt8Z3z_q zH-5iPTO$I2ZV**r&e|R`xROWO6Pihc+)}1d`t{5??wR|dDg5tMF-TXK>1K7ngdfVY ziHZ^w$!m;O5SA@jzcjDCZR%0jvTAioUey~rwCHpy3m?rLf6uxel0tJ@0NmyhkwdjG zLcvi;uCa^#MvXb5d>1uH0QZh+N|B6sqo8eRt>lyo_q@Hf#8hbe_=7rGwEJ5%sT!vp zI}-Mee>indhX((QIrP9;JG^5+PUq|;-ngc>_PZM2Z~I%EB%sNJUxZkAVqHc%3S>Mi z9Fmxw>wQV2W!hfUNJLh2o8?S_n*(iMIw!FwYO{X-_y|tT-&sxI#K`#gRyHlPp*pr(tG|aUuLw9hmXeXpQNk(GpNt zobIRNXeetvNJeD16q{bOSyWLkLPmdH`z zb7u|xlP@0gmS@J3T#D*sv2b!95X;nM4D;BqR8%ESm5|3Z30vg6lNdiy^hNyHg^6#- zVkA@;v1wQy#fzRlos`!xUPqF9&=)9UBtg-8@V8Dgk@oZkoTSf}^2@C)lj`SrL_usb z|D?!y!${p`QuT1)&*D0xgV6!uqaM~t;o1W2* zQxHg<8wW0zhFJuLtbu4PX=%|yajNVquR4m3IaFXmLKs~M77G|Lpk<@}(5<4O;;H+S z)i?)Y*bfd0k*PzpYcr_NN*vwXYbX*Vr|L!Fb>CU(W&vD}K!NFY#F#)14xEw@5CJIx zD3X+z7`zhLVtcNn7uQ>8lmqWFj1)A~*N{i1d5QXCB)@+o>k^K)~;;^MMbtv|$$?L!y-_;IGF5ae8Vp(0L9WRIw% z@k7T5=?=gP8>s~BJSaPAbHd7T(i!-$A{0SAYxS0Hi2_L52FI3K|O^OBoT{gzr z6NmJFqJ--;6LbIXSv@yS6CWi39{wsE29A!~bn{&(FKY?lAjLMc-h}F>ck`b#pSH+r z7*eYv5A;Jfi!mQZ+(CxrKpv)U1rhK zzyJvLL)wltHI}imH7Wvt7{SBpGPIREdaoluc!(U;e~h>5aa#TmW0tyq5J^i`^;rct z$8DrokFc^fCm5n#epGOy-OtuH%uLO#A+J9bDMFP4kYCY)xvlI-Ndu`y>j=o9kvD;P z^M|knS%B}wF9~5gs^@x3W8t*7_8#(uNp29n{&(%b6|t{)Q(v(g-$FhA{tw<=-H^Z~ZL(YbQ7s5WXluh!8W{pgtTm2fr~MQCod+i6$6ckFS7l=D92`LK0`Ne0 zZNwQx3C7!Y9Km864L1*CCM`;B>!SO^}gb4#PPbc#BXD+A8G6UgI zP+hHp9@LO6M1;uPVc40IoejEO!=MFgS8wmc*m`0Dle~bDh+voge%BL!`Dd8_U>p5i zy1*eF7>U_RA^QPV*QI^?@#6=uB%msxUq#HEKU$%4#Ky(-yG?BqShJugm)yHIj1&dE zKQ!p>=?P&bIJkzVbI-jmx?55mZ@`Io}^+`&uPi^lly)m9Lw^G9cnZS zyUDcr{=Q*G;o$uVIap`fPN?0V`zZagfi&v1-&1BjHNctdpS-6u^ozo1wUPLZI{Sc! z(q~pRUOnDI<)KJF9B=RUtSy>+Ad!poH9xO-3jxfqnz7QsBOBuMga~F;clIH$>`ZdpgR2SiF zfd~%%f_QPh4^>S+V{Z?5|MfI=q_m4YZeHq@423dBk)1DZr8@@F)7~UYqx7Grz3}Rv ztcc=DO+W>*36${`@pEf=SgG2VK@0iG#}kkJO~nManGdc-nyd~bRvg;9a855j`tLCF zAI*s9Dt46NF^!78K(w;*l5;zNI9huMkk%;R3h3hvKX{|mcP zO^@WbiWRV&@&~$Vw2szKe>Yx1^lBv7Z=hmAtHD=09Yefod)hOsiinXWL7#*=))#w{aBXWCoM%)e<^zJ$`SUL`u-V;{1ro8>#=wFH7J zkapnEL5j7aGrr&@5Ysp~qwyTy^q&yhH&@!bdn@)U2L;~@GjIDYDf6=7t5J072f98H ziYN`ZKLOa&cm?iJG|~COM6ni8iBse0d|yUL?vekr|!a$HPISJ#G*nTz9MfxtqKf9{f@ zKSz5f0!u0#S};>C`_^ynFpM<5@p4WwCNe$v^`d(liIk=Cj?Fc@%DWkTr`iYl=(1pp zSi9kup|wPC(Om>d&Fvrk50~EVzGod+iGdosNY&)#E$Rcxew(hHJNrI;xyfQfhRMD; zGLrv6qmNg$%tFt5^QGMBU}5+9(e~z+x4aL@zp47A`T(7CLRR1)2H&MBKQ^QJfMl)I zsKKzI>5nmopTov>(C~?+DmdyOlr0)>dyCTQ?$yRr)GqADhImTR_!ti5SnC@8S7SOsYqRZYIc7i8QpF4 zq_oiL%Qvbo_W6dhL7c#TgRUx>D&yz;K*@&X0<=79=b=JBS7O~ym%=>ie)7^t_UL03 zSa=<}$6MlJ!cx};1rH$YK`W2tT~AKHuwCQUE1W&*8BxTk2CN-_fUCP^BsLZl7tTsY zXUXKk!D);3BP$!29HrZKI5>T&Uc&PngX-u@;RAK*Mfpd|EX}z)DR~0<)%8<4w)CWD zHE=UhgZBZ?#zrnV4!TCE6UfGi`FaL1_J5-V*)bHLlEBpfc+3-g1v;0};yxous@ZR0 zaRZ>@pUx=uZ+JPpEm1gUx-YBbaNjzk$(AQxgr3J6bPrQVXPyJ%l;d z;o;#ha=`UN5Q?2G)Zf+y8;AzW0vM;G>IYU4J_sWWaOq`b4b$~#QP?*$WIY|vxQ#3#Rp55`^B zEOL?az*m#SRkAu@B&2aRRV+9i!%~}u!LI=XqbDRmTXf-P^MgO2w{*_Cag@9f4Satc z6=8;esboQ;kp`t01kW&@sbQt!S~XIot=JLdXEwz3xth!Cpu&7OE2Qw~`Dx~5gw$Iq z-`0(eZ?u*og3KOsy`9~-~6mCK>jsKvViA}?qU?%2Mn^31Ye~TLZ!5a88 z(%dcjKvlQzdpi0n>m`eA!i~Mj0tO)aL~~X^z!sW5I3OORf{WNeb#+K&kEH8CzZQmx zm=KJ6RdDeza&e_bMd4P_t0INNeFa2?395JqTvFB=C^KwrGu1QL;HQgG*ZAXRRY-V1 zOT^T985wt=hzuctEPkMBKMfA7D*F{NIS}GpeS|LX2q?X;Hw}JOB9<;d!<=SJ^Ct75li-N2O8PljU<{^UrVRhrHsTz#MaTUmG_niWgTFh)( zOF?qq&MV9JC3Rif4$tkj%oXxqQVi=>adb$|9VayDfWxp|+Ybl*-ji48D|_T;#s2^+ z9ggHsE=a*D9aM4Mg8+^yu30&VBBait$i%LC?wcCEetyGAnT8xS+$3ORXcz*+-@cuk z2UEI7jNm(`mem1gFoH|BiH=U7YcoE@buVs5Odl5xqTXplg2}k`>(VOnjAGmGcsA*S zEa|VBu9YbBSM8&&t}c*kpRM~QpBA@_ppoaH!Q|?zbQ|p z!v~itblg86+1txT8nELOr@X_EKD$N3y~m_0*hf`Q&yP`>^CG#NDb46usbx5UnrTH~ z{1#}2S`pyQ;HB}3FAx=G%j(5}>UAB10L(3oeKOkPVUtp6Klc@3cHT?Eelw{}Y4fQ= zb+6PkMAiK&uQ*Sus&lH2PA5&au?jhnvp=De^i|>%|3=CV=>|5bIrSls-rm_6El<~L zx(kI|52q-ISpVV5RYdZ>ZS5Wc$uf+ZujDU$NIOtsas(Lqv}|3(lKZ2yY~vKG^P_D% zW#_G(?(?|rIb%X1Nl@-!Bc(OyJ)siZxFxxXiLTIVt9z%{reBIoq9sA)WJ;W3qel(A zo53irADPoM>xN}WuVAUtOOD>_?N7t-C+`J*G)e3wSnum+ zFa1KP!Wx7%=|B#Eji8K>UpC7JGpFtse-8PM|IWz#=@0ovh3maZagv{Jg;{JaV(bNSo-wxIm9D2DB}5~Kj0077wD3i=6@+WfhQdxRU$VziO|ZoHSa=p6#mQG+ zmwnOvP~qmjxE%tb(qkG#8>*PfJ!&1v>#x}Rzjhb)YIP5d1@DV1Ox$Nq=cc=*+B4ca^rlw?B>LQ6d2)M^{DPC54&03!gC~sfe$~8wM^(T1bD` zOFhPz!f55%K9sM>DLiVws?pp$66OnrDcy9ZWhuL>D7Ldoe$)?MPM$q?tx@eBF!z(O z+WTX2^Ib!a?<=QLY`2|K|GgtMoXLHck;vDT)LX5%*-E?r z>BbhaLUHk*sLg+tQ)R1gA^|}4Z}7s)i}r!|o6V|mdl^tRMo-**_iTOLzy~s^gs^(c zs2m#6C=FK*K{oHdp-UUDV}j+YJ_%iC%X}>=R{hJi z4N-N4mlWlCrnc{+<(DkpE;+8O1SD}1Vi9;EF*bhwtweXR=g0DL)JjpuEb~8*%+R^7 zy`2AF2=8c#Wc(&m#!!zXCj}MSDWNrpw-UqeG}+b?9t&3R>@*pqfp>$+B68~q8Ri3I z@%=RaA9xJ?mA9-b0%^sj`fd!dT>DzcUp%GQDa*K=mG=FjQS%kW{hO!MF4wQJYPTt% zSo|(HO1S#99WWQe3A}Pu^?ire-&_w7@JD=3dDB{w8N511CMIJot*^g{eeh+Vj%Z1a>a%RLVWips z6%rVLNJC(tEeD#tst}+`DXCdNIq)h0)eU?X%-P@;m6Vb#-<^wg%!XZ7n>0z~3>*&b@zsU~YMT0nlSO z{S00JhZe&xA*>r4e!cC)J2U|RtnL@UL~rEyA$+`c07yntH-rvfuAfC^78$#a)09((nnk=knicYc zSFx1u9UYsTgm(ZvSitB;Cyj1bTTN!N%{}jBB)}N-wZ#IKeo+uR`nJ zV4;7X`Bh8o zBHmZ$nqPhKbDyC|oJ)I|%}x+}RnqkYv=aj@jIdrIA;CNjq3*HYAq_B&1hx(PuXkr; zF!Zysl562Tmvk8cQ7F zgD9tZz8J-l5p(|;8CzT1r}-bsOMeC~hlT41e(+d}m%5wXh`3)6c3nEAiCuzfs1B}p+;jvYMX=NhMCaBi3C(yqnI&BBT% zJ)p+o;SLFP+V2W7R@#SR3hK_!i@X0fkbCR0(}|RS#mbst$=>duV2{|Tx$~pBlJ51s z&HfHi&vNTi27CH7Qp^8e*iXRavCN4=u=b3M*T2~|tuIvEZ*pF|_Y%p=uU^^D{3t>K;Vk zTVi8vdmmcj5T1|~ku7`W3pwe+1}(w$B>ySW*`kkM^Xrg+Kv@b|MuYR}@c4MPo)DmZ zJ3~yhx%N>rCut329F=li^C2yM+aF?#9S0i_SJy}Dy}<#&45hj^R60|C!;zz8ZAXA^ z_HA@eTsY(>NDvc+7^6j~46Iw^lx-?Da<);YNM01PBUH^& z=8n6cNGmT#dK0B(I&qm5m@DLcD9gbo$bO52TPMqeXc3@JBM?8>P@|`T9!WGesMSbL z`u>p0p+DPR6=U{?YSG$k=68%v3ElbyiXFe?mrm zm8@1mAjU4&|4(;zZ!Pk@;&t4bk(LGEa=y+zc*u;@;I=bX{=zr&O)pfweQzjyR~L!I z=-1mLyXyj!<`apZq)m&S7~C27-?i6SO5Sc2)NBUc3qoHBI}R8?q>Wdf{);*Bzj5YU zhR>p1*=u-Jlw42tyGHLONvb8Dn$J8bbQeb#CfjH%{p-+?n>t8zoJJ+iZ^jcH6&O&i zCju;w!Axn9_6U(fl8UQQ=~zCL7;VQrP!~IuH|tJC@;B3YWJKq%CVl9m=eO;vU!G$U zFLymV_eDgPl8iL^B={z6ArZ}dgNNCaILTe9@sCtJp1`Cn-?ZXKb6?q=j;zB3zc1}s zKsf#(DJ|(r{a!`g+^)SpCkj#&P8W91=bB`-rg(EJf2`8oR+%*jOI-@rEBLp?y7Vcw z->&wb^XRo=W?-0ob69*~tfQghQy+_&&&*JEet2-mC%!-rYjw22r)i!;sZ>q;V?Qh_RXuRC8_Ag0!^a)!gSKH1M0IMeLZ_=#FM|dpFz!_ z^~fmo2X9UgHq#PIyj@p@hvH3Q>N~}iPw>!BrOS_NdU^k4lS6OO>Ad>I_6$Ckk2yW% zd&Xl8)i21alT=#O+;0MVB5n&_CLYOzAvP#VAj*U242#$?ZttwG_G3R548u8$J3keP zS1GO}oDNS>K573k!Ioz382G2EX5i+WElFPKeZ9QXF)TnQ%Uj}K1#IXrh%B_VFs6P& zhBa(10jD7ca!?APRaicU-WW20e+vxUS=GLVx(Bx5u@)Hl>_rCb+W&(4KLro}j_N6a z`9~jMsbLd6fH>FN+uKCgd`U-{H*aW2ljiV31 ztR|Et>(JKh7}NNSXu}$W)F1Rp7{jf}HCpSrp9%R@JyqKjnjSMe3nCz)^p^YuYK@#{FodY%^+i;Ix;3E zvH?4QQKW(ji2;Ymx;JXw?pN&OQz{|E0s#@MdtJ)j!Ob+6%2#Gxw>Cv^aVFeiUcsz6 zB~Cd~TF>9x;oYM_iVnic8Pe;+naP}Tk<(-AJwp$K{)l3^|1jF|y}1W&sM$rEx)@!q zJ${wXZ|UqNRr^{_ec>lE5^T}hOfKmdppPIT*W5(J{>FVLV*(*_tqq|fOP+y3N5QL^ z3aB~=jc~6hgYn2`*W8pvkp+VzM=Gbwr7?zP3(QfT{5qbr+h!vxJz1X(iDdnBp10SI zIJHK#C&nfDV23f>6jyswx*?2)7ddcf>F-}xA z-C6hlED=IyhamldUkzICy%U^2V8tg%i;M^yX&Lq_HP8wH+!$X#x{3zZhW~_tmNbbv z65OD4zu$KK*8tL}(C%iABxt^1#KlakYj8s|`79&g(%PI$j^gYqgfTsK}+s`X4GI zx4pz!lX5p>;f%a2f1X>#5;<0Be&Z^KOoPU%vRjjmD?ff^lGQL%YjD&l%yqD zBYNyge}|eo%Syp~OOosJ>MbqG&bM5eD(Sy@s4rAJb){-`tKIbFZP*n48jaj*$JUf? zWV{DWR8+ZaKZlaf_h&nH1Z+~IA0Klikxo*}zaqZXe0*bOK$&(tJqG=DG=zoBx^t8K z#THg4`|OgpzD*Hd$#@g>@PvKtt5WOe{yeSH2{`oo7?4Jf6G1#4@UoX3r;g8hE`2sdA8l1>Bc zai6kxgh+IE;ktVB4T@aNFoqlIr<>#5U{CYv z8d>F=2Na}tUyY~#T;3MX#oz>f%^2r<(8^_MC0KF_XS)gw2&dR*6_KjtazbBu$ucI> z5qZjup@}h9rquIBKgQ**6l6+Nkn)Zz`bMq(B32J6BiX5$)het&bCdp$GA255pLi3J z=ko(N8%Pp%TS^1!M4$CS01X33P|_iXA9J@I=zi*CGUEoHimMk~wlp!{tccH@J#bT0 z4m|>~xWV3uyL4>e`hbo1{L`b80S_OV<;R`7WT$*vR{K~qe;Lwg{o@#9og4`lc$|ot z`8Q8hvnpKp#=V*mAfH1g>v+pby7yxLoomy5CM&D?HY4lUB5S&7fBpJ}QawP$Us~1M zTVbSk%Yh^Nk03K5Rx?&+Rlkd&gp?HY90LPsC)ZhN9btUEZRJ%K@7frNGl>Re2ej^1 zZ%o=CRjyEb=Z)-jQN>MSEqcF$6Z)rN5}chYdvoRjlC{(wj4w*gKTkQ>Ty8c^sr)Sf z96?(twyYxkjegFCxhIbMBqPAi$Ym8f*4$qwD5*M3u6)}C<{`0sgO=5w?9RmA>y6~$p!4+ba8o`ARcwJ#SVw$gAqN)vZHk@Od8x=ot~>?)0N)$m zAMRyj9Wpk?;p!PrlBka#S4(dzv}v+`797Gz&*I4NQZF%m_0pRP=k{Vj&&QpL#k8GEy6FP6vw{1s@*2{{5BBMNML5Ve~{+eo-+s$+>i_ zut}^3AN{_Nol|nlh8md`&DBz-TBU>E9^OeXqft?-U0XSNqBiVBQ&Us$J7Ei&!Mi12 z^5}z-t$bA8E*oUNE1fq~Gz5=cE*1mO-9z0?=vOeN-Y>&l zzCLHIbJ@;r397)5mRcZ6)bJS~W`PF2;e7fv&>`|Wuopkj z4grOH-19ONL4fHC^z?($xh3d%&i&^5Td)pZd3=F!)}a*oGZZ*~1&6OI;KYDfdBJbK z5B)Eod~fWvxRAc%E2|QI6x!p=jFx96>@cX*2X^loXO_T)DS|QwB2nnFN8$VKe9nXJarhprJ*BW(Sn$W(+~GDdeym*OH)Wnn?7P zEU~gGrX@xkegXA)Wo#a}jRkavoO)-5XQI}TA;DN?XE~OE=(}Wk(^nu)`$LNZz%&v1!0R_M=D9yFI8W`pgm(B?+q$4V8fhG-GfTtB z$mR)!lYn2azyGAKJ;@ODfxBqH9egS_XMreAjuSvVV?Vlw|jY5Jp)RU z>j+jhHf7@VqWfw5t3}V8nGg#?wf5k`04as88~R4QgTr7$$rU4O0*o|Rt&|M;oj_)0 z|K?(0V!4G-TS|gfwL8>UJBr0jWR^UpW0`1UYR}7xx7D93M*K}E8IyoUYyD9huN2;b5SES zT8ANkZ^?ZBLOm`78tYY}$$zI7iG%>M_5T=`#CJBFCoD!7A|;R4gemw@Wsrt8 zB}05+i_XL>wxyHwPY0EzklrT1?o~1qpSb_Y6rx5c@zVS68YdQva=QR7c%8N7C!5e^ z4#~Hg1l9Kt#Z#y@nWbWz8E+ZX6f=fUA(T1xfATf&-!StIVev+hGSPg*(a?0Vx$l);7!hW)?o2MoCpc zEDL`k7wwiFD{iJ4c3RkNNhP0gR3wCg&?zB_4#nA7%B?`W^D2iDJ}jnpW(j&P=Yspz z{8z@|TLHjmfCAk#LYAW)(>pq^U;#mnB1R&%_NJU`YK0pT6{9^0`+EdxjGCk5I2b^a z^Nt;~*kFSO)q+`0>;t(;tI!HjaW|d`(Klp)Rvc?)p!l&){M{cFW7 zq{n){ym!`@MkZ`ixwy_(|1dqGl1{By(w8dlAv&df;rCoge#5z3$1Ta!`=&{Rk$T)j z@pJYltr6B>x!-1N2+wol(k#2PH+$mgzPXSYz;s|gh6K`4{v1#0S@i>~Wv_@vtEq-q zYszy!J3t^!b-wuz^A#&-CUh_&>Ge$vvxk*eNkW&M zN?1ez`{F5NpPv=HQd2FiGt>=qkd9gTYgsaCKZ|GU1TU|vV;R0wubw4HFd>$mF>6d8&2R@EW_~wI zQOs?zumi4gQn-m{p%zpF1(J8511|#uLqyUPv=Y4j3XWze=2wOd)Y6opJi5+sSQEQL z#sG*|5Tm#zy&vVr_V#>ewF__;#45t#`uRuvhQJ(4wzuO7HV;nlhsCv`bONr{UbszD zd)i!IZLs=MRKHG%?H%yh1urU&M0IVe>QC+v@zf5_9*?nyDD=`3!SRN;k>=F@0B1t@Q^`V?m!82w#CHSM0Y+7*SE}%{2~c_tm&?NL7`g z@FQ|7Z^j{C;Oeb3?`TP|3oq?ht}}RB=$nB--x0Zw$DV0}w?4GM%C{a>)Ck?f@|?XH z+<5_u@p~>Tu8g_0pbuE5xXm}phaq?>#5xEFBilEW7f$XRGSOqn0*Rrkf3-5aA065NW4DA zE5=G})Jks7j;CcMJq7O^^v7Av3u26;>(>jMpDMAd48pI1g*E;NI;G7f%TS${=P;H8 zi*LvgHu5qjp0mpy*{4W~75sVlt?zT_SX-`rA2ISzS1O%=yeTs!B-4J+iQSpyp6E!! z`-bONkdf6SusB&%L+YAkj$3B~rx%u; zN42?^z{2>Dix_&qbwLk8=nfAsMGFwWHVch-AP_EO1WRupMu)nt@Li}S05KrCv|txz zK$h`9&!^Kyj6uh#9M+DGI!RdemL~kTu{Z`@m|Xk?AC9+?k5KDrS8Lt>zTMvu&C+ew zC==f*R5nXCF$)%@OTdsFkbwjx3G8BZm7l`G>jz`Mj?x4E5F1gc9D?_s>~RHZ#Fb)A z{@Iz)YqU&`li5%o;`HSx(_mYibqdF29=H&cRItcsjX8J?A|B6r>H`Z7xn1V0`#BtQ zFNS#n>DOe0_?Aecv%~MY1 z(WHQ6650?NBadWkR^}eV+%A|oR%J?i(T9bDrXB$QYooWf-J%!5DtD} z7Zr3l%3b-?#In0yl75M9nrmL87I&KIdotDQhG0Rsldv7EsfC5>UhkKHs_eo-M={2d zT}U$jMY|!xG@^Ex!`ZR*I#@ygw@ps|`5ID@h@@ha*2Ssg`T{*{MTY?;{)z+0e zF|jSs^{x>>+h!(-FoGHoj)T4Hb&1$QGZmQnKl6LziX6fOBG`mN4lO&C0oZ@nAx6~T z+r-3PFBv#YYzdJqkJIlt0&sZWMB7#sXKL9s4SHT?-4B}Vw6POk5AoIzNMC<_H_?Ok zq+MTO3dC(JX>39=*6Uerq_8pRdsPi)W^y-=nn0}x9J+n6b$>Ix_tZ&V6Zo{*>nod3 z9po?HA7s&$^A)_y*EUmaH5GCLz;8=W1oYwaoOxeP+g$ziOYNrojbe^%A8W?y=lR;0&i%&eU=i&`9 zS(hZ8W^N>3L)MK-_|!guDqiRB9!$`6Y#7qZoReBtNZemRUwN52d znxU9Nib(7*^4)ZwO6UX@ryFW)bR+>j8WZq6ck76UX02_Mv#G*f>%AF!mh@nMaAotJ z;UID)+D=m~aEx+cBofm+FnjKmu!^MrBff9sKPE6!mspF@n+mev%oV&D)B zCQKX7V1>N339UOZ&?U)DN)OEXzz+s;-WUO6h)faF*y*YOkb8n_(L~Y^O2Zb@DL+#oeJb&=_&)Hs%jE2(T)%XNy)$ zOO<~)b@&C8NfEzIp}2a1OQv%Hs7rOd2qU&&K80KWU`->o0a%Y-?)>(V%CEGyojDUs zBVfy?3*__zgI(w#ckSf#)RZcf7v){BrUv6g24S=->CSV9lA3nhFSy#vA)=c0XK_UDiT#;nS6dUU9KpS zc&@*sJpcIW`>rc#skdo};Ow0!X7_`>EeEOu6^zw)>!({p5{#n59Zz3EHY>E2H-NF= zL%PcG{W=bVQ$Z92iOPV0T=ac5}m_gq z8J8A#ClX3Svn-JQQ4(w(zZ}>+UJLOf(o@1(T#cwz5WOD(>hdBW21~qvL0XzZUju1M zfRL@>TO^@Ir=y%Y$R=d7aCssi8;*s=r6qt28oX~pw+uL7!k=pEbvRvUJeit04gEyM zs8A2bej>~nNh5^~6h2kpQCikHaZ2GDRuBw!X4&44&TDioLhyOspISsr6{Y0ZJMj_X zEyZMH7f30L(A5VANLZQRg@`~n7GQC&#D0D0;sDdat$mOcQK>8KqMNUEpDVU>{xI;81*}}1+O?E&dEHR z!sCslh`Fl+8m#F6vv{-o24hDlgREHpD-RjyQFQwuta7#4Dw+SfZg)kvEwcDJretdqO(P%%4V1?`Ua$9Pe0$7c--)nKpLiZF0oE0 z(4IPOrV*fL7i@xjEy)y@uep;-n0?@qqzHUGXbuF0gNuvg=zSI+sK+6wTs@&Ss8pdY z+CJ=8I~>K-&=Car04F^#Ny32|4E(TPY^pqXev#wrWU{L2DN>Kz*ba;iRrN*Ix4t(c zk`Vq#ty;m|Iz3(a=VflM9cpqPkyF2|9b?I~e}#Y;3p|}R*1E??$6nP{ekQ71dNkGV zEOK5z#!;Hf@HBHGfUo(33mRYkC8o7;j2U&XG7*7rQHgyPjW#+F0mI4QxgCe+Mg32-}dRJ@A>qZMl6uIxrPc73{`AVRBDA zGaEO4(k+8FqI)YuwwX9Ipnou7C!$JK_89Y?CQ0WoQ4-F8`4vb0s_KrCan`k40em%4 z&G=t$p2GbILE4@k3e-4WY#;JZ^uPm{kGI{uJ#hJ?D@CEUS zVSCkQ2a4#d<@i@@-f3TjZ5SecS*s~e;|M-Kvqv|7UCv3l{iByOKnMajM7}K!hnSBR z{FjX0Lj#)p?mJ!gOx}?S-|DbmPUHA9{Lt@}(?NUnC6x>3Ipr5m?=E6fQ|Y`byRJ+W zbNk(5-9Pw_R{@J0D3NV!k1aZy_R}IPN9av^dtqvSud17hU?(#yX!P$w4@%xYJ`IIN z`P4M%btA|=P#Cc8_RB!SOfc7Pc9NJ`LJsbw^4Xfhy}i9q2BO*y!R}Y^9;$<59(K~h z!~_shLULj^_1W+~Ra-|#0q_V9ACfCzq47;FU;sW$&eySdmv~BH^)D_iYWh3QIQFkW zXa&vL+$s6N0p747)JrDbNunI=&q1gBgqnF9EGW(G6ubjwrwQb_aW39O?!h!L*5gDa zz1~J^ZmrO*Ykp-NL`QpAp6S?jB|f{cI^@rzxnw#p9{q^KA(Aru5{x08yu= zYLaPL=Q_Ol*k|68NgC&-w*(Uo#Vc)`6@K#=I8%Y^gs$V%DFv*cf+KXDEGaH#mCJe9 zJ_&un0BrvRkTud9rMsIJX@q+z8JlDZDt%~ZbU9iygpc}n00sC;z=0yyci$wl=-tpT z5L^skV+ECRNv1$B!8;nGM1u2)R8^NIyYw-)I`CvwRckYB9CGp4DR9Y4#k)Y4cPD8` z`61fRb_Q{r=OYNwC)_a8M_67cv41P>>*#>KugMCL(4Or%#91=LiGtCJ2V@`u%$%s! zm*_GtFpq;VLJP0=EtPeyDpPv;uB%~Rt!Be)U=+0>uEoJodhXvJn#K=T1veRq zGhPAnhW1DeyOT;()ttmc9%}u^H_k_v4mEg&>jAy`8&P*-dI&8c$8 zkzk5)zy7ix#7%xf&_O|rmsd$lsRN7E&o_9ceYYMWbpwq8iyMOc{BLRI*EV1xb3L>M zUdx}S?9;@8-}6sc>@{tOhQ5uD&pDKOJ_6I~Lo))-z!F2wtBbBAbZ3Uc89Yrtd>H(3 z60bsJ8wx{bn$Nsn1}ZM&diGF3x5ZIU$gQS7WhY!mBW*;p%5OY*0T3EAr19m0+l(}P z1QtP2wH@q0AR23KR!p;p9<9(^?;eE1mJGx>nd15ZP{M|>vFQOs!0$OJ8JWg$IsO_5 z8n@NV@~-sv&R9T_YFLmvAUoihvX6X9BpU6kK=g+Kpu3ma+?9 zA>H^8gdw{cnvJcD#uhR@a~^}r9QZVj&FgP0C0LMdCM{y~0ayXRvf!a~d~}fzoBn0d zH~9K>uX1wgM#dtw-yz(Zdll79$xRyRC|nky$*YD_lw$H(mcFe-!@WR!P0p`y(#FrZ zgN4Ibv4u|UHx+LnRSOCVF0$T2sN2qouZ^k*b)EYoZk8QMc5nBZ)N&h$Mi(fqVs3NN z=z|4Xf!3j!jlQ1Rt&!I0-cz~^7N52w(jyOb{BFj40IA+zREm-1kT%et}k`HR9(+UJ6EnV6oD~=I)Stt z$75TzYF*YZbn%}_O;wwl&>PHpdF)as6;lwH^WEZ8wCZvP?@Ju#@ZC_lBY|^x-5mVM(Tok)8zr zbZry!2%I+1O#1XApe&NWYsfy3O^B+w9QqZ&;uS$9Wh!Y(VWvE&!aYdhwU@i70>RV( zNni2MT~Ps6mILF2DY_YK@|d)Pn`XZC8*Sfa$?$?hhL9I59asPJ2_MW)H>|K`{SXj~ zZK++kXnBONqmV&`|7Y|L&I0QZK$*mzzzUL84Nwug1L!79XeiKj69&STC)d8os`vDs z-H2iUk5)Ufl5=2-3j{AwHI1tnAYLYlnTglWKeC&7cmZy1$2EvrkgTNbxCe&8t-s}r zJeFj>m4Fs#4k*u##ytqMA%=|+#mG~7f(j#rx@QjpV8_WA9~RK*Tw7Zst3pHEU|)Iz zetGN{yf#`KGLwAWaq!%cK^w{x_uQG8*&ExAeHHtJkCLOll!MAtQ)P~jdtSumrEVPo_}dtHt&&c{ z;an}t(o^A=%OJ2<+-+tLO=0*H1fms#93-enpta3c*o4=0VM7hb9twsDS6UjqhFfn{9AFj2Q_m%mk3Y;z{O2)IJ)0_$0+-IQ{Qhs7C)eB+Y*Yi|JQJ z*v5Uf#X)z*TgK{BXtWPo!$9LcfeDIRro$hef{+P1m%Y-Hw%R=~YY(S7ElG$-+oL8U z=(A-HRq((z%GALz4L$_LE?dscx+~A^#hO>L%{SQNl~UgBOAT?uhPAVtGUw z^eTvpaC6;E=}#J?>+DpZtQltFPHXwy@A)@GMqay)C!0n*1s$kn$x8so$kOm>Z-=wo zSF~J-uw#titn$R^AfRr@r|M?SCy+ZpexA<$9-1Zr9yeFgztNn!$EOOA$|XQZKy?V1 zrr|T4j;=1#h!LbcxJB>n(y{e#7bI7qGihV2r0$a!fS{U8_q-Mt7k|!9=Z(mNK=N>z z^|HRkE4@nSUv4~wg60~Z@J)vK6_7qm2sw`^HDC*v=`IY~`hXbBf#yM5sWqLgEi63g zH5}Hk^{+XVAwYxQiA;Ow+6b-UUqKZvoK`UT{U~$b%-!7Fyv&9Gz)NLIQ~l*z00}2P zyk>HFDvz6<+w9f>(OZyfgSZC+;nyB_m~BvD0!c5aJ_K+x_h%e+?|luK zp0<#t1k-1E;yqq6U=xA#jTs}Z-s_pumA)Vq`V|mhTaW=EeO7>11Xu=`alT~Zgc0H6 z`9b`EPz=!qbXeuWoj${`y&v4ZB@@_%>C~|Uzb)t{-OLN-oewFT2Jk!PVNO3Wyaz`q zpgKr@84Tsg;QANXv7vn9^Bwp1$)5=ZxQkxo?z@QvZs2{q4_ho(^EgALNmR@uP7vmZ zrd%WjCJ5k#=sH?ocV>v!5TQoLoFs5@eEci-hkhmXrZZFgK!HU3)&^$=~>{Ou@ zoj_16?8*L;5lI|=Lv?lFP!F31Jo4<}1Hez+Up4`Hzr-zE*46EQ4=qyDuV(`tGGUty zcWv^+D-hh0A_j$IRiG3U99EzMlT5Hq9TRwb@x>$;?H-m&Jv8_s1n{N#gQ^Ko-l?f6 z#d2m7osO8emCfTwCWZF3qRR!u75X3LEf0H8NEAWz3o-Z)2rC3hhe6%%T@=wj8QwK3;{6iuKakXumtA+YPf6*0a76R>Ls=AxRZAL5ZAeT0O|w^&nt zs+b&mZzNpV!z5xcYil*Dn(2V=J7~c;`J*9=MFGoZ*WUBOWnY}^tK-N=rsVJG4SB2& z9dF#3sd&6_po8o9Tb~yhdNM*=ujU^IIeGFe0&k*aSFu}&z4XFG{_rn{o^f4F#;!*` zR4IF~$B4b}6ZY|T)+haHgZZp7mHf;8nZgsTf=#Qt_LS_x?ak|1PcO3>YGiJWf76(m z=v8_Z+CXF7!iEEy>v%&?Hauaq;(mCMEf^AKg(ujtJOnT(lLl7^cjoJbg1K6rn84B< zyr9@nGw>$|mU7#hBT8D+lrci&?D@bh9_Lwqd1r>(EPqb@{z}ccA~YqaklUQ5-!^KE z6LcV91mNFfI2ET=7M9!Z|59=Ovkf*iBp%51$-x}8FrTCrDSzFQM1c?HWw+N99pN!g zR_^sDSUx@eAxr1ZG2+%nwCEWKz=w-F6CH&FyM;F17WH9XI1_eJErv?EvK_8u_#&Si zynXfx`r4f`x@l<(Gt|(HgO}icPxx1vZsGzKy!%y#8!W-0F!g0tr%uRmXIgOF4-NT|(9! z=yy2Wp%5z}{`g|iyt`nKkJE6A+E!5Poowgz+H%dc4~MC#w-(YQT^4j1yA_C6B(F^M z&JLGQw>+2_sL5Isb zp#53ToE(OiP-y>JQU3+GzKz>N?DS z>EF7gRrY<+^zk$9={HLrhnm7Zm)+m`zUtyUYakO|7v}j$OTEQD-p(~Uw}plcOEMgF zXXS0?CfP)mj6P9?8v=Y(SbbtBWbWo^0wS`w(>P)pF?IC#F_3)Vt6Y25W3JvDG!k!AP1Sp?XOCz;w37H6LDyG2tVB&NU? zoO80%0Yf=7e9|4!mG44A@<%sM{svs8+iSKKWdS`ceH24?dp#d@=?q?ztLD}4(qCLWMtwQx;TaieA@O(D zCO4>?9z;KG58flxr5DQZND`f3RF<|ldZIr~+4ku_6QRmedm zlQBAn&hvq;_f}Yv23Gy{eXnE5uD|=i={Wk}PY~#z=!1$it=oSRfqB;((k!7h_BNm0 zYecg}TrtuV2^~G=fb|#+j$QvkK=F+5SS+)B-{cv6e(JrxE)Ko!Rt;jF>n%Z<`|tNn zKO7j9wEtikBYAx#?69u5gX$Ia+1j9w=< z&=0^({30^NTjqwtF3biIBU#Q1X*@0~4W5sDwRL2PMQk}Y0=GWRwo3Ngl~oI-y)#^( zpV)NugYV#EcKnY;rv9%9Z;$dYt7K*B!?&D6HVX-TGFY~BTJbb%6(l{GJK2DwS%RgPw{%R?bIHtk05Y<7en zz0iCaO;Gcxwe`_(r03xs(B9Y^0`Ku+M&R_10^|>km(?NXIwl8blPzItDv5i=iiZpI z64&V@t6EIGj}-)c1b;ZL@Z+?wFb%nn8KOIfG#~Ba-LN(N=6IRSa^2&L{w?_Yl!8W0 z$u^cMkhGsk_N998{Q4l!>ldF;!FQ3#G4SVsQEYy`)on3G3Zx<==_#G}K4OAN5bc~??uxH2OUeN@-p$U;A8ysN@R$EH!Z67||L~aO2BItE+)c@v%c8qvrlkAZKrm%aZH-c)f zRkzPJw;RU$2362l*Z+*cGS3~cg7m`bA=SOozUwDzOdHQBlM?I#_ymDX0CqjWO9~yl z(MfLw6_lxVq&w|^A}o)itJnrtNyk8UEgW4*2AAu(Gf`3)DwXQSZag`ouP22K4K7h7`Mcf2HkG$4~P zm4EKb>np)JTk^#BI(>7!PIfJ6s?$V6XzW!$5_%m{=TNY zRJ^PF$mN)|0IYPSPAk}c!b7E{VCJz0MfZmO68USF^I?=SH~OU(B?fHN(E9|9kXN8o z;c5$<&7N0BZidpV_)?$EJ*!XDs)=Si3LpsC)D}a1^3>++HAaBp`UfH4#Mjw#Br2+03_yyioWLL5n>6u=3jp&FM zc83eN_s@(vRpvcshp<_97$Labfu!39VzSF#rV)P6(TipwSGKi~8R79*FO9lssD zuOLEE$LmXK(iovI)>k*NaMxDx00$*OJPwq&HtbD$V2@m1sjckI0EI7xYRfXjd?#q# zemgWOGvg-vufl<23)6Qw3+jv_*k*zShyiFdQ()aD?+jM&#c8No?0 z<|+|fnH8%Xrv&x+>>st+@2QtJHgP9c^KpMn9kbt&@)}-Jd5$f7Fi;mh`mMN`hw^;( z+x8DQVJ5R&yc{xJf29l-M9zT=Sr7yv%Efmgw~Qi^5b*5#-pkm$(5n+K(N*In)ufNZ z4bTlKbS0SqltY2dx0~-td0V%*n(d62!N@{F%z%xdCnf(pQSt!~t)J8O$qEzmEraLP zkGsCOr$sB0TDZZCJvP9-0^_{ETwR~23smC>x=XeH*o|^zq<>AX_dJgX?H=dobon{f zyHkinAJirg+QQ9_lp_@PX7E|BW7jhcKY{CG=#VNg(xY^II$Lc_sq6VXbEM79XdKr zY&yaGLFC+bRS5~fV=$K$$ZXPjz)Pd^!2toTzy(hlIwU3U5tMt{eiU;q!kw z_qG=BBh5wxeN`%zKr|1ta4D?_kJ&^2!S*bsf_@cBPB6`hqPi~?QlG<97>Ag6zNf< zV(?q>^sK$Gf8FSOL{jv`URuoQ^Iq2~X>!#rC>WQp{5p()1cIAjMxdw>BrxFSVYu?}jm=(3eh10E+8Ekllz%(Sqg5{q&8->D-nV zL3Wb)$X>R-w*T(%mHNDfsE44#N1+PPoJXaG&wQs`naY2Wcj5$Qgln$btlq8vO!R&T z4MYafv?_$46DG17$IQL+@y(pOO&TaR+h6yxGrAd2d4c8wh9ArCJ+D4q_%U7`mJf0@!%}eF zb>thJA|Y#6#J<|xf^D76?NUQrF|teQ)5wkue`KoV8aV`EDxyJUCVmUy1BA7K2*Bf` zt7Pk$L#VIr^XSu=>1QtzxlE|vC=Nw)IE+`vJIBRbM3-0m@vfTd$6Q(( zywjaHYkCb;?DietFqtM5*o?e8`OGaRtz--!_|!1GK#g2*45+U4d%nEk{CiG9jVQKd zRFCMqGJ*8KWv}P#Pe003E}pc8xqbijc2%lolDPfN`07-7tOI`(m$fmp0 z$}8KYsUP0ahC0dEPaID=br)GXDnj^lxkd+k&-n$BRk3earRNY<>b?36J_cPlJJnE| zk~~M^bN0^%fdHbB<)z6x(Ta(_`d-8HCQWf>5cdO{U_5>+857N8Fol2HMAhKUUTdK?b4ERGtstZL=5sKT#NK?ekHrfCndg_!GNI_#8_ZX8& zvli8RFTj(sbof^C0f<`#d9~Gv@;#6zo3>E1_Qy3RXGe^C#*slsp=fr)q?4tNInTYG z>r1V>+~3rCa{FzZ^xPg1kZ`vSWUgM=uccIWl0Q;=$OQz(b0`lz_vBHaXBQMf5AlFzT(O5V?!eFg+k@IoLme9&~>iy9Z zHu5m>yrEES9KWz(w+YE6@9S~Ldcs)Nto?V#)F0gSBS?BOANr)Lkdp7}%6GWjjF-lx zKmoTQS3Od>ep80{_4;8b*8pK65g?Q}X_2d%8*Hqs!SzG>8li-4b_KV)+|5sGZPQy| zcmc&1&4(^`vYZu-HUdqWr3xgb9HahyrxXMrrh2t8oQYIv1n?==0UM>u01ELE7jyJ& z&M2U`5E&_Q<`bQPTF4Oj@T7X$N|4l)LHV2F==?n>BE=p1si{%t;miz&5CS_UIGBp& z1Wao&CSU8_Ig*}Naw<`k6Ye;LYsv*=FE#>@art_vq2h7g$L=bes<=y)_XhOWdhs!) z4yjbdsM!*})^3LAfG!v0v^26nMC-WSqyZrYR#34H9GLq06z!X{##V0yyfz5NU84a_ zCqS}5O4=!`IK^PY40AQ)p1iryo%KLPRh5zeF817!_zBU``E$6qTp~8B7tV?mD3HTN zk;-!3W}BPqqgyw>H-@Xl?lwr#!d>j<`bu>zs8F{fU%1b>$oDH~Tx|ilj1t#o;8&6d zAoqgLlnz;L*U3Z`s`DkgkOUYAb0q7BmhKndEjqk{p!p&zI7)rChP$9$k3GUd?2br@fwfU$hhoX-16M_)xFld%w2&5^fLIX_BaI@mqr$rakwOGiMR#k6+ck(21o!la!0s9+ z^X+|wG)z_G0Z&zJhY(U}u*W}yJs$Zp7nu%i5#DwTY@!3NKZd*?e^Lc`5(*V@!Lf-l zOqXSY1KRFt5(Gu^NKKm}Tk!f2LY)=cRLBJ=Wspk5wp9LegqD|70gEIs>QvRdq>50~ zHl^+ZMu=?=Kv@2^kv%cgc*3gFDRPvz26CTKRp3g03M$>%{g8DUy>Wh~HmaM)SE z+_SD4hXgBTp-iElUXj&a_cU;quB6=cNCfh3*s3|maN+_G6$c-}Or| ztH}|HzuWGsuK%t2HS|5A2~O8@&vALxbiL~}+$kKFKk(Tc%>t(v8HM8^*dlzZoDcNu z*&e$*5*xz-&F!&@I|g!ip`8J0f{mewerFu+!T@dTj3RPUli_W@Tae4fu$6u13G3qd zvh&~G$0@hI_d%%x1yWu63&(m!F6;}eAUGh4gh4utGeuxBVE-!_Rnz!)jh4>;$zX{A z(b>-f_vu)Qh4I*&Nz|5I0=gGJn;#h zI8ZS)ha`k?${-RyAcOn`;uGXE$dK)xi&l^<>OC%s3QTs?DYiRX3F1>#kE2FSY>zKe zF&z(0>OKex=-=axM8{v8Y#7cU*PX4`yz+;c zoOANhlNS@BNRC%KJopA`*1dq9cwU&;wm8%OIgRf*VZO^)dY-ezKkEM%iz`LP`=i$V zA{PYofYQXBdq{>aq~~emRf7uz>F|sBO>XThCcL;49wXk{PxO$(Ac=1e(!gz4BL@dE zaRWYZ70y{tN?1iC+pEa+Fe1}A=F#D{y?84yIPQchN)b;<610EjH2R4_mt5!ziP$6L1t%tt_b73QL$qCJ)$LsIhz%GLL%GpPCehH3$Q4Y(NyRcPZu0ug*onM#=o zN_mhce|#@>ECg3O zM8J5{9hU^Drl}odf(kYeAaHPN@!m#`g?nx-qgur_9Lb|E&w4{4%I~qGpOGYyu~7Pi z!|BMtb4Q{cLG1&aw)&gLbhIxZKO&5o_=I|9`k1-UyY2LBx{;FH2Q+%Zu?ffa`vQh^ z&v^R?b)ri_B(+f#pMiP>)&zQjBd5hvLy?`jVlpoopl;K8kFZ^pV#}9%V~t;-=h#i6nLQbgQx{CCFK9F+P-2}38RXS}QFI&d{%6HGyF}NK|8DW;)j#5v(EVTR)`_%}_)QXSfg z7u)m1)om*n#dwIBlt6pN64r zqf#_Pswz>8c?Kwq>-Ng6F^+x!L~9)R_PVPO2D_rT82E=^h@C}5&;~mSb@>Lec$Xvi zU5jPFvPphMC-SGKl;D<8Q3%g1cQBQT^?+D%n*q$F|3m2zWJHh_|0-XI`858$D@0ZJ z54Z1cEJ)QHQ9w$-&vQ~2eS8rOv%_Wr3XvL!MDf_fA_L~ zn$td<0ye7QD|=!vBWPgZcm&}qTG)JB_`0y-WJ|c>)F-HblsS=to(H;K-IX!o4+mc! zYbO@})FL_F8t4vNN0avZGpZF=-bn*SMSqaI{`*!afln*v&fP^^ z#C{fNE_W`3a?zYZEJfZ_jLviE6hFnzx3QcBhhEU~ef#$61VBSiuYzb2I02+o<#5L~ zN3&0{?%WYRD9?o-Fve-IKY&k~QY}+k1KVZsV5cA<{J*nZk}pdzSH&FSd%SDN9bvuW z-S`JoWL=HCb0HDZq%KQeK`QNAxg=D%sILek9ON`+tdEQk zP7np63$U#Ik4)F{<586xKTGoe`b2*Hwq5!`))Nqf{@qkpaVSJHHVfS7zW;P@fn_h8 z{CAiA2`84)!}}qhU#HMOlI>r3d%z$4FaAYU{Wp=)s}rJwA0-E(&Vzm(vAYBCeDvvd zLh70}NX~@HKy|_hQ1`^_L&dobl=-7F@Sze_^I?1BO(6=@fRTQS4gm}tZf~wQ5Bv6> z*?5=-r;uIY;Gie$3U)coCf|CM(4xesw!<)-Fnq!kqujglzz_z-MWP;ohbkh_0W?*B3YOC%+>EqSgYeXE+8UWz*J7T=AICHm$G~!Lhvl?*mB9i7=)BxzFfbDrD zieR{dQT%^1rAn_pF;%{$6Z`ot7!&@7Y4GG-%~9^@&1?{={wmi5)}>0saC?<0SywmI zBhho$m@sNU6xp=x@bFM6EIotxC^iIdqMTE->?o-m1JXc&fQuG|i>2(l&(tcF8B*f{F9odrTv4;);C9Sfq%VD4TGM5(bYe7 zQvx>E8UjF4lsU4hT#uw3JM;Y-&nN*2%2f`Dip9P)p-}QcPeAQzLyt01gyD`Og?rnY zgLZ^_eHiYwQcQbf^s;IR*gl}fk@0S$g?KxRGEK+!NVKxUnd+f}=OPDOJ2NX~6;_9icXd~0@ zB&zi}L}V4OD@&zNai-TdXexdEQ3Jc+LkFVbh45WveHV}%w^!v?2zO3((~VCql4CatP1;7AFxMI05&rra!t~SQ<2h^e zO``I)08Bt?_-Hoa{eNV?Alglqjj*+XN&umwc<%`(??cha%)YBe3O}e(uCtIKx%$Nk z;iH7A@(oZpgixd^{$4{Lx5aIolAkPe?|2Yei?Sjc8}GpG+(29AMu!rXWjF%y7z47y zIQNOzcppB!tHK1q-j=`DBC#Rz?zBEKFMF|vkz@F-i9aZ5x1K|!O1_2_Sq9Xqm=1C2-~A0T?^#iZQPb*TROkL;APMa}m^ z_}8)V!?($JYRAqZKE+Wd;*si$9+n(UA9_Z{ht;b9gsjg*TaKv1;Am;VGp?Ec0;=I)#eDwRTdeZp1n)v1nA>EbBti6hrBfHAVu`P zH-rRex>Y`u>t|%V$l}06bNNOx7L^xfx5~%#+Y`gli{|^v5~$t+RM2VItTS1Ydnz3>Y9g zRw2T<;d1=(#itv0LGPTxfV|_o-$vL1cP?y3Zm1rzSVQpYro>GRE8E{TgtXE>8;D51 zI`NmV4&tc+Q{i3FmO;J&gx;Pn9EGUp>j%6MlSG<^)vdP<+Uw;BE4H+M52Qzm3DAl- z6ju58^QYoGUb;}?_#fF8fIQj#T5Ac*zM$HbT7n&?EobkcEn+}nGI%Ne4Ia!*sQb^3 zTR$@ukb(q?mhVe8m!4oht^dNHp|)un52a0x42MV$Gl6Dh^b!|m%*zLX^r*Of$f*(f zeH!1SY;!G|oIr`J`U-ve%#yVzIp8ylutS(|b=;}6LXpvR78!F|bdMnYgi}b7C(b8i zN*(>yTG1;%^YX!+gQz$T+=qFSlWC{@sQYwo`Xw+fWMuHYC>DKgnWLLC#rJ|fX)K}b*diD*S1nKwd@0ac(EuF%!~>d&r)6c=lAU*L%T0YXox5C(ZC6#667 zF+lGvqq!#lUr#cX2Q?4azow>wbn^gza6JI=ZQJ(9KnfUubjnRQPea0KrkkR;lk12l zWzK^~KVa71$p@W}oIw#VejRvx6Sc~bfeS(<2S?&1Xjj+Ppau!w4Q1>JN?An18prO( z?sp@r6LA`a3MT}F1TP0@wLwpxcS0|K!|yGwN)^a*Fl14{;>?pl#QGb`G`95V=?1aw zk=PG&#V0lYfj3xX4H(ckUY_hd8W3E-FeD0ZK5xm1sw+ASsPT@w(y)9PbX=N~bcaeV_PsFI zq(TBkF-@4=SbqF3kI&Q(oW9vMpQK1G3I9o}ZIQj6>-Mx~JLQl|+iny*t9GkCzZAYQ z7}?t53e21Boz_a7OSSTQXy+o+F3vr=Pk1!T`I$ZY>!Cl~r(k zx6zkiprr!k&Pl6KMk^3C2!a<50O1F3Zm)hR`~|2wr9B{?5J zMaHpp)E6pGw!IU#FLoF7#iQn%NU~;MB=$1x6pjW{+&DzMhccl(v}rfAa3#-vddf&E z8S4vJjV4I{P_Ci-!q2MpoQap$eWbmhgET%u<$($wR8L>#EI|bI^yNPG*Iy5@7F3L- z!zAD$Ko-mi_}l!xyR*lAYe!~W;)0#@j)rgHC3_%m2BA8E zQloT@hfUqd-)WLL{9Y**X;Z984w`LL>l4vu##yVrG(iVWNza+?|Hi74>V&9U!su}q zq2O0OQG`H|!g$-P0{kE8UtoX#sB*DfBUAp1ZW8}k_4Auu(N6W@zcow#4#xk}K@$lO zepMU(AH2=fsc_(UL4AKWB1w(|dW_+G^m~7yRDj^ZU4n-X5_=;LT$MiV|Nj1;KKxHK zDm#rk{=agox7&04i6X;Q&O=hy%z?i{UHwmP@Na{KtEc%zgRs8?mL3g#vNwk~b;-kv#x1~L&;xz1^+cMfwMLIO{R-0j|PWwqy_^t>Dx7l{mkGLB~1yoq$mF`JHvky z-~7wwHa`naz@9)q`?@gwNE=QXDa%3jhuR>Blf7izg2AUr-gGCh`yKD_+vfe>-(bj0 zabqML@ITQz{%IHf8}u(?g8mbp?=b%xWtn4>-&O8^0`fb^`Nz>Ho=!wy;ToX{)~;aD7OnW9g1clgCw61fSTUJ7cJoJa z`AU=Jei>Fd2k?65fyN-IJZ~4*4xABf2Airb@Lb_Ygl6U79RW7>TG%{moeJ=n6Y2l> zjS4GRXmMW^tCH+-8GN_33a4Yhp-xgDJOHbK{IWQajVj6Ysyl_f;QUeyeP%+q5c6%M zy9UxPX0PtD`$8l1n}IXMLR`XX=pBPykMd2Bug!b$I1PfGOy&C%I0|1=6I-UU}X=!QyO0yk!Uzu-UrwaQw!Ggj78k5u;%+1y^ZPj0K zz7;XT`UG0k>;;z-PfAr)Ci?-mD=w>8-PMoJQqQam7hVQCz0MnsO`98Q;2sBE9S@Ka zf#1p*aP9}48~ohw-_e)@L6;~!ts`#Le1h`q2})Aj?_<%n?J|VpBUUe)nOJvYTzzS# zUvhi}vF~t(jv~Y?0om`5>Nt!gLQf^=n$i*UQf0ied=n!`w=s06{JQ4iGnbWTLOtlJ zWXX*&s8e;G)Mx5`b+ZZ@HQ}kMf;Ibke%Ae|2Hnj$Fa;MfZjK^5d>U!Y1D~hvqbP*T7^Q0?Np1a-n95O>A@|K= zWA$?-*Xoej4y+ZH>6~$-7AqTz+%mZm6Jv~V%=P`fzU|}ZuO1p>d_SMh`~Ci0&!>EG zOj$|kBxA0D@fTIQB|oQhSJ?F5#bv$*M{0p@K_*bpWiZ-aWfvFN^UkLLxLYZncpOD@ zPzdKre0h8w3x1{W^B>J@{>1z`A>qh3Ht3@`r)|#Y-A6KFu&I%2?2)uCEVD8FLSFde zqk3CiD=#)?Y>KJ5xT+p!9Fv8<0*P$CU$})5QfU?~{#RBT80s@kWqL{HxA{ZZ4byl@ z$)u<&R7p`0Y`9JBg>;aK9d54~0GEAprqjcVt2i+zXnaXfxSj*a# ziLqE3U?nSbfpC@2jOHMv+qZ}CW4r;kxT`l1Jx_?xV_Y4xN=r*M{VLB1-{&Z6Q$Gm8 zI!icXMTL1lhzEH!D@T7QeSOPZSuuBG{1KN5Gm|NrvO=on6$$kxY#Wyi5FmekF~VCw|E!OvPCs8-RxO?Yr}A>` zFAc+sz*zc;ZjiBuLZQ^mWS6Pf94mT*0f?CPYPn9-?@7gHaar@*L{R3OWxN0FeSm~P z+T5A`d#^H#K*go(ncs>_qu$@RP;z;-NrCm^$Z;s4pi2T*O8S-`LzHfEjA*wv*S3lX zYA?tMa$y?h$L&y0j0fLGKqo|}^t126{E_Z|9D<0^#x2bADK6-dpkjB_r#hT&fwU!X zxPD6@oa!UJ-euo>!3Vh`y=Bbk*~kz%n93Wp`8=NUvw;0nZHJBD#$N-JQAtV3!+3RB zm-AYxb{akOZKN3^VjNKu40qtJk9YL9-fO@b9h=Qo(y}yVoX*+W1eZOQ1apn^VgCj& z?{HKSVu}0@RZRybO-pf^A7`?vVk?sxUQx`GVm*x}{ELOfTh@gZHgrjIlaUf3iKLzE zdom_m1XRcc>`Nes688;(y#091i=m z1Z$g@fTRY8!79ya4y#>=s^JJFC7T&<*}){qOQU9?P9`BWHo{6_Gb_detTMn1@l=p$ z?1(NxO!uo2oy1NmiC3pfhcrTQFC0CCnDZm%0-2KBIG5GBBP`?_w=}B|;Efa2LPJA| zMy9k845LAZ=!xU^{Dp;WVsPVRaG*nu!YMO zfgM@q)r+M~4u@01+O|R$Mjl`wKjFQB2Twh28u&s6^|^psUQ7w+!=N+K2m%UQq5ZgVC^;4PC2O1gjiz*InBR3Qkm` zQ~syxVQuZ5Kucqag~dfJ*CtOENp^gJC0jloGDhY36fIi6IW=l-p%3G3r#Oh=0dz)64u;RGMYw#l5Ium^mcyYTSdqv=Dz(GwE|hvnVcF^#Wl3&kd73x|wil>_ zA5V>awk+KQ$?&wt#lzbS{t9l%}yq=Bh*^wwY`Mh5s??`rn~xK@Y3gRm%z5qxNM zb94P(Pd&MsklpS~yS(mx`)Zp%pFeMGGNOY_W3ko}GFyml;5tH5*LStG*i&~c{+iFp zUHnQV?}#)limS15fI!feZ--~JA!)ta;f(wK))X{wffrWShaNTsL4b4TNQa2VmlgWHQ`ut{|FIA2LVbzFM!{{^HqoSZoUwEN`w+-ht0s;Fp%;Y5~ zef1Gs8mvh~c3IS@j(kw(!ThwxTmJ^^ypyBxqxDLrv^lgo5L{qVR7a|(Hyyuyq_9Li znGZM34qf+`319!X`yG;eFC;<1DLjnsg*SyX5xUNlsEZET9(q!ZY#e1DLe4pVms84W z`yJKDPzGxIw&;O~eq`H_L^N=RizQcvSrH@UtXulki0zMBy?i<7x|cF+UBE9wwzeJb4S-v4OmDE)1X)EUwjc8 zpy;?|UURq}$B|VAXjTDlCdcjkUg;>+yq&$ka^jt*+Cgb@=i25^az@l-a4#RJz_s9x8*yWdWdigyTRd+ zQ4sm@r)3Rfka&BdRyk%GRct`N7<}mxCsx6%M>UcMX_fl50-C@_$=ThU(6V4dO=f`? zpm#_+Zx1Spa8m>=gM;bq~`Li)I4rooBf2!D;rZqA< zZi1S5w}K#yik-S`vO+Ws3Xju9BwDevE)vz~O-g8K_G47q#l=nf>ic&vTbz@z#0J(e zEANTY%7mBa&48Hsu-l6|X0>Yq#skgJ((-@aQReDEg?pf*7&<Bx6*C_MAM{&YM?MM+GMoi%7&C1Jw9_Nn04k=CUCjS_t~iD zp&vB0X(B)&uTtd6M!sxxVe;FKYVfP9S)+es3a^WdFofZD*!5+IcoufF@VN{-Ly&SG zkyn$sVQyU%;r73KdctQ$*6EInG-hVCfw^>*GmF{nBr4;l!&m!GoUjV{xBwDoG7&WL zXS!EA`(&Bx?G|s$**xusky>t3xK9TYpvY))H;#@z_+SWgU`(kfQ%_ymAAWB;#n;ih z%5rP)&Y@TTd`#Tw9En-S*PJd1J~%lW22h@HvjDX82VoUHrfXc-LE~4o%1%_^#3{aX zNARf^+j6RdPzPe7q`{PSl*QDolAtSC0Y48T3iza4R-Se6M3!Dsi!~tyLR>z@m-}A2 z^gCvcz|{}5#LSuSx;~YcT=^sy`qvW8C1$^3y#xQl!eky-;HY8Ks5pHDZx`-Y&Ac*5 zIEO*}!g6=0Wb#R@sF=P@uBtnNE{PHfp_NXENmjhO6I!g$+^74j7C#&loPxtQW-$L| zAWLBVgM`{6jvZYqZ9S+X>uHHmPa{G5C&c;gV7I*^%Vf+KNok1*b1@~j%Bct-<6sOt z)k?>t+n}Y^EzcWma*0-}W4p+qB9$bbnH#eKZ1!#zTF~GVA^^wTsoq_sG9hAKTJ%S^ z6KmV?Vj#pVO-)TFnA%vmAg6k{?Z{qH)3wX3Q7N4++>=*aeqp=1&fkX=KV|YIN0%Gb zNCU=8)U*n{da))#8>j0;E_1u_&wVwl%dqS7YP+}pf0@{4%4$?)WH>KBPR1lRW(xij*5_%6wl-@fD zB_z@b0V2I6cF&Og^wQ$9oe^^=V#j6ZYc&ok=EkBtJ-*Ju4LO(zz* zwsvgauHU(L>rdKVRcx@p{9v59OW%D;T^YGPd8w%OzPodbnA#@dK>6hv`h@5|HTSJi z3guQ6w6yP@hguIsK-1wvE4ZB|dCn}!6?S74m2hy;e;(NqVyw--#`4dDQ$R%V-`8gz zAwFICKcB6IM=}2QBR%04VgEh&JBXX-kN+NxU%JWt--B%HZrb1ed$gyhO8eh~B9{NT z)IXE)KNI*rX7L|W`2XFnDo-iRK8esm2q#3&Wk>kg>fw0p?z{5s1w(W5`qdSDn$$_k zOmh#_k0f_H?1@(M623S~CEkQt+q{m%`YAqsW;ijlI^;nxGS^kc+d`7)u=!R^*3jA- zH!S`;=ZDoV5Apk<{qD16>)42>5R)e7nZeLZ_n_37W-~G8B}|T|mK)iwbYkD?gxzum zyes}{H$^=7@diUw%!eJ18O`H|;Oll)T@EY8{?=vs&z$`ICr`*Vh}E&0k&or`QP?zD zCRE~ZwQ7vcYN=}_x{FqvP8_HI*)7qhAk8)D>T$#ph=ywl37! zj>VL%yKyT!J5PNud|dVF0-Y7lkGO|N{yRbf0y2>8^?~-=fn&JPh`f%EA3uJXJY3)% zl&v@4!213vBcn#E|MY32?M}=zSHQNBUWnpX#;;{%>+QwaVQK0NvdY?GKgRw+z(2cb zxx(k?=cPAn^!2yiyKLuOkoLFTo7Ofkv$63Uo6avy5)_dXO++UoB=P$X4h#fi_kybv z6B2Onr=xn}zi}38RJ=(~hgZDOz`vRAIuz8iF4aM#2i6!D{+4z3AHd)aGf^`Af%1Iv z4C*a9R=vHwU0ppoT3x;UzyAfVw!P>Q-@R3;c})ox_u=4*w+d$HeO*+jR=PPWeStga zwEEG<3u%+n_xvTq+;$IL4{y7=xu<;b(Xum3Y);xRV;834qn`>&Fxk$j3 zSnHgG*&L%_MMbNTQK}_HlcxDZfS_-NSt9%s!X&{(>$_ zbIJQV%c#L*>S#hUZD#-9 z-`E)v77#V=u&naWU7z`z_vXzx)Fb^@uUZH4)B;Ywe(f&$viha5rG?Dz-#su88S|lU zNI1a9_umuFe6wqNnydRMRw3)4&r=}sW2C!VnPj=y+VYy^)2D|$EH)L^a6U;%Pkcee|;L|h|kIX5$WtqkZNk`R8>WzliVqxU{yB8N-gr^S&|d}pwD`Fs`wNH zeb+u_ngh4tiIFA<&st=~GHJ@cu`y4XmRs9Ll2jUN-r&M#^_;M+0JUmfJC43bxDCFn zqD9QPH*#ogZFQWG3$un3#>{LA3kv#N{nl}w9>fBJVs?&Hn8jXNd~T?!gUPqytt%KF zWB$Zn*Yc<4mzD~IJ+1fJIN^cPV;77q2y=PmJ?N>I)8``wB~F zEVgCBRgy#6OZGl_dLt_8<3(FrkL86W`Fq(zMOWU6i;EL?AKxwH`RYt6%}pv6|0W485&_J03> z3-l~8h!6p{^EEi}^E!6>`ToHW3o|oVfIsNhPTJm7B-lg%z`hV~Lr33a1YozZYT|>^ zO#gt^66}IxyoBehFL`ka7+X|Sf7v>_pr9bUKq5oBo;ZQn`SF+{CRTC^kN*1gtG0!I zhHR+N-Md24cjcw-sOaieI5W-oxrcVeuukV#u_bEU347Y;)PuRitjW~Uv&iP^;Ici{ zMT+5)na{!&t+n&sKCL>gu;U$6?&`G14vtTZ)zw$84-b0w7ui}#*OhL6YR|LKOoGV= zk+m#C6d{n;nwPp_HYhd_=jU{$A7VbRNw^pQ94#nti135VNR%g_F?A}5Nl6^CA#S8o zo)zJ+g1iF!VD~+KehCSYU9eiyO>OUOf1@8rll#dN5pF{X?MNMSd~3IG^)elsctZml z>m+q*E0>OrP44HU#+yN_$;!Z!p0SnBpFh`ldqrKQ+enolj5blcnX>%6YAc9S38SN) z6=n@b29ryxUk)cH56jlqRnMP~q2EUY`*Dc6I9=k6IAJIzvTTf}c-J^UlR@+=iu-O+ zfx^NaO$kht;}fpA*lWjN5fHIW7 z4Gb4zwHw{y+*d#A*+S8GF!4*oxX+d>tt~}vq>01E9Iq()tV-OQ56Uy*lyUh2gYC7d zXoC6R-&iItG}#VZHDp@QAc~6ASRbMM57uQYXOCboL8$e^hTsOB-o)PENi1%YoI=?y zFp;;4k2lQUX31)inIjZ->IN-=5Uf!+F*=GxZZQzblM)lfy*&({K0P7`2&Pl>(|I`_ z`na_&I;*It7;id1%ZB<;lg!`U^hMMLOv;!9`?~NJ7Za?4=+w30OaTg_ka2$-yfb8J zX^E#x9=bTm=RDFj>rO4-^U{&sIwH*YZ&b`B5?6?I9uvZe&l$MeM-~bTb)b?26E9zy z=7%!wmhe2_M{#yp3Oic zl+*fcU1t7~iIJ3(rHG*@u6)a**6{`j4;**mfR^^%sNl7(EQqkMaD`79I7^$DRQJ+F zsKN|DaT+Paa;fWz;tL0R)HamFuTb$hDQWqHggl+DWpO=p5eeBR!qbrzEr3L2$cSd!3{CX0pr|9J3Y7Z!(}Gh%gr_caX;Tx~TgWH8yu#8? zeu?HPrIwV7Oi!DWkm8g9Ou>`Ly~CJ)lF9KuD70Knnzp9f+od|1U{VZ4I3%#LBbdjmzBa zN0)L!&#H1w!*iVDWfYo{5)$|;I73e7S!!6s+@H%p*5BXNcBAKzuX3O`I2%5GJohFX z_0;?eNg=?8JjNqae=syKVDYm}xWOa;iA_{T_W9iA7Kf|eqB6@jQ);+#=1jvFlPi>?(jM-uo(@d-;!1QJOp+{>!PV{7TwKBRI;n)it80gzYVE- z?)-v+C-{gO`t?&y+MMFzrqG3tOoeZfV3uqgOuQSzSe!>|V_{x3Frc`$&9L5c=fVxG zLoJTC5KRcHhH?(z+XLO*%>jJtyHlU4tEqg{g0)Nx3N@+mGA5e7?z|=@%mPV5 z?9yIe$4~s0O7>2X)&`lodv7|t9yvL_}t6N$|{fJ z+#rXFh}Ohr$@0*BZV4m`X=cwkZhg7?f9g*xfxiV)7-eMpxnaGIHrkn z4)ZwA*M}#OdKl*GA#W}%;i1UluW8H$2DHkh>MR@Q&DMf8L&PLG;h z9495mrPRF~xys^L2yj(Uu>Y-+5T`<*$#lX?Q9^NHmV)n#;C!uGjiaM;zbSQDA^eog z$1xaJ{NT_S!q~?L8@EvC<>jTR*(aZ|7oGc-^6A?tgRI3JZxrR#eoOfF*Bo0O;aK$f z^Tz~2Lz}Qoyg8}q$E9t028xOy5(>f{RQ%wi10{6exiE`_+4$*fHt$(1-VPX)REXk` z>+c;^G%wqB)&SU?8R(HJ^0U6G%FmrpX7HiCeUT{nm7kwqLqkLO-8)SS;=zAo*9DPQ zHJzIZMj}CrpRyodES3s#yL7Uoy>)0u0O9+Y4MEChLgX_Q--9fLr2u({CQROXQ%0g< z6ocBa^Ko$rLVbGx(lN#NfKH9!0-zv8GR}nVV-$ARae+O$c#X4N(X0;+we;STcaAd= z5nvSc9#3(_Z5f%7ckiki7NP_a0UHLl3o}%8vUh&)s&(}`v<#khMZ%ekb`{1fKgA>s z3Z}E)%6%Tjn4fPC-)Co+bZBhZL&Fk)ndMiP$Hd5XyM!ptN2O%-!V!nq!zr7PKcl;V zWo``yX9=6to728J(rC{jN>JlRShS;}S%kibXL}0sZPL-9u0AB+46N)zoG@h3NmppG z%9DL!$O0|JAGR_#TrJZx@;ZQX0K%3iy_Jxdh>+dLxa7!iLsEZJk1HK&xE1m30Db-G%>A({5v6YQoJr>yH&I)Ft9hOGRdYkqQ-v4L+a1* z(NVJV9QI1vdG$yssV1H*i1l%hkVTV2@1>z9JQhjG$wOa`irE=iZ>)OacOznx;k8eV zjCSY8Z-OXWYAC)o{?(1u;u80HnqA6TG05Ot^98 zW7?|4LeB^Wx=$>q)Dud}k$)z{&9ZfL35pc4-#%MP*;?6I{CjwM+G}hYZ_(8BB+|5E z_J-{IO@gO9aHE00PZ2IR8V?}=mX|{)Z(Al2u3dny-@LJ^8QIhnxU(#LOu#H5ov z0i$7NORoG8VZ#iOaRuQcUtBOF44Gsy@(!obd)skt@)CRmM-`Q44o3w45Ohpg0 z{|XZu^|Z@oW7=D<9Ew2Y{*(3j>NRW{7TJhjW)52GmrB$SCyu2Q#;hN%A|ycym*|!8 zD{EI+xX2ATwk>C8yvSWvb|0A$Ecx@H*Y0At=A3b+O}>HP;j#BpzkyDSzbFdOJ?9Ep zUYamP+Lho!*yrcnU3dTb;X(SgA#=bl>5i|{rDqHBA&nQsy*4@}V6JaZz6E}<_(P30 z$Dmj~h@{<=9Lqi=%g(~m_3d@&(rI?)Cd>i4_1Y`qZA5I}5=cB0sPsl*j9g?A-H~Yk zcUVEevS%bJG5&9mZCoz}3|?Nn;WO%(94htaHN}A7=DwwmA1^LFYOK3PJ>cd~ENNNK z{C<7bGNZxWQh!PL`QE%fzAD0lcOi>YYA;{ipsA=aVEz*IU{pY~w|Db${ZS8%=qA_U zE(}w+InxAL{P}qM&TT8122-JTR!IJ2Od0-G>@8rk$9*BcF6iXqA`d9s6cc=9=)`D> z=jc@`l_KmFNS=)IdUP-BFA(@JJAWp8f#&ZR^^Lt3%@MpCoRlS{7R>9?Of<64@Bix%*$?9cfBSUp=Bmzq@0 z{=^&2ON~FP$Q_nex|8SF`2?gCjT!KLQ+cE+p7Jw|{9H`@fpya^&98CZV3j!DZEIlI z#Qi+pO!OO5#6+GQQ=J~)oi^KhZcF?HO+W_$}Y;EdZo$D-g#s+UnM_x zx?#_dvzqknihd$cRqhM+C%W!l3(R4B{tRW^t1=thu*`C8TyH0crRY%Vba;-bmzz+( zF$h8}Q5+Q)XDb8c0;xZffSSzSbFeJEGrsHXH=9BZS9_j!d@A{s z0Q$J)s-DN;{C7UDaoW)N`1(dLa1>eQYQ=>!j;<#7&M&V1dA+x{_xO*fOiAd-R=i_P zHK`w0Xi%K6Q#w(h7TR^HE_1Y=+pxr`4-(r&`D&gBhM>JA{^L*D>3=o%;u;F>9zY|^vyG@8kNMc$-O~hS3&i1p;PEI~fE4;#)@=TUH z-@aTU66xsIO`RjGHvPzY!DfdJ&xDh_FchB7& z25`fRdVGncbFH?@8@_Y2kg@h>5o|I%}1Ah5Qkd)NrJTk zs<6YI|EDiy>2bnl`F&^O^YZaYea+I+#+;>=!rG3$D1Lb2y`Hyz@)wNwC$e9MLQX;x zYa88Hq@~KF)uq9NXf|=1|HT7X;&PPdt*NAwf66N3{|FMEt|@IO>zd9 z%dJsfd&j-T#AM1og~AcL z*1?0YqqbyxxxrRpzHe8^`8MCLC>MsD_mGKF=YEhvNkcPZ!s`6ZFu}1_Ukw!QJ?> znDmne!%k*q#oLq1`+gG?jq?2BrKO2x35wgA^3C9TeZ3>(Fvg{&>#PTK2Nu>4T~hx1 z*Li9Cu-IPe@M@wefUme%+6KU#a&r z(^)vS$nTxbezltiDuT*p!Sek2?#PMf{_1qGKF`}9pCUtPqKgaO`Kw8Wj!Iv-$gM%* z2r-t*u)cGAeB2g+z4?2CA~w{TZaZgrz-D)ktO9&Wkrt@cD|h87^WIE(Q-7#M2XB4L zv19O`)}YXH9gZaU4}E)tdRN}R>`)M&32P2FDqhE3ICAlh&nhw<3*D=j(5Kt`H%0yh z=UI9aloD4Lms|daMdf{(Ik#BEL?vMd&EoF|Ic7kn`QnAc1@7&gOZ1%bBl(7Kz`A>6 zE*tclIi31PDE6eAaqSYv7G6yly$^;%`A)9!_bI;r`t_C|tk>$r#cN|Lp^C) zE^WfYo1_H_JFh8%<%{8@K7V0UR{QYzWOfIBD2{inwD_Gv!O@Zb@@c{)UHZsi&mXuA zSJyG>Wc%S}IXfL5qL3!)z5^#6mQ-%yxPxpO~Xc)vo*_+$3ES3v

$5e~O6);LSKn-TF|1{x6{{tch)FEPa9PNO zGXm+>*4BSz%{g|-L%43|G`p}lL5YFswO$35!1F=Jss{enrVf8K|kn#m3WSoJcqu;`n6Y=xG+UUMDy{$ufYJSnTv)ur_E zPyV`2emVV$0`!636PPxoBNwXnPi3Ae_aclcBG>Gj)CcNlygC`M+BO_TEg@14?$1oU z{`xon6Ne34u+zda$46}n|AuTORJe(}(mXKHaT_4FJxy`{@pBqoot<}-En^c{g8fL_x@#tE(bsZa9aa{Nnub!c85vNUyt*`erX@xt zLK)NN$g`n4+~=dq2J*S~ID%LGKF7z)Kt@YP@4o_4lEVGN?G>&S^&T5*>(j;$hKYu+ z@7^iA7c1`Oqf2w&e%C-fb~9tq%fVzHRQL`^hI)F3Xn0mwxJ@F)i1k5TbFe^Ir|PYEtDF~A}~1ey!`^_9iN&B>;UW;yC~BSJ^Lq@bW+ zyUu&t=hOIE_~*m0c6DLo-RP!cWkhO=tc3tvadX-`yH|J_D2RA!K)q2pQHG!5*ZE6u z7o-x;MV7WG{A1lvPEbqgAIpv4gUV)zY-|**J`CIVZ?H@sZO6rR|@@VVoq zYCt)WD^5?$fRV4u{FSYnpJ9`>|Fyil+$g4zQwrGMsP0@fs6hbNR z7E&S_w&bO(oVRNc2t*D^JY^S5_Y*eJuM9xoqlELR-=D@-hIIKNC4Ph_WN5 z?n0Cz(qvm*JgBnrjp2|$z7j+pP4@vB0(1dVi@3Hq6ARRPeuv7N)jKn06*!1DEsN^| zdRa)#kHWPQN!#@&hVgJ7Z=JwBcY6~fJ-tStvk%JAP?66E?xw#plyUIG1dilA+o#I2{k2FihI)KL z`w&MTPZ19mhF0$I7WPJhJ8N*8p)iVHsdLr0&V((6`mIm%#;TYBBOGzu0UQ&vZ9Y>^ zG8RHUSWsH3iqGOW`7u5HR6zz(+w#zExzO>ABL9Z>gWc_)Bk?vtzMXSCye?)BdP;?L zllpF^ma!&qDSR_INoHq}cj(4&6u zu{^G0aFJ<$j#c7iNg1#gCAS@B&;Bi$0{lvNYYma4L#vSUcIv4~RRjldu*hfsB6Uf$3*eY8mui_p94*IC$DB~nF+N&wY= z{w$p*mDzu1Kn2zM)z!JDe@=x6&Z}BmBYoBzAGC3GYC1sg$adWNbY~)X{KE**G<)z5%xh@dh-Q0YLCcS&z_oNQhO{4x=&$-cVK)bbF z>)(Qq6SBVC*Wb&mXxVsUd4=dpt>g4~GMe+2rsc1|mTPoC4nYB(2F&cnaCvl9m44?& z|E1*Cp+CZ(2n#fw%>s0KU0D9hUq{By5ANK7S!CJ1rP-^gu5M($(&UXfjo|dHkq~q_ z9$x`PVPRuFil6EMq)qKIM_x|OH1wyFo8_BiHKL|%va2@?IGr__xDC)YF)@)#4x%v? zTccay0g4i31M2dWpBphA@IQ!vX z8FI1!bhM_X=Eaqjt`2-cyjS3}4d`J^(2KUMf9eCeVj``_yyw_Hc1+`5xC?H;?KfOv zStn7LBvcY~myiwy7{&<9X622r$#kLGfle0Wu)gqleWeDkku_0A?z#*Xx<|ul&30fp zj}*PfJA8El+c2kec5{eU?J`}_wpUxk`AJFFW8l3y8B)Aq?>|JNQ1S6~bxSjzE!)Gt zGX9b8)~2S8luq|Vbg5BE3aD=^P_0X9kxhtM9EYpI$@a{Eg}=HhsI@gRzZ!Ci2QrF$ z5Eb_QW0i3@_&~tI^}q+u9+8fA zI7}Z%T z$TB@kuxkYKg1eodm_dIL`KM!8FYzL`RuoAHD1`@v33uYY+={SK=Df$tNBE9QD>%cR zHKN#ci6odmaA# zl^@IdWXqbbD?vwGPEoldh_lTu_#M=vI)7DDHOICrP2|yLVTXcnyvPCw-x_DjSy?55 z+@~WAj;ih`g3zYuzO_~+Js5#>e{aHm6eLdnw4SN~V_Vbirjp@Tc(j>VHd?MjK zb<>MmOmM>w@)((t;%p}%AUtG3Io`c7TwzN0%FX@h(=YCpHM)Wo6K8bhPrcCLLNNl96lEpSG^6b0SO(4OzllchZp&I zX0#6~p%p;gbZ}f*b#jeuJot@{P2^<<7xAN}#^uYq5tr9&2;VqorKhIFbea>(a&qPf zo9NLg^(JG`Lxn*r-~&lDpLUYBw|78Me`{<1r{@9(D7azIRs&QL?egQ0@>}~cx4^vu zTZe|F+aU3!QXXrt2Im*O}5XGi}{otgcSB zfZ-OZ{&5`8WGejvWD}>PTww+d`Bwr`9j&eXGh?$uT{SjYo1lWyJKNu{wdj?9ziA)n z4xr|Ko@U>o$%XP-sSCt^++g>80YMNCDyynIR!EWodiZ^J3O|Q9sD+eg$dIdFbPzQnPwA4ZoKySEP9*4EZmu+cqn z@ay$PSF0c-HdLq*@(Pq{*+oSsRfi|``uhFs*9TG0 z$e^0+>r?CP<>l+^8+@=X4b~fwtEbK6AuwsXdwUz}ZnG9m@-ntYh4k(Mp`$eDz)qqe zug(q1%&0L4##XUWgT4LeQM8Mj`&3zq6>uIfor|k_E3ZSs8A08SsZ&lI_#J3NxCjps zmzI{`zbmAG`qtW7mRPEZ8RY1U;hb(qs|I$rg^nap{iE2KpE|p0vhZ>VAAsU2@GY=F zaQ?d*Wk-X*^w>j6LneoU3+R91$$kFy>%#MetNgvc{tp6y1>+a-dYX-OA|A(xh`526 z%DvPMhLCuw_)i0e9#csKn+?65qX*8)z+g158p=jO%wQzkLjR7~UrHw-tt+3>J^*cS zB*clik}1u9_}Q7CU!25zz*o>YjG;3W-vbx%L`vl65dUqu4hR9=(~q>zMOY>jyiB8X z(14Z!IxCKU&i=O077}(QyTHq9JIIfOt&i5FBIQ_{h5q^Y@2-!aVSC&N9}?5@XJt+RGLJ7fYxeabk6uj-1?lnXSv-BY4h|sz? zep%RmcPFMz z4t}gqFYu3N>M5DiS-qxaPaayJVA%9zn_*aP1tLS+R{xcO*K~1BvZWP!>a@9}&&Ie? zc;l7p6)@k1z%IO&_2JI+DMIEl$GDt;fT^0rNzPCPM24tp6k==pD%9xOHy%Iy%)iqJ zI*Ex^L8@qsLYbI^i`dSdIy%W=`}jKro|KrZ&`e}zW|8;^>(~yGl9G@T_GKcVR<*k7L?jaAn~BSd%N0-IJmP{2HlZKXc<+ek>gQ5V zhm*U8EV0l-HzmwGDoTc20b%&}%U-+)+vAfsMR!yZH^^8Rte>q}s8KCY1zh{TJ3@Y_ ze}9_0$0x+CPCK`-bo@=OLRpz4^-@|wzGr;AduH}dCDc?% zq&$ghPBb=+)sVhZVTE+R-51?)Ox*4v=I8cF)jktw0nFEx(EZd}M;G%>ni}RVPqIq_p$HabHw9_^q_DK+J0a1%U=9c!qh zl-tX8E9}b=L2(Ezw4g@!K1Mqo@hrvXGoNROb9RL!vCY&0QeElFi_%vL?n_4AV6JlN zvEf4PJZG{fO;zaoFven9Hv527gULHqXy3W2xIPUP5t=i3Mim01i~OK@_3Kv)HPqjb zjHOZ|qxT3J-3)sAzTRG6=MlHvN+k30-EFj8p-{RM3#-4VnT;-{qj>6X`HXqbyTTR- z?b9!c?;&zCvFu(4XVL4}qdqm=OkE8f0RaJjSHfQ|PF$jfJkEy48)bo)Oto%sFy4%R zxor8=*0!otbH}msj~lB*b(uw$YvogwlIt#c$jow-u7IfM0*!TfudcrwKji!Tgf1On zwx$$SYvEyiTIQM5{BTM^()K;${E9u>#)ez!= zobuh>o+w&Rm!%1w`SzPtMyf(Kg_c_=+y?Lc(?{{ShM`4i&Tq3;8EK|wD~6AA#PM^V zB25`M=`@~bIX`z5-z@&kPkU2G-n>uq$>+~wX!mKB>)vD7{yZzTgTu6*Ey?%+?Pz>} zeLCaU%A&&xL7LWbED(Re<<;mn^I@SY;+aXAO~4@8Mz#PQ5VV+NU|6?zd4A9R;>8R2 z%8eiW7+!a*-@V;!It~tZ1bX!$nnDCi8XFhi-W=-V?e7`LI_hcFZ#&*y9W7`cvId1m>3h4Y#XBF= zi%pUo+RpF2^!GM!gAp|Kt@W$yNLxQHnTpnUTV{})zi!VwlI`5TA9hbjNGjyk?R&Rn z?+CU%vT1;M62G5?^Es-PZQGaT2d+@xgtaFK$}p&@;>8|3FhUxac*}`+41FIPsrsP! zzM$R&0dGh|a)>Het*%e5hdV~7!__@2hfXE)tLnlTpHr48goc{Cx;LurSK@Y(%Rwix zV-J_b@xj`|7e8j>QiQ|thg%K4Tu92B@e<;A=07MPTP3D&v>5-cyban_oIMOY<{QK+ z`ZAr%lvFQq%-hF*fQ`mC8v#M zZ*^({KiRF9f3<3>0R*7sC6;vsulneQw5*2G;twN7!>-PR6iSfaC;s>>n}DB_Qo{bp z%L8uVj9}txP!^zohRfAV+jo}*9mez=mo{-t+H9T;{KEQdpd{ptw1CKoAT?>S04k+T zKDDNetgNf5t{02ceS*9JLD&q@H#bWwz8{dGClZPKtMmA}Tu%@|mNOz|0^m0#L>LuA zsAH+0M@KK%{VrXhppQr*<7%Hd>djOFA2$l3`17e1}8b05#jG>i=EUEnr>;p^;37r1mW znO4)aFDv1R>6sbvn7X&Rg?`qOp7|*^*pLr=Er-faJp?&ix#oqA>>^EFlR2%X4^L{P z+1Me+mQKm^@&;i^$)=Uw_z%Rmt)%6G(4bmL5wVAm;}8$)*9{Soj9*(?o^Kuql&f_f ztgqoqOQ8_kk$NjNzypGOU1z$pr@vd3l6{hx3&69KIz8L^eQ>n37AV|Zcr?+;RX22# zulO!o1IJ}JdkP5BfCiY2b=sQV8u}xm&d>Mrd_Vb3YXy=bC&%OZEK7%jEC)KpF&{BSz=b;G7#0E4Xya1VexP>1O9S}oL>t3)v(w|@qL;) zAFxEA_)6cZENn{IO3F~vilR#scegw-jgGGC$gx{7hfAkYGpabGI70v=Zy$c|{iH?s zHj-WRB~9^8v+uV1?C6$;^6m%|HzzE8DX*qkIDGdih%91~qJ)wBX#L#a)FQnWI!?!6 z^srujp1m7Bw9JARkDHyAXlR&iI#4zbDoA%RJ*b`-9hc<_5|lVKX^)-+_7G6-X9AF> zv@ONkHWErLRktezCk54d=$n{uvO;q0EOZGAX=y2kfpGO;4bRFfbJerj%l28iQ)-%yf;#u=)4X{?0|8h^r5K8%Yi^DTmCh6^3(-~EoT4Pz{o zTz??pvb(?Q=yMvAe14w*w2V>M)>ZMD&>t^GVYRWi(5fxGQsw^o8YpUYSfw8NWUrC3 z@fczt*d3Vuz13yRvuX4 zajK?sK8yb125w51HZCrO10wFS-T2-2*s{7=Uieo>MihZ552p#(7}#YrllitV*k=zj zI^J^SI&^zIA5$nlTausa^TTlm0>64?W2B@55Nr_ZvkrZp0k0knl=lBABwR5(tVbXa z)(9@iPEO-{zg^&#|FYE*vZBU75`~Vexk^4yv})9fVvFV&(WRZ+T=qFK_fdY4UVEzb z@+o;>v1j4DzY~!!%xP(@s5r+Qwe*M%Q!+PJ=eZD>(EpTVcC-kNgZ({uXJ3QR zn1S-~#W_}J5_FC?`mIiMLF)0u-Iua_)=#^vP+rm?-=GOD1v(J9iPaS1kOD3e_q zf0|Pwr*3e4RlVMRo|aA%0$}X}o@4b9FJEDcVz9MLOtx}`;+e=#`~-m5VGKuqn#q(! zpMb9QT+m;Vsn@vhfe#?Ke9+0cVqKTQecXzWWz&`GE2FKTB|X{ZCExmSk!`3l2TJ50 z+MoHdU+rmDszepDB347E!Zzzt4n{*$6YjH{hyg7tL4l3wbQh8_MMjFJwpP6p7cWJI zGY)LH<#R`&B`1R?j2|Kpv)9%LHN(y9E>Jq zM%UL!NJVrP^>q_--EG&KzkIdOVwYG6wb6);J{PA1(2Pd9tqI9ghUR~S4| z*~4uEe2b&T-0f38Uh*iNEe`&)o~6LEl#wj2=+!GXFgev@-K1n|8)t;PdIeTkI)1~HO%VP*ZDBU>(}Sq zx|iya2g-W0(#N0;@10F_NjU|u#oi&|p9f@N&eiT_(2**j z$MY~FzQcqfP&`Dkg!fBmJitpdFKIc}}@xUd% zU)?{=lxjK}4Yy2B?Z9gdcJ~(_-Mwcc&dOTCX+<82u^od}exLqQH?by;!7lBvAAEHIB`&>zDEngXn75 zkauu-E=C|ca$fIc?KV7?)6-aU2bkXEEO<~6y}kX>sN-jlcxh1)EiyoytnmUMSvr;s zcGv|PGys3I1R6xu7z;f+-6$H($~=tFjooMPSGo-U=wV};ty?jD z?hE@$GY9NM+?25N{&M*6`v+~u8fic=H5~Qey;GPW8>}AKz0vQMb5-%Z2K3@H510F4 zPnr`QZ2cTZk*d*~F-gUsi__Oc{(Ih{4|h8k(eJcsGqZViu=}%PahXBBsWygQ9$Y}i zci(`DgFHY+<4(?cPFfy&wD|C0JLjOJ+*k@I$WTblP=}|%G&xd{L?o- z?$_ufW`%yUBAkndgUItIT=@opoAVQ^h@Mgj<)a{RKc7li=Y_b#fM%)bnY*_OU7Ve* zP_IPGv>!#|9Hqoa)($2yjak=HZ`_pF+0h$fGfRR+FOAq0x-`V6U}_vG&Eq5A>$Up| z_A=H%-9b|HG|;I+Ez3YabEkH}vBanGA})jRD`-?6I_IinE-v%gPeejK-zr7MgKK1` zj9oy}+lqt3ThtKrr4Z-^>&n(G_!Em9?e6p*k#DfwWM#WKce-q^{kWgW-NGPI5sWu*10Xy-?Rhq*5`jCF{d<_{+M3osImxxy>{>0>ROBO}3y;s0KW1^(8aXvgJ=$zcHv#CKQ(*Ym8`c!M24V6VU5AU6{D)Py|v(Fh`1)g&E0-;ZJn`(^lXX)fPQ48W{J;xnm;7= zWIy|DtL_W#u_kI@;mOo*9t7UOO!;8v-R{>Jt#7cFM4+z%?WD9p!wO)PpEpe5NBE%P z`$$s}frs3QqoY?j+RX2AX#k0=wN$;$MYgTN;JSZBWzmzMzw>|TAOZevpEF>IJ~nY# zx(mBZj-p)0v=_L4ryw@51Da2C1mWrw?U9KISf`|uXR9sfCSt%t?lk7xoaKdA4^c}K zBH%{e0{_uhCYD3N!cWq9UZW>p029k8 zv#~Jsgf<5Tn;7q+Jo1{(fKz9Y7f^#bt4E2{*iH{d$&$itkAXGRQ2+oGanH zwXhi2@Qx0dCbkw)p>XKDHo0k~*ypjg`>ljixepJ5?ukJs1 zQo63(m0V$6QTsip-b5Q~`PpzZ-f$Fb2n*T7Gn++o{gbqu(G}d}DYN(6D^oamwpy`c zqI=@}c*h}An-#uFB-lGFwcla)^Z4=C^Cwy7b^JiInQ0VX-p_z*BWArlmOLV(YU-<3 z@d7QQoeW%ZRdxpu1&&4?bKB-(IaUoRzNQ6W*<^ z{j0A};940akH&ORb=T=lWrt60>V~7xOS6$2AB1sxCFW;a= zZouV|%5cjIBSm{usG_wQ7>?$^Lkea6+^lrOq`Ae5*~XjxKb&4(!k1@EC(LT6UU@a~ zuKSb8skF58$yNH&vpF*UJIFNg3Kdb$mE(P|w`+?gW|csY@EFg2!}Rq7aOLtDc7(!~ z6BU<{C!X5EF<^|9%Qrac!cBYNV$5_X;@k6lXMZt&~oP5*3|&~&nY8#*~@Ztx&9yqsDF`vAf4n=^t*t5xw}5-1JU%!&`EXE=4_s$?Y8 zH8qLLFf3B8Eu8U$30UGQzv0N)%a=JRFT^ce`_e5MFskeOTW&ri>(lz8fC0SS69H=X zYFJ>TS&H#1#)r# z-C5l-6dRqWlEulvEQAl)o2CGFW4^mp2j&g{!Z^FVJsaMU8iDx zOnAQ$3^;RG(q9rHKJGuVPt733o-3Y?ga3=Y_l#(+*G>lOqB+#o1bL_t76rAb#T z2uN>IqawXTdJR#eCq~La2$<03n1H0;KRR&w0)}p7Zzn^^JEt z3;&+0q{m41!(I8U}+QF(N+Hl}05Srr;m$c~G~!JiSBD zlSbCY&V!?1UFqyS)l#y`A+zgjhw%X#jB{x?6l_(@?e1rWn(+w+M!Bu;-@j$f1v^D^ zTk*M+3_7U6H_EEtJoMOKW?d|kJ%B0MwYbpwC!4*aGRmN^%H1vdWu(Br;G^-Wt6wJw z^Y@`&Kd3b5c8eNmHfu{+e)_Xr#8gA|*!uTDOH6w=m~LzK(6KG9+xKlg{N1=tq(ppp zc9bWvrByW-6(Fm`2(b8hC`EsPFx_OF%!!t8aL7)2c%ynlO0b~|xTn^aZ%yWVC%k$` zAjpopc{|ULhSu9p%FEstcNWKbk6_wc|E-3^MCbCw4${p`?W)Xz=cK3nYM{|eTZvKk z?zy*#KLi+0Pk=O|^YbIq)<;>Bg^ceS_4moUEzK&f^dAB`4)iuGWOExX5sCHQ!rbuR zZr6v=y%#(-gA}d5Pz~4~K6cxUAiPGXQyto}+@Y=tVe{7^NVHvXU0v5F705(A;ri4w zaj@*kvHM}+iYr0dW-uoc@|{Y`98dDv#5*?^7m!~2c25tW? zDsS}0bzn!jE`Dkzdme5j$Lo#A9?T>yw6_-CTn0yp_8Vb6a>b*!D$k$4a$wK1dp8*e zI>t}x?9y7)RmUpFn`G1p4R)RT$Zc{NZhY$JV|3F=V&g5{JCalVeZlU(FE38t5Lv7I zNFPk*{^;2|N)aY{C&wq_GQA!>8u`}kz9ge56M{}P3P-^;3*l%OwU-2H{9IvZLF`hK*I0fjk<+|y)-kMMg!$dR6$0P?+}h7 zO*g=ce|}}v?a`xv8Fc-7@m`Mjcpck4Y#_O85uHMmPFVqlR#3IkR*8xruKmxwh}^pK zLSP7lGTFCFiH;UFI)1h~W@SLkm);!itNgFeni3dA66LO&Lpj%Yf`31Wm>mL23MfXa z)Wt|gA8^cVNs=6+DO>!=N_){i*~CX~5Mg1T`~NEAcv`|a_;2ie^D&d^|MTH2ETR8S z>^J}K1c0yq7kl6TV)^^eoBp+d|JjKDY{d2dCq*NG*mBs|!ez9-E$VB_8mM3kIST4T znnsY;*!B>ePj6UQp2q(9&LXn&YV_{r54J6Kc7O8xpDLa|dit}uVZO=A(l@u3zdg>g zu;d;4^ZjX@W~8@+Ls3-2eUGeAi_miSAhjK`0bF^T$Bx$qZToF`1TJr4TGlvOSPrB>lgjdV z@Dm=qa-xPEWlj6>1eZT(LoLIOq}TFmFm57*+eLP*@v?d5jr`iV8l?3hr~1h~+u*$) zmgJ{F%0@VG-9$uytF$Hy3$t@)by_=McPZ~zWl|**cZh_GAk4uwUSd4U+62B=KdgPt zsj1D}?rZmDEyHetl~;<$g6n^61z!Ib^7(N23qHC|#}Ga*{x7{rg&z;)xSv z$`Soh-J}3bdLnTe{vz>RV?p^r&kIs=s{zYXTirXmyyIt1Z4GB>>%tZDZA^OqPOt_v zekCG(pMpH4;>;D2a(@go+u%?RL*Yf5T*7)Xa&jP61YYr6MtmD8>a(tNXqAg@3RI_V zr~3y^gF;@W`kmyxh}}u2*&Rt2!c<#!W~<8D=WQGR$-ouZCB`i!TLbmfHz_HM^?f=$ z>V0P^ilK^f!ZL?l`w_lK(SkasUIm3(5Se|}^;S?@J#+aimefGo0E*mJj*$Y`hCzn>wW< zq-Q8EWy&T7D0_JR;bIq)b8PWj@NoGXIn5&ov9~ufF?q6d*H($sp)|A6$6$F5Jb1vM z3f6bC4}iJlaiU3BR>B0OHty z)G9YKobYp;1Dw6-b{HbZk-{;t`+-fDHC*f#AO%~Hu}(wSA5c*SU@z-L7mu(iLGhJ_ zi1x&mn?uL}k^Bl|t76%H&Wl^6_zTbrn4QYy0Q;7TTVp&#Hn3 zeH?8OrqtE^7mG&rK6QBm+t$sawU-Ad{m>ayys(^uO-fo?ZjNt_rOAVO!N$ufD$2@l z`Gka4)#|vG(f$w2HXn+0_4KQkWt`*``qew!sYx9Ww&o{z1nUiOipmO~-=@k4`>ddw zJ9(~E#)><D=W zBV%;l1d7vsHgg5TmVHjX!M z+q&jEmAIo#Rg8X{9b)%zpn$585mX9z!#9xKYSJ3l`X+npv)KA^DLJ|CKYqwvx)kw%ftYLt zZR^H)hnt&&b3U80u)NlI@-nn*G#ee2Qgj;u!Tyu``n+sNsB-Ca>PGYef&a0yvjJDE z1xQDzsz9Yu>V;$vv*biRj^;jT7p&H*(&)dF-S{A#`w?Y=tv9wGtze{YryfLtl9m=m zE|-O6ae1A!kX-ek1n8Xo!xgp$VWefJC_+B=q9^040%nusqaO|Jx(3*WtQZG8a4)o=`pG zg*R=xKQbFAurR8qS3nT1_p=Lu^kK9;sn-D-Ee;Z1OMnEv!{c9n4#111ZiynneP2~CJJvSHz+kFI$xQ!#7u$32$Lynh{1Wwmmjg+(z9bg7D84Fiu8oi>cheC+0S-!74# zKP^IgF0HHZrVblm&g0TeHi6nUJ~VjV^Nb>}%iNsQp!@(2XZJ{~je1x+*fw+qFoa>O zb+x{mBB~x<-M#Ts^(FYe&O+3pLm1~&m3!tQBTJ1z2s;r0*cu!fKJBj2#*^PTKUM>o?93?nATRB0_?b*t0xZyzs||7cj>n|BAlsJ!VF zxc39|YOcgF+{psm2sk=EI7A22CsilEDV&Paos&>)RSBlLlSZn>f2-hHBN?&}#u zVNWeiD!B>W5BiKihZz@{F*YaLI0q^IUSmH8#<9rIII+$zC+SUx%YRFdX+m`!`n zFtf9x)muB#-S4>T`JTP9fNKX-3SZufcgA+i&f243t;$1JXocm7OxAr+4=5n8q^|O$ zZ2P)6j&Usr67{U3Be_vG8Xpt;7}PtE z!JuutD{;so%J5zw3_!BBh}t0Y@=oFv2#$t?R#@H6QiBEc7^C_>DBF0ztpmKKn772D zi(+mUW!uy}#*A%1PVuL)i2;Th(26E+-$n=4l26{wfA3y=YO0mq%SY=~re^l8US9Jg z%sc=%t^8j%L)W(UiQN<7!)ait#l>5ivNS#zJ!ZQz?s$DUw{12 zDYaNjROfD&taOTNK$nBvxwAGQ9u15Trb;;9M#=JmW!W}JE41u{scOy@7R7{`Bzz{? zr~s`Lyg8EYiLFH1Z@PA~3NUw0OFazq3wYvJV?21R*@IMU>8bKJs}h&zq?+d3e4?N# z-{K-c-dDc=hOJ4J!MC2AhJE6$g$*BMZ&%_`MLQYHy zZTDORDf$%sK(ZrlsBRmu)E4;l=E5i-0S^d{>}q{#xxbF>eyQ=8Oq%AAFr;j{c|Cp{0$h&W z=tAq*ws%cXJ54*md|%k+-3;Dm$%P2Sw4CBK85z3_;z!e)X|a_9Xv2^CMKuh6n7%$Q z*m6Tgn|&6sw`&bA82kV_^{nu{c&+QbH?}WuwZ{1-Q(#lU0UKS_rf2SJ;Oplagj>nD zb}CS`?Hbbu8I6TiRaF3~v=&eorrPR(@I;Ut4(*^xq= z;MVtf;0FjS5ovp&GWz=H8q$?DmHA*h~ABWdh{Y@61+^YY$UpH*dRVC%M8K|Rqxo+PjC zMZF88aCbEU3JL^{Z&T8K9#Ez>3VBL>m*XBmL^Xb4d(F#~nY-cn7L$~fp#|{TCq(&x zi_qySUaz^&?fsr9)&5y(7AIktc4M>6 z8F%fUrWbi}j)qC+$+&u$;l!5@`N;P3(I0?v0ySYeFC3>^@2W#zjpawq6!#bPfhOC3 zNhr@L>oDI9*22lfo!ij(`;AMVafp&6lBg=ZUipixj871B>X6jg?d>PVdM~C4?wQXzQzzd)mtCD$f*s89u!O2@N^wvgo?&09rV!&VbK{eX|6 z_#o?@{}$#M?BE0K=2H~p1QD3evci|GcEAI0%vl3P|3DQ&A{CrmE$*F|d^OI)U#}iy zy^V|RdTz$<=I;E^*?n)<1=A5q-06}>rKQ3@s4YLXjhIhOkvjGU)XWJq$yGNv>;EuKQ-0p(`67B02`Uh99kq5q#lJkuMXQ=s4sPGh}gzu~&n+#IQ>V5#n z&bG}f5p?p1qNbZeyDLeaaye?C*%qRKE!8|E#-j1?adgf4)=#K1!l97a7|NKfmpOn5 zv1Vxc|EP&Usb3q@gLrs)s&HLGID!6Y#^-^SuoirTDxhc3Wt$-oE+E<>7BcD{xIffd zT8Pwn0lef3eWPq*>=0?8blZe>tsC%B8I=XAD_4>yf?E{+t>Y8T1I=yP?0}Or9t{_$}_tlvTCr+I? zbEZRKlky695B>c=!o^rvE)HsMrTI+Mn6nKP`NxTtz#TeA(PoeQDqZZarF;3qcctnK z%Y*X2I8*n02Sm|yQywkG^_F;hP+HJABp$=9Uu2fSUFzR^c{#F6cJkaQ_836LaH~3y zCMOn+NDL%e^(%SlT2yD01%^^3O(9K>G0j{2G2E$qi^Co1yF)Z_naKRGka8Py(PcSJ zZBb+QC#2P84&cY9G>GVFN=(}=6NpN{X+`TNOC*K|5HCfKwP@;R^pus|&M1nBX^Ep` z3l+wE{rC|nnW%8}DzDD9Yh&)7Y0O^2Nnd@=_YDl#G(C0> z!7@Fu&`Ly38lXyabax2^i;6=r%j#-!5Q#-a&9Q*cG(@Wk=d<4+4;0|(W~Fo*{sQ@xP)jf*$^uGRW<(TO zpAyzKwLQK^3)QaDQv;du)cqPCYER;b9jF8XOb?);{}g-oaxEcKiGdHCO$lmh(^8Zz z0WFq!`hNmvzMG0-w~~Fb5B9xd71hlBi;E?kv_gnm--n~Q6(Z>rpn&>}P6g7tq}=n* zqAp7)JK9=FS%P{W{i4mE>03H%00|h)csF__w7kjYY1A%ta9i(aI^r((jL;g09vT{| z5;pDjC>yQk^m+K`Q3OB;RE5aB$R7Ef9g5Cmoxm2;4jp3i_XxlC0)b$_(G9*XTtL?Z z9KDsk@(ov+vwxmVtnf!=;Vs{pS!W)nf-*jLK)1azYm)u<%Fh-7T zY6*!R;m%0}Xypmsb}TXj)vARkn%n@T=nUr%50T#`j&cgC1^-|KZkY{Nu~i)9@b?GC zd+EsvASpG<*77G>RhpTq-L>2{nZ6(Nz{Ej5G*sCX>%NjQ8`*B5DPUzIYo};%Pp!_q zZvc2=8R_W_Zfn|pW4|+u-+^N!9mPIU?5!R&$bjKGc5iX`Z}HP8$K4i#cikrTIMPI# z{foFp+?Q6S3hYY2+jCG)+=G8=EkBC*FE)N@()XBzw4%So#S7wte*r^)z4;GJ6Xrfc z-%D`rYc10{bQi`p=eG|+2#RU%BSL-=$$WgL5+j3m(|CGEeSTeCrC7rmPG3O03;LKq z*PxP*FRcHE_!G@i92gUMfxO* zQlN^gQuz`HjLjWdu0GcZ5Gb~7jogYgoLn5p*#||vr`j&p8NPn@?8d`~GtcI40vDwD z8Wz&-^DaQj^1*d;cjdoL7th}{Z`1nqB0C!q6O$fRcyH^RA?$mE4WJi2RroDqyD*AM zd-~ZWlB=RrPnd({dcCUJgaJp&zyll0>6U10upiL-Wjgs|C(kf;UWnCCGP7waGiPup znM#4L>d}?Kh7L+AJKa{c3$ zoqFrK?V9&8jJ?2%G%g`tohBu}(8u2?ERwq6%Q}dts3_gF2d=I%vKLc+00J4x-;bOj zkDw5wM0T*>rR*XxRF)Qaz9)dIcRVDpt35mv_;7EEUCJDQrO!=-LAO5)#&G)|>;?k` zUDn6qO4u(M%;M206pcsZDKG8YH|6B``1Ig$W^$8XZOnNbTi77e^SV=tE?EewrD=R7j!U=pcW%sGOY$qoqE*1tZw=+#! zoiL5Pfe~&WhvG4hx%E09)s$&5B7V?s%S3ZSkB5f-fgs1qwK8}69X6JvjB`()D6asX zjOnBmIUWaK=#`o4`j7nL--mVV_2hZr4cJ*e9nH~wp=+Le=oG)ZZ5G0jUre@5tqQrH zuVrI1uC#ooJP&kUWgHVb7u=nr8GM6ns4+O$&~a&~%h2}P+YIJsiDmr=!cn7@p)3%O z$Jl|0e2u>k4qE7IsD>}pTVCZUb?4d3ZLRWc)EP5cPT6(;)piV0|5^0HfXQ z=ROW!J166I$dgVzb>G3RgoibiQ%!TD59>*BHJ&YcZk%^_xJvS^heh{W4{S9Hi`y)T zs+F!v3tqnQ%|q{dkwQ=ypekOQ0dc`HNoBXC75gtaz@aa_Lt7zGayqp8S_<{wIZlj^ z@9&4Oi-F&gvjqhDCq?*X=SZeu)0j%lR_ZE`&eFi103M zKxTRI;ze@$3*)yy%F609Eu<|=)`K9gM0u)xOzpQ_q@p9|%l}Z1oDmZ-42uWF;Oj$* zDf*{!Un@et)Wkz(A3!B#z3^vb9hw^suKPXmGd!q6QFktj-5O0q4_!GS=_as0aozo{ zPxlXs`niIlk9(Ub7+b=|x7}Gat8;}U~hv?ux7#VSRd1)yk5*|Nvp%0sN&1V063Kw2b?8N3+l;A&kHuM5o?<4Pp z3x5ikWQnwN=g_3$qJtf5ls`f4+SnwZ)G!+yUHsbJcQP~heKzsuWVs&ICp(&Z`HZCN z0Mnsei?E0&9ftdyMBirHI1;aTBK%zG9m@)uz1Xd4%pt4gB+D+*$B zBASa^yV2*}iFQhv9NVCj3UuCrKdr%Jho+Fw@0<0oqiu5DHJ4T39R|AY%^mLw>hd^S zK(3JQlA~#&cn~{O^28j-HKi>=`E|et6z9yF-<%=cEQ%?d?*wQ5P(S^T1)r!t30}?0 z@LT%C{n2@gg;MZd$>QDqt*u8j`Xhl(U8#57XQ(K?RF$EncZGT>DcSy82-CTv9Xq3M zMx4;Ev&~9h&pxoTBepE7uzWk^pQv@4PtK+})bqVrsYc+zRnmeCmSlXlp-wS(r~g*w zj~-_c3zw^(JT$6G0?YZ(PX{DUEGfKY>dwK!^5DmaO&Zc3r1G?V^)v3a zRm{H^f9RcNjp(jXdbB&GR9UU2$7H&FgD+|LdfmfT}{ZDNN(+7SQI zFim33hmX(2buEh~TM2$3f8SH&Z9D z)B|ubMaH&)_0`OUR{}3*KTgZ9y|nZ@3eGId`Brz6#y#z>26AL$uwx+qkXK&LQ+?rT z8|-dakDgd`j?XE5emg;;yu_=$*>bX_G{zJ9%42o(*VnHaq%mJ7@taalM`RS*UH?{V z;K~M(3A7)rihCIuN=SMLlHsP(MH?Ta0hTBYf_)6VVsl}xr;P@J`8qG8%v}-owbg}% zW4M6<#G5{lCXME9$;htkEksKL4h#eE74{$>gm9ai(*u+>G^cWjr>){NSy|4^YJDyF z_NLp9mkJFCoDAA~t@L8czQvxNI{;32N7FK2)$Qv^D-1$>d(!iv6tbQM3 z-{N6ZaqykviQ~)}b^V7KfG4oF7OJN5 zL;wVK5Q1qtY7CtVjj0TrVUlfS*VNoo4N#OMxBz51 zu25Vk^y2-|O~p&-sOS0#b+1QntT=^+EBQXD8b+hpp`vCDT*6;)xJ;vQi{g93N8uC&DC+>2S!LZxY7CX}4Q{J&@F8b#f9Hg(m-j^7q>x zjBX>#hVqcfY5Qw?HK0MY;*uwqgb1M=96soaSAYP-%a$hJ>7{KSu| zpYhggL%%T1`JRU<`-{(*wzjsw2s}F!i54(qeG{c`fUJ3bSAg%`!ZGh*eRj0%(fx93 z&Lu37jCa|m?E#({et!P;F(kIE2%adURp{Ubn(zfLryc051Cj9kGUt!KLo zauL{J4(~10xgQ03PGFlT23-XmhRO7vde2GQvKMqlTai+Vl?^yfSJbiXl*QjHaELdU zc-82ms;YKeMDYR0xF0=wG$n_d<+?1o4BdtsY^mi(E-dixhgtA`n33YG4kWyOSP;X_ zy5kcFo%-}AslH2oXV-tWk(yvj1^LQ1tvhw>?Dh@o!R{YrLHM08F#RXjxc3+hvKQXo z6~=`ua2dA4c$Nj=l!0ivqna+4dW7{}>zse1?V}Y2@Q7hmJ>6YH4N;=I=jAz(4RNpWT@2Xinj7 z^gMh~-V(0*_?H1JOdAr%02O%GL$xBH^GV-6fQX|*c>C(;x9;W}u>7LHy-m!p>p-c6 zG#~_aH`hWEwX{@60K5d1f3AO>5z8?H#s`EwI1=JBSQSb5Mgs9U>u6vyczxXsJP2A4 z)V?ji@Sax$NOS?XX)0~$QY-=VQ*51&>i+Z$u%&_1y^?YNy3>2VK&~@zI!=->4)X!k zg&}X}trWhI7=N)mU3v3gj{z9TGQv3GOo#}G;r;9D?Ee~T-uCa~&o}>lcKNq|pK<=5 zH~s&2_YwxuAdxm$Scsp`0!~)WbZq=S=oAo{xwC^9MC}EJThKcXwgUn|4ZXbSMVA2w zzdessWSX5r@Xmovn;&st`j{AkPMlv#06cRIE_MXacHp-iPkO1|)ulf565)Mr|O(`mhHt9vqVd0S)wVmhw_=0N$g z>b<^msg<|}m~|Uy!YrsR#ozd!T7c)?l!?;Gk4vK@b6xe(lZ`?3LBAN_;9t~^uU5=u-Di>{%0?c*4i~db7ft9?v%qJ`gB*V8}4l)QAIbd z(bEPi92#b6FK)SVCGpH(#t7GsE+OZqTh>_gL6QOt3=38vtT)IO%BM)D)P1)RIBoXQ z+*NL6St}4<1Ihw&TSMwA!#5N`9UW}tXE$KrXj%KFu#|61>+VbUXWk#GHz)Fk)uCXt z%8ndI;T=vE6YE)?{sWqMjaOg5!egnVGt4edDr9rL1ZF(-B~~t}y(Px7wO{f8sLhc|1M5mi zTb61S*RXeC3I{QVDHQbJHwek3VSTn|(6g)o$O78qnbg#$#!)dPV7t73= z?Jj^uQ_*E9)n}m6%{aS#8@@Un93ixjt-U^OnU?wZdpi}^nt0;qup!4G9rk0#qUX_a zq_OD9T;%E4}0Nr|y=I5AP-5kF^=pyvGB zgdY0-fyhBFgt-Ul*lMmDak+*X_1)5${S-Eul^MhLnr$lib#2+Frg`IJjqr7oFXIr( z?1HCbdT#IznE37c`@xL_zB8ip!T~H#x2DnbK;a33z2DFWD_MoKMWpxIVwQ`shC(n% zrT0(AQ&pd=-?jB9>#H63lGT|ykJL1KvKs3t#~A~fzCn6D&a{)Yt>!Fp zH$<$9h&rf7p7P%;~0b@ET%q~Rl`i=?C|YeWLXre-S}rhsf)O6TVzjWw&VeH@lZ>{yJK zQc4@I3#BC><-)_UxQYrZ@!0Lm(1nrt`NA=CDq~@}F?fxUMxNrK5#aRQWbwEiF#a@b z15*^o$se>l#Vio*CcQU_-}KL3j@OQvh{p)>%h|LCALN&nef!MR1fO+>;t=(SMBnGvV*X4>Q1%Lw zkyUbDUHYQppx(ZZjNP5AGWxUbwko&IVsBQ?Q+4jsej`=PG~?X+;a|su)v0ZY(7mMW z@IU1xfuxYFz_gvU@e4Z^H)g7Jb0YTo5m0Lj3m4Z$5v@XeKh}^duG0=Tq2xKx2FA%< zkd&;4VuMx+-Rr1$6T(*<6r_h+_Lbtt^Nl0&q$vys|3CyH`cpbp~A{d z#xtn+DOhhJhG&SMS0t0NO81_`OG(SQr@d2h{`$h*{EnpLg}*7HRT&n}nC)&T zGx)Z%^ExhnD;(ce4vzVd+hzDhJP7}gr{PUtax1@~kZLG>Jp);WjFCKbJMm=6r)v@O zOYNxL;a+IF%Vbh;D)K(W)>bPIl59K`WC%pS>zSLP8lk(}U<+$yVFA36t=aDd0`?By zU!vo3L);mHlULEhJxN0Ev!`1YYb8@!wdu=;3u3uWt44gTP~8Fp+e67*9sh8~%?f1` zE4W?y)2#`S$Xa$p`ynuOWN&uLc@mG~IdTI`hL&p$nR=1Wa5Bb>pUD`(cm`)?WmUlG zUyzRmds_aa0ht?~!AB{)MHeLXGc6G9jdLr^oTDdFkeEQHtTK6{4I%&eZ$qvSTl!*Z zqw)LHM4^d;olIl}vctn{i1W$f>QucvTNIdJj@k19(IG2D=v==nH2I9`#cR{z{}B3o zYn>B*;-{!%9gH~C7Din`0gvEu%xYa7=jVrQ^#ymO*9Husv1!Qrjs8^TtL?hT_ErZa zCqJ*e-_MVCkGPX!5ysl6-=FiNM5fhF2#>f9XHHUuuW5iCCn9tBTuZVxarS%r#l0@5 zO;J2;7N{D&wg!)%-);a!_o?F}k@x8EiUq#PI2-EMHs7U9N9967mkY=EKH>CH`<(@t z1vglE<@B($j0^$QAPv;T;W&067Cngq5!0CA3`M;=*}|0WW-#0~az>?N`{fE0-#{YM zm#n#Pkz1BPqAtlJU!c$vmiAaV;T4&>mF-Dd?kcX{iICKcyz~vU9}q@P$pEgLTNt%*MH<^f#qK_Uv2ysy{9{5ggB|>VMcL!noGUA zjccv(!=F_f;q1F-kIUrRC5dWp>{P;(MBU${JJRHaZyw!o2M4}}9*F5QdviAcU_qW9 zPD6cmpc0ttC0w_jFCkPM9Z$W~V9>wv6gdY+Znx}Ph5i2hMv16G)_(GMV&3#7D)Bp{wbssvOxiu+@P#6tWL}Z*wFRtl+s3GN#6pr9S9x!IJina7 zbTw=s5$kAX60-W;b&;{dlsuq_YV#lYf@Q8bspH3~U^nPLC4H8L>YcccluAhn;zEe| zy}1W})a~kM^KQtWxoR9qd>HJ=%{Yz&U}B6644d5-w}YC;LC)VM168?LO)%yf424ZE z3RbpXYxEwSirLfN>%V!hNeaJnf{3T7Q#E$SI8stla$8_7+(**W&-kVq>$B^Ad;=Ih zfTYTQw^MhS&ok{jdgO+Qqoa+<{p|$2=2dWpHk@MH8Y23^1!Hs*^o`y-YQbiQoVtOg zr}e{<83b*3^^RV|4es|@7r{6nG6N2CSv5-EYHU$pUd9w{ zOPflboU1gRYJwi*m*iyUrly9Wo9qk+Q8kZ$zPMrSs4OMKuBlAJrSus4VP2214i>`kyZ0`grcEk3IGH|-)t)ZBxyCxBFIhT0Zbar6`u!FBssq3RY7K+OqVk|*9~*C5 zSx7ar*s~l94O zjsSI4b5|P`c3HBue&^^_d+Oya zIK3RTItdTyxiH7NO{xZ}(Pr>xLM?sh<184<+ok~z?d5E?9{j)yf~Tt+`_j>wE=EXy z6xJpt=kZ=Vg?d0qKY4Q>@7L*^|HO?p8NMiIe^T&t8B#&9qZ~W#4Q3VXfZN%c*i@1q zgfJOUm@-?`Jhdlw<&u7;Euwpbs_7lECs4AIV6M?SMg1>`uHw>_7bw^`0BNh4zqL$Q zYir(-&akaJoTb`I6>)o$K2&qtC3`T)3lA;@2HW>ALj`ot4;0L~ToY;`CS|TM_HrYs zpDX0?TW_9RtJ~-&8EdK^)aJHG8C~B^>&=0*dG&9XIssq6&epcWXKwUVxk}H~ujV50 zrNgP->TrMj92TO9fRKF*Hb)0C6sQBq4)}bB$uW0h-=H5-*ri`^bj1t?Maj${?(;L#NybN}gy1W5y0K!tR~Xrr#gh;CFP`|Dz8V)7F+S z1^!vW0z_3-E`B<_kxNuYO$(rpF%~Rq>6->rJD~&>>UJHKBig#K0#GEqTL%7wOh)Y$ z8IPz2jE-FCyt5|p`}w<*Id-GaiH6AGhK%G_f=btds!a`Np17f#9&Yg=+ZdhJ-OLr- zdQSs5TP3eVMUH&w+e#DFw4yVhaOI$nzJKgL38y_3G`D~u+`|BZ@$E!;n1)r%D^bmP z2$|8D;GgDjHPFk?44vN@ldR2HF;tK*O*$UOxw6GL*q;U-P{+oE!AeA9L2o?j0MQD_ z^P49i9eEwKej@lki&U(M`N@(<;CZU)0#xJ0wFrIk(_MOCV_&OC~>vHtQlij;B zTgXeQA)oSKtqp+UV%9#@H=3g(s?orAW!_ZUYqer4L>E5}FPbh6s3dE0XzVbB8Lu<1-7<*!Z^Z!|v4 zb_@ypj)TQ9_w0g$L&*MyxoXP)lsYl1jd5DFEtbHiq_1c*cI(^fMKe^}Yic}b1Db!H zHxt&wTv=bJS_RM{9gOjbY0544ai>~H^Gc1LioL%t_mSsEx7B<< z2I%1?Ry?hYLS-<^g#$rh0Gbn8-@ZiyTmXSnx~t*9g{&55ZLc{C9bP11P^yxz&V(&J znIVgOppcM}9?aPkaC{>O<-tA7rbPcP{N`xoAy_zg2-<8>j+$!LkZNom*Zo)7+ny+p z70oyK5$ZNNcj!bke^j6z~7p!z?B6EhlNB!RyV*k!!n) zNL-T#Nf5vPLlVq^6gu)HkJ;W5OxoSTYian6J^gYxtlRzJoVBev?DlQSU*5KjyWip} z*`gi*!zhTK>Ij`27@K3|I&}&77xNpP+5l*ci94t_9#BQ6`CT1An0J46k`y{?xvzZnW~xs@khZV{W(Q!_S! z14BdRGB>Knk@m{oQ=-i65#V##6Qr(Pf%?G0XWn9R+h8t~-rTFcwI^K3L|C3HI!YGK_KHf;%-t-*;1Bqh&g_8 zhin5rmUC?jdawnT?`YZf&+Slk`xGDPI{54vBb>RVLN4pzTYv9ac=!BfNn9?7@ze0I z#m?#rx|~EfS)NHJ=;qMkCMhx$MR8(@7kkuWYG1~3ZY+L)YNQZ zu^ne<1mG5e0j2TTsURTM2^=G!t$>sO-Mn2lVkGQ8hsUU@N|lBM6^~iO^~NWOUVCz$ zEeeZmx3RGa9V$fi4~+nzF>{l)fS54Bw+51?v{IXn-1ybuV8ngI+1Z&QJB2c)0*I!) zkAXaTr`dzr=r8V{WTlWX)jbB*PrirTi zJt6($zy35>RL|I;f4~D%4fDBl_IOmDQ{2>9Hlz2c*v1}{i%Y}ho~wlW?Z8fo9Jyh- zv0<3mXnb0|;#1T4(GS3VB2BcT@~Uv}7@o`%+~MU6R!*#a9BV36MS@DeW3F zef}tX9_%m}TUQZD7PPjTYc$I?TJb8Oa&SKB=!3LZ*VL3jk(B^}KRW1+J^skt$g*`O zJ|XtRayWVLpe|&b2x%gYuGpNJ6WaKLhGr>=RL$LO;PAw8Bq^6#ip~cIW-Hwh0v7;F z3*DF>Dpg9Fs4lpdb=V5bT|5eI^cTXQ_dYlZK=>NQ5nI;YFYsT@%^{|y2q<|CqaEyq zZK&Ix_VC{t&3<#YaPheNiBz>rWvH>SLH6aK`-w`$J#f7sD`JNEgYNOU5-YK;WicXt z%*xD6ZSL(xJ^r$ht6PLM)B_ZmUi9{;=OcuNu`hVeP1+z9HF)0jCIg++*We)8{#}Zf z#Nui+xvHF!VyVlXgxOo|cpmrstec$^->+e%bH0(nD$mb;^BCSt2*x)&-plv{{lSPg z`@r$SYtWQNznkT$>F;_@lR}sX)t@wUeJF5K!sEwpJ6NZdFYfXBQ^#=|L~7-Ie!Hu1OwU=~SAv>NSIV_YOR{-$ z8=+WbfyB`qR2k0L7Os0Du9F(Ea&zI-sRO#`ft{^YBvn{tbX?EK);&wQvedTzXN99N zA&C^rwbr~usSy@QCrnFiwk3P&l_(EjqJuN$t8Tv0a;hfVKQ6LZ7eH^gfl zIivEdlLvzF|A)P|jEcI8`atn92qlzKI;2awySuwox`qa6Bt%d^x*O>b=@yjkmhO(B z8|Iz?-&^Z`yIj%W7SXP+Ivz0W@OswqeDl4L_*cN$4ND}WY*ks4qN5w^ES z0}AdiRf8sXgB{?}o=;~mW1);)|3LK`PX_?gD`=ZCJ9;P}2+ePyMKaA}f9Qeck_{ z@IYO|2!tznr}Pmv<#$;8KeO}X zX>Fpr%eET-LzQHH?{0AUE|CKO?lB(K%I`c2g>K33a@dCqQ>t5?cquAoc-mO?{N-0-)|K04zH(sgZ!Vp5e|p*|D|h8BLJb!FE+|}( zcpPQ-jlCUmj-*6w8spr-T$dNg7|gc+z0Rm(e-%_BCoOG|*zB@0c+0@TidVjs$j`90 zkl}Z&4xEom7vPPx8nxLAOi1I=Zvwx#Bw64f9G&0sW^ANDkw3h7m`i;Iag>qM3S>d( z3JW5mBA1|py}id#jOu)=coyKSsxXFeGmH`0rd$V6_=oF!#?)COWDC>@*rKNA8waeH z(O^nkUqq~*HB;uyk8#;_`P^sQDIdY{~`(fXmm&L!|M-BG`)RYwA{+>*XO~B zlYd)81HsL#d8}}=l27#u^p$j4`kNu%)-hr0z8##qE^yF0GvIxgGxa6#cLqnXZhM!A zj&XMQ%cmv*)q)NM~`g=vn^+KS8q+QTeEel4Iooe%T!qa-W)EFFO%=ZK%K zQ~y#Atzmud=h@1GSQ|Wow`Zm7rnsEmbZuf*DN%;?l%#Q;da<%M0+=0BvAOQ>L|%11X*a#0VaXMp58%6>f$v1GQ&)F+ zz$JNY$K^TFyUE#?+yvA|OsTE=T3}$Khf98{Nttl&b$wkw9(mYI@(u}7*r;=Qu+J|` zT&mc|`oly!r+W6BheN?WdtYlk>}2k&-Tp_JF0$h@df3iU0G|4y&k9E+dzjv-p#${ZBCo%s+$y9M!b<6XF^w7erZ*o+7!c% zzIYI8m(2cfo?01#dCX(N_G5TiwhaY1sS2Ci+~H*M6*t=HB z4TH|L?M8=lg}S}#n;ojj-1bW3IoDYKp?J-iz~_B_RN8QQdD#cToS8C5+Y>@WJm#>o zrXt8#BIh1#I~fEP`Ys1Gz!_>PTip#J^2vzkgZ#*bXH>`)8$HKNqKJIlAs|8p9{=A3 znPoM5;=x;g$)Dvq0H0^C5oMhtHuL_5M{^2$h!{TFOM*@m#$e}w=5A^9DYuH9 z=)IKP#sNB70+VWZxnmP2k3Uf_83*<%FobG?_|rp+b~UcyjMX!5$dUZ?Jx(>DHx;AN z8UB}g%wDZ!nah&lgtoni$bo*cxv|E(rvFn55ULMwbas3W^qq^uc94+_=x>tn$t7m)QiG=xEAe+L~GdT)WPC-3#nUwi_QK&}u( zZMR7kJ$-v1Rn|S0q){>#J(e@Ja8`n4y~Q;LaGWqAfrYSeJzh_#W{J7=(i|?rN^o~` zoX>|HpeSq;xo;9*MfscrNfw$phL!{4q^G2rf!$G-**wMo=J5lvT=iGO(h~bd*fJZU z&ueT}nOgs*xz1HxT^xX;llexF9Ss5Yl-BrnMpX~(bZWx(`M`~UJOFmb1`5(+eU53) z%j#sgdX8;*-p6U)2ezZAsmH?%;8%dV;xOp*du{#YI+G3)BWu3?ZKNrerlg~BJJVB2 zDBy7X)OGd+?}J*9e4Ef`KiOQzC19W>;>d$Wv>Gur{LHp-NlGeB@FLS`=?;h}oM^fbY(76?k5k;7t6VNyT6Ux3tgwzaDAO$gP9-on04N#I ztsuif!Nwx02RV!Lo-_czZ2)NUtQ_W5Q&HpzZ0qKx#dKA3`F^E3T(SO~8f7k*giIHa z2TGW`o4FJTJ3sIOZtd3icTrINAbT{Pll>Z?-_Ct?eN272 z98OB}CHhm6GcxM$)GN$FB)oE`N*6)|q+?HNC7hxGcez?{T$Gb6l>ByWU(h zJKN)B`{6TS{<_X(dAt}TW^+q@>iSogt-t?O&}(w=lDLcq&NLjy&8}tIaeY5Z9Yh_T2n`9jg!hm|88}2<5kRs+p+}uD_%=G ze+ItIVwas6URUMY@2VXqJ9GK{&TTZbc%vC!{VT}74Yj}F28&5N+xA&_+v3Q?6b6gz zS`KPv@wxCq?@x6b8{O0AZ7Or?So&UwUt1T?<-zAoIJV$jS}eaETRp?I3}Oo=y3&{I zMLy!)91o!@;7+)4BssjAJblG%qXbGOc{VLnu0ns{72%q1F z3N}HR!ufqbex}&*Vo~%`fq?f|(Z|aL+9V1N+Y7kwoY#fw78H;fHm#Er8-p;F+h!V6 z4;U(ICqyO_pap>n;>mimxW9yzNp-3P>?eV!IQqnPZ;G4Vr9GK9?hbek2MAk+jNqYe zs#QV$@`qZRa*%4`u^kG5=<|R8V;^V%js{K3ywa>NeD?EdbiRbKI+LCMI_Q{^D!kTH z4R3EFY`rhS0Ib7B7pmI!*cn^LeyZwbmmi*wiH~>)FTDdn6-czd54E^gYtbt!ie64@ zOx|am;XWq|qg`2K>Je^VGzoZA&QF*;8%~pPa^IM~WLqkIvQY8udFp(Q+)I1lxhAKK zxxa;_j-dnC@}kjpj{U0kF-ThP|1DbrkWxn&9-sT4UtG|rh*a;BuU!Mp*Ha~+hl*{DT*M?Jrp?xlJ))6K^Ic912XF%fiYfdv{y4E z+PY{rU8Et5k)4}s!0feDP-zP~j5V!@Sq$u@G}}!u0N#KwY0leh{!)J0K>r(Vs07GF zjC39m7^$eLGA434Sj^S^#eQ}$wugXP2jm_k=R822(h+W{^$=N>=2<@+BlUXPF)8DJ zd4|X9FbK#8V9?HF@klYJ{_XILNo_5vqL2@Y;XY_D_Jl=b;lIQ7SVEA+i=ui~dNc_l zuAt*fd=fi?lhf%TAJesx2$ywbu6oZRW-%}HmWd7_@fiN;E!vmY&9g#~1& zINw7{$CuRB?se9|^kjjFDU?XRr1pLH@U?SN^`3P^!tEkHV|_zv{lXubDSVj6!v(fP z0Bl23_179IKJ2w(Q_09WPMg952rE+%hg-CMHjI+EOE&`DSqg)eq3t@9)H~?xM1YWb zz~}Ne+@9__l$V0ivwlrDIt1@30pgX8hK;>#}M_G#dDdwVC5?4l%_+bwG@m9 zl_^n*2-t~<37fE#ivQN*n)yyWO(p82o^kEIC^nU<7?^$u4+KS@oRl=>FFrQiYrU?X zn{%9?f|!38VqLa^9Y@)Et%j+_9|s?WS)ww&3D#hJJ+;1CGzs|NbGs4_uQ4ghdO}4- zC7v||AiAdgtzmIlP1y3pUeOUf-RFxApBr_M{TG03w$HtI!NuD@pV)GP^zlgDLfh@O zAQX3Ng8`w>YJJZu%X0S7fnS-?#GHNBR@2uVdXQaAw{fw0@QzHW_<>N$>8>CaT970v zF-HAEJ!-7(zOE4c7n*8%!-Itn7AipRb-UZihLc?l2Hjr$iuW4}O|{&pic`o4@-KqL zfY;k-^2OaOmv0`_H8mfw>ZZC#OH0F38EZPcWGKz^s3-{QOs`1oZnc{nejj3Q^E&t50M%}_SA}5r03}FtSppTZr;#jt zU>o)!2OigO)H}0s_?8P*K0&ar=a|ZN(-#=vk``7>S0O5b- z02~kcUpe?+9)Kf8|NrH{_eiKBv#2%+eqKqB%Qyf5!LFoms(iJRpC1(K&CH;JgBS>m z7%ZRzE7wE}gAc8aJ74W~`WgfYd~Wv|;Xn8#8NQA> zV=XB$wx#lfKBA7CkHA0F*uk51FC=R;d1_#U=7iOk)Z&99cK`Fs3Uob5DKl1TDp#rh z25P+@NOPq#wY!*Zm@?iR3w)g`<&Mxj+MXnvOwimFVWA@Gj*vn?h(k50G-!O{DhfXL zeH!u=5Y(?6k%dS*rx$B^|81wcZ*v!Twk4CO#Bdl$z$cSzdN)bQ7>2Zz(K1Dq>Xv^r zh>+h+M)~-|yS8@md`yV--Y4*V1YD9=;Fw(Um+r&$N9x!Xko!>{+>L@8v(6tgTH@g_ z5b-}Od3`sE!m*fr+vVf@W!q;W8EX)5;tp8RNI3D_xt?Q=Lm?`dp>U3veZC`{+r+K> zw+%1V8R7K5rI5QDzgnzEhIYIml4q}uBwG)DZ%IFrKWIg62chw~`EIT)J@w(=IQI-- zSOw!0JE%O7v;0Rq{T=b5nb;<)g@$s3K3?CVXJ%~fR$J@tCxX<9yx83{JH@iV=)Yc3 z+>tM(J-4G|niSolnp6)akMJu{4nFPmuLU88>I1ofpO$l6^&du!LyR5fO&+ zd)5hM!Z(kQW5G;ihUGwP#Q`VVz05v>`#HXAYE7Ol7$~`k_Y(i*ZtP?*cJ8xeg8r?+ zduG146O%;wvAJUPdX5~0H$&9$S!%%NpFSB4_A650aC`qhi47KqFIHa;e3#7I|DW5H zqzt|*)7{hvXS8dLfEf6eE8KlK%k!d$1W%sO)$5+OQe5!mGbaLq8RZcwXE|T~ zk1GUsT=DOW;E&PO%nswcqh#>e9o^5m57W>xhbj&z@xaX(2#FuzqLC%A;3gy(i8SPMn>_|9h{J*x!lIPH-x9 zi591z^EIk4_2~WHcoBC;VJZ-IaXJ~*j-!Z+zZ0kA8%GZ6{uv0!t>Ch#1F+;5g&^>@6d6wg|v zOO=1cOdcIOIU^64g7D#<#p&I}7ZJaq*$MaZA9qLnGg`3T|MxxmYyLH->qCS@c?-8= zJLL+SLL~{yyxM6839$md;iZjWqX_tWrn!av@t-pA&W497^Mza(J>ar*!7Ri5=(OabSrO56}(No8$ z|C0RX9@o} zjTqQQS0v$8p{1Zl16$oiND()5^@=bmqu;j!(l$x<>xnp-@WGocK+z-7q4>zREF`$Z~{qhKzT#&a1}6rN+75PNGnY;H8Z1#ooXs!@%5_ z+bUIN$J5NV#}UhcLy@gg2N4?weo|dS8wahOW&RPZjS*a|>9J{__vX}UJx1n-XO3et zP8*|mjuo~zUe|6ftjz35lV*T0J#VVA%yMAut5wrK8^Hw(am}|rebw7GLtOQ7=4q4P zVQ~`8`-Pdrv$I=?_PL~#Mbx2t(>j$tx?>*+)e);S3I7_XJLMq`>6tqA-WM$R6NDK4Z>v;0^hWMC)E(7t zZRYe(PUdktkdatbL???y9*YlQp2&r3VZ0!st<1-oPz-tL7PvGk*Sb26 zrE=1tH|meY(};>LrUOP#_e)Rex7zRqmGe@WxT00>5qlg9F zNgTks-Pi(yZr}|5^5-q+mQ|c%`rbBVbAdg5NcSFQJxwuVWA2bvM&s4~y#nI8Xic`@ zrwr7Z$-)ZQ2mjPsmvO+az)dmjpYsECM zo`#?D^$Ja9i@}E*^EW>?YKe_2tv4JRfx4ROi}9E%nmRIFP30^^YgWa!p~iYf^%!`8 zmC%&Yl6x_Ar(8Xil2NeOF>Cl$VL=QT5ty-Es!tQdLq<6hKW$cbd+RZ_C$A2*E-K>3 z-Hw=zAg>4hycQ^_tWTBp>iB+46gO<8=Ae_a{q*CSW~Hs}SV`z+T3z+MF;_wafX}-+ zSj@0KqGD6P8+PvFlxY@02YdJ7D<8{1?)W#IYY?!(WLQ+_{$57^(X(WKXDoDOyZEAH0$>2b^{LkcBYYe1d!#wJm#u{l!d;c6VlvNRg1!T-h zf2HW`U5qAwV4Ut=(GS^05c2#{*k`)E-0MSGkWa1%I;uj6b)qqqjGZ{8KbP*uKu7Vs z@-Iiy5K);6&l93lSSUKfoyZj4y{`RPl;!j7d?)rc-NCJQ7)&72B8< z`+Mt#+!6UzcP-$hs>oOF)yOXEx@KK_4iA^8#T`ojR`tQ6B(4go*&mf!Br8mu2&4G1 zGgyu)4ZqI=JrUv79sZ&wwhTZ}LL~fZv{Q9#C68_`D!4f7@oB_=TFNDjQ=sNR&aZ0` z!u?n<@XN2#Kxz`b6=KRmwC7{fV~^a#kUuM`MkuN(p9TLhUWjB$g#L+#q_M8fGzwa4 zL=Cj>ya7*lv|9O|({{@XnB7;>$G)_G#GoVulGXU_uD~(6d-)z_0I$bcUKkla?QPXG zm(}MT={?=}7Ro>IFZz=sNRX^&@jK0lbSZ{kz<#<6k;(W+x3_fZJ@6@Mc=NBYVSpFM_RkM9WA9whA*wb+0;5#SFIy?`>@@?X7_~`Y7qb zGq;ELazq$}M~X1YPDFd99iiM2x}??)As2rRrOf=+zx0i9=?N)WL9cIDQR~l}&+#JK zxu5^ME$9e}b)*s{*(emjH|Z9=dm{SLf?zYV;bx{Q4$mD;dwG$o5(CddH_ci?nx zErCClmUb5RwsvxIWQ!%u6VBhZnzQ=GSrLd^gx;9xV8Ba$=AWj{IA@b)% zx1b)@ur|R|l!iNB#af5M>#a{+kgQx*%K+!7Zq-4vf})PslQE+0m_GOpQP#3*Yp=Dd zd@}g2>zgL%evXU2{*Sbo`9ZqN8wp92MnCaehI6$4R^MuKa8vA_ru@lXVY5ov?p_@_?L*B&z$VOX*#Oi zQ=W%?w|jj}q_?n-#>jhP$mNtD&{>M~qDLRSd_Z%Wbty4Os=pzzD@JyCuuQIK$hw*@ zd-8mktUw%Lj&++QBEACewFS43!%@-ZjH#%|*J7!n{&(cf!>My`H(2FyroOZvD&Wlf**^8}4rcwJsIoEhw? zvm7xLuN|$e$JZfjk{|#4ssjx? zbpB*T^>SM!(2XWGl-SZUG32uE+vDorZ#aQTarDMzW_|GtZxy-cXEpS-!RLAcTpwm=5Rg=EsXr4%a+a> zTqDis6G`N>8m>|PruEfP^lt9Hmyg#!A}C$i5lGisH`UC4SVhXxoL0p7PW>VWD<@LL zFlx5{2mNG`mrv2}D8Aca!aBOHQ#$k$+t8lfZQN$1p)X&sv|Z_gG&UfqWhYoU^KW3> zxv%qzYhce36j?n#{pZVXq?Y|_Omp;&AC%-o%3Z?L`a0%sjcl2H+!CX~kac~{Sp^>lf^y1%A&+p$* zB8*bjcE$Dt#eYGJ_p5Lyi9SQZ`gEG3dzzc#889J8TbHD6nI?#iM_}T|`KgiA#Xg7?;yf>IwG@B#Pm!gfz`dHM8)GwLS#mQ1Mq4JV znsv6{USKriWa2b#3>s7n-`vs<)eiK`pMU?eXYYNf=}OVX=TpQtsH$~pDBV}-S*z{( zCYI)^uv)Uy;(614ar3o^cqyZ!+-GJ&t&)Kn3(5dHg5Y_x2DeX}f}(S#@sC!4M;q|Q z-h(^pV|)5gK}2eVTPb5a=)e49wxNMJuUp`(p&!yP2r@oL2rcN{IRhrj0*eEY=MauRyWF zCfZscDIL%K=gO%gzzuq>+>^aaK1wrTk72`rp{-?3gbGnOBDOK9ji~VtL33~X4%OEeB2sZ{OQeLfW~5|! zV}8@YW);s31^y76t7|6ry%0IJWIUwX{qEm8=(@W1aTP1|LAt@aOtQxcsY{f*7icQ3 z@qMw5L%65Je09)YLjixX9nS2X)Wj;Rhj_>uoW-<~KHJH4dcJsHmS>-p-X7}r37yb< zHgm|lRKMAz7}ucQdwc3cx6Sgz-&I;#UQt?p{s3wq@>z-6dYB7T^Ny$RR&5X065b}= zo83{B=-Jgw=jqu|`joksQ?GrYBnbO2forkuidRZ#^p)r8jGBr`xaT%<|3O8^vJ8vP zL(bElCEZnd3NMEyh+i==7dPD?<&!gJ_dVdWl_x!mg@tsBFmLi0j2lI&(YK_@5Gv1y zz1o^SDn?0G)_l{Lk=^mAZN>JYUuApK=@MMDzAN~_Y2fyEZS94L>$e`0-6@IVo$-9p zt8p7LoshPg{jx(7lhr?lb$HmA9*kS9-{~ff-GBc+1=k}%|2D~k!Xw@aE5hN|@gk1= zLMa;ezXdQs-;n&qj%QpcPkizIq%W2bm}XYJ`nmTP_cR-&z(TWbOo%71_#C;}-Qk-` zj~TbZrAA6@hX#FkT>0BE0t%FXw?VPW4^6wKpT+E4Ys7t+6GJMC?>u0?ltuiSC8&H} z`OAz>$ji9y!)Mm5*Du*FpE)=lx$ewVo6TpiuD_Qv(Yac6&<=7bu6NlR1V>yxYrG?S zURPZh8yD9h#1szBO+FXtHSMXy=QLVo>}WR(3$1rL((r3@fGIKX@^U(?H+8056`gGj z=j-R9+0?s0=CkeJ*lym7>;iInI0k}cz}KX;c&X(!udiKNTk_J>l%Z2!ihEE)zGLT) zVO!R4#>I)vv+?f|lKX+9ORi3uF|3_fze1Kf+q~MndYq=nrss==q}=5qPZH^~_JdS= z#;T1zU7mUtigf*?$&n4xkDVtK(WBmXl96GnKP8%~^O^uRB@h?!XQ>mc&n2%i)6mYb&dydyt1t5WSa&IsX1+H$J0@e_o~| z;p^iOd>I~r(hoMT=Cv(R$$t~@yYK+krg)%jIJwu)e`2!$#j~a0^uLSw1YS9v6-)V~ z!|GJ1qcU#h`}c~;e7ag{dM?Btg)rtawZr#PJY2R%^Hq6u=u2*s5tOw5zh?8GBl6GF zoFf=sY1&)*?`FZ$EAS|;@S~Gmz0B<`b#s7zlWa3^uBHKbp+l@{9ldNK9DpZ0@al#i7siM(oXbzsgg+X8Wsf_HEEj z2=ww6tkEaIaM~I)9qj~XIL}Uxo!04y)Z*LT*Sg=VC6uoZB!_Z4Qx@cD>AMh1N{XK` z5_+8M41;#}KrwEiBO?stH4(CoRX3y$-WcLc@0Y4r;us(=TDUh0zy>Qf9T>^ zLfhwvt*0RY#`^?_&o3z~GcH|vmF81zzmHt={C-*9<19}0e6ia%=)GJ{;}sh0;w%S~ zYG5kGKLD)G`;A{^SwVb{VxCS188T1`xI0-;}_!xu7H1sg>ZE6pn5A8{1r@rP!QMv4)+`}JB}qw#22>8?tfIbmg{UJ!#@|7kC;hBW+9 z*O|W&iwucJF{lL}H41rsNWJG?;{p*vgfPo4^C;VGzhFipgtn~Ze4gD<$*#pB7v5lXX}WA z?++aPujIWi+DcNUkK$1aLjC<4B+vT!>s)W&FZu!NLf)Z3%;$N%vPqpaR26LtX4lRT zfnO~~8xsZ!)jS|?)0!IV>0-jJ+rf=};FWg-myLqMA7z;p(U#ob_PBJmo_~%+9+el6 z7iZO^tM<%k^!6hSz%nVLGs`nP@66D4J(fTi6>i^1MM! zFa9rL_0%I~QnD6@vgoUAav;H$_>UqxrR^jniOL_f?pUdy>HbK(HGHhrf zgpxgZ_Fu3iVS~iF#p_(H8?`4;D|dz16L)pf>LnG^2OcYAm}zhO8Q(j8(Jl|H$)`q- zNzYHqQW|G-NV8`!Z!95);MpUl&3Z-WEn*jZ}>q9Rq1N&9~5&|-xlq7m}iZN!U_ zZGuxFos0fYPN57@(iaM-cg$AGO$QK9%%(!c<|q${u5Pr3(yNxjn50B_MzC;9{N4RC zrJyt{nud#EOqHgembn~`(Y@Y1;eFrauOx;<8nl$KT7Rq=+2-)01RNMKVCwUQaoc%w}7!ZZ1OlzP1Dp(qH_*aacui+#>c`v+41lX4b$;UqzT|ll-yg6-t>#5%34f z%Hq!Gndr1A0d6ckGJ>WiT3vWHy7ZC$zGgmbdj3)!MQQnsas(DbmhQKeq1u=uv05QsLOFMS!FZ;e2Ajv z;mu=EQa@K^RIRef^+702nLKTKvXOO4<=)DsJ_f$MwjY?V|USarupKCDfmb{ zUn8qtkwwtH(ZJf`1*+o}T@0+3t_)}Btb;iX-2La$=N}%OXq9jx+x4~OXSGD<;}6mg zMhOVa_?l&Belk%+91#X`4l&ykGhJV{I3w!oC+8}7Z&v1GrlhFxJ@MC}C8dyj{X!yu z(_*?{b4OcMRb}LChnJw%alWycxw6jq_ZlHS2Xz0~iI~}GA;6v~fCGJzll7j(41oYc zQTtXBU;ks|gG*G#>t;3Q!TR7IM=vct_yi;1x;(s=T&0Px)4MqHNNl9(6-O6I%@i>I zZEt)5xtXQ!IU6AijGug}#2OF%^W(xI5?64M7fG;0ukm0r(-xhW-_7<*dDPbKT@<@r3nIT{W8nl5Ou5RN*i|X zog?xfR25pni>IK{1H6>3;1aG_OdZ8vK4kYUH-b5u*6BRxOP%~T_Fb|yx|c%o5_$qZNUV2rd#>09K^B9X8}{{ee!;ON|wQH>$R?_!0$sRif1K&KBDN=(u}Z{Ju?R#}0D1_G!Jh6McA ztH?&zp7?5-c6ame)j9I(QKl0N`;I zVi6XY9rkpp7)Nif9qg~0t{#gZQWZ=T{;Zu&elie|C4{uE6||giR4wmAVze`yC5ESJgd#iZXE>HMF;~axSl9@u7?_080c3T3Bu)Nmh>deUR+3B_ zF9m%{!E!?D51^pW!o^dR03~fXN=Hjm?;o%c_Ut+?-bN0S)}e>f{Y3L80I%h0z z4HiN{F!QCM{SoB94oOt;B7ESz)*s{5txTJY2ZvQ?UH9vb3=m9@l`5h)WeKN5MyJXm zA)7B~l+_2ZSsWku)oL;l7}mRM!^7zlG*eR(rVAJC4@m|iW{tSmn7S7B_X^Qc5RP&Z zjL)?g8qhSkM=uiPp`m%p3YcXT+#J0qdo(8OOBJK~$DqNg`R9U1){3q7=1U9+{-u|| z@CVD3jG1~m6y#(S zDj}PoYiaOrUV(EUJqgkC)=FouvyHDj>Vw+`DhNJl(Yr(5&NFOlwk6M7lC#=}=tg(#N?CxrMR1m+sBZYe$JM%RsssmOQZ^yB9}G6t2TGn7 zybp!g0WAAMMyT2=3`(L-P0DOBxPLHDuMa8HLLyb5U1bvVzJQK5C$gcf@#2jNo0d~h zkV7pHLj^>B3&9ixJ)xE}2djb%7SlPajk}tqv&p>BsQs!&0odWy&dyBt<@z1j2$tl4 z>%SM8Mo(blcQUV%vLW1j9&;BRa1^x&F)YYFW zr-z$1Z*XIxs^4DYqF4nnX04Ai4ZdVsrDywFmVdMTCb#$Ze?liej49$(ONX7E2J z@6o~oOl)atnPC^Cd0|?^rP=6P(e}L=$H7Y@sVIpnSKD}Yqn|>)LcFN9Wwc6T(5q---KI4$-56n zRZTzfR`&1DZe(Iwo#eo}pP}{pt|0<0l1z`QpVwR*5N1abaybCPG=Tib4*(Z7^XDBO_3=SmLya{hV2Jr5Y7S;@FV^ART%=VzvRO#= z>|OxEL7b51$`z}}-FnT{^Z_=&(>SF;nEhR?8hN;yvw)|GMBEu8-NB7#(r0Q7D~^@Y zY0$dXr>4{WYOu7fcltYuuLNf9c+3`F^kz*zA_O5z~A1ti#<#@jNhboG({fgp&-*t3Z(Y4(CGKUAk!QSny{+l*2#yqjm+}j z)pR&b&DOp5yCP$E-I*>-6Hc)2nG4QSOCTvV>j}2dCuubth|0gqDHbEeic-k>4z+gP z5>#ch7ixTvepp+$BWSaX*=yp88PXZAh|NMFj>MeQ8LP-fxqOrPR?lw|f{>pZ<=ZNY zlRm{f6cMnzarzthC>8+3r&BfasoT(vH9;l?9uuOA?Mu!a1#ICAN+WGX8g{ChEubO3 zHm41AJY8$=Ah=xdcWrF6WjY?%qZc7i?5nc-I_@_Ps6 z*0NoHt&Mh_>GMZF@jNzx9|b`@JQ#>a6&5_vE44~Zt04$mDm=p>*_kPuZQ)O96-hcs zLfMKr-uW-9Yi2OK8ZRS8CO?_q%2Mw*!_fPhg^T)EU0I&i&6ulTQ1vc5?6>&jWbHNO zz2(kx0`b|X7sZA3issaOPQEJRtgSu@gevu-ek(8>tAFkTF+!fs4cSgmuD&z}ajlkj z!Vhs<0N732HRsuUT9Gvsk@?d>Q}LeuC*n>rLDi;g92?I>nH-g34mv z6AxHcTOQWEUeNcOLJ-n7<%qzvLKSS#@)Q{Eysf@YFw8UWW?7|^C2UMeM@CNPdE}0Q z?>^Z)gHtkr6CGVnOG;|o$?FgkJ)AfjH1-ubiuf{km1( ztf{tDV0ZsQdfq&Vu$9zxlkK8AeDrt1hmmI7-o!PQOG%VCo{Q&bWJy(1-sS2U^tHXl zTd_An&a5Ey=#_-xC322-SX44!!~fxrtZS4C5^052gXKjvYp(nfFV?7J0(T%!BXj3~ zKa^w@0|_*PJmdN4vt7sDqbnJp%ei&(3A=E-YVR*}M0`Fr+WcJpzItH)D_lqTJ4Dg@ z^f73|nsVO!9{>J2R%Mn_$_$o;vMsAhO^2XXKQx;yY@&l~1dW$uqHT&dXRP;;GPD06 zs6$6dbzQ5$VYXcd`OFc*U)v#rTyvv*+_4(?|YWHVwTlK(XT^Myp#g5{Db}7&g3O!0hj~0TIgyx91NvMxr z$aPnJy~sR2Ns~QIH49;@N)-W~k7FMeRB{)UQX6d*q&GgY*y4KnJyhcal?G+;R#)!> zfGJl8%`@HwP8ZE8g47onWroWHxa%qt7Pg4?xb3 z1ocL%Mts?QY*~^RuG{Z{kUWH5{6~4@TCrj_>;&51InDRN8INrk@vdWMN*^0+B3y41 zQKs_iH8}%e~MAHwX2bvNb-<%u)`sqfv-5Qe9CiwxK6Fg28Cs81Fi-lcORb$HF$IGKD_;x&tH6ahA}JWmJLpFjxf z|BUWTv6xrT>(HmNz3UBRMHDc2qX{=p;Cf*Dg&+1&hDlMesa>{wcjWj|ef;k~A)7Y) z*WPIdLH2FTyk7~dre6+g^LeBcK&@El`H_@Zm9g9$OxIrbu zUH*>x3mu+iZIi?rO@xwQXK2KIeROw2k*`+wZa3s~m+Tzv@rgdLlpx*R-5@C~-8>@Q4bu1E`~BT@*SaqMa>?SH z&zUo`XV0EF<8adpz^T7u2G4$2R;j|7E2U83gu{~)^-CTu%~sw1uIPA_tl&8&{!fhb zTlMK8CxDAw0%e+SRw;R1n*axb3t-zY+g+V6`}PPj^9h%tgd68}MvNTcc1)~%#bNN5 z#P8lwU?)q5vCqxD@7tAyhoJs>r8~Ki6B|V-+uQ8t(ObQL&a=|uyT(oah6$(a!PXMA zw_l^cieji$r^TZ%Gr$6%^|>a_YTWz`WH>_J+oF3oX-bd1+9LG_f`%i|6rFEA=ga>^ z_rLzS&qQCFus^}P$2+-`W!^ZKa8a4&h`3Sx0;`pPO&&ssAYW6Ur&H4Y($y%VnLyMI zv+a^-VeFz6Dtj{GITnhA#3wFEheNN#Jg0bD{(1f7f|;EYMM}{N6XPpKn&wIVs^1!b>@%vJBt)ah^KB{!?Lcbl(-EFmrwSRv*Z{KmH z`bf?K&O8SCzev7;aY789|LPjy$bS#D3}-(|gum;Sl-S~T=fXHmLWD3VP~>|CO}2H} zQj0$CeygKa-&DE9SiMJ!7exuXv!Bqd;4bnG4Wd$(v@P>FuAou&z`A3xY3uiu$b(s% z&{Y|Jj+Ah}Jqe`5pF%N9S8ox-m=dqqC}~ui<=P9VGOkZbTADn3Xk-hg;dZ!*6tdpm z0YM&Z-1g`H$?}#$gvI?9#I%QPA2iOcyr{1b10AusjN7}X4C3j~o=xGaB{R6)6uMy= z<)9ev9mIXw<16;}w?yspnSP?MrAV#0DH~ z0^;|U@K}(Xh?AkU@_P1wo3T0KBf*e>AvE9X%y1)CU+3XBMD*etNwUpq2XeSDaL$_CEh>kbqgz|P8`~7yOo+65E?_? zw_aA$DD?$MAwas-d1DvZx8^?!(|Ph2IhKfX$Vi+Zv$sIB(I#qvgD<$tCgOvNm5Y0z zWs)G+P@h^_rJ#}2jr_F7^YiS*Bc}i8jLmb<<@UAUZ1mA5H8oZWUX_mM;pSdCZzAUC z4TM{gy7?|U_IEQc@D(D^m)6=Wi|v*!IlqHvUIQYr4K6DAaWy*@0~y5je}A4XV^cYi zk#Dk!_eD~fj|v$T2&o2->iB3;32$I_b5L@qW(a|RWtT{S;dvoS>cG#QgPcVa@~(yR z)fV5$s84POw1FyEb*G8;63M`?qj&m*($Awf78Rs~9g$5JfC&e*D7K&6MimegF`o*V zfZh{uCT$gRM1_w3TSBJ3L?l`!joMcAr$>WAD zQAUsO%Uh(-OJFk{SGDPs1ZkFcl?^+y%oF z-ZGhOcIh{d7*b?kN1f(smEP=u(v0{Q8LGR3tqNkIb#K~dbkyWrddzuk%V~d6x(Yx_ zmd01_v{1r=@Zy=cjBM!UeKZcpal>4YpV|sRA>1keo3L76)#fA zGuqX5re%KGvqB2_1OtLr6m5 z7L8>{b%Y{Q+tc9p1KND{QNuNZ4m(pA9+Fo_U|~(&>KgH*;R{4_5D!~cmbhoN?Hc7Y zcja*#DQ8>;9>IRx`*4xtI_kNhasiLo7B<}V^rTphsf!JCb1LSihZ7VnTrvl)OsE57mpRImc|VKtrP@=K2lSIvsl%a?(gwl@b!B{G&hEuiN8wTea3T=r+P&$R zIQJ!ZLh5YA#{}B3m%?Lhyo})_+%Da*mH7JcIdaf{E0_is@^@sfLeyQk0hjvwuXX~D zZm3vF21WZqk~E$2m=F^29p+<8n(CEyS0aGli;bHdzP%pIwjRh-t(iYvEs!s5pp78J z##xAf3PLvIWCrZE_s9#c0;Gn*EU;ajAM2xWhZBBrJ{4hn&*!x%`xTVNey$4;{QLD& zrgU#0$CO*;roB^taz1UB&BCc;?QrVypP`xtprL~co0)ID5>UU==o`DrgCI)W_eNUH zn0~pM6e^bflhk^~pu-^rEn9;h+`8B8YmoSE!w>9EhynL(7Pa2SRp9B8eBy&dokW=B158Q zYfEec4IQ{sQa-Hmyb@jRap*g1>3Myt#tsWrB>|W4e!5|(v;pdM>++P%ok8LO6qcsL zxba*)I#U%-2_c$m+gXEkDr3aL!7eEnb~it@b_xrd_Llw!g#Gy8iRB^Fa(X?s*9uOw zyAST2zx2-m`?t=Z0SEJ2wh0aqV3j{a-_RUs7uHF>Y{un6 zB#AO%rcH4QpA+ON`Kn67AeLEhtTjC2Pr*e_#Y<$PuNwj&#DhOm^dKG9$jEg++f?SLET{8`WJIrb}&rG2T*$clg1Uq}i z=Tz@GYnVFTLUze$HVyboc{YCcxS^>PuI4}LoSyRRhTy^@012>*6MWkJeW(^&gF^Hi zvLB1vq$3^7#@0cf+t-CRSs!)k&%?Po9@qcu18I#86ZG-#VrLeMZVJ+Wu{50+xD8Du z?cp+?iuGUio-^X`8XaWOvphamgSWDu(iUsGn7bHmHnc#r+09C^cIQw9C8U zjk=*ZGA^_4o@E2A7VVPA>u8^A!i$COU$DcsV}hriO$cNcKAsLztfkiKF+cQ_>M`#omJ}jDc~R0SagE=gK?I*m z^ZaPrd!<$(Slj{+Nyh=TgBRk>7i7LBI!L~j9?G-yHbw(o7Yi5tDl~xvJs;|G%{Jk# zy-ta9>mA+P-apQsN@hHa@hj+*F?*OU=w*06M9xQq|Ch=C{+_7-qoT?Cjp$MS)4AJy z8=K!}^lY{aI-+Hj6gWS*-=s7QgCF3@>9vOsqzpf@iRQ5Zaej&$CfzkrSmC%+#eE?& z9eshQjB3xU;`SdI%X1s!;LkbR)wfLVDv&R6>QR3z1gcO{wWznmO#dTJv z&F^Y)yKC(ft3JUSb$hao*hAAXo~gD+a2ni8u(#bSF;6_Wf5o?gleo-W@o4wywT)(O!sio?%ic6&Fx$Nf$2otNQk<3=wv^<0B+ ziV?zKFlc%bMoyK5-L@r|iZ)YM~-VppD$9<1e9f zw?i^xJ-9G*<)7hj3@G;T1V9o30NEGfjK%A5lC&EA{H6NNQ2`<9il z22g-7owxtSOe$dHsalo%UpT&Wl_oT}QzQ1;qJK$7y`^Ku%y`YFcD3zK0ntDNl@5l; z`8TfKS&?^g*0$SOFLJj&%&(Is==cFekH3Z(y^xJP%Y5jQc|DWY%I4cZT=wOS#^;S9 ziehmYDOC|||1_e?Og~NA=)2|`wFXaqe{=rP&5b&+RY+}Rsbh+%f616z7^ddSXLG<0 zx9gXy5#SDMRs(IQ^rij5WJ$_JKhVQm5TADe#r0psr_SQzOl+@W`Q9)Q!97nZ;SoFe-dgP5Y!ZQ@F14!Je|~Q;m{g!s=`Wpd$CRB*RwxlPV7IVhMhL z?gM23JGyB6^1P%P#Cf=eS5}M&m&|oUaQzM`^$*r<_kXt0X9%B#wX#jMC!kS!`Wb){ zL410+SdM1Tbgu^>4K~}k0)X~i29yDkmA93^!tPc{1R(G+5Z*I8EEjy+1g|wdJO6kLMz#OCOYkSTKhg&C3QsDQyJop0i-#q z9|=FFkVfOjRv*YeJYV?b`XpCrOo;~^l&jjs(d^mErQPcuc7!~h6%nKkz0Gk*vAV8S z`+$w*_8uUF_~psanEpajak3P_(7KAr9iY;2U>O6H@UHYxhs6AAwI|C}i7cl{c|3Ac zY=Z5Apsr@RIv3;{qtbp>5j~x=>L`x>R7lOG~sMrnV6Zv&#s~B7OQ= zX080l_Dm9iT(RYgv`_9=hL1`pAV6FgL{`#)o*X~(-pzvVM}2$|dOz(WQYmgHw`qhg zwTa81wGs-G#xZ}d(Z^)BZ?WHmCLtc9Ey=VQ;11Jwr93~TC#P=t98#lTWv~@I$mPA= z`8%@{Avzw1d5vXETRfE>r3>^Kf^TD_dj6a=_hRco$nH( zK&mWy(Fm3PLO|Zh!69|<)ZazgXa6el$HRt=8~bqee*3+2m#)OaTFzqb7WZb_6dYwc@~epdKww z)H!+WMK4lby&~dgek~LSed_*D5g`sUF&-+7INYO|J8!pAxKe}dg1dmvX<$~Vvo`LC za`8oIvC3#?aC`<4BPACNN?5OY9G=XDR{H!NAS%KYR^{BWcw~DCQE^Fa{Ur^rVhP9w zRnf*@HK1wbUBadL@HKU5$mD$_c-4Dkh=q=!Srq*;CmQEnW32m4mm&*~W@d$SWrw+) zLy_Ba9!=_Z=K_QMV!X)(l^4cb=Rj<=DmwQCVkH~xX%DV8C(#l&+W)2YFRc-Ots#Esm{Zyl?= zo(?C=rE1H8C8z}XsNZBQolPMc!so!7ym9q6?X}+Zf(5;otF_p2SfJ9Kyqv-{h{yY$ zZNUS=5cO1L2+J3E1tdo&bn4ULZR)#-OA^w`7~;b?PBLHZ8|oElqj&kL4Fue@r+Wzp zI&Aq;-g&}^D;JpaFO1O}mh#Y;zZ zzMqxu3K!_1fut-+E$1m?7F9`Tv2k%cAJzi_sdSR|B+%TA0TEcP8dQ9N0rum-m&H5o zrW7Hz1SPmg9iDSdN*uyK#$JFMQQ&$OvD2`#`^RPx*XXt9B!@3Zc?8F|h#;cWY90tj z#5}mB^U?coezg5>q%oorYPUM1@CeAN1xTX=t-X)?bNZF^?7=A01T=Bg_=XTCutfup zcDRG7g+t@dWMg3+96dBi(%xFF*SX`TjSHmxG0Wq9=o#2)tzl=bm8-mmcDhQH9k@`j z-JC=Nf*O}KQY8jz@;3#O%HA$B29Gv?FV=WAvfGqA?R2dKu*ET+G&8%aJjM%mcxe}( zFw@xXXOf@{gj*|2G$!c94f%J?3!GeQjFww5q{f~ow)0db!n5!G-?FBOA?{=tn4tj& z_x&pV09J%ORW1lQac%;Si38nR;KlFdM-C#CIgf4ocFWE>4L z1w7p6XTJqxgr@Vq4*|QV{dE5hOcZ-@k+}CJW!v^+&(N9 z7_cyosuyr<6zp1=A&H)0@mL8_p$GY)yBd@HCDZi5+v1w^cSybK*RUu)`Ga&*a4>!% z#-Rs7KLaZ^U>L$)4}eBP%ALQ0P%)dA4K*ERu0IGyMmUkk*SAk5Y2v=4VT>~jk4$f@ zpu7xDxi86AQ7@L7qfp^o3#umY$5$kkYsF-`nP(halBqJ6*#Uye>xIr~=1E z^19{cLv?NxxiWaCyWi~6ZGislX;W{ap|B8mqw}TBXf7V5#rCsU`XWFGBU4sF$bqo# z4%&zkhwc*y!R|?gN-~)>zkEed=UJ5Kd?6-bcXID!7@1Dkkn4WJ7`8?`rrQAKR9>vu z$5p>}(zarzoj1_Fy_9JRk85mG&$(QUL=5AU-D#4wBs)M>!aHYSi@W zr_&ogbM)9%f1Z#UD_aOR#x=#SWWW9aP_-mM~sAE*aov+oU zzgF;4tNB`#>FkIvr)ju4;JrleviOm~@cN(Jj}B_JD7J+&+ei5uR8;2g+tO$fe!&Ra zMc%dD5jG;csOO1;UJ<*1Z}aV}MV zx*+XXKOTOh&a59CWMmj8@MpECeF=IrK2eQs8IqoW)S8mbT&Q}gDk0cyf z9CPX*VYVdN2_<1HqUwVLywGxx+5Dk4K&tGG>7l62xBv~Of)1eO=kRE{LN2f~*`BH7ra>d(47<{InFSxuo@rWis$=1rs_|15{%~s3 zRVJA(`MGa>wdf`-D;eFgt}Pk^YeKxg`z2&8DP$kT<(=K<=EgU;`ym8IksOyntDo-M z6)0lR#MLI*Uq<770>pf~#1G8hIy7du#B6@CKd+-e`Qq4J(dR%}>hit&u6n@ps!KP7 zlip%wh~`t>{4{ORc<%Uf-|)vkw5%@?H!|a1VG-g!pw|X3X6~}@1KSDVi%hNmm8eiC zWg_n)62BBx6wu+c@Bc3Myc26t5$(H(x6{u=JT3k`P@_T79;3h8Zif<#gsZeKOyG6l zn!Ivch!9gqDP^-Fcxn{OUEkU}hw zBgL<+LxY#0d%WM!#4YH#d_zr!9{v(N^Gp8OGq(UI>7Mw^-4k^AMU78#xd+jPQJA#I zqcXcD3&=57L>!w>XcPD`kL^)c~XZK4&s-UAcy|V@G}Q=Ve`4sy~xeJlgg` z^|qg;0N~;Gxt4e%!1-E}@&jM2-mn($t0{euWJNx?j6E>(etlB}D4YYoW*cAptcr`y z;O%y!bH}3LkHwJR(YpUP@!|Ll0T0@@_ddrpF3l7pn2I9Rl^=c>5cIvXGSBimf7RVU z0@2K-uug?c88OMb`jkY@8{MAD=jYORgMk6T+K7-TmCZ*j6ij(*V_YmzWPoZAHAw#S zfr{ZNo+AR6&sUqBySG7BF~5rDA6FjdC_ZZZq*G4F%t~Y}&5}{Uw}*IGMKSs;93D`- zI(I=mcj3`vviuI~rdFNJvcl)HnxB}$z}s1(>X#U>|3EiYjTE?!Q?tU{NLA%aPd9;8 zTqjnDK4SwKD(mg_g*=yPms-66A#FFYxA&;KlA}l4?j+{HJBnAMzEq7$?X>=s%y-6l z=4G5ueyT;a1I?4eH+k!`JjBGN(flq|mFqcm$sDu$aMVIj$!y2YM6mg138;RzAd9%* z7)hY4-*f}~Q|=LwPypKj0mO?1uRR>POeYex11B*pqu5m zoP`}aZr6CE$~6;U_;AQ9*B;Pm64+qT?$r;a>~Bp@kFjLM>r+l{0u^L1`29EyY5zN> z$$YEEKoaVIhlFL1vJ-?oxF4^(0bwSjYEbcqFC{s+jbC=D#JW&$1%&%}~3u zTOpdTK$%P^HCC2VA^WAu2gTzBqQS$8>c;1AC`_gl2mav&`YE`Bmu&9^d+=Rib*sD7 zU6q)Jssqv7+-8fxL=y2->}N4w1slI)F25Bbe?y1$*Q4deqs7%0Gd4lbnU?x?_n}GAL_0K^U&VncExNnuI<N!=dipyJlJb?LpJBfy_4{{CabW; zGTrKNe)?+7JCo@=_DRPD0f=$8mdU9K8W{>2nBswgVIwr`y)~os`_P!HO|(^{SBQpZ zDxcxVrZ;XP&mQT~US9oHGKhRe4$%qzC+}V8*{cW1 zto)4kWHuY!Z~63=$U-WfcYYF!M6*e&)#xxvuVr12x~-rRIuJ&G~eH?UM#`l%||sH2*C*`Ogw8fEJXKLEMWi$jPdm-=cu;}$slRl zaMXo()L$5gg}W|SnQ!B!l410onIFFjZI22Cocmlrt=KSZB*3jT+yCeAL;a0$Z* z8xDN8)UkA{z4WU6GYFHk=C}yQL6n{QWvRl$;r$hR?aroo@%;?}~w zaBw77h#A?NI64$W96YA4ZZ~LxI_3E8_t1N{A2nB4SeGpt-Q&Lwkq4sT(mGhAZ05i@ zd$@c!iYmJwV)!8a2ZQGzQpn{*%FM1xKiW9!LS~Es3&B0T+=A1pNZdc*@@QqMr}sOW zn!YNZ-KjNS6KWEQ>7+C2;m+yd@%Yn}KqhD<^PT!o+9nEin3a`Zk6BEW&^2v`bFzTO z7n~eOWH}zl>N(Rp&)!(D%{FQ*#=N!*Jhv4K@|NKkShpoaZiJf`LNxyYI zp*EE4K5fAn3=@52yq7Gb>m=>EMcVjxjx7+8>uFavCSlyZL`V5d+~I?-$El&SyXVT+ z$f^e(5?$9cZv0<@h{f;t1OLjFB;fM}?YxDRUaL51B>H4OOJ;ydI!0r|jl&NLIs5*b z!iQMp1+U+NH-mHB?1_Xubxk!jeZ^MiWNV`PYd4iXQw3#?j zSZ!=N?&OkAG_pRg-|Oj=Co))wfB7oiBv9kuHh<55dQ-c_x3KDok*NyGi1me3(hZiM zYj;qg!|yaXTZM zXc}6#E&dP3%Mbr9U;z4Yp)avVG>VvZp=DasmvI#=Kh-3)7b)tgMT@*w8PAN?Imr0zLlUak8h0XEc0GoP_TyEaIqqqr4U3x% zZ_Uf)mSFokTkMc-n!tBJSe1b9G>v-2CXq9aZE9H)>w*5Q77FT;ndO0khyc0w8OfpB z$iFSOn-nhs@3fSx>|u*N&IS!jJ%beT>;stzoZ>F@ChS?dBo_g7U?HIcDLHx1TLoK> zMI*=f%*!;&5SP^>Z|IN7yit4UXTq~OOhkm((|KGWV0>S$%@Uy(75B+Z+q)pO_p=7@ zkt{+jteyR7BV8de33!~DM}r#~czq{Z59tlt&)Xg>(}GrmRB88Vv3jHNMxTrb0;zP| z=0fDoEtcu9jM30FzVltQuV=M-`SeA9kl2R0I``KyiP{!Xvtem>?j5sFitTP@_yo50 z6{2H^HxixhFuIy}opdbIb|jsazJHVUjPEE4d)fq=g&-VFNc0<<=y%@qt|QXmL^TpK z)k(I+%u=%5LAdRauidp%rs@?s5^Nmwe~wW7MQy zKua>pvY;=bZ04&vG6%Hn$c@`v@;ANUZ(Pl|rmr zm{KqrkmcMV2M6PUJ7m8)S3a5R8>yabinyYS`#zYOh&pxbR45tp$1DCy+_2=pIH2+E z``@SceF`F9wI@<=!Al>`!6`B7s$?%9;RTbi6E2Lrv{U^ibnc(?NoP;wXSIa#k7O_w)cA6SB}*U zxf0Z6nx3vkXJ+L#+`^%yzXmr*6XWBFg)ec**Y8aYUcCH!wL2cPqcWqM@X-%UBe0>H z6nnCb&YRV6A{~*uh?V9yy3SDExD}Vu%tykfkKD^RI%P@9{pf_+lTGTUt163v#+^88 zN%pixrh1N7AF8d);#mDh%&y(Jcd&8TeH5k&6T5_#Q|*_BmPwrKwz807*K1E1^zQ79 zX*8^C^~K(1RtQXr{SGp$nctf!e|0dGjs|9O6pZPAO^@kyrrRq}`m*}B-qfqk@X0Sg zAUYxn^!^ExbUU_3vS(_M2TkH3$8aoNo=%X;mq9yyd8ko#Ofi(bHh2*EtZzi?b$b%? zd~C$Iqc*Klf8ixn)9psVUm*cFTrBMmo7m8{j8A-g%e|^Z+gmG3izCy$pLZhasW}?H z7^@2FD}G0L#u6q9o~pN0>=0WcS;vjn)=*04-dc|lAtpW!xy;^X4ClO}${v>o|MO{e z++Z|Ar5OSCeSb@JDV53R%5KQY&pcNwnhBUr2CQ6s=#rp6!Cxv?h;`nG!UXmTZj^h- z4%r2Hy_XJUY5fktDrIz7&@z7=Q_0snQ{1H?rpDCNLo%EjuNK(rX0K*GjLvadtQnYb&?3;}8hx$pJ~ zR9jq;{;R>P^ykZ^n0oM4Drc?qM?Y;PUs-@kX70_$-ph`#FqreFaZOsdfV%<$HWGLM z`F<$cKpjgGo)6RQVe%1-&7dgBi|wft3Yc3V|KC+k@p~&-sl=&>;;Zg+WpDDCmCj$_ z5|a%{{yj`*m|!uwXMIl#p=enl6Lss@Hm~>&cp`=V<0ndR3y>V8gOXm2Z);B2|ITRgm0F7Dei8C$O6u-Tc7Rke zpdRA!pgUe`U>ZERko?#lv?tuq1r~RDdCC5&y4vZxqVd~(Qf#Duumn6g+yAG+q3Qn% zOTvOKa;JByPs6;VYXKJKBniv*y*}BiJ-?-IIATZ`+^KjSAylP&SBc3kC{jlie6Txo zVhx=5(}%0X2A-|1$w}2|TfhXB8BTkKk?{c57z^~_EeP`4)V;N~0ZKh=@KcM`NJH~+ z!1CAk)@<=UY^}4U^oY+dBE0Gq&y!0_(2o74`Eh@r3nzTz;1FYd3;p-r=!RjNX1Q;*74R zYgU6aCJZX61UD^f=TV4#2Wtc>k>KTR{=x8O6~S&#&YO}=>HqgmqQxtBdd6?;Ji2S= zYfS{Ap+tLb5PC(EmBPV67<1#tuiYYR=3ugtQeg|f-%R+5tKJTbZM*3I#^T5O02{C`2(gW0#;g6Bs3*drCdz=!3<5u#ah!-^as4XXwmteUcn08n0oq$OLIlX~-T`lSyewrsl#q~6 zDnEklq0?IY&^u-vXxo8mBr79h$UN0dShpEygazq@-OQYD0HpxmTsW1iWlGK(a&MPa zQ|ykP8I@4Wuc48*be+@<2Y(#-_GY~I*0)9B2S$brPFH9ZXsc~@t@|w#QgMxb2t8GC zAe|USIoLXa$uw+?#ZKxGwpX5t=+1<+hTIsCPNamBN~t+Ptc*4Am8glxI7g63N1eZJ zO}92Vkggw0c@5K4dD?IfK^Lc?iwXHV{(nGEOjl0|{#FHOj}Ci&zw|Gb}Mqa4ofbr=84VLM>A*j#E1IQXpeG|N7Kg>?Z|=F6h)xo$adc zVFg!dvzni?3lKpekeSzt*;yBXXkFMUE4IgcC@Rl=B*AkT{ucWo3UHPXAOYlh6>Fcr z?aa?js6G(cpR1GfUEBN0w>?W|`{-a7Iy^l&@_i>Fl9saISNF6mllUGAS>RU|VGogq zB`D}}(bSx{4i|}7sf+2-uI8VhO3TM)2QPX*1#r=vZjV62A7LCWx9x@8!wKQg(Y zG(GbshJ`RDVwf-S5##l37!m?m@KhoyhORv^W`&)dqLAPcJo)tW>3M`g{@NU!L{+6; z!MBQtZ7xfTXi<@?h}7)uxL6UCR3SHNu8zXg9X}Q##YucV$E88TJP$pRU>!n*%IsSlnX+g?q>OLkb;q?N}ePc6G zDkI)znbe*RXM%IYxf7S7qfxwE1@b_YdyR&NOx=IuShVt2WKFoEgT^gEck5rH z00hE&@~8D=R%PWs!;vkZ7DyZzbd02RMD|)7?VxN|f$_VLPqfgO<|Gg7rT8J_uFwy| z>Q-B-5a;ktDxNcYm&=Exy!{-T055;KD+3@f4bbArzHTA}5IKigSN= z2zu2=woc&5LuX@W?e7A1-rz{Q4$TTQwp*0>-?_a8dU~d@>F(koIE`S3@baZZC! z93<0W`(l06!z8lO#h#M@xOaZ?-~-Tm2x95imy>+8LXxVW^<{5>{Vms|!*7v!5hffZ zLSp zsJLg=IGrf6g-70vmf%ib_Boiol(TxmB^gOtKM4_qx9IL4{F1~YvAsKF1@q<7Fnh5jl?^5WKo zCiNZs-u%(O(I($ur?D%kimh(OOxYu2X#$S}r`EBJKjW2MLOQjUqIfQK>}JB&wu6;F zf>W#@q1NT<2(L>!o}w)dF+y{@d{!fysr15I|D)gRSK%y0Xyhx!@-lY%x_ z$>W#sDwMi!%VdSSkJ$ZHW@a2S^=4UPWMtrFr=`lXUBJclp&+N{k}p?bBmSiUa7q(lxuS>0W2_-Y^ z#Qoluhcw$mXCA=gKY4@UC+8%wVtWvp5h<@C zD*eXz9mOI({XN?D)(>aIO30{EREF<)xzXKF^GK1~MAC>!{S*QlR^r$mCx~26t1)b0 z9e~FXrO$Emp_;UU2a4)^8XwcK4ZX#5;7HBCChQ!&XP<$W2s!?a{M-O^7mc5A$s zlW4x{lkrQXIyEA%nUCi z5oL4f7Tl9o&*XRVlj+Un!fx{SnHu(VXo&9dEtrg*imNn^F3zHC&#i*83oAPA=uY!g z`O^-kzSjT}DQbT@az|xTG9{*vWneQ}@Be5FOMt|hn4hJ00lR?-Jl#l-?#WvqnD%Zs86Sn1L z=?F3bNy+B=(iD#Q?yI$57hmWBI6^_J6MVuSOy=#-d%g9n>LL@X6rf!{=XbjG7*D z%BC^DY2$p`%hSKLYY3 zmb7pC_pQ1)*$Cv`0#-}r#@yWP&-@OWJbyNZ{-XSHfE{rq=9j$93GnE(k;E5G1D*(c zuD~a$tIgeY^(kWhnrM!r1BP@hYBGE4R&JIMU_2eqJOuNu#ha1%O~v_i+?HK zH1Ez~yBvI?x^8zpe`K&Xn^94DaeVyIYwuzw_+-n#w?cyfKhi|1_AL*f+)tVfae4i0 z`P?Zj2OgHK(xRSjongLj8`|UaKu+XcF0hO1u;onmkNvzCtnOJq=P_BdA(ecma{SjU zYpu51D#A5J5GMbdKK$n9u3cLF6c3MJmqSr_Ft1oTI?b3Lg|%CSDV4Ldzl74u&0@pg zoi4tQTD7IYQ@~?)3ct8X4iA#!652Aij9i4v(0Esr(bUY)f>)EO5)gEz@8o1H2k-IK zpb{vV!KXz6*=FTjYvmK`;MeEGA#yabyvVKJ%<8>77Bs<-zXn`_hpJ`W+2!FoPgc*3 z?{Qw`2DZZmQsc108kfiiO}2f^Ic1(_;vt4+L=YQQ@!E;TG~G8Y1c7eie#u3c#&j{y z8x#Ss(Qj1k2EHQ)mHU!if|Uow*eC?8WBhru0`Yu{)1n_k(Vfh@KlwgY!Nyrij~PB< zP$*r?N$r6jh46F-ru~h)|92K7IM>ZDjmjPhK@sUKQpFmxwu=;V1Ofu%Uhqk zWyNxXHY6Nbu{|I4E)4k^0WN|Nj2&8QZ&-1WudA6!K{Gm9(i6L#DIgG5Tv*KniBGqd ziyQKRXvz7HU*8p5dnIjs29`xx=O6V@y}+7cv^7j{LJ~#(0-~2YjW*zGk~97*%lmVZ zF*m9o?5{n5`P{Pt=*ur=r=(E`e?DcyTcfn=>%xZoC`qulA-!FeIh5TCu!GpeCMUDp z2ay7xgrlqVS?YGKzQd`3Q@I|`1xs~^W`VlYRbOECp&P+fzCYaTq9|$h>T;K-XGKx5 zLvUqa+Eys?Fh(VyNWBjZmYX(ELqn?!8dw(iCNU#_NO412=i1i|!g_N3d$mMb`FEvG z;Mdl$u+P??<{YR$@2G$btTnfF>A_m-{pcv?5)qW%<|<+q**u&RJc03pi1_VmmXYx% z&Eu1k9mQwSfK*Bj&}vkx2|Yp7_x*@Tpg9iaspz9g?QLz(Y4v+`b%wSY?n!V7rfP-n z@EYp(P>6xkT)poyi-_FC#>rN*d}#jK3c`8JT#o=gDkni%It}zNA>}R}@Uvs={g(Hz z-iIPknaLWY0Ppv3A!k>g_NTZ4txqZUqUaeu%*nxzb*)xH_0RK-?)I8rJ2v^x3r48F z52L{f#}$s)A&Zo*pbfUxg%I+z$zLsVtbD`>gD00O+bk0jz>z$fc(`)?1X;||`X%>H z&0lfB=U~v!Kx<1Y9#fo`X{I0`xGd#wv7hx#-CmIR4(#ygfzxhJcb^Z(YR=yu%Y+xo z;K4!fySDyDz8p=eu{?a?>VI63iZ^=0dxQJ{&e-phH25#|8V*#86nO$+(8T}$XiBlM z&d7d$VVSupaj~SEu1FE?nv*YU^=Dps4WmDA{1Afl{;y~ z{d6-gc85Yd(q5`~>oxhhH+{;DZonna&8^)(j_CkKVy4)+Mi3rka&vyPa;Vilw+?eA z{Y3#WFz{T){y zl!W1$JgFQn;e_`h`Nd3A_HUVb?Zhh;3(AZ7?<&}L?@!0-wfoPy#>QRwkey2ZgRK`Mxyd@gWecUQIJv6}xd ze$|Xnxnl0xUixL-2dYg08YCRGT~HemCAB_@wW(002tg{KwW_Tzjl*@ZQ}jk?7H?P}4r~IMLYl zPx$_}vF7cgx9uzIAGTYMa|II{Ed&WKYxq;V>>k8aRn7=Td{rCS_{2ET-sHC5ojNq{>9O zQ7e5v(dd&@UWSxbtBHfc2C(y^B<@B1&h>CoE^$^|U1YxVvJ>{70|C?K`CoV5Kp+vJ zi@EOSzH>BT+LPTderU;B?}d8~enn#}a1iIIHZ^e(v;U>;#?wQ{`i8}zo9}LO$u_5r zH=Q>U!KRv{kLYImC&_*90SFO9Pz}hK2K7JO_)PatpX+@Cp{u-;O!KG-5Z-Tx2?J~0N#h=Jqg<)e%aT&2>yS+uV1R_|pO2-ajb*8s0>cx9^ zY6X_3XXL$qT-1afwG3lSkg(#8LQ z*s33)+TuSWiZGD_h+8gkaTD1IUo!b`hOR};*@Qi5_6t&+Dk{J31`8__gdvrNK$c*mik!9)l_jZ>NE^=hOISTug zt|24sV8F;=MJdtEqH+obGoD?&v&*CP;h)cCAYczd+K7HIVVP}@9;x8<5x+e4Z*lrQ z@*L8cIk|^jn~o+7n+ln)3)tvH@rM&2^ zqM~nB3g%3@c-e3g|HRNl9qkKPM_ePSgJ#3zP3x!`v+ zl;l#3+b3KgCjF^@EoH-7jMkaFG&s@TAa*(REDMB772XOx_}P)rUbC23hWk3_gV2)Q z^;_i~>E?$u;P`DxJLzJbL~wkx~7eM^(I@S z{R&to!+ExN?L#nU7D9ZD)w%^hZ4yJtWg#q#2kCSbYRpj-GXH*9Ev`olfn z;r&*X4TXkn2qpgNgF*O_xb1d$w#)7Ld36yhET7QVGfAJQ-DuTKX6G3G$$>r-diwfA zN51U-(Z?hqDya=L$kJ( zNxT^Fv?{W_X%=&gyZY23ROmSlmp=Bf@JLbydD=~(QZ-|(iX)02<#()}^^jAxgdeVr zWA6a^@e*)R5aApH=fWqOW5Ej{2ne*Wzmof|Qqxse{~)X=%UMIkUW~s+alT4OkvN)yJeLBnrRW_o zTV|AVAMh4GX8>~YAZGC_28X9m++kLoKSH-k=8qx;Ic{%xPj0i**Xgfb_7G3PMQ!A3 z7q^U#NcL7ggw!gxJKBGFY2fpX1m1+L6@+aWpHMC}G273xnGjlo$|=?9ia)P4y!S|! zET#x*S>q@2@ZQWTd6nB|3+@vAq~+ZDI?uObAtr*?ydxqP2IY4;DnGN?YN;o`;ac?V zFOAcfuMqZ6{6s4XWC_VDk+_Q1h4t@3G8G@#{PeBjX0SMagQ zscsuFaq_r)h_T)<@s^Q6yek8!-e}Jq1^xE8RNacDd@byI zxd8~Z$pUSy4SViG3NEiY9`A$}wOqJnz8rY*cQOS+yyr>q{9`bRe-Q!iO@FqkKzWdu1e?qY@_iJ>+!6@-nZA$Pgv{v5mB%&g=E&OJ zbvxBY8XQ~tQR$yhFcSYuo9JKen23b)mp-~nGL#=9f5)a<>-v1?h7o8{4m6+i5u@+( zLj3M056Lund_m(cpZrM43fl7=#+o*Ot>Czn&$+a7aYoEL@Z-UzkPSv2JTgjO1b@0T zezS$jf(2$S5?I6sTwb!p&~n5J{1y5liE2KSOkd|VwE*g^*tb0i{Ac=FlLim=FU`#* zlb>s5mX_eSEPgKyxMKuq5RrCtzaDjDk6iPaV?uEFJ+r+`Fw8~VJ?)5d;((Ef#OIsq z2dL+au_jbyI*RPKo@e6l7=^TYz7N=|XJ}DyN9j84Di|2W=Bsx{ynKC7nOEE@!ok6_ z>)h;E3U9`7=jUZlKk5QFZaF&XIekG(fxM<6asNoyN+w_kdWDL{cP@G347cbsTGMS} zbRcH=AA{NFmYEWW^a+DMi?)6D`-a@3yG3Km5U@D}F+z-Q}Um=}Yv7`# zh7x#q9MZD_V>&7mbS474eJ8itR7+>jL{HqFk>iWK*^L=uKX=Y~0e-6Zm^TLQiU5qZ z8jlW@&@pPjd?R)|Qkxwmd$j(%a{81$voZiOS8%3HQ(`b!RsxQUeeZqTM7sbyS#a=x zjn7)Y6A_CmMcc(1_kPNH>aED#=x7=9U7K`W8u7iZvsT{YON;T~ow}HC>HSmF;P0NQ zn2J`h;I~dG28;%)ZNUP?gQ!~Nj$WNowX`lKO8ftqo&cdW8ukU+ANa0ot)DTWNQL%ct<Q9U9$Gun?E#}W}c*XTq4pE zY+#;F@YvQpBG*Q~%n8^2e?i!`QLR$6aqa{md#_u7t=&z0>^@1y6frPW4%Mzb0P{eX$FdIDfYEt7r_<=6rZE+O)mhhR%ieE}Y(g~~l4J>Bf&{D1yA6o{3ER7O8y5ei zASWJqz`#W=0V5(bmjG%yARsSjEL?8Ly>?zRcA9{SlvMp~yO3LAWAJ6p3Cm2hMo&U< z9xSML^bCLeJ})R%Q>pCtb;F-cIK%Yg?Vr-%Tq_Uf^-@+sU&ik;*))tI6fG^X?$1Vj zlwBwpnq7_i@PJ2)zF^)u>16BWw)VifF#?VJr^8ZI`txy+rntaZ$`;VHDCO(d<~k0k zzcC&*w_Gs4IjB1+^F@r7aRyUVi-FD3{}7-n{vaI%y#(dmqXP)Ym`RIaacC?FZ&0`C zw9}D08rT-EtB;OrdcFRG#x8p;WJN8=!;&of=mEi?@$C`TdFcJHXp-;#x4^ z(5X2ip(I65n4^-ib+c@eI!3LqFsA``xx>XT=wMdjemP|<{060YXNJ+BPZwG1j)qH; z`LG!@J|D|zfcjvdb?wuYL>A)t>ulrj^(CU~tYCg09z-xOpg(FN{Hppc(5w=o3F(#=R`SK@4!=@3NOowWTGJ7u+RVshRA_w@V z;!4I+d`E)-I$n}SqK2m`rl`JRAko=}m|m?x?Zev>30Za>u1e51Y8RI_i}u=vHxV2O zbkUDxW|&-+PbbS{jITP~&wBS)fGhP%VclvAXrR7m^^_P?LfX}lnr+dt!klBa9^ks_ z=E0g?R;85gACjC^&F`V@{rH2AYVA4)9VJ~H+EH*cOhE<_ynR`kyxAXv52~@E@kz2t z;P|UwJWc=_C|vjV|9Bc)FojL{!d4ytk0iMvLE+MIEQzr9?Q`L#U>jQ|cs$cjszXSc zr!)EIlYXiuTgzQ!@Y^QR2jzlj(qdn!%^D^F(j{2kv0n4~=MH_qE=WP41GwP|%y6j- z&0oAvt0MDxrl&dzyNct1c%&e1N^TW;W`}^_U;A4^QfAjTH0+hkLviLHAVr`0kTOaYHMCvP#kp7}~E zSf^c4$c%i1zifOvSqU(e9%%;Hu5?1DXM?nJFZyZo%v>UY{9R)1#uI(SL8rXc6~KJP{3T}-C;z}K>=*0VEL=w5McM6G*}S} z7@hn9`q`j~{vC{DWN=8k1^L4BKRTrlMBLT$R)4@uL)KtS{BE{uEsV0UP8MbJlG(DTzql7E&QhhXX5 zh|fa-elic2!fg?s;r34>r9AeUrGHBR`RvSE6C=W2&fsjoF% z6E)9MtG+>kYS29dc^?Baih!vl1L+%|GyDKv0Y?&WZeQ38-CFlJp*db}KOqK-LXc4& zN{&88uL0~y5?OVPT&^12eG9x>LN$GxBcej`CjjF-1>=CHkij& zz>nE=H68qatrG2DjYabD?;S|{-f_JG%B}&LPQHqv@CYK|M=h-qWqK@$kPee2?|_Rk z9x8*59jHXsYuYa>x7LkKc1vdfE-KwpKlA?? zPj{Rirn$R!w%s%K-Iv?@xP3G1wk4?n17RAHDg|ToD;tgX5f08@!z@X!N5c%_LvFz+ ze@5MvFaYHPdqDJfmWo{MZ&#aHcD8!J#TPU8hhwDJDmWolGT-@wvgtIW@^6lv#qu^^ znZ-)g|XZoxOHULNBi@D;vQU~!Tc#Ri(!q9Jypx3URtfKcpm6Ag(cCk!hX4& z5ghv?>NVA^^({@kG`Qsm$~;4-IBdg|0j=p^H1prx{~5UL;+rZAJ>KgSE?|hBzyw6c z7r*gsW22uR+RrBeAy7*Y=^NRahmLkv_JZX|Xu4V3*#b}%_|u92SPkjD0Tjp40U4qhA z?(5yzwcg9b#ac7KNxDA`GP}7tg({2}S)-Mp_g?*d=T5c0t%LD?_QY6E5fb#AZ>Fuy zzX}`LXP=iy0;+HgZD?0Ap*dd*r1kQe%U|+FfKCQ!jXms?xYr5oYGAvj>*0tZA`eUk zc)fUh*pd##KP@6e(EmnI_lsRw0F$y}Ar7IQ-2#pkV%;a)WDODlZ?n10|F+WHq#!j) zdy|le;d$PY*UJ!ODdZ~%45E^L~X$BUl?7{+N&ra0dzpL#~ zqj$`e)1<4SJ?WrpL*&m>K`9{0pnYi#LMzm`I;<7L zu$Zypu7#a$kEYw1c`s+{ECCFV^YU&t(Fq5{_ zHU*$F7!KerY6$5u;sa?l{eD_K&)ZepWd5nF`UFpXz4z4e>iD_gJo1R0Tg^3Xn)kN< z_pCua6{1h#>rG-j*hUW24ZF@$sUT=C@IG(LHn$!(=>#}dHyH4gAF*e{w8;RVZaDfWjEV=s}7CT#N?LJ!bWtqpM~@%3fEarXv!FV4=5A6m zz0ul~Z8_KGbWVbHzdBh7+}=Uo;!9~ax(qMomSqBUn!PuDvAccej%CIpjsU*}g6mpm z``_yOp2rVchLItq!|{0HS)fmzyY@wKhZ-4d)n{845-hCk#zcQN>g+Mculw+2ak9~V z@w)anWtK?)(vS|9_%KOUK4sj{SJ7GrUcK5w#(Z^BGI9`>kB<*O ziVp|cY=4ugT@k+>hb?&FzuY8JX%6OR)Zb8?(7)VBjo-=;$}P6)Yt;rW_%2e;&Dp6@ z$g6^-FvyjY|MmjJdB4R|Wpgn3o$-VTABpi-%tMxcn7hUq*&-!ct)9j+{4KWH@bq=O zfZAno5edn+QOVcB-V)<1rRn22TK9uvX`qb%H#Cec_0qq_;}&?EM~0a*RHDrOqU0Mw zh4-?BiU~vzpo+0@P6w;-EVN$ZcxBR6qhMkY!4fJ~TW;Z)>>D|%E*_>yg>3KVETi_l zrD#pe_>>8#z0#Tn!1aa1(giHM%>Tm7-X6h?v)0c4k2yISHk2TRIU-yC^Cw#hP=Z63T}>< zd2(iqnKOG^P5UoxUOG6jtzTLE+6A7woVHBRmLhz}D>pO3Fujv~_xXQelL}?L!g9Nv z@->r~zf$*Z6&{zz)BMV!<~z#2*WakLP$dafVjuF} z*Bv=OJ@g%~bn@2`+pl?|xCA^J9^9hXx*N?rQLH+1thejx-Or|-YYt`39$th(A&XV! zG1v-yXd^oC+J4#UvJqwecGi_RYl?!>+qgp_Bfx&iW@KT`1+?A@upyL z*1|r`jy!`~@8IY@0av|OC{aTUOeX3{XUnto#iChb-9J-)Nj>vN2r$M>i!u6p%)^h> zFC4-3H09?Eh&)&D-AJXFu^5$Uv6xxb(Rn3}!c|`!oZbZ$SeW`;4txZJfEAf3CZvXC z`5?uCef-Ga`k@u)9tIOAV-|= zCcwdbm4F{3N=7DLIS0@W)7^$XEt-1Q8D02*fvcGz$y|?B0rAd5g2l8JT+ZFYr$&n% zndd6M)>PW?BMs1M=F-+-t4_0y{(~Yg=oLzIQI{LpS(DDaOS8VT_2tUJ#r&L9qw4L6NZw=*`A9|sm=)ek_Y`t*+I-HiJ?03sAf^9$Gwj`hh+qcCWVbIm+?xveiY%C7;|_+uWO59IYxhwgcA78s37jgQ1A9 z(`Q~gh1d=-r_N;#QY9*=8=+8!YJzPiMe4?QLF`b3n*QyG)S6R_H85#9C~Z~rslxAKHn1G*~4KV9`= z@dX%(a_H9-IkgY8`y&rJxOpUOwqqwQhOj*Qc)k13&Sc54`d9ZV_OYL8&Igl4(0Pm* zTWPQX+{WSw^n7nzo@{xBZ>oiG6|V4b5)(J3Ik_X~OJdTTd zQr5sCM0*_e9DGgpp)Xh;p*#mtyAV zcu{Et7RH+Cg&z2~KPoHBeE2XkYxef^895C=C&02lH3cF0)!S0~x}{4f#iv^*s8dEq`sye?iAbZ(c=vOeE{miKM0#m8m4hDao=fMaEF1f-EesoU(H+fXO12sWx69!5RkL*8R2 zM#zDXu4CIj*-SD}v28h|Ycdq{=>5iZnj*iDQWJ}IRH|mZn%q6`N{h=~y1y`>JxCn7 zK_gIzc^IF$B3rmD1qLnd{p4N~mAR&h>tZWdTm>2MKj&ceBPuE&^|^h&+Lf4A0??(W^lWyM54k;jOU%sp&|UOTuq)w~iaT78OMIr?7-xRA0yN9%)Z>*) z+BPWq-6HG{Qs@Rrpik;mq?#K%pEH^cc(te7F_U!Z@k^0+yMXwoP&LzbDvj0;eO@$a=V^5|D5Mg z1f|mjU^65rdN8o;zc1tNM;E zz^~bWx;@lz4r43WNWT6???2-w1S~yAit$cq8pD6TsaF!vQJ$qc8;v|(UD5Y*u4pcB z1+mZqN$Ke!9By*{f@L`(Js(#!1Nis|g+)KZ^BaYY!v4DmbH6Wu-pM9Lp=J#d7kpN7?MtI0O z(BI*Hxy`|#!Rl2lFqpR0rc@sVLi!?d^15cEXd%v1pH>nGCW?npkRNYC`6YrwJRh5o zS{qDDCz=c;qtT8=uEGNorZ&~)V$UhP8e*!$QQOS!ioJYHagWb%4_>`dCD0WuPJp3d z^dF98uHVX#ap#Db@Skc^0&T4P_W$BwV_dsq1K?ZR8rN3I@BciPj=|4P+)usCZ8UK|ldBB|Of7SiF8RL>ta+p7WI@&d zPcBT%!Qin-L~4QCCkQ-pepEkt3?)LF2l}_RdT2E|ma6?Vx&ETA(6%A+S_ACSFn{!z zObeWK8;zaUX)_HJ!L_(xjo7Rj`@M977zH^g6Tr`;q=-(YH@VZ;4v2&+>2DbjISbzk z;s*(g4$y>d8YcYPe#xi%H{9;>gx3O7-z)lqg%j~LIYYj_af^qTO@fpZ`J(wQ@Puty zjCMQ^udK0Wf2A`EvqjxWl5Tc7lGeNX(OPN4kdtk7lv2!n7*7M7;67iP|AtkTSAnp~ z<16Em#vb(HwDBQX^qa{{uJnI|K7`HDu$KNXQZ;b(;dhvIj$uDVX_+!po#3Q z@q~fNIeHK;a?K}%=*Kye(2&Zf2Oq2O?JOTgu~&n3Cy(T_c~x0lE+8IILL-Hay*cZ| zKR)X15MA5+UPiDuNfXEw;-otfTqiIs0JKMCvAn15&2gB|BIpy9gPoP)i}?xL(USk% zsjv zDNq;mm%q756_rV*0%Ipcna%;@LQH4t&l&jO;`5>q-=~G67S&}|`!UNjqBm`yOWf9N z^`F?|?k1UHn50vA5|IC(p#%s(Hw?o*l-*HV2=xq z)$`lWPwH0^Wo`Zp*cK{=fUXJ+b_?JC29V4}x67DGD%0^-dyj6W+1bX+rBH$UkWN%vsz%{$=Xt;wk01SJ-J_?9VB}BYP**zjO}9_l|w_ z^JgY{j~v-#ytU(Cl-+cJq7qTzo3=9k)*{0!pvMWE+b4Mhkd}as;;oN?zIp{P&V+$& z&!je=Q0NOpR;)oWFu-)l9Xv3Qv<^#wg}_y*%lKEb26)M>l)EYt!dzP!Kn3@kx^n6p zU3tVraQknlwN|XDMPI3b>v_01C|E0HjCpP)BCu5^ea$#p8;Y;?Z9xAKWe0G*A z92iPPR;E8Vl`Z5}z(7G==nj~%##d%x8Dj7|6%XAx%XPh4vk(YD5xpvES<#Q4dfrn`3vhCI7VqtOdjiKK4 ztaV?H8u#+Oh-PNA@2pQ>nOLnd^oMsrNXr=$fgz-?c6l&{u_x0FU^(r8YN`A@UMOBt z65%@~uIIZ5A{2kLUk4@=FI!2lqlx|ggtOCzW7$O-q(&@<%N4Jj&m17oG%va*)rkF~ z40N;+_g(pBDn95>Ar|xbufzeC3!Qzv3}Wu;C;6BA(Z(<{dhTz8sFu>DsW35I=2%(+ zy0}_$kLjjHUux`S38Fpd9|yAH(@b;aQ0mLG&5eyKe3T3f6znJsxbOZzyb93i&pdinA8?7_#! zc)}E%yf|Xe^yiLMR#ETN{e07fnEkLYBEo)UG=sf)!EFpd6b(HY%6+f`1>LVl$rb}{nayD{AD>8x#J3&tMvjj%`=?W%R(IrblE)-A;|pz^(Y`K{cN(v z6g0hf`o_b*8cV{NaFQd^fhoJ|F0p$mKs16eCkdGwwl3Y7Ccqc9cOu5xM9u%Ls7l|Edg+^kSGW=x z1vJ(DnN?z6@>QCvpM)pK3kN5CyzWI;yn-H6%{$IU?-1^pKrcNEEn9q+%}IN;!TJlZ@VpDmSfw)$H! z4xex|2=3Q}p&&IVqW}K>sk!hXl&GqKo{q(*<7Wc)TA> zFdA#AK_&w^`*rWK?&BK>vQUqIoR6f6+-p}*=y2MF6}8;VGmDA2g&_7CMnRabo*##V z&0HPt4Fgt^ks#XI{xk3?yZ)S-shO6MO8Ua_u$fQkTsOTk88BAWNg;+gw0|*=3N4zs zPN=S)P&yvAAEWcZnVT50lVK&Q0%o-&p1^HBQ^1wQ0CU?>+#h@AY_Vg?9W9bX&}jq zpcfqTMm`@^R%*G-MlyxhX#r6J6``r@<}L(95WPr<5PW&p`<7pb@rAlrSy)%R9k?I8&NV9m!pM^peX77 z*_=m5aITM0B5{UAhwdvTUGWDYA15`yuiYRqya$u%1y5FD>aQ|TTc24&gGwM^`hfEw zoY7X2oZj{*?VsatbP-5#cRtC)Ou@~i)Qn*_ayL0o3C3rz{Y@Z53HE2g7&R5v=fP}u z_Erz5NmQ@@gYU?VN`M02`v!X#l!KCTq>3`Jgn0AvOPGrehLK=UpFKt@e^CnPazC9@ zWs7amZ$nqyreM6*>~HK1Itme>L&|5z|LpOdSlVQ8*8%1HZEU_Dk4#NP=XN5+3esO8 z>C$hCERog$@nfbf<1k8rRjvvxSJI&WX4MWX9O}uL39t?vtUI5ye#c$i@I7RU;t)$ZT8eSj)9|DH0p_AQk17b3wZ7=No8iW?9V7Oel6pb(3(AY%9&p{006;G^0F&+Q@B}iBRq2}bG(C7J{P6w9Fo&Y#%M#zeIh^7JH@7tMq?&F|+dyWcOiZ%)lanIJV zE>It@Twd1SnxB8Qv2um$f@-9c%WzoVoh?|xju+z5Xre?~ew!I*dW0^a7qsL= z5F71$nS**ZjNlY%Mhm3x$#+WGY%9$t+Tu8!hnWSCaJE58QVFGB-M&&g9!vp0lk)zt zVWY-nC2A!SO;oc=VqCxGI`(Fa6^VkMNohz0=!9G;U#fOrMV`v1T{4NAl!O@1eq~s^ zJJ4TzaIt*5Xg=Armt-Dtb633s9igZyDU@u1y)%H_q82lb#Nr4Qq3G!0$DcW2c_8eC zB2~Xrp;h7eqG>i`6!s2JDAyDY(z^ST3B}SMW;+at>?E=v6;fIk`AVXC|7v@bB@s}k zA4z0K(N+#bmci9zmRDAC;O6)k>Vmy$O#F#Rw`UZR z+bMF!nkQd*eDDbV!$mm07t>l#(OG;j25;@#kLzxED`g4fa=tr*j@0$sqn{D>?oN0UgPGO~87Cql6YsIG9-%b1qA*^YgWf3_r5&A*B<3$&G3q;= z_rZc%pHGPGC(@dqe14Z5C({>V1Wk|7#4>fbah0nkx9WR{jKox-CH;ciM|3o>{xRTj z`w8heTq;lnB}cWY-AhRLM9o~g^pQ4NGEsLQ(?^K)v%ImZYfZ?c2Q?nacP&yM+I zZr(H@_r8(-w1b4k&_zp4%fMs`7r8Wp=SjqcRg$Zd-}6>Pp-(MibT>@}7B+AO?ag4L zhaLf229-Au=NmEjK1d;dWpHsmzAHSzK~yQ~JWGSuly-?xX-LQ(^esj*SMC((sOFqb zg$ryl0x7g0Eha-upItA}=f_tuJ|3Pp;&=B!0cs094*{0~6Vf?-N*coqC{Ch~e3ZZ` zoq3pB!Z1`LzNmLy=lNnOi%5@=KWp`^7s8o#yUF~?UQqQTJNLa41c-n)K%`hR%psm5 z^4vpir+aS`G|y-@$gXa{#*cNhOn?<)l)lJB&s^T}<$+Jsa-_OTH!U6c0Io12THM;) zzcLX%6P8}5Au|qQYS?dSaU)mdz04Ss+{mz^Tjqo@+eum~&zjD%PO8#-!z2buaKmwg z>5diatGj0{ZNkC%*!O+>(FxUu#|)42xOd4mXH=VEdk$T!zdzydUlresk?=KfxK7|J2$KB2cxbK@3&9FlONo{xk2sg--vZRT3f46 zh5Hw$!%jdSCcfum4f&v#HJcSGDP^~=-$JDV$Dn&3V{BK~Zfn_zUax3gt{>ll2l2;k z8qsuVQu#p|&+sq59|=y1^|y1Cm{6}Q5TGB@bTkJ0We8&zy$*V%qBtQ~nBEbQ*oi|` z+j%Zmq$5}9X~$Wb;QAeDOG}UlalBM08A9Sr759oi2d^5P^Zu=KV2Gj{-V1o>qLh8_ z`1sfv1>qfVo#}oo1iD2P)MxXr`1>*E44E{$G}@e6)1b+y)+4B}T{3U@j2FzERLEz3 zX0&C97Iw55K`tKPXO`^*e2#abtS(}^+b|wC9b!i6#AKOOCiGRN%0_C4G-dL#bx;u5 z15MbUf4Ou7UcnUd@_fh%6Z}3(F5pCxj4)>c^5XS{D;_@IKkp=M{k1BQD;I+HpZ8w>5JQ2csSv1s;87s@GQ`R$Nt-Tc zF3njDH^X%35=#0CQzsXuzVq}^_3_L$&jW6gMPCS{kDwCIx&xLUx*&* z(`evvnDOJLL@jsOi~jj{ywup!HR-`h$O#BS6ueF6bcp(Op0@O{Gz))st+9rNej0xU z{}6F9(<38O+qobxaPIsC)?G(C9#7o_5_nBmuw~P#BLHnjl;SdJyc$5DAO7#A6QwSr ziwE$7%VIP0mGau)BY@9@#Q>&f-H?j~cBe|F0k951Xur~xBrAemIv`3s%hs35PQ+dA zWPrlzgimmgEp)3`J#7!P)G@jF^rQcku9fcF40pOJa zHVH`uWd}*0{=OhxW#$UTsEktvBHQt@7iOk1puaVi_r2pP-wbs~4S!QXJszHD*t_X^ zr`?&gS@t`#vb#I_b}WkXBPzt3%Ap7};*D4(97hP+T{%Nbjxv*Fk7d{Ndm|eO;9Z0! zihnVh8BgD-KgQh{ith;O3`y+l0gkpDB9ejInVOR%pXmFxcH8w_Z(m8t3V7m^&o)vn ze)4JJB1HNOa?uMF1GN`9(Rvd*_&q2=B5=PoH7ek{F72gSgXSJ|E1bnTHm z@D_<%c%hxCPiu?9YEtQ=wcno9fj9f9dRUk?KQyWVozT^@16l2B!o6HUgSx!{Y9`0jN`GSt1A}W&4p&WyV!|+k_rg?ty_)3>?>dW zt8-Ph4_gN^+x16Kpy%;%;;R&JMt?_KpX6O< z>rJLuAz9AG!WGXrpu`xf@ey3JFUaLYQ6VZ2xy|UsrZ6#tIw6q=Zbl1$&*1=x;dx7V`h;bJ>5Pya*Kh&Wae-S&N-8KJyCwC@Wzx6VvplPrgHP42$;7a zn2Zx70#~qC9#T~MxPOIEc_8yDlH*S?WqShBEk&bUt!87U2#dtaZI#6B8 zYrRUv^h8F!Mi-SBpn3`QO1`E`J;~`_8XjlVpjE$JoVIL0H~t5zJz$$igbN(2MVP7M zeVCc7&P#QRt0gyL#stu5D_vgg^W7IPg3hf^rTIAw72jT*vsVW0AKtrnh{vLdzUQtM zdFh_*ZGU}D{2?J76tUloe+u_bYow|kpeLfWdQ56qg1(81JU<1#KBZ>bliV@4Z||UW zP-w#HFBDhpwK|Sg3(KI5n7QvtS|f3sQT?Hfy#0vCN(d2X8t5qeC zq;0vE!jF-!-1vztpCk2#>U3ax4xs%!a_PpE%=_6_#qPK&r$;wV$HO~dA3eaidB48< zh;@42@C5^Fg)2KkXK8NKKU+Gn*`1s>Q*yglwDGJPO^UJ3UlL7{?RcbN)i{{j;f*H- zB|O79znSsF&Ge=tsd{L^B!;D$PRg>BombR-ol2A}STg@dl^L4J?3Tyl9#+t|kKw(G z5x_^yussS5^am%jM{8N|_htr+p#H{!RKLT+p6>SMu_j5Fdr9xbn-6~pX01`(OFhi@ zkC0^ICZEK7wqc@;`qFT6cI`SpB0Za&E{l2AkA7T`($VH@<1yQtsf|C4iW-ToL=XJC zFz0rXmU9RWz?{WO>Nrf*Ch+B$btn7G{wIAMc1hTJNmsKQ%Q2RP>W4O!;9O#vx?dk# zIv{ckhVv)MgYY(|E!A}0JQ@UiEOBFpB*56%iAV%OWQ>AW@#m1MP($I@C*(A`nIoNk zI(f>RBck{dwX3o{RRh?N7c{=2CCqa(zdAZr&brj3cl(DXs|(w`Hp(dyBtn&=Dl8{A z|H1{JEh?W`vH?5ygqoynitTyDMZ7!e*}!l=Exy4=X z(|>f?_hEQ?HE?eb9e8u>3I?nqz?Hha=D@OiZa$2#kcDF4G|xAz2NdaJ^io8ZHWcwCU+tgv~KW4t9+sYzrbJPqTt4$2xY` z+GdXVas%cP$sVOxmndRfDs`y>2l8TQ*Sdm5Hy=jzU2vsSLCm*r;n<3OsXh7Vur}3% zP@Sx1y+L>) zLj?sz@x)m73;-_*z)zxE8a}yz!Qz_d4k9Cd+_U8m9|AoZnwvO$;xZiX*EcBk&n1Rv za1%9r?hQ1LkIEy;UmEz-7Hrfs9fB@tmcXfaon-`=e29dP^wn=(Ou;}v7#rq(k~XpW zyt^GA)F{zYqrBPqRha3OlQLmy_9VpfO5OM;>G2@RYPyb!oSj>=jNSLkbD2bSc+n}6 zsEf7xzqe4i9=UmA<{g00G|lj`?p4OMP{2j z4{ddz&62a3Y>L*!2juNuqxG*F+N7RifjhHpp@j-y6WVD(!aSY4FE6wgwGxlcr0+rU zDGYQ-02u`b(0LSHx2Gs)FIVZTa|J3^E(!+c5cvwk4?}$$i6<@HzGPsaU(j@0A|~+i z4JlG1t3Bz6L(Go;$8lKxqd9bRnB*qMeKWI2F|+fAOxASMdDqf()9}j_7oT#(rGtyYLAc4_?tAEP$xrMyE?n+#ZOfo zm;7@i-)f|buMuqdN@fbDd^1cH*kBGlz zZfGqMSXHTAtOS_?2Pwi0=?*_W@qBV`lv_LFYPCNGWI1sV)gAtho{AREbu7FT!a}Tp zQ)c|jDZrX0gLxKwa#*{Xo<5d*ak`0nKB8F3+((GJ&Wy?*;-cU#E#si3-stXyRBmkB z^H&=F9b3)z68esA{f$d>vh%vM?FNMr3#X_QO_a9z0L}*sNq5j>>G28`#IonGbWO;y zxb(I(wqM=u6$4BiR(r}jBlRWXuQZviP|&|wnclvO+@{0oR3&m(Vpr{1>_f>+da3ov7P<3CKu~$J0_R+Sswnp}17#pyFj9DY8c)r33lh z{z%UGHoT?#Hfj9n3RW}wt?ZIP&&MS(tQ)vs^JAk|hO>E7Ow80?L|u1eZrUk8Kg+kV zg8_0D>0q;HBLI>=19-m;PlL$RfPVj9?pwmv?f`1mOYNh%e``2IER@YiI=O1#-5}#v z+R-CMbTrW;)7;{#;pv#IYvezccB*PBUmtYeR_~M(AkZUmBQxeccw=bF{l0{09t(&%NV59 z;|Q@q(IMrn13*peEuKPlnq@%18rvnbGq}@vS_A4VMzDr2j3uEmZ>@b(4&4XKZMn=Q zDN>9e5yVp2P;c&vG}l{k^?>PtdE22Hd;7^}2#g@g04J0ZD0iSigbt4?tCl7lXxi8{ zOLL2#r9a*1B5tLszwJ_WEcl2qUR%I+T(UEqF;QkH#HCDV$m0p$Rt7AR88?(6)e>=t zh*`Z_6|Gs0Xn|7J1{pE%-Cj~r(D#sU<%2%~8gyKe`cKnxLeF-8zNOjQ4$M5A? zHgpN2u7o8jU(p_48X#mySb3L+IR(z+=%ND9M5(mPx@a^8^%LTVaY50c&6OJuBkVx8 zwj(REN+A_$oHvgLuk=Rx*G^ZdvY@sT#W5MRvlOj3oZ<)jT>_Ep7X7Jd4C-fp7c%g# z=c{6f^rg-=7%O(DK*m5Rfs{5&aeh(+Q!HtiWF#fH%dI_d?hFD*q}&WwGdt;M_Q#&! zH&tV-p`Dt=lLBA0O?nmlTN%_S9p7#e}eu)!@jh0NWs4`dxPjy{i0D3i-MzM0b?nv@}0@DL@45U8Uxclen&{RK4Lw5Thtse`4|!zx@WwV|Nzr zL+eps2+?FUS#IneBgtGRR5auR?Pbp!6tHUXoMZfqdY|~2fSOCsP%V_b`$@SknEopD z@(+2gio&~~5+@D^?5Qfw(~V{|-EB3kn5E?=L_mN98M9ERD+&ddrV){IIh)5;$qn#r-8*R{7OO^m^<3$DL>` zm7Lz`&xY4UKucgt{dJi+)TX%Jj)$I{kXEkb^MmQj3YoHL?~>f*V5i3OeXP=&r1s^0 zGrST0d>c3M;%UO%?LjKP^%+5)f{d%ejNB*`=mAgkR!g7e0ezDnRijcR;6$AWLOC_A)<+=4Z9Q}JC|(yM|A1135)V23bw=L z|3lMPz{K@8T`yAHDNu^LyB23D?(R_Bp}4yhcXxLy#jUuz6?b>1=y&`6pD(%DG%4BK z{mIOkGiPRANHXmLG}&pvaBI<#38wm@a7(#(^(h3}?lL-6U57b1-U}hejjdW}nJiL3n$-1o59?Cc=|30j~F2@f9;&rX^Ti=$imY{R(zbTX%dB2MP zx>>?;_I3Q~)|rbzlBliNdHHfw701Z)Z)6}AOwpZxuIT2|{P#}@Jdm^WY(gl$wmn%i=?hkSgLg9Mp> zVS^euS$&YwW<^kCR7<}@{ygiPaFCfcG29=#hxEjl4 zVNKa+LhF0mD%En`fi6=9+HlePdG=CLacdg%(>r~UnPk-1JU$p;XnDKXzp%aQ>CBuR z;$Txg$+=)Nx?s1}l#Xm%Z2vYf@H{wn|I(Qi(d>Rw{Uod~c;CxYr}brOpt7bCt;0J- zLh8j?_~#>D)#E2RjEkKlQ6xY}tm6J)md>LP!+?$uZXX~N2U{d+Q7$a)W-WlE3o4bS z|LkokH09P69cokn2q1=QDuv+o5>qBod0RMNM*2_)N}Q1v%+zb*?N+~Iwce*ZzA2V@ z@B~csfqi#>!5hQ_o)C_tP@znYKWxS6#6!tIcwQJKkY%8xH*;cb?5_u~`~vXUy#C9y zz6oS=T%j?TAfXjaT=gy`QNE-KqTLR)vouYb|4W{PJWcF*+?Bik(!EQjT^LXhMHV86 z6>JrvE|dX?y!uq6feFDyrbmO2CyUjc$82wL@{+>sf9Ds%Gh^b*ei=P*X*zux*k#so1ya32pLyk%Z(pC}1kx+rxB2^XHdKuT z0>@_n0{J+-=P_6sNLt(Gl^ne*(_>V_cWrli9Q8PsAVMeYiv9UUi*UuyMteapO%8`l zPmTGuD-wWa4%H&%e5)9_F)*(jLsTh+rsj;TJ8OqV4%a|L?kr>jAI|<@=}~-X51iOG zp_pT5f1HB7{v@{v>Am%|q-_L1Cv8x=C?ogm&k@szCFS%YcgOF{1OZ+X%Wz>WYxfic9A zFad-*rh>Mra}W%6EE)8_#dPHf4Cz=gm!>CEyTHNKSCWEGhc*~`$V3qlZ0#O<`z~o3 zSH9eBtH<$+`~%2~=6q-`vpWLq^tj+zEatPNp&_6BvQF4nEj2wB^T!|tJar_3k5Vo= zvT^`IfXFej9s-U_E<>vai`+}$`-;JIZ=d|ZN@XMsV0A^>yBuOgij;niG{c3h^P9`~ zdhfRP3{R}~uPd!Va`BPI6T<~5zzW(iKGwN>+6iBm)Ayg4x3h;X zy>U78f|mWj!n%~T47#OeYGLG%km`9r;$`*gUT!~Mw7@OFhurhJ!`tCE{~fQ_7{TZ@ zAY0(9NCK9iPXW{gcawo_y~}R$sHdlvw!$u{fWk~@@eMEWQq{p`Is8uiNt4jO@rG!=>+TD8|hHdIE7l0t?!pstD%{&_h|>CI-44hbcxI z84f(}D(l$+UxnT@W_`)4M3-x$6_45AcIA#K_`==RiM>((FL zs2Ce7X~_5|&8%*)l}pwyRFne!5;+IPW^c@#dap{06aE+3M+Z^` z-{5?yS_4phw0eQ;nSm2!fejoWXtCK^2s7A|;_XyP#jW`YN3o{<@}<%6P$|0uXRHxI zf$31?nA&c`Zn>c=IklGa#Zt$G5-d@y0#J``#QS+U9Xr6FSAdnQ6N?j}n;eW`!ZPJw zOhi4-Pz+(%S@P)F`w&Y5HX3YLO)QgAI1 z2}meq40ovIO{3bY>m&0(eC+g8{&0Jvx4`n$yF;bdmR{aZ4`h;bu1`vzfnl$KoO)H& z*@8URZ5CmY!*F@PGMspH2HpKB{-V!V{U|<7M;h^L412MItw}Y%+MWjRe_vG~G+uo+Y4h zm(%w>OjII$9z`J>EdGrT54sBsFaTsB9xd*CGV;3Kuh(b0>c=~%v&!P9(s#AioMg2( z-)b72%2AkPJ?*u9TpA?S&f!RQm*ZFt%^0<7Sat>cqdiiyTmPy(&iCOQqo7&diw!74 z1q?j>tv9dz5QFlM%*BEf4=@CzFQ7vVPsigap@lf@8CO&cIDQUMJpG~l#@t5d>bJc| zTJZ)78LzX~5OvEph9iPC19E5(h@-x#zQtwh&s^FKlWvPYTC4ks?ZdhV>eA*CgBg3_-V$d&50HHLlN&;8A9ItDMupPL(1NSe0+tBg&yUMNqUPcgz^oOUh zRGo&Z+9rjMJ1^~LE^v*9GouS~yQqmn!9}g9m)0j)88JUu*ZI%q$4mjgKgD@gT1zVS zAPhtT;W9F%>=YM2%%IhwL4LN6_RI(8nC=U(71IR?AITO}G;kG{+h!P$X;ctssRwuB zlW5$DKBAHap|MUERDK=>RL&3}q0lS0RVdF!o4<|^*Yg?Q@#Q7Q4?nZs)>L&pARo!O zm`CXV$@~NYU?6}otEaCzCswqBv(^p{>Qg)#bLkjHhB9ZMB6_;@_Mq~`Eb60116$aK3wBj}y2PC1@H>6s1Vv@3vtK%zxG*Q;!0 zl??~U&qLMrOH_wrahdf8%Qy6t@z4rbb8~rzpZB6`f}e42&Wi&9;OJMfh^p(+oSGi@ z39l6OpC7o^i;jiY*8+hEE;pNRuhm!(#j{wa`L)U4%8w`s@m9FQ7CS%CBfxzZEI$<2x#hst5(Vi z^KHLb&$6@;Wq9;Rc;StDKxE!M)Hu+)O#9_@1U-dy?RtNcUk~uKpeaje|7vT@|AcQO z#*+OmF$bPJURMsdyllr;qEFq~cg?=2AP~D0!&>@Ay9aG)Lf6w#Ki!tU?$e-vhm@^K zlo9|n5~{wy;4Olb(r^{Dmb$p6MrZ~Wb1*81h`zXpy?)!h^q*gEKFfS`KKIO8&!R88 z-#6Y@-&Hh#D_~x}BLNmh1i#~`9_6U%xVS~1T1Neqziu0VWcg2eupmE=?!sxY=STk3 z`pebbBkzqe0vS4*x4%7_O{!gEU%X#5ZBbf;)!bhd>b!MK0OD^oj>n{gkQ`1}WQ37P z+x;wlu)Ih)n$V3)_|WR$a&qV*n*pc;Vq7B;{_{hafTT33oCi#cthhA`4X}dT(G#zX z9!|ouUlYx~;@11yEoFMp``KStJ4vS&mTu2Bomx|Jwjx?-5pm+Ei2@z5)mhl51ntci zXZB8jw!89ZUjf?xFn79(L6_O$h5Dz672j7w3t@ZFP|k56FAicarn6eN-D(rR*G;)z zN|Zyfn8eT{4f|2Kb2pOWcR!(l5_Y22lP30dTcwGz}!iWR3<_6YJ~i?`K=h;G~VXqT2FP2}s0EA!*T$>IdDQE8c~^vr4yYB|c8v9P^^{Xefj_ zld_4Ij!3JSbbO1_6*oi@5_b<~w&63UN)B_-qeTao#e7`ptT+_RIa$ZmiU&=7r@y@g z#GYxhEzfBsG4JhoimJko+{lZg)D0=EG&&eitnEx8RD`MxO_%V%k>K6qFp>)Y;E_> z#ef_8JI!hu#$$ED=NzD=Kwvn2S=DGQZ5s6j=9^rMJ!lWyFB*7iKYjpGwOh#f&;tb; z^9@vw&O%#A27xv`uNSU@DkkayWFGIztn}s7JUJhIonQc9t*%p*(au~R_P}Kg?j80T zT7e`ki5X1v=U!{gXirl!zuqM|xkf?*k|DZDyAgm; zAPqvQqIQ%AXhcx1GcDrPt0Q#qQ92D(DCcdQl^Z=_WP3LidjpGz^0T-9eeCd_w9l(I z0HcmeWGEfS!*jZxT|Tua9 zM~RL4B79{J>9*q2@EozAs|2yix;UlJRR)- z4jl8^VS|4nf7-LbiD-JEL^~q+E{>i?MmbB1aYS~8-q6&cR*#K){ng+>SfL98@j7Op zA3r*Z_9MbNzwN~0bFuC-@Y?}sY23eHet7-~-XkhNE3bWtlTiKbz#UNYguEEf`xAXg z3Qm8%Ocss=^cU>rYrw=bt-#@LmAVb&rOL3f8;WVx6ER(@&A2TVuXY1sQ?xf$g{Iid zhjXUbV!$!@tqsM){K3$0o@F|eVC^01gNP%HgM>UJT)HMKjb7)&Fy^aiaBt;Bkk&wBJ{@oU(tczfM0(v4AXw&@c^7Q zSvh?VIyi=LDBkE}pwBjsN3WC^XlM(-wE#`t6%s_~{n3$`^VmJxw5bOucOP%+;o3lW zsCRt7YGY|N8U&~%Qg?J^4W{O4^&(=LPyh8&F{uEvn_~`P*^pm!PON2_8 zgm+Mpd3B`N`?};vna@?gZHA~?4r&Cf zW@j!3wQ#QZfpJRHk3FSv@^)wG<=lQ9WnPUQq~lL=Xw$eBW;;2VUe~1>w|W^5Pqz7p zv5YZsx&Jo*Y}_?pNem6T=NmgBh!`)rVAS^qQFdZ^j?$6&Z^&wOIQ9o%{+-RBVV#eD zOQCqv=21Nk7B)5}blpu(U=tb9`D$l%QXAgV^ijyKQ!s6kp%Hvr7>MC+kVtz}55Gr` zpy}Z=+8@`23Vc-`T*gE+9c)tr3H!!;mpenJd*eeZ*$vTlcJ(y;QQXELAmwhvlb)m$`1;AIKp-+QU+HY}q#GznXHPaRo2W)F5_DG zKzAJtG{|ov5LLkcx^MUcjUd1@8n50$F}7ZJ_C*NOzwX%RBEE#eWiAnZjtcPDnhxHY z3URT`dhECP0xo6OZZI=7ImsSd-*Ein!Q2K3N-O;+RVy9wEd`r`bUKWxZg9tN=O^1d zlOz@bQ2X?w_iSUtO5ud_4MmQq(S1;r`Z$sO#ZFT7Z*B{w^QB?7iKETy1QgGqrtL)9 z<6uelfJtJqisvu@k2UZv&~3c&BV{GRkPp#PIX^V?xKYuR#**^Ti-SP1B|qT=x%Fco zq-HJFhSGUF2qycm2Y@k%xkiyuNT{c4_ZN9Pf=s$(7-3ildlz_(YaK)lCOcBFp4vYP zX7vz)S$Vq(J^iQvjz*zASXzxHi8e?fWi$HXoU^^vk><3$_)V;&^e#;%LSyznkGd!nArqA9|>|Ys(i{+ z=$G-F?d<0GMNmpgDxN{|H4}{0GM|x25;F?<+Qd{ic!i7d04=?+(UMAP7_2i%9r3&M z%Z{@Ajvfw{n&)upDDs^-e|}X=O%@ak!Taw6e>%)ktS8>KUTR}#@b@DKP%PPV@bwfp zuNE4p*SIT;(&{}p>=lIvft==36?+39UrQ!ZiLfw3(OMsfDRUjWJ_yR>u-Nc@{J22P z&;}=xT7^~l5oY;tJLz)8^_(oooc;K^2yqDIpnfv|qrW0F6GAYa0M7iS9h^FGc7($f zk+YFV76je@xFyF{}`q_BSR9mD8> zen6ul7tP2pvZjiYNzE#oz5%GtzM(gz(aH1#YLzWpU9R4gv|#Pc>&=_hi>&g&xOsHD z1a8v(-h-b*xXFqlB~J?K#0{$hfzqqZwyxPKz9OU*2a2zBjsY*A!UI%B0oaOxSNr6q z1hsPGm-kLGfmD$cZ4#q)%ht?1-U?@xgS`tmFHcoMyyiEcEMjp#oqkGXc=x)ft4dWc zo7tr>qXMzz%8Szd_Di7>tJiY^@S8D!+M}&e_bN}Ci5eVL>}|NbLeKCjc{-1`nAvI~ zOetZ|6k+u~Hey>S{ow)~E!mmO%Z}q@&c3QjseU~5u6hDSbw0F~cB^hny`#yEou}tL zt`Rg1-sSvP2hF~ULb@<`bv-^NZ1{?f@)cXR zB#488inzhwSs+2|)cE3Cz7ZFM@fgrxID=0JgoEd-{<1DW(HiI&NWSYn?u_a2w~b}q zDECB=2Pw#$CdjS9Oyux-@4r({5fj~qD3y(j(4pKr15`4|vi2yZOL>+R9V`|CgSbCV z&YL+&5rI;m#XW7kIf=lIzX)pewtA&o>9fZ%hW0N6lUi3DF3GyT6MYd|xJ-ie!|rAt zn_QosT)%qo3B*LamLKRNXnmkYT~xuQEerp+lR`7VRMUy%xMn>)I$n^H!F%fiox@b{ zi+tO821ve>=}`?Qj#$?C{8`8LTlSkoD`n_}7o}Ap34*rMdI5-^UNm*I!4w2QqHtW| zgv|+HasE&DXTa>Kfq~K4*Zm~rjK;SP`x}%NFiFC2a?QHic~4SRhMIeGTRcIQn#3fH z(pIxB8!%$-mmHAXiPCl{C3^q0=?Ue{@38Y%Ca4*FMH;_m2J?>$2n9d@{caM!&hUoYl_jr1F zqdN>?zAX3r-)>`1{!mJ+2E)OJlK`>2IX93i7srMlxn@@7Zy z7!_z$;qrciHtGsO69}dc;`?=kq`uVi1GL$0t(9y>qdL=d?(GCbn3TVY)JJ!PbTpC+ zwv?4sRK`1ydBisTd@%BMdGIJDX#|KV!$%N>UZ_7spYPxAGVmzJSRB*ZRaH5SXzgM; z7RZvau{)SrDiSqI!PXnMCSIEDqrk%c+Ai0A$3w$Pp-Gw=Y3 z6o>!{!%})f=SEg78Sx7kBm@=K#VFV&Rv2r^N$hl6jQSomI31dY0q_?Nw$+S_OoY7O zA9p9E)g$^bg262=J2_O7neX0v@zq&S!_m>~v{>13+N;xeQndaHf1UfEb`#ly&~heM z8r?ZWU5kbHrp?NV8`(oS0lb{f$b)&p>VJkk#N8y9_zDg~`==X!A^sQtr1ej;&8Kxy zf<9BU1pW9!=Kfu$++!AI=6Ar18RH^{Uo_f`h$*A6uGt4?tkFGJB|TmDwC{WUH(tU; zPclI8!^H7d&+0u6x4SGCIqsu$J$vV z1P45q(LBt?UlbRUWAAR+2|!)^{or2%b_zX-@{X&$}1NB$QJ??}Qo%`|__-|pO z%U-Eqa#Rrd|1+2=dfpY5Lp=$4rYR|^SqX|-@rqgwyNc}Kq0E}a?(XTyiRsDix`reU zr_?#c4*?^v7G*##gQ&PfV0@g?as;v5N)iBM;_I8^}SJBNi!f>}J5bo(p`1!^Oj0kKnd5 zEtbAR3y|HQ)#GPhenT*rny70n<}{sZ+~6Y9bNA^eCEh#3I0LXkr(-ir@}=g7Tps|x zuJz6g1u7Nkur%RTumXpQ#^1?FqeW5k}FPpK; zzuy|rOBs}TX~l%0qv7ze3d<|CdqNRF3J-?8#yDJIiFOOeOPde>ku0T%G?lgX=4;oB;ludf47`%aI zdim$`6Fwx!GKP3+mhNYTuVfHKI>qRx4-MztNNN3Bhl}?y6Z5M*^Q(qhgWo8k1t%uj zbCMkdIrfA-EQ?f>oaO2y-2RI;0G2Lz+tPN+$ZBJKp|dI~E20dyFhwE-uqxDaY&>jd zilzgznbu`WiaQqcpF5(WY;ylyH)T&=SQIENKadT?5x#;p5~t~0q*i-%;IqylG#3witF*P9Q2QN)8uQEh+oXN5aHK!sMzy=N*I=+I?C3>-yKv zd*HD)U7P6%7Y;U+zLO5#8SWPaBtF2A`uEwgWMX(VD7Vl23HV|lZt<66w6U?)WdCD` zW=?jTK#@%tTB&HCEnJLcyNn)CTgg17fSr2qr}$TliQ=UXM<@9p)Mvn z1m!bB!YDd8ucPf^r9+RXJOF?g$enQGEqodYV z@`OLbXa^-n4`eB=5UKwxeSKSAX?dM9fSrb7?Y!``L9+V2_~^~_GWp=UZU(lwaR zl#%haKI^bK13uRns>~G*myFH-rfv!T&V`tRkjAkD1ITo*UB%}Z0Hu%g(EdP60=;MH zN{`vXyfiwSM~Cb@tjKEM9C;-M2QTSs`x3F1UgT3$#4FDL0_-x}9m+`*q)Xj8YXdr0 zuAfkqMlHc67-g&713-y3NC++un5N8vX;zLIM0j59YolA?2oDC(pI~RtDf1^lH%z6` zR4BWghvTY<-Kk7U-;WMnVoGc)R%<*PKjjG<=knL40t)nWz^lBZ9i9@jDV8&VPL$Zp z@$*O8*je*a8&;n}T2j-2$9uM#A1pzh!ap$)J}eV6Ea97(kfyG9bv}%cbPR;6^{biX zWM0^#^J6{ZJu|>3a7~xy#+HA_=38w{AeRX#r0J-ns%XS&#HMH{T>d1kk)VFOX!=z9 zMQ?!ZL&F?;kz+yzib(ms8=uP=qQDwg}39(w+wFP1%iEb`6gFa6Hh@WGC-K$5+;x{ zN(z8{1xbsgO?1qg8$@()2+Y&7tKHW3KgO_k{U0Yu;llhNqQv~qe^Zz0QrD|;4g9mp zP#8s!=-fcs1e=KQ0k7wjJ$-z;`y( zd=3NwrQi$TqGP`X-YBqWGeTJ+HI<^t#fKa^(CP6kKHQJ>q8 zaqe6X5hZU@^lj(|ykt#0Iv9z^d*hq|<-QSELXO>OGnVK#pj*;SbNR>%>gME^gKWoh zm=teS1tB4!RZ)s@Z@LuLQR|HR^kj?TBd+l|yW+I;G}EfIr-liY#uSEpi}aQYxO{42 z>Qdt83Ej!JXDAU-29C|u(bsnU4lnyjex1%xzvs-GFOv^jGgK$KaKHDqWafUg$m z;_11%u3Ij(NEp0XZk3x9!*dxR)B6#rb;>_IQK);j%rsE&q@_KuMR4gKAT`0r$jfDA zXD`bdvoM} zr0polP4-91M1&7V5}16{TctFhL+GypG)+zsLe@jPIecdqE$c1mDTu`t1(Lfp^xy5vSCX4*>W@+H7; zTDD^xeSU4Tzo~+ezS(*#@R<7Fj#r$gE2W`rTP}9G&nGo$rMh>~c+-Zy1%Pl=+$Hqh z!z+GXjv+`j!g)M(6r(%3jg(Z2L4S(Egr$d?$oM+4sxgO^02bd7eN&_`3r(A|PLuA&pK$+rd-zaPSjWA+bsf?BR zbC;$TT^n2VjFq+1@9aEh&IAPGa^eNOie`3p4&2axudxRZI5RN8y`e$kNKxT(QIS$n zKf)1Fu3FZQKq((IQEnrJdzU66FEy(9{&IFyK!Awm)JlBbGIzdqA5G(l#)Wa=nhBT> zXfcC8mt+)_NFL8oI8A4NZf@Boi5PjnaAjpcn>@@e@2!oR_m}BsyAF4j-bZ644Lwa9 zLJV_6X5auma!AJeI$RjF-Jg;LDSW(}?Bc68@7C#vXay@xbB<4s6iib;Huu$?LKQ6a z*!A0Od~cX94~k7z9-(?a`4nrK`l@`B8^_%R$z4#Uv~-!?=5~@6EElO}d2LgCCGD1c zfm)|vEcDG-Xmeh2SXVnUUtxMqWqQ@kC1$)eI@(rNs%@xT%j}}G-W9*Ps&f-1%PlaE zjfuT3Gx$I=qz+dWCLM+Oa}-P?n=leDkjzvMYyfv z$B#?rR}$gNTlR?ItGLLmU*lf54X{sgr>26YE`w?>gQjZ#?;o{jY-*U$U4d~Bw~;}0 zr{`FNV2M2vNhhn!GQsv3NTHlB?UFioTqSKgx3P2PMcDink~!hL%@Cm6L77g=m;Jk( zuJ`GK$@kZyrzbJ6go*0iWX|K6_J!B&T!LT*hf(Av5q<&DW~5=W(Q#bZlm+ar4d+Q<^F45 zqVpf4>|^a4k#F)K8|UK%*R|&^jJb;^SymX<>(4#|9hB0w^0I{OGT6~X*qFTZl=r7m z`T29dnpB$_)EXMHEv>3-ttxEos;q6wP7ci-T`$x;gahaoS9p(^*d-P9v7=*9e)YqA z0G`1QA6#9OqL2}yQBjVbTVg=#@J5h_xPjuIKS3!P=S%GNL40wCd>exNO<}*%Fz@R~ zxa_w7RujZ>x?Mu=kN)befC4lH`1WZ(THCJO{wiSa!uP4COW)4gk}?RNmR8ea1H%n_At@MM9RAm; zMbVLF;Q)v3!(rd2q10fneR|u$&};%CH*#UJAQkM9ckfa%dwSj*2xV*9YJ>nmOFIQu zpTni588YdTCWViZIV0mIsuBO>N_60yk@?bXm$+finBV^<_7K%oQAShEYgxk+m>ET9t7S5apX_+p%SIk1}X?k4Wd&F4b&_oX7F-pqO#-iS7<6T}>rooo1lT zVF>h-xz$DU(Zcin9>Jm4;}n$Z+awd=Oj99|B<)gJMCu6Sa!sA*!E5VE+%E(OMTexJm=r!WbC&~fr zk4A`3Z)HSBIKL8)`AF z{(mjN&V-i5z{m|uIxy59pEb=U2|x!G;}?7#AKQa`eD7JRh?t!T?Ma5a8==HaZ(7Eb=NDohPe#&%={yO7-c$*I>$B-rK z*Y1zEF}(ijGT^;mN}l;c*mvJ@(Z%asN7>d=6DDp28D<9TkHe$ zu`V1)hqI4+%80et;iD)VNi=rDaqWvp8C3iJ2C;j>UK^`{fqbMaki@;+eG6V?- zbXzwSy9vCO+j_OL>#9dV_xC3o^OHv1`U9q{wr@+VcAO{L>;3Fa%PTyYVfMf#^Kfgr zTLO*ivLHxA6@6b`6JCjl$`)3f=10i$J&eo8}%{?;3<2lgTV+w;%9j$%=T3 z)Gw^{uObUNb^hkzfG+v(DV^7(t9^Ag8ehHT)rCH#STx^kKsY~X@k63Sz3E?V?E>O1Zak8b3(pfS^pjabP!G$c$lv}8*jCa zH^(P9x+1eB@^Sa)D_y72um=q7CbP)|Psy&G*CS6t6*TnRLKooQ7pLnPTqo%nw9)Dq zVD4n^o1`v5w+;DQ#&X!9!TYlD5;uUyd`DyX(Q>4D(rZQ$TrP#c^@C|+{cXa?W%UO6 zRZP38^$VpfXj-ST+_{iiNBG(Vvb3rIS$9V!} zy{;ysUaRx!h4)_5@2DMCy;H!ew|B=D2EDgG#kA|5Tr}T_CG&Ss_hzrY;MsPXT}O(3 zsaUi~zkjWnG8?Hlo;LsZw0`{{*kL=I6(u_!gqAS;_{RJ0`kHSAyto;hrz3Upf!M@! z8=7AyCQItSSATtnm&yLV?^NlPb35=#U?*rreX~72oI;4L{{|0S72;4_+yy0?)mzC zw)WriY?Xqf^!H{8N9>S&02AxN^rL!h+4;Ji#y#tK9Yw_m{8dUM2M=xKlew-xXoz_n z7YKHkZM}Tah0kYP=E-ERi9W()b*hATi6Rc~^16N?^15|D=>74d_F?0xi=`m+L|YXV zrK#xJo z1tZlllP}D!;iK#_t@DYjas8-lpx=K}&9dDAeD@@$9V!x^^ka&+1M#RSQtjBye^spl z1FRCfIe-r$4pK=Ox_F)OajP%&<`R=1H??~=6Ix|nErDDg-55Z6b7=<&QV(Hsn=Q=o z+IEB)r1psP|D?wRfBu}IR`a^QhAQC9qkCjQLs_7hvK$_rZF%zDBt~v}uOg7Byuzya zD%o?;HcY@$s&BN)LidRA~*E*vQ= zQJAZRTg@|HY}ThMv23s-I3r-9!#`Q)*17PxvB*-Gv=z?#x_aM~SDdHL%s-LX(|f+! zRnA_p1oyMR!9nDz|21T_>t&#eo}J$&qsZ`n)Wv&>fCL;PJSysIuig8>W~Zz? z|6h%-eMiOr}W?!3vt8xTg_Xp!mhb}c&MJ|?YU#))ovEMR6f*uVX zwc+_AIh4o_uG<8z(7^Y5a0zx?c|;%ly3p5_Ll##(yDptLa2i5pZiu)_bfcUAd+Mbs z@qKDX&~tMO_TuR@qDIgC^(>xD9G)aRf45RErQTz$xsHse^?jnHz61}cgZG|aP1BC^ zO3|QE@0AuLE)}!(K7|5**TgHxQvNi(g`=+96y_rOspX3 zbG;vtZ{Rd`7!3;ul=6Q^)PC`*P4LeA^O3m_G`)FX;{*ii}1lF<7s_V!4f|@^Remeb>d4r%8w3eVgw6$G;>!z=8OK+W?e~?gcSU@kr zR#mP0ITJIwW-uo}WY}FpKfI$!Pyq?x59FP}D8l#i!|nK=>Kkqc^-P@@U*ph}^fw-B zQZ~Hy?wSrJvV9f_xa3DBj+dAxkFMvq8ls3;3SspzH?2#`t$gnvXEU14Prc09Kj*kV zr97-VOAAI9^>=)+K8EIjd-a5149}Oe+j*bQxU_lE)%{ExupW&=6#|GR@+eTAQ}-)jtxb&ET4 zJnw7J`B0+JNTqi#DX%UmSzJmDR-IpJaq&lY@XRu=kB0<#zMi_*7d?5`3Bk;q z7lGXkQKvsb9adMkZ=9x=O*TE!H|~bp3Qi7XOKCXY#W*yvNvd=xVIq4TJWX!E&T9&hm)^|`ku{>lxFGlMPJJr{ZTda?f3ss zfcNh`2jBa>r+yM{Qeu_HQdR?^$h^=y|jn+Hy}P@QPWRF=0Id z$cHbyo3|%@UtJxgtq!mw`d?0{2wN`BSCC=4n%~OV)f(UEdw-s4&Nc|nwqfgU11P|k zcbyIh0e}+=*n5VO#QG*Sn0P*aJZA9wtWZg; zhR54Zav{A84h#syJ#Njp<}(`AwBcZ+_txFC@oK(z>}=Z<@Y(=44tj#BkNRg{pG|j$p|0OJz|+`@PC(J7C?&;8fBacKMJgd@79M9HUaCQE ztsaS2$yK4lp?2N(z&|?9a;-9K#zSr{a~xhHU!%K|fWnRSAorlDwsoI5y*NPA2}yRC z<88hp^9dgEk=Zk$^@zds%`(rb;roElfUa4leHVP!+SKIZe@G|y;tT#nQbp(6WdhT~ z`O3!GN9)JhUqxJc0LpgXGRVVub`nn9c(cAamF$H@jXZoc%oAK`G@874e~bx$3Tzc}`BvJKrw=#PX7PG2 zPNSi3!V=KZ{(W_P|MP24h8!E;*b~6_=WWkw*$2+RQL)tSaB|9VAD;EVft(Vo<4a+`1LuWBf{V6y?Jk($d>y)F3YVzy$(g6b`Mu_M7%0<9yttKS}1FPDGJ zi_H!u6ONw+c6uXa$UE7v5 z>TR4bgHw)#oB-N^>6x*!sgumA;|S>$e!wTO8uNKR3qsyj z!+{u8Wy{*{^{W$LC!O9mBUSEMcXn4IVNiNq4=4ZUNH+O-cuwimeLJ>q=Gz|jD-QHH zciepzZig2cGP!I<3Q-qNvsvsll-h}^uweNOclOViI&TLi)}C7%nUNfCWEWqb!o>3n zNP&%XJ)4|R=1mWHPp)t!=^Xse$aTo74qtg4{pP7NZlmUI(~m_8&rkUIC(+tg0(&&~ zD(%9GyE^eaG5Yjl10dVANU z8x&jMSqPZzt$LORxZf*F8S}_BEvS{e^|!>L34rm&eaL_Wv*3LKlqhDD=YBg)M9%mtSn3Q^L=EL2uf4905to?m>P7oO*@~JBpy>7Op z?XU?M&zB`;Z#f*z`0grlC{QKm>NxFkmotiH*7ila<^cTKB{EBHPoKe^oCNK7BzH8` zWVmE2i7h`UZsTkTRE(vNrP`<*tLjK`0)MhM4j1bcMjAIFqI4uz=9ckCtH8xGIbZVS zyU0G0fH$pK<(q%P(hocBv}~=&i!48!S0Bagq9EmfX{4ZJpvmK5G4$L5yC~KXbA)t1RZJX~A1_<=v1)0KM57NXGNoio<`) zJe*4X_!afb+pg*Jg+Z87#biQWkU`OEsXhlUVn(BYsh=CZWX_FriywG@57GF#25VRB z5B?ec_9X1VZP{J+-FAn}l!9{vi=Z!CIC*Fb4iSthSexl~4~Z!G=E;rI-&Z(3GgBPw z0-O>dp3sgH-;7NZa{2?M@A7Zh93OOPZRtEddAi%ErP4|_bC`>cQfc&pt5{~@Bt1Lr zth?ipVJg+6Nw=m!bLZ@g&%dduCLT zA<9=>*ECB_+u_LgitP}C_MfJw#*Wc_XtxlNbwNakO{W`f@?ct4V#fk7Q10GF{`z4R zUze>qyj(^uzc+#VnXB^I`*#c*(+}T{0!L}6m7`hX@VRN0_dHXEl4ZNveRb~bexPuQ z=Atu;_Bi^H5(S2fMF7OQc&g;&sV51`$@s1d31FY-l(;LEUce&a%n8fA#AKej;juV9 zv?Eybj8+*ST%@3`QX9I_3`n@448(-~%b-_q-vC0C%1nlDbY-#mM3JpFV>D?+|gK48j0 zuUooCVVWpf%MPxk7SlBzO-m)~9PI4uiFHECMxsZ})^b$Raa`^;@9gBKl`v|SMOO=? z`MXY?S~}0pT8ql|dFzajap+rBx+lL_YfSbr$vmrM_?@3Gr0IMjz3~5oK1NK9Uu4om zglv5aUJ@T}DSV>kR0aab58eV0gdY6V&Qbd$xdBJP698T-pmrEL~B<_3|{y zlC5>0)Bu3vT=K)ta|ih6^c%)2Xg6mZuK=f}mxoYf#H8zf?Wsf8QZ7$`L}%~x$K;=Z z#X~eo(s3ZmeUc*sc=1}s`z~)r`}Y_X^DZ{JhVk2ItN7Sh?c3wa7;8WxroJ9U!(<;4 z(>7`|{dm6Hm9FeUWn0xP;*aXMe_DZO;dHfO)#gt=gFu$(RqmP?}Jaxff*}t*>b+>#MtX!>+dUUrtbt6Y? zR0aCAHGN5=>ezg=)byfEZEcO1+p&*z^Sw+*QgUdN<|(7(pit=j))0~iHj5!}$?xw* z8m2xJ>CZcB5vc1NW-7-$O9O?vlLo098(;R9^@7O0zzd^9CoW5}C zM{1aSzpcU&E_@8*N( z<~P(#-^9>7HQV1?86Pf+yI07%%5Ncl6wQy^?!W6N&U}|x`w?#^USdbg&5Pr6G?YF@ z@Mat`x)Tap_b(F_C!;ZhaY?8vP$p3-6c-J(_{u{K#|GlrjaIr+QI}4#beVl;Vk{28 zq?{D){OSW8r;8Kt!Lvy{_QTV^(0V<@dT()lGTD>9OdmY$~+aUqQf`8I%9a`*c~Tj%BpQts!WlH?h{ z`}DCWCx}1lp$as>WTIitWQ>x=r%`CJu`igA&y1U76Nwa+%v6_iS<>Tt93CwCK@fdF z8#VeDCzajf2M;BWyY}ZmX6rP3Kt7XQiAGXEd>%8cMEC+(YxD8mK|O1emtB(=tI)eU zP0?SmZlWBhfg-Cjqbym?x{RjzJ`70utF>B=n@wnQ2!bDF#q!x|QB?qU7;gP|_C3KR z!y0Uf^f{1xhWC#LxxGUnm0{YJH?hfcyfqc;xMEd6M@u+CPsMFAPdhgOu~2ROPB$6y4Ib zEiNMhhj}eNe-LY#z%FNTFaBYc#6qP_TJDyh#9DlSwwNg01A;ba^FXO65MSxrfseDr zY|R{@LLow?mhZ>chJYw)G|e*=W3s}RUL5KK3Be37lasw4>BxQuGwI{1x7W*QPCXQq z?OA{_yD4wEL$UQN&ORU3P;X=jPLPeEOLK0y^WnL7-*8cvD5+c|J8n2!PXGKG08Kgg zA+H4**lS{CnvP(9BU;V?76;yjS26L$C*XK#Yr4?=IHBhB(__0$ML9JkIkn^A5fW|+ z2Pr1fglK*pr?d81BGFVAzvSmW{=sIGrKZvr$UHu0hpKsmJD}pE6S<9#x-;c6^!7VY^HMAHe^*{I0da0V3YHc=9K3LZ5LaI8+lvb;r$CExpCP)0@VgDc>m)z3< zCqZN0{tFLcRFTmq#%j0B1Afpqgg^UUdP~lL1jN5N4aHo(Yq2b*Do^qhhez|cVic6B zmdxZd%wuZamT+Wl9-6q;obg=sbJ>9kzSMqa5p3?KI4!6Xpj7WSIx#24%y1(s4ROrLi$_*1BZRqrJV;FVJ!DNjbx{ z-j_R0Be6V|jbq4}cy)VoGK&nQh$8umw*xlvHyEes@h$E;A3oByDm#eSzm6sIFZ<*N zWOG>+vw9-L50ZLO?6_?FP4q-TQeJPBBnug zJtK`{x!3yYo*-+GUFpaZ=+7)FPBuZ`EWCy^OdQDrqfj3w(wLQjHz8DK zGtM(x`;$<=gNewS@LB+Z;+3!nbSg0LwRFnz{^4Thbuq)?z0;IwPQX~Bc+;Q=$l68} z9%@J*SA1?bQ0NMr&uf}^4J^%LUYIngfWzEL0f_NsKz;Nv;lm}uhg*!C-~~ty4id7j zA7bHPO!y!3=~IC5S{tydAHj4nSq#WFl9iEA<#)>f!h&^Ngsx8DRi+lKc_r z=iViL6NloTfJ1?P)vXxFsI#3oai!G}^rDq>e>1iFF~cd~XM(-Xrz-*;r53&ErLj=h zpf}ZY%$$^fg6Bkk?6!RHT0`|_nVs_p|cTyb&I@b535Yg@VL zH2%0z(9gOVqS3`mF1_0C4?GQ^<;iIHTv88|^%Y98G!H_p-6p)U3ybn%I4$-R=2KFh zoEOS_Qj(43RB&^GWTscZe2@G)e+X6DxkG<$$#C|jeO8T%eW^+B_3ThnP>NkLlF``A zKy93A{gqnOU~E_CtO@<4BHZEk(j(khIO zK6-ZW!6odu*zj;kL`24m>?~<{#fS)nIHvK+f3*PN;gRtF0iL`3*#YnoOra7Z(*oDcAseEl9ZtDB+sPmbOTcOuPNd6F#h zU^1pH57R<20u%D)z{o$a?F@Qa{7>i3==W4;Le@u=(7&28{3ziGT*5034-E} zE>G>c&Fvf2M7wD9Upnq{A##7&S7cSqy+`E-jJ*RiN8-|Zh-5HXY^OMs#XU<7TXz;ecl7(WoACbdI9LhgV*sdaTjltNN~7tx&y;uCy!@5uchS z3rB7Z9G}5iD~I1?WMoIZAtbS}>sNA#iH%W>iBV%%Kg33(;QJvudI#e1ucLIH8~{gGhhs6Ps)qvgK?xpRCR1nP7Xz=RGj!mY1$5E>CaT z&08DYHvO`>?YhykJW)gu-~Fz-|2tRgan~?dRf6)0f*0nvvA^YkyI*!?s6xtu1j#x z&Q4x$qW?ms|FOMOa(E4IMZEx*qV*z-;!Z6C>Dz;;|6+n)mQs)1*)I1M>d!XvHRyC{GhZSmzH_Jp z73ot}A#cg$yvmk`5l{tl&_+EvV3fe;t|XaFgCR&7$=OQi>VhApwi0R*5~dQS*_m3;Ay}!fhVJ*g9-SdFC=dS zMMYB1rhPU>1oS(;d#CfwI8Hd2Un(_DG4kfJEf?c21`e&9BAU}$e>_(&1}Q-=k47{0 zmT291v{ucAF3YkEkBkk5#njuouxud0d2-bNI}?oA6ZuyzJGTXY8$F zZcX_zeFfD`*kmhHd!=fFz6<^Q7r80BP~*;csyo>aK6(Dke}Ck$#Z|r)SDFm{`s2CF zgneuOzHcR>}5re=`2h%&G?rU@8Ogu0N){#rRf6 zZ)`HX{npa{a$Km$AZy`j-PYM1{oO$VxUbQzGkVEq_@@I~_z8s?;PIY8oArr7T~fM6 zPrnh}9~ogT=sKJ1(|D}1{Ofzr*L$CoBoaGsza6CDUtKYh$Ar9%ZCQGu-@yX+ZNL!w zQ3n#n$NBg6l(5Y0L96;fLp!4rCoQe*ZxFwMwxXb@qk*LEOF9mlQjpZWI-Dr=pfj5~ za@e>Cj-4iIvJ8KHLD5O3#o;P$13n|-x@O!x&>NKi@w*gH@QE8LFJfPM=);8;WYb)Rs#;qb{-CcNa*CSqA^ z1$5=&_&A|}KA4<`c{nr7&+pTWGA!j2bnZXhjI;Xs z-F(QG3-SH7+sWf%{Och)kpbbDAj`zhivMnzk%-AB1g58in3Azqrh;aS zvRzttwO1h4>Z(k{^##EsWUpT0FPDlt{?27hX9E~2P$%(u9olI5)-`NwlLqjTiH$+u zo7!+W-eaI&D~L;&tP+C~dFcd_@SjXffJ~OLMOh{;6YFado!OCX^j8|vgt-{swt=bp zJNh^lEp^t1)I|xzR7L^H#VTgJNp`OO+G2Z77AIy5&tloE!WkC-=pTtS5Xy6p4Y&Vk z-Bc~FW=xHO)r$wx5Q-Pqto@JYtj8_@2=fdJZ=nhUPNISDoTfJXgHo}KEpr~I{W4xVmkh=KHF*;V@T z>Zm4TFccDa)Bn_#Ac{YT7BJo}UFMdun2^FZz?ODg421E1jCaeXVUUAScz~rGWF|tf zV{1b>!Y^KY$H7WfL2$be_2(*VL`gXMgM!K4#AJ(4x+_Cw4DQ5)`Riay;;X~%o~`ZO z-RO4;%3fxc)pi5*9K%=QQwFg|feiC@Z2VE38NixYsgEgz2Y&uc9IEig>tsljqG{ zoq-JNOeJmjU^lZMZcb%Hp z(<7edDTX$?vSSY8kB~@g1eI*=>HuwmL;VgDhmX+<0gI%+ zhWrEt7;-{T$G67wZnWzi`UqAP{~l}Hd+st0C7NvHlKOEWtu8Ugnrdwnw=h9?S4fRT zzIlb!-cZofFzHff_#zf;+Bly7(|QniFS}ft>^JJDWif!``rSXCL@s7Fq}-)>P_f`= z_Wq%)+I0`|sTP)AqRK}9e)w-FZh*NL@<^HaOx5F{1koYre15)Fs1ceDUbnFVze?Tb z-)lr;ZZfa|l?&$I_#)AAJMcX&f(I!zoz7-jig`*Bk%z7O22EneDa8Cn>M+f zlsM@6zv{$b80vIn?{Xk|U3y?8jo5-nB|-Di)fG{N_HSe$)%3(NNtUY6Sjo2Ifim1_ zG^}GUSRCeN)$jGP?_G*v;9&-w7J1p(yVUwL9U{AZF@M8Hg(+PVkPpk6+-Z>7K*ZC0?Nl}E zRy8x@%p+B%UdSg<-^`fjZpg&9zyn3PXu;y7rtRN5)1>AC;S&DexYfrf^EscM-jtT4 zXqaSb;$)B3uQfHwGEUtoM>G(?vL-*dSSbE9UKrWtWI-oE^VT&O$1UQ>1PO>v!Bf2? zbys;fLnxLF6)4SzSl^B1gyY4Vz2Z>19w4oZim{gD+;nH$)xWk62I2=R)#eWd@Xt%e zjVg&tgqEB8t~Jytzt`PW1E8j+%aS@lzQR_1qppT|wd zHO^4EMcE~19qkS6H)}K!dYAQas{o(+T8^(gb*6})1n~YFSY|ya%JNGq`K5OZwp7a| zBjTk2Eo#ksykg&^1ZX8v&C29U)xc(jPgYY4%;uEYweOl@avM^qx@T9qrDTlLqN`3*Fp$}r~;n6@sQjbqz~Z#~#bFz@zlW2BRLDysz<34g(sW+Ry?>-i3> z#@gy8&ipM;<-r;IvJ)@L_@&kNm)HKHPXM!m%T6>0q8#`d|-)|_Yi%p~!>o%?4;#|^WNHei`8!ih#u5;c* z$zj-m0v@pI@kInIS&RFylVnW4{q^i?&`AABtsxkp`b+HQI6dh|SQ>{nX%A zmd#ge5krwQCHkGgxO@*K8|S_cH%wLqGCYDArrB9CxiIjfvv5P+ASWvxPOBe>Jh<#auDtN`d#y5gU0(Q zSzoiBRJbL|Cr}TEw->%4rR(pN%(?ld@O(RO68cE_KEE#}MfV%3mtdtv`z@jO4<$98 zvlT~YTGMRg&DmC(B$&M)oOj*-=@RzjTPZil5141lsF{XGnv|3X8)wPGET&zU9=`pg z8~t66ifXv;G@k|~>G73h08QviRoQ|%y%S~$IlyJyy8aSH zp5IVp2Q3HIH2F?kO^77t#YehtV=3aB{Vap1T$K2rd%;PcQ&xW%Q9X~r2~Pf;Y#f;( zi}rGox$z9XMgOkm>k52~MBXp}KJ~M7_ys+iiD}80Y|3vEg~QOUeu8 z+NF4n(&q5n)_87`owMHYMF>UIxQT)-UQ4tcR`-NM&wSqmSyq*hZLtwpDUu??cVSrs z@BgVOp3!F!2;aWn;6%mz5i`K>Q}VC$$Ja7Yzp98W^!}AO`VUd*0-+qd3@C8*;;<^7 z9m!WEqc;)yBI~Jcyn@IV+~s5-&ftm}rc$NUUN)msrA5wbe8X$p3m+9$5i-(6X^DQ0 zD&mCI5)1wTOf}0rRO#KNLH!+rHCOJf@=apM8!O`&!8al4qHn&j_NN%X*yUs`)36<9 z3jseALNwMVPTOXBrkhCR8ZZB! zXn*udOuv54Y!S^i+^Lw%9HpC_F94+S%TH3oZ3#8~^L?tO!%M{ao76Z@5{l|nAX807 zX><+6Y|uM*Gf)Lry?zZ+s0o3nint5x$q9~E6nM5uvP-WRz;eGY*JE=+4$Lzj9segk zoFr4V-gkR)@_UO6v(zj>QG2VxwD^2b8k^@1yK!PrUWuAt%-_q|46Bw*2IF$GQr_ZE z0znu4#AJ}PGcFXZJ%6>&DeYPL(-d8jScA9)&QfPqIsqcB%$GWRi`XA*aQ^ex`ayv6gWKSC){!B%9JL zxsNC=Qxi;+mOAG4MgpFbdFo>~Kf$WE>GC(HlnxLh*N_xt#}F*A(l7oMJ@^l#QUe}# z#aTCObc3~Yyp`E~_=JGci>J``JExnjy)Xxi$x>=zmi@MUKJ{nUclX*JkY}>Q z=FkB&%Rge{x*dG!i4W;8Q@XRw8@rW;<8`BqCWLqH9Fu;FL}Xsf=O*kYy0{fYH!#xv zeTY=bs^L;@2CN|AYrmR@2M3+OH@5aZUy9aLo8KzEcvG{tLwof7y~`N}1H>N_^T&An zS_hiFM3R$-XTQ{b(FqY{pV2kf0J7Zx_v!>q?EDX*lYJ%I$TouL5Cz(4ay*!4R=SG2 z<^1g>tOQ2BshtV3$OH!LKV6A3j`@0w&*O%sU%2*{P0JTN7xbDElv}VoR)%SP-x)}2 zA)0bxt#6JWDgJZE`jC#rQxiG-f1@C9lUdM9Q;$jZ(wT1OXtkC0WTHa1JS5@n-Ll!J zznvr%+!Am6HPO!X&rme=nLhmC$i(V=X3(Xux-rdfOO{))@&&(KCFZcb` zwOa?=7?UGDhuI5#%&S!eR|f^Cng7A~u492p--^}|i`Eg-S>)NrWK53>5V5$?3;SOTRi) z?5v9aP3`}>5H;NU!Cr@uI>BF5b^xNDe~qFx^J(4ppKI$K`6D+kBv?rI_v8r*grn{2 zOMjezzo7WPPdf3Gq^aiph8fcTmAIG3&HuU5iANh2^=7cV0PAmGew|q^of-!D_n{|2p^Ipld}5jLe@ay^il6T@|?LYcmO6edCn?1@Ghib5uwD z4GIkZvJJ250QvpzWcZQ^VL++PUvjo@@IOsP-QOR%ud+}?@OOw`d4B${%fK3HpJw9w z!vCw2-v9*s5_^*K1)6-2ckQiz)B)!D?=7c9>6A14EG#Aj|2>D#e{RZg{~8;`#-;rY z=q~V>ub&89yu%%{a1YmFrj`CNYlEq}P zW~}^)WnR^N`OiK5vV70$mV|p!lr=%KGhk~A@V?fpbBh@fwa5kYE}KCJ%XUqR_q;D8s^2Hs=rBb ze}GwbNgX~;b_4u1^Pj&qZ01%Q;S1-!T!R(|PHh2CULEo0lG=`{ggt@}iT(X?;C@KctOd z)4x?@z8fTg(cwJ5mhY>?zQQHs*YMEFnX#!YpdZzTe|wr_KZ%-I#3-Lst`=h?=`~Li+nKg{ozkg3{ zfj@oYTk^-1p*&Xorz3du|5VhM;Q9ujr8fcp6e0QTKh5ud^4HLL8GZXZabFexo8SNE zG}lpcwTCv~{?&h89-(uu)4Em>qP&^(s;i}in2K5+|FxfQgw7kx8~Cb58A;Zu=) z(Y3tDVLFY5{=8g5&>0E&Jau$Nyh; z*UIz%G3)k!wcdG&W@qJ{S9v0*f+tU}T%uD^<#9ts-D!>1kpm-&*9cWb8Be{8q-2j4 zvhGrCy3Y>LFfK%H4IZ0UZnv0hJ3VaaahW$! z{V7gnR0}!R26JDxB&b79>cVQvAuK7=TF6n?))2Dc&8qnL_)BoNB?``BtEujB0jELG zlgs_EX?r^dkBw3i!k$>`PILiv#nZ#>iGHGhi{ZpN$Hwx0!)2nl-}A5>=b72%v9a+7 z_ZtP=cOSH*Eeh1EfAm;gy}B#<0gQJ%cVw6YxzFJ-G&Xj$Oi|<9TI^?7#f5VAX)5bm zf{Vpw4KBYec#vt2&A0jcNxdJ@DKrk-+O*zDiXl6vJ#5-%Zq!W158?;3l$uRfS$Az+ zpa+1u7`5>+W|p%cayB=3u^4S+Z%=18ry-xSbyN$4Hu8U3g5G92XGS6R`nH9x3d+)7B zjSZl0TYScOxU;g>!S)qs*c+Y>E@NH)#Jk)b!+%=ps6}{LS;XS7{aIRIcOzzQV2?Uc zW51V#VSTc;MWfJW7Kz$IDRP%DDo9ZtzOco^slD;DKgBz-%K`Sg(V4}=b^8H$=-ARt zV`F1U2?;)Al_`fAF68M`Xd0H`7rb<7G66?;cyMrUW>{N%P%kS-nb~CYhycsH{gO$= zIZkU``9k9tlg6k$e?fv@`A}7SvL<06GmSXNn!m2e6rs6hx$*1=!%qu|En607o zW~FrTHth?V^I5WEuy3I^9z&JU?x%`D8)5x2N4o`z=PoM+%WZ+ZTbnpX#;t>1MVpeg zTz=_b#;oU6&-fXRK=J$~XkDs!L?)d!Tb674&nY}CCR@pLZG{m{%K{EtY67UyL>Eil zafQa?32=C6&$vw#L(Jr1?ouSYXWUi@V_A}1z^M=H%F_FDL)sGoFB*xqk99!qZHWkC zOrO?y4l?p>22y0y)eUO8v3Z>F^C(tm zX9ht-0R%mycvL~K)Ac$h&ymL?SQ>Q`qHI(EZN+zlPoiR11=!89qhc0NOQ+B;aqP>`84Ebk?iAVZt zj327~;7V+Eh4b!O5~gYZb$dPFJZ2kc5K22gqF5_k$2Bwn65nN8=#7rG>SsDjFO2f6qpCq5h z1*Y{{dzFI1GpW3|x-<{s`g>-Sg{p3IErN!o0|EtIhqXb-~qLU>PgWW=1o z^!k+F@G6c9N`Iyi|>7^5E)VX zkDGMgf=%&QBUbPPU0vJerR%EA4Kqa2d^aFiy@?*9op1f9qva?BJlA_`(tKzK z2V{jqNIja^2qj~_J`gQ9l8aK8sO1PPv%w!>hR)#b?v7BFy% z!$bN95)>@lu8Vu8SKa5XV|1L{Z{N@6(5kfQw^zzL>0qSzU1E62ad~KaiPUn055cFA z4UXOMY|oH#YK|KhqZ+uu?c{r9iRV@rNqM!qA4L+n1m3G_s_+@Tf;aDSZ99#pE$0l} z_6;A*`6h?~rh<#}Md@S(dY0+BZozNgS)nNU8281ru1dvlGb1yX6Tc{jW%8dtbEL$j zi23byLcbI-RD963v5_q!@Z8yVE!!R=6fD2^%t0R`zyjUbygVHS&TGCc7;#>QTv@fN zS}whFM}op*?#tYO|5{8Xi^MjH z9o9ruOD=T>JHvIFu-1gN?#_JAEae*q$sQGfgX1lV|hvWDdm-p}> z>kwk1fbG~ z+%@Ufij|;XV>Q?E;rd8T^+wVTuoAceyK?{#yvQ~wVm`+v;Rb6Ajg`o<;V7JB%Vwmj z5g`PM`Qz+HAK-!engtbNxFtOu3|sgSY>d^_&gXgV;5T5~KBFHcvgz(sS(LN#@zFzn z0U=EboY)=xYDhe7uA*?aIb1s&H!)c+^1=2P8D~s%Hq(lzMl_;RxFYKtLQ6|aZ(I~D z!Q$EN+UwcY%ajyHkZnqr%rrCw%MjlYR?z|Try14BXXgSH)J%}w!Ln&tt6vM>sI^|u zXKy4%TaUgC$Sc<~Mu>Rqj{9_VX6IGgsYsen2AGychDYK)`fA_t1uMVYNelr^V64s7 z96QOL<|7b5rtoZE_QxNGJmO!-_B=Mk!~}*Av2~s>q4j>Kf0WB^r=E$6GOup$6MvzK zwDD+vpUcvLScdvY4FFmVB8k&l@8vaR;-g0`n|G3;w5pwYyJ-xqJm?5&&w?lv<%ir( z+$~Ai?fRrWPG8x`4qQ!Z7UJ7{uV)<%6nbdZv*+<6!sE?&b5}+8Cy)6G+?6~Azfpch zJD}jwS!p4pb1Rxqy>r0E+{nNDC5oeNXKyLbljjfJ8ugivqx_1E7lYnk0e}NITo1P+$02Q&a>~Z z&4(xWJJ!=`E!CRR8+SBJ>6iORE!esuq3$9ZHzOl0Qc016qYR4I*_j<;039v~k^~R} zm~C4Fsfxu`+AY_W=tBQBq)PONX2laGh<2;N zZr)7+Koq%M=IQXW+zfbm+NhrOl@;|m=ViWQfeL4}t!cM2)O?1Y+}zxC$oNF?+6HoN zy_si(3}zO_?jDEfmTt$ru!~dI4C#0)pja_6!5hEhDFmI)O9<04G9>xfAB_x*5zomT zaZB;(TQ*D>^EfWIMcuBNjfrUT#_3&&(v(&0hfhr@PHW1Izhrh@shYN4)HvoP7W@tz z9ze3Pa-lxhL_?#a&W-aK8_v3e-@q+{W5&D#0}u2AmhY}4o-P!bN-amgdT39ME)iKp zEbL%0MOuIX2P^C15*b+dQaXcJ&DcMeeXmR$&;ksi$Ikxw(H775gvglh-#Gzj0Fh8e zqSgr8z%ghlHkdV}o1EAa{crCQZCr}TY3aIh_hf?A&f(8$xB2KMa%MTcPvkC zN{a2}$7I(nPn+Jrj<#u8ht!J2%Zr^d;19$_MNjH;M*wcd6Eu}j!$?Y7Ebh!m7+0;b zZj<&reMLv0W`#Te5T1flx!PktFG%ee(Xv_A8^cc%CtRT*FAJaQmr>9d>HE!U^x<;Q z&7|gMav>G8kCBg_o-J*erK&TPaKy}cp(E=4eL>;6gTDD#9^J9jwnUX8gQlZrScW|b zOkB{Nfw5eGQJZ|Qw+QgOuXcQhiHjRDzf7&#a@HG`hz0BFq8Q>LA|eXRMWzO`;ko1G z^AvSEyB|>trIcS3gTm_tLeEEFG8ewJE%d=2Ew?{D(l`g(xq0kSZpR21j(uB-1UA;m z)~72Q*s;j3U$3SG=onEnA1lAXK@G)Z4bO4?kB=Amb!(03>6EbQUl7K+olj}Y(}<(4 zqC)0I5{)Zk?(|V!S$^!0@=J?;+a-SArd$*N2EYy48yXrO?La^!{KzPgt94K0yxK;^ zjw=X-ad`_(=1%&7nrG+bPFL_;_Gc({Vcd{vtDq3!)&x8501%zcs6{^g?P=cf*peRM zF>qOKi0CN71OeUAIW7?NN%K}23`>xJ6?7Mgb*IXLg6-p;K+5UB`4QH+m#3AO12qTs zei6iaA=}lLBw80{z-3!#zD9_Hx!DaOA`bJ({evufW8+K;kL~d+(={-?z^gyi{TfY4 zpRTfp%Bib@d2vtBY3G}QPgfRL+B*iuRbAEtMv)K$92L|$MOAHl&QcEfF?n}h7%Uqq zPA|;^Hb3D!@tw3pP=5XBev9K#AWc+Wj+@JHC0=7;i*0KV5tmu}4=5Bbpr!WkqKY^+ zSlzi}ptyJl#Ka_J8wt$T@3guweC`tW?MH)=L2jxEm;&I*3R&)yh*B7V=c<&sD{Udy z^D1Yct97B4Dt_0h^OyRO|8rp;pZpRhh@|++} z;3ArkJzPQF&>*!fth9X66-y8U*x{P3EUY;Zn`5$#80%?5IyKhi3PKKVJb2DD`Sxk0 zB#%yk@``m*e@23uz#;!@&wAVVCitpfBJ+w~1|vqI>eNR~3mXih2! zM7!a1uxzt?yr}F)QUMJuOA6=)2u$!AKkXYr@wRHW{V03rLBQ-b%Tm^|eTaZv9tYH% zev!^qJw7sJbdlV=X~k^#o>IfD;ghjk8!j2|OC(IA;#@+U7H%uar(Zu7Rw_6M+>iX(q~C5=it{<+bnSRUK4<^nFt zI{`QXhZAE?!sP&+;x&feE~>OoDXe67)M{wlH`F6_Y#T>@np;+~yaTDeTr!b!?Dl6UI>z_e&JrfRbvy<14C8lv!Ih1T3CABR z-7Yur*p@v=S5LyUYI3(}Ok?v{sx&45r^9Nv6n-)Rwz-s)yeML{cuvQy?ovK?l9Z6s z?LpzPJwA8gwsd+3Pzr$A!Unh+b<%Pc)cN-HAN(3HN%VnJP@@R6+deE;P9Et;w>>Kr&sYOc^i~bJX8gTYSmH|yyE>26-6;2o@82NM#<{}2 z6u@k>q4&B-!1$)al<)*arNq}tb;Mw6Yb5h!R3P4EBL}eH%ut_!Uy&~{9k7Vf?i)*a zvGhwl{M%7teq?0vv879S>im{~Z75Ktwqn zhV9E6P8YpDg#EQxO->)5@ajge$J`g5>fNNra;9X{+Tatq+RbHGacvV|c5;|iW@&wmZC zaqjkHz{jW&^MDq?vM<7p0MjSVvTy$835|?l2K>iW%(}tXXRlf&3xQe<>vwVU=z^gD zBq<4T$WtmR2L}gsvUS;sa6jESAubJC?Bdc=Z@kd^Aq4c3{O!4I4kRwR5@&Mkj%afc zy0@DafOwEdpr9W{(}TJHI=ASVc?Yb43^){KHuSqe!- zyv@!6Hqld+myRw7HW;_V>kY+U&n}EppXh?H%@l5#pOclFY3{3pVfdl_#|<$tKaltE z+z*+)BIP<$3kc~Gryb#}#NXUm$FoQ8xpB?x$RTOhjh}>nWG_e4FLJ0W0xVh|E?H;aa6?NK+#44+lV3$ zh_^{lv|OuL-eoEQMsHq;j;p8bF62N$+MUmxg!Se6iA)^`Pk=`U!BCy{eqKb^via|7 z+GgGMg;YRRo!|@r4sL)_y4DWy`9MNozrqLQV8hNNb$Gp$4|tp8(84eK22u%M!%jK_ zbgG5L1!^vp1VD^;TVlC2tb(OaNKJ!Mzs+)IPytO|9!W#VdttNA##f@rWb7tYJu_Oa zf1E~3*n-+K^5fTyvK8`Cspk1hRwT8Q5v|+ zKRlPZjq@Ls_&L&57d`@1J|;wdWC(121<#cS7|eBTv;;@u=)LBjwk&Ww&_&4+3+l{&Z=0HzF|T~psK1Wgpsc~%L09= zw^QG=s8EzHeYQb8y)W!=VefqCYX51T@0ho?Tm(?{wzgfXAvQSVb_dV6ZCo(z%BG_j zGHE7##sNNdZ`(rfIiveabR93X5faCf3$7$aM2E&rz`-v4Wg0GrVn(6IO^6+MkxBSsL-GRr|`<+@77ow8p7a+K;H{{<< zU^ySJ^(jRouPQGibzEGl^pD>exO9nGr;KEXsj(RB#cK?M-@jWeK9lggIB#P&njY^O?H-5aL2v9U1MoR@|Fz;KoGy1+?rnI{jugt zo3hUb0iC`vJS#S=0FO_}&hDqvPk9Yostk+PTY=WtieEu;=sX}8Q|lN(Jda@0YLv0w zI9}>T_HUGe3F%*sB(m*G2GEX9PSc=QL44ivRcG;b2|iSIgscjXP&^Snl?&s_`ts`L z0HXLL?8C}=B)D77yK{PhcoR7>0~q=}0H#+)XGkGU=DNByU!(XnFA2+Sd2C!>0v(q(XkN+Rj8oB_E$BR%XQ~CH#q8eaNR0QHTTb?5(q3`H)Q?L+BJmEWefi+ z5ltUGYD3$3?X}!DX5Q{f);C*Q%HzeK^l_rO0(1|g>Uaed|J-W9LwU>PSa<(6;6<3Y_B0UjhIcBfBkfjL*le z*sqLtUh=LPxc>6`hS$C=Xfr`X+?UKGLnd?s6`zXYQPBrOXi;EMK`)jgp3bGwJc5Ew z#8%vdvC<;6-%v2djKr;=++j70fJdBV%5QlQ9Nw|J=(!NwA$9fF?PL%bwi@Lg2{GUB zht%=cT2jmSx@Km+(0%RFPht6YWmHGLd^U`s7~`M&V%PGVSEq1136g3JQM1G~)~GJL z@6T`<;tu`8#P*;Grj>Wkf0O@tU)Gu2p7j23rj-_kCCn1;A|>~= z0s7*(1vFB_uj%bn*`mHl^9%tp}3NCGFyT<~uvN)rgVHO-zyTlA|AKsCEzg_$&+i-NUj~u-Jv*FHk zBFD<331p={Zt*(IXAVj@BNI|T^-UOV%DsPGGr%|HOJBp`+_tv8yoVWJDkpWPA02BI z)QvvuSFi?(3RR|KdPuU|2Lw#Ofn%6D|FJ~IJo$ZkSuR{+wb*VxueBPtSR#WHzi?fW zw(aWDg^vT*7AOAP8!zZ)1_z=Y;N&4I|375Ub~CP(c|QKS*D%yuuzRK^n>n^wN!o&4 zJYLg}7~RJX6t5CuPIMm7bbM3KVrfXsg5KU77~6Y=79Y@z~8kZ}Ip z1lvU43gA;MU^HBuEAfX1n4C%_*d_hoe##Tt$oq}6XX4`6a{3`oj*fw7w4Ay+Jm-tJ z{1S^2U(aERJ{E`XbTiuCK2cp=-B#!sKS>EuzGqRLm&YF51$bfG_U#kh4*@c$aNzicc{(U}h-@7^XD=?~?o8fjrCDCB9d7d<3o*_tQg^>+l@> z5HC1h|BHQRdb5z%w3?V5gl=WFrfT=f@nz!Dn;@MwJ}TON0`vxo=-noxejZ+CE07GmnckJ<+8CM`C0LUsRsosH`6vZ1j(JS2mu z2RCd)&M;4xa|Sooeeb&3T85HUkTQ6p)0sDeMgizGys8hrp~%3^+H*+VCG)(+d9 zV}=Il!V5f(H2D{G{)R&>rz1EhXl)sbGY%Vi;-Nz%VV_qvL`>SDD&Bg_o(0JUIS=>{ zz3Y?BP0q72AP53-7)^hG`rQyXyisB3_NZ<& zp}@e-_aTQ%ntm=6e~fmp1|!mDwr*lg6L6O&(p8XW#+8G-$v(h#zM2bHkd|X4Wovs! z$9f2FK>(_6P>FOj%6p*yPAD){W#d^@tB8-#&o^2K;WmQnRsp)1X)*V)jJ|sMfl{MD z^}@xYsYGJ*S4TwFGY7eGCv80~eLY=qkBUH&krL730H?yORamUQ<_FDvvSm#)R0gD7 zYU(yfPb{r@m8p;nSbwi9_=A{tZ&t27hSbnquZY6@d`nTwjkVKCj&_;Oi}*62&Lj0A zIJpTp6pBW&C@n6C;5VS#bAYGR`XbiA?KX!OfC@^kCUfY7oYYiv7{cDTC8?ig=*fFq z)dJwlD?Bc?YS;2%W%YZKa||8ExE~6+62PwXI57d*sr>V935h3Ay-=(@$b=Ee1;LoeLJhUMIa%S;LPy? z`pv;%XE#d})E9dP&!7$wrhfb@b1mw8IA>sOUemSkPDxP_vtH8*e~Bk8=X9ixIx+7| z4iP9H{yw_Rtt+^+yydU9Udh4-%A}S<$HM0XvrEg=dyq~)e7f(utfD8dl~$WFk{DK4 zP?lRt*&8$Yh{1icai7M|)H>*8y(F@8^jk4VQ)rx_!oUTGgZS$C1=CKsUn_gm5~4rUL7V3Wa6%APd^eZIQ3t2D#v(l73^XFt9fjao{8PYM$@&u&Ze z+O%9E^tv#oUgPm#Zh_m|^7sRo-IVzoor-W?wIU;D;5^J~5_t^FA@~m}up^dplLM5U zO#Uz-HMOiR^6lqis{{hUUFQKQCFOTmT0fsTZtK=HmwR2m{?aIVmTF187GFERefxHz z{sUO^T#(50#_}^0*(>8jGMH)fSoRpNRIMseUQf5=+0&M07m~hP4ailS%3^&r&5}V6I+l!tho~TKxxXWc9t_adD7Xrk*|W-qH_7l zIwG`mk|a>i;AtNhithk5PUhB!Q?B~Wk#GCb^t#AXGYcbq5BbOM@>!BFEg@$HG`sl&S5m zlVM^3g#K~mQ2^u`^8u}5M)n~z3w#Fv3Gf?A1-J!h`e_$818Bd%q_7&RnyfE{b^!Q3 znM$pwERJBVJqC~fQxYePGl2l0=^DFGr?vr^7lYyIz(xK^LnDfjH7~)4KzfP9p`M;W zrQ7G_)eZ-)OoDAk5vd>`y&_+JAckcX(_vzw7qBoFfco?vlsso=XM1v7E6YilJmU$H zy9V*>$mq5p=nm8lNB)9D`nnvqJ=-(7wrT+NeDN=NiZ4*{(S7+X0IY_Ghd}^B+5x5& zGiKiC?eXIf2kcbrf^df4sdFEO?wSE*CBDls8v9gc??s;IH!%dg6}KF<{ivCl$wfxo zT-xFMLLLC2uJvMxVf46Us-I`R(y@k;O5Se4Al!Mlsve_ zV6K6edz>9+88PgLB!&Jf&JIayPB~}m=@|qg2q3Ug!v^vlQ6hOGW0eOmb+lxheR_X9UK`NU(^KL9Dhcq2 zgTChuHSL?N=K!+FS4fsrJ3slU$;ntSWvyim3LU4@v&qpTWQfhc6loS92j1nx7#6`o zZ(?Mmpc{ z-KAsi-~bs=N2e2z(1gFJ{2e8-0)NP6gS@QzCRoCtVc2Vz89WU_UJhgmU}D~XTKeonv}q``yCLyFbNWq}}ZA z?{Be5gjWJDy&HlyuVx!_gHIzLAr{6ma|7ZtOO&NWat@@V>UT#&VneX|JSEzgr)4c( z$R9>oxGGC6KYH4?$t8yR>#qPhC&WB6G)G0u>&}rxpx3;ZOH9;kmznTxInLN%{`IhL zj<0^{NOXI-S|$v+rqv2f_{a|!jZ5f~{A^t<#N+nPHl975Y$m#o)&wP$-$AXT+)eI=Od{(ev3p)mr-?@&JRtW`pE&r($( zL);#K9pj4o1+$qr1|ByR2+uKuwDry%#4Rf3b3*Aqn0LC?%_;WwGpO)FAZXgK3zK^x zTe>PV2&M}18q;;N4KBWU;UNn=8^WGjt?Ps>36v2tsmVM)d{($=UL}E$y3BV_JAC1n z5P>wjtGXsmD^nWZnPRqFtJ0AUcsgh_svY3!dEBedt;Bv&%3S8C*H3mA~$!RAGQEM;NY?`b_sY z5+Wf5PKzTRI{P#29{%5O_l+-)670dw1uM0i@-Bta__WyQWD&GC?u4hlf qf4=CSA^2w&Nc}Se|C>WV)LeZY=aP3|5-V8>kJ~!^T4sIb+P?wqu2tFq literal 0 HcmV?d00001 diff --git a/conics/docs/images/conics-overview.svg b/conics/docs/images/conics-overview.svg new file mode 100644 index 0000000..51b80e6 --- /dev/null +++ b/conics/docs/images/conics-overview.svg @@ -0,0 +1,805 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conics/docs/images/conics-parabolas.png b/conics/docs/images/conics-parabolas.png new file mode 100644 index 0000000000000000000000000000000000000000..965d346384321c037b39ac0b22a6ce2c5f12117e GIT binary patch literal 106443 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 Fe*iNUqH6#E literal 0 HcmV?d00001 diff --git a/conics/examples/README.md b/conics/examples/README.md new file mode 100644 index 0000000..bafa227 --- /dev/null +++ b/conics/examples/README.md @@ -0,0 +1,10 @@ +# Conics examples + +This directory contains the editable Ipe source used for the Conics presentation: + +- `conics-overview.ipe`: a multi-page document covering a conic through five points, extracted property guides, focus-directrix construction, compact hyperbola branches, and multiple exact parabolas. +- `conics-feature-gallery.ipe`: a 32-page acceptance gallery covering every construction family, advanced feature workflows, explicit degenerate loci, transformed objects, fit-and-replace, inspection, and metadata revalidation. + +The corresponding SVG overview and real Ipe renders live under `../docs/images/`. Open either `.ipe` file to inspect, restyle, or modify every path, mark, guide, label, and group. + +The documents use only Ipe's official basic definitions plus styles embedded directly in each file. They do not require the author's personal preamble, symbolic names, or external style files. diff --git a/conics/examples/conics-feature-gallery.ipe b/conics/examples/conics-feature-gallery.ipe new file mode 100644 index 0000000..d5bc93e --- /dev/null +++ b/conics/examples/conics-feature-gallery.ipe @@ -0,0 +1,4446 @@ + + + + + + + +0 0 m +-1 0.333 l +-1 -0.333 l +h + + + + +0.6 0 0 0.6 0 0 e +0.4 0 0 0.4 0 0 e + + + + +0.6 0 0 0.6 0 0 e + + + + + +0.5 0 0 0.5 0 0 e + + +0.6 0 0 0.6 0 0 e +0.4 0 0 0.4 0 0 e + + + + + +-0.6 -0.6 m +0.6 -0.6 l +0.6 0.6 l +-0.6 0.6 l +h +-0.4 -0.4 m +0.4 -0.4 l +0.4 0.4 l +-0.4 0.4 l +h + + + + +-0.6 -0.6 m +0.6 -0.6 l +0.6 0.6 l +-0.6 0.6 l +h + + + + + +-0.5 -0.5 m +0.5 -0.5 l +0.5 0.5 l +-0.5 0.5 l +h + + +-0.6 -0.6 m +0.6 -0.6 l +0.6 0.6 l +-0.6 0.6 l +h +-0.4 -0.4 m +0.4 -0.4 l +0.4 0.4 l +-0.4 0.4 l +h + + + + + + +-0.43 -0.57 m +0.57 0.43 l +0.43 0.57 l +-0.57 -0.43 l +h + + +-0.43 0.57 m +0.57 -0.43 l +0.43 -0.57 l +-0.57 0.43 l +h + + + + + + +-2.07224 2.39104 m +-1.69365 0.95639 +-0.84999 0.27895 +0 0 c +-0.84999 -0.27895 +-1.69365 -0.95639 +-2.07224 -2.39104 c + + + + + + +-2.07224 2.39104 m +-1.69365 0.95639 +-0.84999 0.27895 +0 0 c +-0.84999 -0.27895 +-1.69365 -0.95639 +-2.07224 -2.39104 c + + +-2.07224 2.39104 m +-1.69365 0.95639 +-0.84999 0.27895 +0 0 c +-0.84999 -0.27895 +-1.69365 -0.95639 +-2.07224 -2.39104 c + + + + + + +3.7613 0 m +3.2994 0.11273 +1.26901 0.75153 +0 1.44672 c +0 -1.44672 l +1.26901 -0.75153 +3.2994 -0.11273 +3.7613 0 c +h + + + + + + +3.7613 0 m +3.2994 0.11273 +1.26901 0.75153 +0 1.44672 c +0 -1.44672 l +1.26901 -0.75153 +3.2994 -0.11273 +3.7613 0 c +h + + + + + + +4.21463 0 m +0.64343 1.34572 l +1.8178 0 l +0.64343 -1.34572 l +h + + + + + + +4.21463 0 m +0.64343 1.34572 l +1.8178 0 l +0.64343 -1.34572 l +h + + + + + + +3.17348 0 m +0.19925 1.76338 l +0.19925 0 l +0.19925 -1.76338 l +h + + + + + + +3.17348 0 m +0.19925 1.76338 l +0.19925 0 l +0.19925 -1.76338 l +h + + + + + + +5.10841 0 m +1.48842 1.20667 l +0.28177 0 l +1.48842 -1.20667 l +h + + + + + + +5.10841 0 m +1.48842 1.20667 l +0.28177 0 l +1.48842 -1.20667 l +h + + + + + + +0 2.09215 m +0.57773 2.09215 +1.04607 1.62381 +1.04607 1.04607 c +1.04607 0.46834 +0.57773 0 +0 0 c +0.57773 0 +1.04607 -0.46834 +1.04607 -1.04607 c +1.04607 -1.62381 +0.57773 -2.09215 +0 -2.09215 c + + + + + + +0 2.09215 m +0.57773 2.09215 +1.04607 1.62381 +1.04607 1.04607 c +1.04607 0.46834 +0.57773 0 +0 0 c + + + + + + +0 0 m +0.57773 0 +1.04607 -0.46834 +1.04607 -1.04607 c +1.04607 -1.62381 +0.57773 -2.09215 +0 -2.09215 c + + + + + + +0 2.29141 m +0 -2.29141 l + + + + + + +-2.07224 2.39104 m +-1.69365 0.95639 +-0.84999 0.27895 +0 0 c +-0.84999 -0.27895 +-1.69365 -0.95639 +-2.07224 -2.39104 c + + +0 2.29141 m +0 -2.29141 l + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +% ----- Meu Preâmbulo Padrão ----- +\usepackage{amsmath} +\usepackage{xcolor} +\newcommand{\wbg}[1]{{ + \setlength{\fboxsep}{0pt} + \colorbox{white}{\ensuremath{#1}} +}} +\usepackage{siunitx} +\usepackage{contour} +\contournumber{120} +\usepackage{amssymb} +\usepackage{bm} +\usepackage{physics} +\usepackage{tikz} +\usepackage{pgfplots} +\pgfplotsset{compat=newest} +\usetikzlibrary{ + patterns, + arrows, + backgrounds, + calc, + intersections, + plotmarks, + quotes, + shapes.misc, + shapes.symbols, + shapes.callouts, + through, + positioning, + shapes, + decorations.pathreplacing, + calligraphy, + arrows.meta, + bending, + decorations.markings, + decorations.pathmorphing, + decorations.pathreplacing, + decorations.shapes, + decorations.text, + math, + fpu, + spy, + shadows, + patterns.meta, + angles +} +% ----- Fim do Preâmbulo ----- + + + + + + + + + + + + + +32 44 m +47 44 l + + +49 44 m +64 44 l + + +47 48 m +47 40 l + + +49 48 m +49 40 l + + + + + + +60 744 m +60 752 l +66 748 l +60 744 l + + +66 752 m +66 744 l + + +56 748 m +72 748 l + + + + + + +60 744 m +60 752 l +66 748 l +60 744 l + + +66 752 m +66 744 l + + +56 748 m +72 748 l + + + + + + +280 724 m +288 724 l + + +304 724 m +312 724 l + + +288 724 m +4 0 0 -4 292 724 296 724 a + + +288 724 m +4 0 0 -4 292 724 296 724 a + + +288 724 m +4 0 0 -4 292 724 296 724 a + + +288 724 m +4 0 0 -4 292 724 296 724 a + + + + + + +288 724 m +4 0 0 -4 292 724 296 724 a + + +288 724 m +4 0 0 -4 292 724 296 724 a + + +288 724 m +4 0 0 -4 292 724 296 724 a + + +288 724 m +4 0 0 -4 292 724 296 724 a + + + + + + +64 88 m +74 88 l + + +86 88 m +96 88 l + + +6 0 0 6 80 88 e + + +76 84 m +84 92 l + + +76 92 m +84 84 l + + + + + + +64 88 m +74 88 l + + +86 88 m +96 88 l + + +6 0 0 6 80 88 e + + +76 84 m +84 92 l + + +76 92 m +84 84 l + + + + + + +-8 16 m +-8 12 l +8 12 l +8 16 l +h + + +-16 16 m +-8 16 l + + + + + + +-8 16 m +-8 12 l +8 12 l +8 16 l +h + + +-16 16 m +-8 16 l + + + + + + +32 16 m +32 8 l + + +28 8 m +38 8 l + + +28 6 m +36 6 l + + +30 4 m +34 4 l + + + + + + +8 8 m +16 8 l + + +16 12 m +16 4 l + + +18 12 m +18 4 l + + +18 10 m +24 16 l + + +18 6 m +24 0 l + + +18 6 m +22 2 l + + + + + + +12 20 m +19 20 l +19 28 l + + +21 23 m +21 25 l + + +21 27 m +21 29 l + + +21 19 m +21 21 l + + +28 16 m +28 24 l +21 24 l + + +21 21 m +28 21 l + + +21 27 m +28 27 l +28 32 l + + + + + + + +12 20 m +19 20 l +19 28 l + + +21 23 m +21 25 l + + +21 27 m +21 29 l + + +21 19 m +21 21 l + + +21 21 m +28 21 l + + +21 27 m +28 27 l +28 32 l + + + +0 0 m +8 0 l + + +8 0 m +8 -8 l + + + + + + +-8 16 m +-8 12 l +8 12 l +8 16 l +h + + +-16 16 m +-8 16 l + + +8 16 m +16 16 l + + + + + + +-16 0 m +-12 0 l +-10 4 l +-6 -4 l +-2 4 l +2 -4 l +6 4 l +10 -4 l +12 0 l +16 0 l + + + + + + +-8 16 m +-8 12 l +8 12 l +8 16 l +h + + +-16 16 m +-8 16 l + + +8 16 m +16 16 l + + + + + + +180 716 m +200 696 l + + +-8 16 m +-8 12 l +8 12 l +8 16 l +h + + +-16 16 m +-8 16 l + + +8 16 m +16 16 l + + + + + + + + +104 48 m +107 48 l + + +117 48 m +120 48 l + + +109 48.5 m +115.155 51.5777 l + + + + + + +60 37 m +60 38 l +52 36 l + + +48 36 m +52 36 l + + +64 36 m +60 36 l + + + + + + + + +0 1.43333 m +0 4 l + + +-8 16 m +8 16 l + + +4 12 m +4 20 l + + +4 16 m +-2 20 l +-2 12 l +4 16 l + + + + + + +8 4 m +8 12 l + + +16 16 m +10 10 l + + +16 0 m +10 6 l + + +10 6 m +14 2 l + + +0 0 m +10 0 l + + + + + + +8 4 m +8 12 l + + +16 16 m +10 10 l + + +16 0 m +10 6 l + + +10 6 m +14 2 l + + +0 0 m +10 0 l + + + + + + +16 32 m +18 32 +23 24 +20 11 +17 24 +18.698 27.32 c + + +20.3356 30.1176 m +22 32 +24 32 c + + + + + + +150 728 m +152 728 +157 720 +154 708 +151 720 +154 726 +156 728 +158 728 c + + + + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + +160 652 m +156 652 l + + + + + + +200 752 m +2 0 0 -2 200 750 200 748 a + + +200 748 m +188 748 l + + + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + +160 652 m +156 652 l + + + + + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + +160 652 m +156 652 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + + + + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + + +136 668 m +2 0 0 2 136 666 136 664 a + + +124 668 m +136 668 l + + +136 668 m +2 0 0 2 136 666 136 664 a + + + +64 18 m +66 18 l + + + + + + +8 0 m +-8 0 l + + +8 8 m +12 8 l + + + + + +62 640 m +68 640 l + + + + + +2 0 0 2 0 0 e + + +-1.41421 1.41421 m +1.41421 -1.41421 l + + +1.41421 1.41421 m +-1.41421 -1.41421 l + + + + + + +3 0 0 3 0 0 e + + +-2.12132 2.12132 m +2.12132 -2.12132 l + + +2.12132 2.12132 m +-2.12132 -2.12132 l + + + + + + +2 0 0 2 64 666 e + + +1 0 0 1 64 660 e + + + + + + +3 0 0 3 16 8 e + + +1 0 0 1 24 8 e + + + + + +192 48 m +192 42 +192 38 +190 42 +190 38 +190 32 c + + + + +200 48 m +200 34 +200 30 +198 34 +198 30 +198 24 c + + + + + +192 48 m +192 42 +192 38 +190 42 +190 38 +190 32 c + + +1 0 0 1 72 40 e + + + + + + +200 48 m +200 34 +200 30 +198 34 +198 30 +198 24 c + + +1 0 0 1 72 40 e + + + + + +192 48 m +192 42 +192 38 +190 42 +190 38 +190 32 c + + + + +200 48 m +200 34 +200 30 +198 34 +198 30 +198 24 c + + + + + +82 720 m +96 720 l + + +80 722 m +80 736 l + + + + + + + +82 720 m +96 720 l + + +80 722 m +80 736 l + +y +x + + +z + + + + + + + +6 0 0 6 68 48 e + + +68 54 m +68 56 l + + +68 42 m +68 40 l + + +66 51 m +68 44 l +70 51 l + + +14.6667 7 m +17.3333 7 l + + + + + + +6 0 0 6 68 48 e + + +66 51 m +68 44 l +70 51 l + + +14.6667 7 m +17.3333 7 l + + +0 8 m +0 6 l + + +0 -6 m +0 -8 l + + + + + + +6 0 0 6 68 48 e + + +68 54 m +68 56 l + + +68 42 m +68 40 l + + +66 51 m +68 44 l +70 51 l + + + + + + +6 0 0 6 68 48 e + + +66 51 m +68 44 l +70 51 l + + +0 8 m +0 6 l + + +0 -6 m +0 -8 l + + + + + + +6 0 0 6 72 8 e + + +78 8 m +80 8 l + + +64 8 m +66 8 l + + +73 6 m +2.23607 0 0 2.23607 72 8 71 6 a + + +70 8 m +70 6 l +68 6 l + + +74 8 m +74 6 l +76 6 l + + + + + + +6 0 0 6 72 8 e + + +73 6 m +2.23607 0 0 2.23607 72 8 71 6 a + + +70 8 m +70 6 l +68 6 l + + +74 8 m +74 6 l +76 6 l + + +0 8 m +0 6 l + + +0 -6 m +0 -8 l + + + + + + +6 0 0 6 16 24 e + + +28 16 m +30 16 l +31 19 l +33 13 l +34 16 l +36 16 l + + + + + + + +6 0 0 6 16 24 e + + +28 16 m +30 16 l +31 19 l +33 13 l +34 16 l +36 16 l + + + +20 12 m +22 12 l + + +34 12 m +36 12 l + + + + + + + +6 0 0 6 16 24 e + + +28 16 m +30 16 l +31 19 l +33 13 l +34 16 l +36 16 l + + + +8 8 m +8 6 l + + +8 -6 m +8 -8 l + + + + + + +0 24 m +10 24 l + + +32 42 m +32 32 l + + +8 47 m +9 40 l +10 47 l +11 40 l +12 47 l + + +6 0 0 6 16 24 e + + +0 24 m +10 24 l + + +32 42 m +32 32 l + + + + + + +24 25 m +24 32 l + + +16 25 m +32 25 l + + +22 23 m +26 23 l + + +16 15 m +16 0 l + + + + + + +28 24 m +32 24 l + + +30 26 m +30 22 l + + +28 8 m +32 8 l + + +24 25 m +24 32 l + + +16 25 m +32 25 l + + +22 23 m +26 23 l + + +16 15 m +16 0 l + + + + + + +6 0 0 6 64 32 e + + +64 48 m +64 16 l + + + + + + +28 24 m +32 24 l + + +30 26 m +30 22 l + + +6 0 0 6 64 32 e + + +64 48 m +64 16 l + + + + + + + +6 0 0 6 64 48 e + + +64 64 m +64 54 l + + +64 42 m +64 32 l + + +58 48 m +70 48 l + + + + + + + +6 0 0 6 24 8 e + + +18 8 m +30 8 l + + +24 14 m +24 24 l + + +24 10 m +24 0 l + + + +1 0 0 1 0 12 e + + +2 12 m +6 12 l + + +4 14 m +4 10 l + + + + + + +0 8 m +0 12 l + + +6 0 0 6 64 48 e + + +64 64 m +64 54 l + + +64 42 m +64 32 l + + +58 48 m +70 48 l + + + + + + +-2 10 m +-2 -10 l +2 -10 l +2 10 l +h + + +8 0 0 8 0 0 e + + +0 16 m +0 10 l + + +0 16 m +0 10 l + + + + + + +112 616 m +119.056 616 l + + +112 608 m +116 608 l + + +112 600 m +119.056 600 l + + +12 0 0 12 128 608 e + + + + + + +8 0 0 8 56 64 e + + +8 0 0 8 64 64 e + + +53.0718 68 m +48 68 l +48 80 l + + +53.0718 60 m +48 60 l +48 48 l + + +74.9282 60 m +80 60 l +80 48 l + + +74.9282 68 m +80 68 l +80 80 l + + + + + + + + + + + + + + +4 0 m +4 -16 l + + +4 0 m +4 -16 l + + + + + + +0 12 m +0 0 l +9 0 l +9 12 l +h + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + +\& + + + + + +0 12 m +0 0 l +9 0 l +9 12 l +h + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + +\geq1 + + + + + +0 12 m +0 0 l +9 0 l +9 12 l +h + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + +=1 + + + + + +0 12 m +0 0 l +9 0 l +9 12 l +h + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + +\& + +1 0 0 1 17 -4 e + + + + + + +0 12 m +0 0 l +9 0 l +9 12 l +h + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + +=1 + +1 0 0 1 17 -4 e + + + + + + +0 12 m +0 0 l +9 0 l +9 12 l +h + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + +\geq1 + +1 0 0 1 17 -4 e + + + + + + +0 12 m +0 0 l +9 0 l +9 12 l +h + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + +1 + +1 0 0 1 17 -4 e + + + + + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + + +1 0 0 1 17 -4 e + + +4 0 m +4 -6 l +16 0 l +4 6 l +4 0 l + + + + + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + + +8 16 m +4 16 l +4 0 l +8 0 l + + +12 16 m +8 0 0 -8 12 8 12 0 a + + + + + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + + +1 16 m +10 0 0 -10 -5 8 1 0 a +8 0 l +11.4018 0 0 11.4018 5 11 16 8 a +11.4018 0 0 11.4018 5 5 8 16 a +h + + + + + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + + +1 16 m +10 0 0 -10 -5 8 1 0 a +8 0 l +11.4018 0 0 11.4018 5 11 16 8 a +11.4018 0 0 11.4018 5 5 8 16 a +h + + +-1 4 m +8.85664 0 0 -8.85664 -4.8 -4 -1 -12 a + + + + + + +1 0 0 1 17 -4 e + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + + +1 16 m +10 0 0 -10 -5 8 1 0 a +8 0 l +11.4018 0 0 11.4018 5 11 16 8 a +11.4018 0 0 11.4018 5 5 8 16 a +h + + +-1 4 m +8.85664 0 0 -8.85664 -4.8 -4 -1 -12 a + + + + + + +1 0 0 1 17 -4 e + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + + +1 16 m +10 0 0 -10 -5 8 1 0 a +8 0 l +11.4018 0 0 11.4018 5 11 16 8 a +11.4018 0 0 11.4018 5 5 8 16 a +h + + + + + + +1 0 0 1 17 -4 e + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + + +8 4 m +4 4 l +4 -12 l +8 -12 l + + +8 4 m +8 0 0 -8 8 -4 8 -12 a + + + + + + +1 0 0 1 17 -4 e + + +3 8 m +4 8 l +0 8 l + + +0 4 m +4 4 l + + +17 8 m +16 8 l +20 8 l + + +4 4 m +4 -11 l +16 -11 l +16 4 l +h + +S +R + +17 8 m +16 8 l +20 8 l + +Q + + + + + +100 788 m +97.6906 784 l +102.309 784 l +100 788 l + + +1 0 0 1 100 788 e + + +96 784 m +104 784 l + + +98 784 m +97 782 l + + +100 784 m +99 782 l + + +102 784 m +101 782 l + + +104 784 m +103 782 l + + + + + + +100 788 m +97.6906 784 l +102.309 784 l +100 788 l + + +1 0 0 1 100 788 e + + +96 784 m +104 784 l + + +98 784 m +97 782 l + + +100 784 m +99 782 l + + +102 784 m +101 782 l + + +104 784 m +103 782 l + + +76 784 m +84 784 l + + + + + + +96 784 m +104 784 l + + +98 784 m +97 782 l + + +100 784 m +99 782 l + + +102 784 m +101 782 l + + +104 784 m +103 782 l + + + + + + +276 768 m +284 768 l + + +277 768 m +278 770 l + + +279 768 m +280 770 l + + +281 768 m +282 770 l + + +283 768 m +284 770 l + + + + + + +276 768 m +284 768 l + + +277 768 m +278 770 l + + +279 768 m +280 770 l + + +281 768 m +282 770 l + + +283 768 m +284 770 l + + +276 768 m +284 768 l + + +277 768 m +278 770 l + + +279 768 m +280 770 l + + +281 768 m +282 770 l + + +283 768 m +284 770 l + + + + + + +13.6732 15.7464 m +14.4951 14.9789 +14.7167 14.082 +14.6667 13.1667 +13 12.1667 +11.7587 11.1093 +11 9.5 +11 7.83333 +11.9355 5.76422 +13.6665 4.58969 +16.7108 4.25948 +18.5533 5.42642 +19.5311 6.90976 +20.1232 8.50105 +20.3452 9.61126 +20.2712 10.3514 +19.8907 10.6642 +19.4132 10.4781 +18.9065 9.86388 +18.108 8.58945 +16.9021 7.08516 +15.3656 7.14426 +14.616 7.81467 +14.1431 8.55081 +14.0913 9.44733 +14.4707 10.206 +15.3333 11.1667 +16.3333 12.1667 +17 12.8333 +17.3137 13.578 +17.2874 14.1405 +17.6862 15.109 +18.326 15.7458 c + + +2 0 0 2 16 20 e + + +4 0 0 4 16 19 e + + + + + + +14.512 32 m +14.4951 30.9789 +14.7167 30.082 +14.6667 29.1667 +13 28.1667 +11.7587 27.1093 +11 25.5 +11 23.8333 +11.9355 21.7642 +13.6665 20.5897 +16.7108 20.2595 +18.5533 21.4264 +19.5311 22.9098 +20.1232 24.5011 +20.3452 25.6113 +20.2712 26.3514 +19.8907 26.6642 +19.4132 26.4781 +18.9065 25.8639 +18.108 24.5895 +16.9021 23.0852 +15.3656 23.1443 +14.616 23.8147 +14.1431 24.5508 +14.0913 25.4473 +14.4707 26.206 +15.3333 27.1667 +16.3333 28.1667 +17 28.8333 +17.3137 29.578 +17.2631 30.4725 +17.0268 31.2489 +17 32 c + + +276 768 m +284 768 l + + +277 768 m +278 770 l + + +279 768 m +280 770 l + + +281 768 m +282 770 l + + +283 768 m +284 770 l + + + + + + +2 0 0 2 280 760 e + + +278 768 m +278 752 l + + +282 768 m +282 752 l + + +282 763.464 m +4 0 0 -4 280 760 278 763.464 a + + +276 768 m +284 768 l + + +277 768 m +278 770 l + + +279 768 m +280 770 l + + +281 768 m +282 770 l + + +283 768 m +284 770 l + + + + + +14.512 32 m +14.4951 30.9789 +14.7167 30.082 +14.6667 29.1667 +13 28.1667 +11.7587 27.1093 +11 25.5 +11 23.8333 +11.9355 21.7642 +13.6665 20.5897 +16.7108 20.2595 +18.5533 21.4264 +19.5311 22.9098 +20.1232 24.5011 +20.3452 25.6113 +20.2712 26.3514 +19.8907 26.6642 +19.4132 26.4781 +18.9065 25.8639 +18.108 24.5895 +16.9021 23.0852 +15.3656 23.1443 +14.616 23.8147 +14.1431 24.5508 +14.0913 25.4473 +14.4707 26.206 +15.3333 27.1667 +16.3333 28.1667 +17 28.8333 +17.3137 29.578 +17.2631 30.4725 +17.0268 31.2489 +17 32 c + + + + + +2 0 0 2 280 760 e + + +278 768 m +278 752 l + + +282 768 m +282 752 l + + +282 763.464 m +4 0 0 -4 280 760 278 763.464 a + + + + + + +80 42 m +84 42 l + + +80 36 m +84 36 l + + +100 42 m +104 42 l + + +100 36 m +104 36 l + + +84 44 m +84 36 l +88 34 l +88 46 l +h + + +100 36 m +100 44 l +96 46 l +96 34 l +h + + +88 52 m +88 28 l +96 28 l +96 52 l +h + + +86 20 m +86 24 l +88 24 l +90 32 l +94 32 l +96 24 l +98 24 l +98 20 l +92 20 l +h + + + + + + + +196 702 m +200 702 l + + +196 698 m +200 698 l + + +200 704 m +200 696 l +204 696 l +204 704 l +h + + +200 696.766 m +201 696.766 l +202 696.766 l +203 696.766 l +204 696.766 l + + +200 698.771 m +201 698.771 l +202 698.771 l +203 698.771 l +204 698.771 l + + +200 701.246 m +201 701.246 l +202 701.246 l +203 701.246 l +204 701.246 l + + +200 703.244 m +201 703.244 l +202 703.244 l +203 703.244 l +204 703.244 l + + + + +200 704 m +200 728 l +204 728 l +204 704 l +h + + +204 718 m +208 718 l + + +204 714 m +208 714 l + + +200 705.04 m +201 705.04 l +202 705.04 l +203 705.04 l +204 705.04 l + + +200 706.298 m +201 706.298 l +202 706.298 l +203 706.298 l +204 706.298 l + + +200 707.981 m +201 707.981 l +202 707.981 l +203 707.981 l +204 707.981 l + + +200 710.015 m +201 710.015 l +202 710.015 l +203 710.015 l +204 710.015 l + + +200 712.312 m +201 712.312 l +202 712.312 l +203 712.312 l +204 712.312 l + + +200 714.77 m +201 714.77 l +202 714.77 l +203 714.77 l +204 714.77 l + + +200 717.282 m +201 717.282 l +202 717.282 l +203 717.282 l +204 717.282 l + + +200 719.738 m +201 719.738 l +202 719.738 l +203 719.738 l +204 719.738 l + + +200 722.03 m +201 722.03 l +202 722.03 l +203 722.03 l +204 722.03 l + + +200 724.058 m +201 724.058 l +202 724.058 l +203 724.058 l +204 724.058 l + + +200 725.732 m +201 725.732 l +202 725.732 l +203 725.732 l +204 725.732 l + + +200 726.981 m +201 726.981 l +202 726.981 l +203 726.981 l +204 726.981 l + + + + + + + +196 702 m +200 702 l + + +196 698 m +200 698 l + + +200 704 m +200 696 l +204 696 l +204 704 l +h + + +200 696.766 m +201 696.766 l +202 696.766 l +203 696.766 l +204 696.766 l + + +200 698.771 m +201 698.771 l +202 698.771 l +203 698.771 l +204 698.771 l + + +200 701.246 m +201 701.246 l +202 701.246 l +203 701.246 l +204 701.246 l + + +200 703.244 m +201 703.244 l +202 703.244 l +203 703.244 l +204 703.244 l + + + + + + +200 704 m +200 728 l +204 728 l +204 704 l +h + + +204 718 m +208 718 l + + +204 714 m +208 714 l + + +200 705.04 m +201 705.04 l +202 705.04 l +203 705.04 l +204 705.04 l + + +200 706.298 m +201 706.298 l +202 706.298 l +203 706.298 l +204 706.298 l + + +200 707.981 m +201 707.981 l +202 707.981 l +203 707.981 l +204 707.981 l + + +200 710.015 m +201 710.015 l +202 710.015 l +203 710.015 l +204 710.015 l + + +200 712.312 m +201 712.312 l +202 712.312 l +203 712.312 l +204 712.312 l + + +200 714.77 m +201 714.77 l +202 714.77 l +203 714.77 l +204 714.77 l + + +200 717.282 m +201 717.282 l +202 717.282 l +203 717.282 l +204 717.282 l + + +200 719.738 m +201 719.738 l +202 719.738 l +203 719.738 l +204 719.738 l + + +200 722.03 m +201 722.03 l +202 722.03 l +203 722.03 l +204 722.03 l + + +200 724.058 m +201 724.058 l +202 724.058 l +203 724.058 l +204 724.058 l + + +200 725.732 m +201 725.732 l +202 725.732 l +203 725.732 l +204 725.732 l + + +200 726.981 m +201 726.981 l +202 726.981 l +203 726.981 l +204 726.981 l + + + + + + +104 728 m +104 720 l +116 720 l +116 728 l +h + + +168 672 m +168 658 l +160 658 l +160 672 l +h + + +256 680 m +280 680 l +280 684 l +276 684 l +272 692 l +264 692 l +260 684 l +256 684 l +h + + +192 666 m +196 666 l + + +192 662 m +196 662 l + + + + + + + +32 24 m +32 8 l +64 8 l +64 24 l +h + + +32 18 m +34 18 l + + +32 18 m +34 18 l + + + + +48 26 m +48 6 l + + +50 25 m +51 7 l + + +50 25 m +51 7 l + + +50 25 m +51 7 l + + +50 25 m +51 7 l + + +50 25 m +51 7 l + + +50 25 m +51 7 l + + + + + + + +100 804 m +114 804 l + + +88 804 +92 804 +92 808 +84 808 +84 804 u + + +100 804 m +114 804 l + + +88 804 +92 804 +92 808 +84 808 +84 804 u + + + + + + + +160 720 m +160 724 l + + +160 736 m +160 732 l + + +160 732 m +168 732 l + + +160 724 m +167 724 l + + +170 724 m +176 724 l +176 720 l + + + + + +96 180 m +95 171 l + + + +152 726 m +152 730 l + + +127 216 m +128 214 l +129 216 l + + +91 216 m +89 216 l + + +87 216 m +85 216 l + + +83 196 m +80 196 l + + +95.5 176 m +91 176 l + + + + + + + + +160 720 m +160 724 l + + +160 736 m +160 732 l + + +160 732 m +168 732 l + + +160 724 m +167 724 l + + +97 212 m +104 212 l +104 208 l + + + + + +96 220 m +97 211 l + + + +152 726 m +152 730 l + + + +127 216 m +128 214 l +129 216 l + + +91 216 m +89 216 l + + +87 216 m +85 216 l + + +83 196 m +80 196 l + + +96.4444 216 m +95 216 l + + +94 196 m +91 196 l + + + + + + + + +331 60 m +336 60 l +336 64 l + + + +160 720 m +160 724 l + + +160 736 m +160 732 l + + +320 60 m +325 60 l + + +128 172 m +132 172 l + + + +139 180 m +138 171 l + + +138 172 m +136 172 l +136 168 l +144 168 l + + +134 172 m +136 172 l +136 168 l +137 168 l + + +140 172 m +142 172 l +142 166 l +132 166 l +132 168 l +128 168 l + + + + + +131 180 m +130 171 l + + + +152 726 m +152 730 l + + +127 216 m +128 214 l +129 216 l + + +91 216 m +89 216 l + + +87 216 m +85 216 l + + +83 196 m +80 196 l + + +138.556 176 m +135 176 l + + +94 196 m +91 196 l + + + + + + + +139 220 m +144 220 l +144 224 l + + + +160 720 m +160 724 l + + +160 736 m +160 732 l + + +320 92 m +325 92 l + + +128 212 m +132 212 l + + + +152 726 m +152 730 l + + +134 212 m +136 212 l +136 208 l +137 208 l + + +140 212 m +142 212 l +142 206 l +132 206 l +132 208 l +128 208 l + + + + +127 216 m +128 214 l +129 216 l + + +138 212 m +136 212 l +136 208 l +144 208 l + + + +133 220 m +134 211 l + + +139 220 m +140 211 l + + + +91 216 m +89 216 l + + +87 216 m +85 216 l + + +83 196 m +80 196 l + + +139.444 216 m +135 216 l + + +94 196 m +91 196 l + + + + + + + +156 740 m +156 716 l +180 716 l +180 740 l +h + + + + + + + + + + + + + +40 618 m +40 624 l + + +56 618 m +56 624 l + + +48 618 m +48 624 l + + + + + +42 616 m +2 0 0 2 40 616 38 616 a + + +50 616 m +2 0 0 2 48 616 46 616 a + + +58 616 m +2 0 0 2 56 616 54 616 a + + +46 618 m +50 618 l + + +40 621 m +43 621 l + + +55 141 m +57 141 l + + +53 621 m +56 621 l + + +51 141 m +53 141 l + + +55 141 m +57 141 l + + +40 621 m +43 621 l + + +55 141 m +57 141 l + + +53 621 m +56 621 l + + +51 141 m +53 141 l + + +55 141 m +57 141 l + + + + + + +40 618 m +40 624 l + + +56 618 m +56 624 l + + + + +42 616 m +2 0 0 2 40 616 38 616 a + + +58 616 m +2 0 0 2 56 616 54 616 a + + +40 621 m +43 621 l + + +55 141 m +57 141 l + + +53 621 m +56 621 l + + +51 141 m +53 141 l + + +55 141 m +57 141 l + + + + + + +40 618 m +40 624 l + + +56 618 m +56 624 l + + +48 618 m +48 624 l + + + + + +42 616 m +2 0 0 2 40 616 38 616 a + + +50 616 m +2 0 0 2 48 616 46 616 a + + +58 616 m +2 0 0 2 56 616 54 616 a + + +40 621 m +43 621 l + + +55 141 m +57 141 l + + +53 621 m +56 621 l + + +51 141 m +53 141 l + + +55 141 m +57 141 l + + + + + + +208 672 m +208 668 l + + +204 668 m +212 668 l + + +205 666 m +211 666 l + + +206 665 m +210 665 l + + + + + + +72 704 m +72 700 l + + +70 700 m +74 700 l + + +70 698 m +74 698 l + + +71 696 m +73 696 l + + +4 0 0 4 72 700 e + + + + + + +20 668 m +20 660 l +28 660 l +28 668 l +h + + +22 666 m +22 662 l +26 662 l +26 666 l +h + + + + + + +20 668 m +20 660 l +28 660 l +28 668 l +h + + +24 654 m +24 649 l + + +24 654 m +24 649 l + + +24 654 m +24 649 l + + + + + + + + +190 300 m +430 320 l +285 570 l +h + + + + + + +-20.5383 172.942 -138.043 -16.3938 301.667 396.667 e + + +-10.2691 86.4711 -69.0217 -8.19688 301.667 396.667 e + + + + + + + + + + + + +0 100 100 0 300 400 e + + + + + + + + + + + + + + + + + + + + + + +-105 0 0 55 300 410 e + + + + + + +385 260 m +385 560 l + + +468.925 444.487 m +183.608 537.192 l + + +319.401 581.314 m +143.066 338.609 l + + +143.066 481.391 m +319.401 238.686 l + + +183.608 282.808 m +468.925 375.513 l + + +0 85 -85 0 300 410 e + + + + + + +390 250 m +390 550 l + + + + + + +90 0 0 90 300 400 e + + + + + + +150 310 m +450 310 l + + + + +126 496.15 m +300 243.85 +474 496.15 c + + + + + + +445 330 m +205 510 l +165 300 l +420 545 l +h + + + + + + +115.494 56.2364 -44.6021 91.6002 308.75 421.25 e + + + + + + +160 310 m +440 310 l + + + + +-500.156 973.085 m +-349.814 833.375 +-238.998 732.233 +-150.15 653.636 c +-61.3013 575.038 +5.30403 519.228 +60.2195 477.361 c +115.135 435.495 +158.191 407.702 +196.208 389.58 c +234.226 371.457 +267.087 363.061 +300 363.061 c +332.913 363.061 +365.774 371.457 +403.792 389.58 c +441.809 407.702 +484.865 435.495 +539.781 477.361 c +594.696 519.228 +661.301 575.038 +750.15 653.636 c +838.998 732.233 +949.814 833.375 +1100.16 973.085 c + + +-500.156 -588.912 m +-349.814 -449.202 +-238.998 -348.061 +-150.15 -269.464 c +-61.3013 -190.866 +5.30403 -135.055 +60.2195 -93.1889 c +115.135 -51.3224 +158.191 -23.5298 +196.208 -5.40736 c +234.226 12.715 +267.087 21.1111 +300 21.1111 c +332.913 21.1111 +365.774 12.715 +403.792 -5.40736 c +441.809 -23.5298 +484.865 -51.3224 +539.781 -93.1889 c +594.696 -135.055 +661.301 -190.866 +750.15 -269.464 c +838.998 -348.061 +949.814 -449.202 +1100.16 -588.912 c + + + + + + + +205 325 m +395 495 l + + +205 495 m +395 325 l + + +396.881 323.317 m +203.119 496.683 l + + +396.881 496.683 m +203.119 323.317 l + + + + + + +205 335 m +405 485 l + + +112 265.25 m +-112 97.25 l + + + + + + +205 475 m +405 325 l + + +112 544.75 m +-112 712.75 l + + + + + + + + + + + + + + + + +560 360 m +509.964 369.978 +474.561 377.141 +447.336 382.795 c +420.111 388.449 +401.176 392.571 +387.079 395.913 c +372.981 399.254 +363.779 401.802 +357.795 404.02 c +351.81 406.238 +349.068 408.117 +349.068 410 c +349.068 411.883 +351.81 413.762 +357.795 415.98 c +363.779 418.198 +372.981 420.746 +387.079 424.087 c +401.176 427.429 +420.111 431.551 +447.336 437.205 c +474.561 442.859 +509.964 450.022 +560 460 c + + +40 360 m +90.0357 369.978 +125.439 377.141 +152.664 382.795 c +179.889 388.449 +198.824 392.571 +212.921 395.913 c +227.019 399.254 +236.221 401.802 +242.205 404.02 c +248.19 406.238 +250.932 408.117 +250.932 410 c +250.932 411.883 +248.19 413.762 +242.205 415.98 c +236.221 418.198 +227.019 420.746 +212.921 424.087 c +198.824 427.429 +179.889 431.551 +152.664 437.205 c +125.439 442.859 +90.0357 450.022 +40 460 c + + +205.789 391.551 m +394.211 428.449 l + + +205.789 428.449 m +394.211 391.551 l + + + + + + + + + +388.801 350.38 m +366.444 366.847 +353.011 377.912 +344.377 386.975 c +335.742 396.038 +332 403 +332 410 c +332 417 +335.742 423.962 +344.377 433.025 c +353.011 442.088 +366.444 453.153 +388.801 469.62 c + + +210.123 356.749 m +369.877 463.251 l + + +210.123 463.251 m +369.877 356.749 l + + + + + + + + + +245.313 333.346 m +264.477 354.518 +275.991 368.744 +283.391 380.397 c +290.792 392.049 +294 401 +294 410 c +294 419 +290.792 427.951 +283.391 439.603 c +275.991 451.256 +264.477 465.482 +245.313 486.654 c + + +262.118 342.118 m +397.882 477.882 l + + +262.118 477.882 m +397.882 342.118 l + + + + + + + +210 320 m +400 510 l + + +210 500 m +400 310 l + + + + +-217.304 -94.7561 m +-124.766 0.0830254 +-57.7735 69.9884 +-5.71335 126.036 c +46.3467 182.083 +83.3136 224.099 +111.044 258.741 c +138.775 293.383 +157.184 320.543 +169.188 344.526 c +181.191 368.508 +186.752 389.238 +186.752 410 c +186.752 430.762 +181.191 451.492 +169.188 475.474 c +157.184 499.457 +138.775 526.617 +111.044 561.259 c +83.3136 595.901 +46.3467 637.917 +-5.71335 693.964 c +-57.7735 750.012 +-124.766 819.917 +-217.304 914.756 c + + +817.304 -94.7561 m +724.766 0.0830254 +657.773 69.9884 +605.713 126.036 c +553.653 182.083 +516.686 224.099 +488.956 258.741 c +461.225 293.383 +442.816 320.543 +430.812 344.526 c +418.809 368.508 +413.248 389.238 +413.248 410 c +413.248 430.762 +418.809 451.492 +430.812 475.474 c +442.816 499.457 +461.225 526.617 +488.956 561.259 c +516.686 595.901 +553.653 637.917 +605.713 693.964 c +657.773 750.012 +724.766 819.917 +817.304 914.756 c + + +367.882 342.118 m +232.118 477.882 l + + +367.882 477.882 m +232.118 342.118 l + + + + + + + +90 0 0 90 300 400 e + + + +390 305 m +390 495 l + + +295 400 m +485 400 l + + + + + + + + +90 0 0 90 300 400 e + + + +390.343 317.308 m +309.9 522.073 l + + + + + + + + +90 0 0 90 300 400 e + + + + + + + + + + +90 0 0 90 300 400 e + + +220.392 244.235 m +474.902 403.059 l + + +474.902 396.941 m +220.392 555.765 l + + +347.647 250 m +347.647 550 l + + + + + + + + +90 0 0 90 300 400 e + + + +$P$ + + + + + + +100 0 0 60 300 400 e + + + +200 400 m +400 400 l + + + + + + + + + +90 0 0 90 300 400 e + + +120 0 0 60 300 400 e + + +$X_1$ + +$X_2$ + +$X_3$ + +$X_4$ + + + + + +90 0 0 90 300 400 e + + +390 400 m +90 0 0 90 300 400 300 490 a + + + + + + +100 0 0 60 300 400 e + + + +$C$ + +$V_1$ + +$V_2$ + +$F_1$ + +$F_2$ + +200 400 m +400 400 l + + +300 460 m +300 340 l + + +175 304 m +175 496 l + + +425 304 m +425 496 l + + +220 436 m +220 364 l + + +380 436 m +380 364 l + + +100 0 0 100 300 400 e + + +60 0 0 60 300 400 e + + +116.619 0 0 116.619 300 400 e + +$1.90678e-06x^{2}+5.29661e-06y^{2}-0.001144068x-0.004237288y+1=0$ +$\frac{X^{2}}{10000}+\frac{Y^{2}}{3600}=1,\quad X=-(x-300),\;Y=(y-400)$ +$a=100,\;b=60,\;c=80,\;e=0.8,\;\mathcal{A}=18849.56$ + + + + + + + + + +109.772 0 0 95 300 410 e + + + + + + + + + +110 0 0 -60 300 410 e + + + + + + +170 330 m +430 330 l + + + + + +140 475.625 m +270 264.375 +400 475.625 c + + +220 500.833 m +350 219.167 +480 500.833 c + + + + + + + + + +180 440 m +300 260 +420 440 c + + + + + + +109.772 0 0 95 300 410 e + + + + +475 485 m +285 485 l + + +380 390 m +380 580 l + + + + + + + +-110 0 0 60 300 410 e + + + + + + +110 0 0 -60 300 410 e + + + diff --git a/conics/examples/conics-overview.ipe b/conics/examples/conics-overview.ipe new file mode 100644 index 0000000..348ed02 --- /dev/null +++ b/conics/examples/conics-overview.ipe @@ -0,0 +1,392 @@ + + + + + + + + + + + + + + + + + + + + + +0 0 m +0 680 l +1200 680 l +1200 0 l +h + +\textsf{\LARGE\bfseries From five points to exact, editable conics.} +\textsf{Construct, classify, inspect, and expose the geometry behind each curve.} + +40 70 m +40 555 l +390 555 l +390 70 l +h + + +430 70 m +430 555 l +780 555 l +780 70 l +h + + +820 70 m +820 555 l +1170 555 l +1170 70 l +h + + +114.193 50.8421 -29.285 65.7753 215 295 e + + + +6 0 0 6 324.006 354.501 e + + + +6 0 0 6 214.211 371.121 e + + + +6 0 0 6 102.64 286.739 e + + + +6 0 0 6 149.733 213.205 e + + + +6 0 0 6 304.487 270.282 e + + +102.714 -33.3738 20.0861 61.8187 605 300 e + + + + + + + +502.286 333.374 m +707.714 266.626 l + + +625.086 361.819 m +584.914 238.181 l + + +452.435 268.083 m +500.332 415.497 l + + +709.668 184.503 m +757.565 331.917 l + + +5.2 0 0 5.2 605 300 e + + +5.2 0 0 5.2 502.286 333.374 e + + +5.2 0 0 5.2 707.714 266.626 e + + +5.2 0 0 5.2 522.972 326.653 e + + +5.2 0 0 5.2 687.028 273.347 e + + +788 302.196 m +920 162.804 +1052 302.196 c + + +918 304.45 m +1050 195.55 +1182 304.45 c + + +855 170 m +1135 170 l + + + +6 0 0 6 920 295 e + + + +6 0 0 6 1050 330 e + +\textsf{\large\bfseries 1\enspace Fit any conic} +\textsf{Five points determine and classify the curve.} +\textsf{\large\bfseries 2\enspace Reveal its geometry} +\textsf{Axes, vertices, foci, and directrices stay editable.} +\textsf{\large\bfseries 3\enspace Build whole families} +\textsf{One directrix can generate several exact parabolas.} +\textsf{\bfseries Blue: inputs\qquad Violet: fitted curve} +\textsf{\bfseries Black: conic\qquad Gray/orange: guides} +\textsf{\bfseries Blue/orange: inputs\qquad Color: results} + + + + + +0 0 m +0 680 l +1200 680 l +1200 0 l +h + +\textsf{\LARGE\bfseries Five points. One exact conic.} +\textsf{The direct homogeneous fit is classified before Ipe chooses its native representation.} + +120 75 m +120 560 l +1080 560 l +1080 75 l +h + + +292.063 118.001 -65.5562 162.257 600 300 e + + + +6 0 0 6 872.051 449.158 e + + + +6 0 0 6 575.729 477.101 e + + + +6 0 0 6 308.758 265.07 e + + + +6 0 0 6 453.289 100.461 e + + + +6 0 0 6 855.632 272.17 e + +\textsf{\bfseries Blue: five defining marks\qquad Violet: resulting native ellipse} + + + + + +0 0 m +0 680 l +1200 680 l +1200 0 l +h + +\textsf{\LARGE\bfseries A conic is more than its outline.} +\textsf{Property guides create native, independently editable geometry for the selected curve.} + +120 60 m +120 570 l +1080 570 l +1080 60 l +h + + +272.547 -83.3259 46.7795 153.009 600 305 e + + +$C$ + +$V_1$ + +$V_2$ + +$F_1$ + +$F_2$ + +327.453 388.326 m +872.547 221.674 l + + +646.779 458.009 m +553.221 151.991 l + + +247.996 331.577 m +293.314 479.804 l + + +906.686 130.196 m +952.004 278.423 l + + +5.2 0 0 5.2 600 305 e + + +5.2 0 0 5.2 327.453 388.326 e + + +5.2 0 0 5.2 872.547 221.674 e + + +5.2 0 0 5.2 374.456 373.956 e + + +5.2 0 0 5.2 825.544 236.044 e + +\textsf{\bfseries Black: ellipse\qquad Blue: characteristic points\qquad Gray: axes\qquad Orange: directrices} + + + + + +0 0 m +0 680 l +1200 680 l +1200 0 l +h + +\textsf{\LARGE\bfseries One directrix. Several exact parabolas.} +\textsf{Every selected focus becomes an exact quadratic spline in one undoable operation.} + +100 60 m +100 570 l +1100 570 l +1100 60 l +h + + +120 454.412 m +390 25.5882 +660 454.412 c + + +340 427.606 m +610 117.394 +880 427.606 c + + +555 478.879 m +825 -23.8793 +1095 478.879 c + + +205 155 m +995 155 l + + + +6 0 0 6 390 325 e + + + +6 0 0 6 610 390 e + + + +6 0 0 6 825 300 e + +\textsf{\bfseries Orange: shared directrix\qquad Blue: foci\qquad Color: exact editable parabolas} + + + + + +0 0 m +0 680 l +1200 680 l +1200 0 l +h + +\textsf{\LARGE\bfseries Open conics without broken fragments.} +\textsf{Exact parabolas and tolerance-controlled continuous hyperbola branches remain compact and editable.} + +50 65 m +50 565 l +580 565 l +580 65 l +h + + +620 65 m +620 565 l +1150 565 l +1150 65 l +h + + +115 340.562 m +300 169.437 +485 340.562 c + + +105 155 m +525 155 l + + + +6 0 0 6 300 355 e + + +1090 187.04 m +1035.39 225.924 +1004.55 249.928 +984.86 268.87 c +965.172 287.811 +957 301.343 +957 315 c +957 328.657 +965.172 342.189 +984.86 361.13 c +1004.55 380.072 +1035.39 404.076 +1090 442.96 c + + +680 187.04 m +734.611 225.924 +765.453 249.928 +785.14 268.87 c +804.828 287.811 +813 301.343 +813 315 c +813 328.657 +804.828 342.189 +785.14 361.13 c +765.453 380.072 +734.611 404.076 +680 442.96 c + + +735.231 215.154 m +1034.77 414.846 l + + +735.231 414.846 m +1034.77 215.154 l + + + +6 0 0 6 885 315 e + +\textsf{\large\bfseries Exact focus--directrix parabola} +\textsf{\large\bfseries Adaptive hyperbola branches} +\textsf{No marching-squares fragments.} +\textsf{Subdivision follows geometric error.} + + diff --git a/conics/tests/conics_runtime.lua b/conics/tests/conics_runtime.lua new file mode 100644 index 0000000..ddb4689 --- /dev/null +++ b/conics/tests/conics_runtime.lua @@ -0,0 +1,196 @@ +-- Minimal, portable Ipe runtime used by the standalone Conics tests. +-- It intentionally defines no Geometry or MCP globals. + +local matrix_mt = {} +matrix_mt.__index = matrix_mt + +local function matrix(a, c, b, d, tx, ty) + return setmetatable({ + a = a or 1, c = c or 0, b = b or 0, d = d or 1, + tx = tx or 0, ty = ty or 0, + }, matrix_mt) +end + +function matrix_mt:coeff() + return self.a, self.c, self.b, self.d, self.tx, self.ty +end + +matrix_mt.__mul = function(left, right) + if getmetatable(right) == matrix_mt then + return matrix( + left.a * right.a + left.b * right.c, + left.c * right.a + left.d * right.c, + left.a * right.b + left.b * right.d, + left.c * right.b + left.d * right.d, + left.a * right.tx + left.b * right.ty + left.tx, + left.c * right.tx + left.d * right.ty + left.ty + ) + end + return { + x = left.a * right.x + left.b * right.y + left.tx, + y = left.c * right.x + left.d * right.y + left.ty, + } +end + +ipe = { + Vector = function(x, y) return { x = x, y = y } end, + Matrix = function(...) return matrix(...) end, +} + +local function object(kind, attributes, data, position, symbol, children) + local value = { + kind = kind, + attributes = attributes or {}, + data = data, + position_value = position, + symbol_value = symbol, + elements_value = children, + matrix_value = matrix(), + custom = "", + } + function value:type() return self.kind end + function value:shape() return self.data end + function value:matrix() return self.matrix_value end + function value:position() return self.position_value end + function value:symbol() return self.symbol_value end + function value:elements() return self.elements_value end + function value:getCustom() return self.custom end + function value:setCustom(custom) self.custom = custom end + return value +end + +function ipe.Path(attributes, shape) + return object("path", attributes, shape) +end + +function ipe.Reference(attributes, name, position) + return object("reference", attributes, nil, position, name) +end + +function ipe.Text(attributes, text, position) + local value = object("text", attributes, nil, position) + value.text = text + return value +end + +function ipe.Group(children) + return object("group", {}, nil, nil, nil, children) +end + +function ipe.Arc(transform, alpha, beta) + local value = { transform = transform, alpha = alpha, beta = beta } + function value:angles() return self.alpha, self.beta end + function value:matrix() return self.transform end + function value:endpoints() + return self.transform * { x = math.cos(self.alpha), y = math.sin(self.alpha) }, + self.transform * { x = math.cos(self.beta), y = math.sin(self.beta) } + end + return value +end + +ipeui = {} +dofile(assert(CONICS_PATH, "CONICS_PATH must name conics.lua")) +api = assert(_G.CONICS) + +function mark(x, y, symbol) + return ipe.Reference({}, symbol or "mark/disk(sx)", { x = x, y = y }) +end + +function segment(x1, y1, x2, y2) + return ipe.Path({}, { + { type = "curve", closed = false; + { type = "segment"; { x = x1, y = y1 }, { x = x2, y = y2 } } }, + }) +end + +function new_model(initial, configuration) + local entries = initial or {} + configuration = configuration or {} + local page = { active_layer_value = configuration.active_layer or "alpha" } + setmetatable(page, { + __len = function() return #entries end, + __index = function(self, key) + if type(key) == "number" then return entries[key] and entries[key].object end + return rawget(self, key) + end, + }) + function page:active() return self.active_layer_value end + function page:visible(_, value) + local layer = type(value) == "number" and entries[value] and entries[value].layer or value + if configuration.invisible_layers and configuration.invisible_layers[layer] then return false end + return true + end + function page:primarySelection() + for index, entry in ipairs(entries) do + if entry.selected == 1 then return index end + end + return nil + end + function page:objects() + local index = 0 + return function() + index = index + 1 + local entry = entries[index] + if entry then return index, entry.object, entry.selected, entry.layer or "alpha" end + end + end + function page:deselectAll() + for _, entry in ipairs(entries) do entry.selected = nil end + end + function page:insert(_, value, selected, layer) + entries[#entries + 1] = { object = value, selected = selected, layer = layer } + end + function page:remove(index) table.remove(entries, index) end + function page:replace(index, value) + assert(entries[index], "replacement index must exist") + entries[index].object = value + end + function page:setSelect(index, selection) + assert(entries[index], "selection index must exist") + entries[index].selected = selection + end + + local document = { [1] = page } + local ui = { explanations = {}, finished_tools = 0, updates = 0 } + function ui:win() return self end + function ui:explain(message) self.explanations[#self.explanations + 1] = message end + function ui:shapeTool(tool) + self.current_tool = tool + tool.setColor = function() end + tool.setShape = function(shapes) self.last_preview_shapes = shapes end + end + function ui:finishTool() + self.finished_tools = self.finished_tools + 1 + self.current_tool = nil + end + function ui:update() self.updates = self.updates + 1 end + + local model = { + pno = 1, + vno = 1, + entries = entries, + attributes = configuration.attributes or {}, + ui = ui, + registrations = {}, + } + function model:page() return page end + function model:register(registration) + self.registration = registration + self.registrations[#self.registrations + 1] = registration + registration:redo(document) + end + function model:warning(title, detail) + self.last_warning = { title = title, detail = detail } + end + return model, page, document +end + +function approximate(left, right, tolerance) + tolerance = tolerance or 1e-8 + return math.abs(left - right) <= tolerance * math.max(1, math.abs(left), math.abs(right)) +end + +function assert_contains(value, fragment) + assert(type(value) == "string" and value:find(fragment, 1, true), + "expected '" .. tostring(value) .. "' to contain '" .. fragment .. "'") +end diff --git a/conics/tests/test_advanced.py b/conics/tests/test_advanced.py new file mode 100644 index 0000000..113b9f1 --- /dev/null +++ b/conics/tests/test_advanced.py @@ -0,0 +1,518 @@ +import json +import subprocess +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "conics.lua" +RUNTIME = ROOT / "tests/conics_runtime.lua" + + +class ConicsAdvancedTest(unittest.TestCase): + maxDiff = None + + def assert_lua_passes(self, body: str) -> None: + script = ( + f"CONICS_PATH={json.dumps(str(SOURCE))}\n" + + RUNTIME.read_text(encoding="utf-8") + + "\n" + + body + ) + completed = subprocess.run( + ["lua5.4", "-e", script], + check=False, + text=True, + capture_output=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr + completed.stdout) + + def test_new_exact_fitted_dual_and_mixed_constructions(self) -> None: + self.assert_lua_passes(r''' +local samples = {} +for index = 0, 15 do + local angle = 2 * math.pi * index / 16 + samples[#samples + 1] = { + x = 12 + 8 * math.cos(angle), + y = -7 + 3 * math.sin(angle), + } +end +local fitted, diagnostics = api.conic_coefficients_from_points(samples) +local fitted_properties = api.conic_properties(fitted) +assert(fitted_properties.kind == "ellipse") +assert(diagnostics.sample_count == 16) +assert(diagnostics.rms_residual < 1e-10) +assert(approximate(fitted_properties.center.x, 12, 1e-7)) +assert(approximate(fitted_properties.center.y, -7, 1e-7)) +assert(approximate(fitted_properties.major_radius, 8, 1e-7)) +assert(approximate(fitted_properties.minor_radius, 3, 1e-7)) + +local noisy_samples = {} +local noise = { 0.018, -0.024, 0.011, -0.015, 0.027, -0.009, 0.014, -0.021 } +for index = 0, 31 do + local angle = 2 * math.pi * index / 32 + local radial_noise = noise[index % #noise + 1] + noisy_samples[#noisy_samples + 1] = { + x = 12 + (8 + radial_noise) * math.cos(angle), + y = -7 + (3 + 0.5 * radial_noise) * math.sin(angle), + } +end +local noisy_fitted, noisy_diagnostics = api.conic_coefficients_from_points(noisy_samples) +local noisy_properties = api.conic_properties(noisy_fitted) +assert(noisy_properties.kind == "ellipse") +assert(noisy_diagnostics.sample_count == 32) +assert(noisy_diagnostics.rms_residual > 0) +assert(noisy_diagnostics.rms_residual < 0.01) +assert(approximate(noisy_properties.center.x, 12, 0.02)) +assert(approximate(noisy_properties.center.y, -7, 0.02)) +assert(approximate(noisy_properties.major_radius, 8, 0.03)) +assert(approximate(noisy_properties.minor_radius, 3, 0.03)) + +local tangent_lines = {} +for index = 0, 4 do + local angle = 2 * math.pi * index / 5 + tangent_lines[#tangent_lines + 1] = { + a = math.cos(angle), b = math.sin(angle), c = -10, + } +end +local envelope = api.conic_coefficients_from_five_lines(tangent_lines) +local envelope_properties = api.conic_properties(envelope) +assert(envelope_properties.kind == "circle") +assert(approximate(envelope_properties.major_radius, 10, 1e-7)) +for _, line in ipairs(tangent_lines) do + assert(#api.conic_line_intersections(envelope, line) == 1) +end + +local mixed = api.conic_coefficients_from_constraints({ + { type = "tangent", point = { x = 10, y = 0 }, + line = { a = 1, b = 0, c = -10 } }, + { point = { x = 0, y = 10 } }, + { point = { x = -10, y = 0 } }, + { point = { x = 0, y = -10 } }, +}) +assert(api.conic_properties(mixed).kind == "circle") +local tangent = api.conic_tangent_normal(mixed, { x = 10, y = 0 }).tangent +assert(math.abs(tangent.direction.x) < 1e-8) + +for _, case in ipairs({ + { eccentricity = 0.5, kind = "ellipse" }, + { eccentricity = 1, kind = "parabola" }, + { eccentricity = 2, kind = "hyperbola" }, +}) do + local coefficients = api.conic_coefficients_for_focus_directrix( + { x = 0, y = 5 }, { a = 0, b = 1, c = 5 }, case.eccentricity + ) + assert(api.classify_conic(coefficients).kind == case.kind) +end +''') + + def test_special_constructors_and_all_degenerate_loci(self) -> None: + self.assert_lua_passes(r''' +local ellipse = api.ellipse_from_center_axes( + { x = 2, y = 3 }, { x = 7, y = 3 }, { x = 2, y = 5 } +) +local ellipse_properties = api.conic_properties(api.ellipse_coefficients(ellipse)) +assert(approximate(ellipse_properties.major_radius, 5)) +assert(approximate(ellipse_properties.minor_radius, 2)) + +local parabola, directrix = api.parabola_from_vertex_focus( + { x = 4, y = -3 }, { x = 4, y = 2 } +) +local parabola_properties = api.conic_properties(parabola) +assert(parabola_properties.kind == "parabola") +assert(approximate(parabola_properties.vertex.x, 4)) +assert(approximate(parabola_properties.vertex.y, -3)) +assert(approximate(directrix.point.y, -8)) + +local hyperbola = api.hyperbola_from_asymptotes_point( + { a = 1, b = -1, c = 0 }, { a = 1, b = 1, c = 0 }, { x = 5, y = 3 } +) +assert(api.conic_properties(hyperbola).kind == "hyperbola") +local value, magnitude = api.evaluate_conic(hyperbola, { x = 5, y = 3 }) +assert(math.abs(value) < 1e-10 * math.max(1, magnitude)) + +local cases = { + { api.degenerate_conic_from_lines({ + { a = 1, b = 0, c = 0 }, { a = 0, b = 1, c = 0 }, + }), "intersecting_lines" }, + { api.degenerate_conic_from_lines({ + { a = 1, b = 0, c = -2 }, { a = 1, b = 0, c = 2 }, + }), "parallel_lines" }, + { api.degenerate_conic_from_lines({ { a = 1, b = 0, c = -2 } }), "double_line" }, + { { 0, 0, 0, 1, 0, -2 }, "single_line" }, + { api.degenerate_point_conic({ x = 3, y = -4 }), "point" }, + { { 0, 0, 0, 0, 0, 1 }, "empty" }, +} +for _, case in ipairs(cases) do + local properties = api.conic_properties(case[1]) + assert(properties.kind == "degenerate") + assert(properties.subtype == case[2], properties.subtype) +end +''') + + def test_poles_tangents_focal_chords_and_conic_intersections(self) -> None: + self.assert_lua_passes(r''' +local circle = { 1, 0, 1, 0, 0, -25 } +local outside = api.tangents_from_point(circle, { x = 10, y = 0 }) +assert(outside.count == 2) +assert(approximate(outside.contact_points[1].x, 2.5, 1e-7)) +assert(approximate(outside.contact_points[2].x, 2.5, 1e-7)) +assert(api.tangents_from_point(circle, { x = 5, y = 0 }).count == 1) +assert(api.tangents_from_point(circle, { x = 0, y = 0 }).count == 0) + +local polar = api.conic_polar_line(circle, { x = 10, y = 0 }) +local pole = api.conic_pole(circle, polar) +assert(pole.finite) +assert(approximate(pole.point.x, 10, 1e-8)) +assert(approximate(pole.point.y, 0, 1e-8)) +local translated_circle = { 1, 0, 1, -600, -800, 241900 } +local anchored_polar = api.conic_polar_line(translated_circle, { x = 440, y = 455 }) +assert(math.sqrt((anchored_polar.point.x - 300)^2 + (anchored_polar.point.y - 400)^2) < 90) +local anchored_tangents = api.tangents_from_point(translated_circle, { x = 470, y = 400 }) +assert(math.sqrt( + (anchored_tangents.chord_of_contact.point.x - 300)^2 + + (anchored_tangents.chord_of_contact.point.y - 400)^2 +) < 90) +local infinite_pole = api.conic_pole(circle, { a = 0, b = 1, c = 0 }) +assert(infinite_pole.finite == false and infinite_pole.at_infinity == true) + +local chord = api.focal_chord( + { 1 / 25, 0, 1 / 9, 0, 0, -1 }, { x = 5, y = 0 } +) +assert(chord.count == 2) +assert(approximate(chord.endpoints[1].y, 0, 1e-8)) +assert(approximate(chord.endpoints[2].y, 0, 1e-8)) + +local two = api.conic_conic_intersections( + circle, { 1, 0, 1, -6, 0, -16 } +) +assert(#two == 2, "expected two circle intersections, got " .. #two) +local tangent = api.conic_conic_intersections( + circle, { 1, 0, 1, -20, 0, 75 } +) +assert(#tangent == 1, "expected one tangent intersection, got " .. #tangent) +local none = api.conic_conic_intersections( + circle, { 1, 0, 1, -30, 0, 200 } +) +assert(#none == 0, "expected no disjoint-circle intersections, got " .. #none) +local four = api.conic_conic_intersections( + circle, { 1 / 36, 0, 1 / 9, 0, 0, -1 } +) +assert(#four == 4, "expected four circle-ellipse intersections, got " .. #four) +local two_parabolas = api.conic_conic_intersections( + { 1, 0, 0, 0, -1, 0 }, { 1, 0, 0, 0, 1, -2 } +) +assert(#two_parabolas == 2, "expected two parabola intersections, got " .. #two_parabolas) +local coincident = api.conic_conic_intersections(circle, { -2, 0, -2, 0, 0, 50 }) +assert(coincident.infinite and coincident.coincident) + +local function translated_circle(center_x, center_y, radius) + return { + 1, 0, 1, -2 * center_x, -2 * center_y, + center_x * center_x + center_y * center_y - radius * radius, + } +end +for _, scale_case in ipairs({ + { center_x = 1e6, center_y = -2e6, radius = 10, separation = 12 }, + { center_x = 1e-6, center_y = -2e-6, radius = 1e-7, separation = 1.2e-7 }, +}) do + local scaled = api.conic_conic_intersections( + translated_circle(scale_case.center_x, scale_case.center_y, scale_case.radius), + translated_circle(scale_case.center_x + scale_case.separation, + scale_case.center_y, scale_case.radius) + ) + assert(#scaled == 2, "expected two scale-stress intersections, got " .. #scaled) +end +''') + + def test_workflows_guides_trim_and_fitted_replacement_are_transactional(self) -> None: + self.assert_lua_passes(r''' +local ellipse_result = api.create_ellipse(new_model(), { + operation = "center_axes", + center = { x = 0, y = 0 }, + first_endpoint = { x = 10, y = 0 }, + second_endpoint = { x = 0, y = 5 }, +}) +assert(ellipse_result.created, ellipse_result.error) + +local hyperbola_result = api.create_hyperbola(new_model(), { + operation = "asymptotes_point", + asymptote_a = { a = 1, b = -1, c = 0 }, + asymptote_b = { a = 1, b = 1, c = 0 }, + point = { x = 5, y = 3 }, + asymptotes = true, +}) +assert(hyperbola_result.created, hyperbola_result.error) + +local parabola_result = api.create_parabola(new_model(), { + operation = "vertex_focus", + vertex = { x = 0, y = 0 }, focus = { x = 0, y = 4 }, extent = 40, +}) +assert(parabola_result.created, parabola_result.error) + +local guides = api.create_conic_features(new_model(), { + operation = "guides", + definition = { coefficients = { 1 / 100, 0, 1 / 25, 0, 0, -1 } }, + axes = true, vertices = true, foci = true, directrices = true, + latus_recta = true, auxiliary_circles = true, director_circle = true, + general_equation = true, canonical_equation = true, parameters = true, + group_output = false, +}) +assert(guides.created, guides.error) +assert(guides.element_count >= 15) +local equation_count = 0 +for _, entry in ipairs(guides.result.properties.equations and { 1 } or {}) do + equation_count = equation_count + entry +end +assert(equation_count == 1) + +local native = ipe.Path({}, { { type = "ellipse"; ipe.Matrix(5, 0, 0, 5, 0, 0) } }) +local trim_model, _, trim_document = new_model({ + { object = native, selected = 1, layer = "alpha" }, + { object = mark(5, 0), selected = 2, layer = "alpha" }, + { object = mark(0, 5), selected = 2, layer = "alpha" }, +}) +local trimmed = api.create_conic_features(trim_model, { + operation = "conic_arc", arc_mode = "shorter", replace_original = true, +}) +assert(trimmed.created, trimmed.error) +assert(trim_model.entries[1].object:shape()[1][1].type == "arc") +trim_model.registration:undo(trim_document) +assert(trim_model.entries[1].object == native) +trim_model.registration:redo(trim_document) +assert(trim_model.entries[1].object:shape()[1][1].type == "arc") + +local rough = ipe.Path({}, { { type = "curve", closed = true; + { type = "spline"; { x = 10, y = 0 }, { x = 10, y = 5.5 }, + { x = 5.5, y = 10 }, { x = 0, y = 10 } }, + { type = "spline"; { x = 0, y = 10 }, { x = -5.5, y = 10 }, + { x = -10, y = 5.5 }, { x = -10, y = 0 } }, + { type = "spline"; { x = -10, y = 0 }, { x = -10, y = -5.5 }, + { x = -5.5, y = -10 }, { x = 0, y = -10 } }, + { type = "spline"; { x = 0, y = -10 }, { x = 5.5, y = -10 }, + { x = 10, y = -5.5 }, { x = 10, y = 0 } }, +} }) +local fit_model, _, fit_document = new_model({ + { object = rough, selected = 1, layer = "alpha" }, +}) +local replacement = api.create_conic_features(fit_model, { + operation = "fit_replace_path", expected_kind = "ellipse", +}) +assert(replacement.created, replacement.error) +assert(replacement.result.fit.rms_residual < 5e-3) +fit_model.registration:undo(fit_document) +assert(fit_model.entries[1].object == rough) +fit_model.registration:redo(fit_document) +assert(fit_model.entries[1].object ~= rough) +''') + + def test_every_new_constructor_has_a_public_workflow(self) -> None: + self.assert_lua_passes(r''' +local samples = {} +for index = 0, 11 do + local angle = 2 * math.pi * index / 12 + samples[#samples + 1] = { + x = 4 + 12 * math.cos(angle), + y = -3 + 6 * math.sin(angle), + } +end +local fitted = api.create_conic(new_model(), { + operation = "fit_points", points = samples, expected_kind = "ellipse", +}) +assert(fitted.created and fitted.result.fit.sample_count == 12, fitted.error) + +local tangent_lines = {} +for index = 0, 4 do + local angle = 2 * math.pi * index / 5 + tangent_lines[#tangent_lines + 1] = { + a = math.cos(angle), b = math.sin(angle), c = -10, + } +end +local five_tangents = api.create_conic(new_model(), { + operation = "five_tangents", lines = tangent_lines, +}) +assert(five_tangents.created, five_tangents.error) +assert(five_tangents.result.type == "five-tangent-conic") + +local mixed = api.create_conic(new_model(), { + operation = "five_conditions", + constraints = { + { type = "tangent", point = { x = 10, y = 0 }, + line = { a = 1, b = 0, c = -10 } }, + { point = { x = 0, y = 10 } }, + { point = { x = -10, y = 0 } }, + { point = { x = 0, y = -10 } }, + }, +}) +assert(mixed.created, mixed.error) +assert(mixed.result.type == "mixed-condition-conic") + +for _, case in ipairs({ + { eccentricity = 0.5, kind = "ellipse" }, + { eccentricity = 1, kind = "parabola" }, + { eccentricity = 2, kind = "hyperbola" }, +}) do + local result = api.create_conic(new_model(), { + operation = "focus_directrix_eccentricity", + focus = { x = 0, y = 5 }, + directrix = { a = 0, b = 1, c = 5 }, + eccentricity = case.eccentricity, + }) + assert(result.created, result.error) + assert(result.result.properties.kind == case.kind) +end + +for _, case in ipairs({ + { operation = "degenerate_line_pair", lines = { + { a = 1, b = 0, c = 0 }, { a = 0, b = 1, c = 0 }, + }, subtype = "intersecting_lines", elements = 2 }, + { operation = "degenerate_double_line", lines = { + { a = 1, b = 0, c = -2 }, + }, subtype = "double_line", elements = 1 }, + { operation = "degenerate_single_line", lines = { + { a = 1, b = 0, c = -2 }, + }, subtype = "single_line", elements = 1 }, + { operation = "degenerate_point", point = { x = 3, y = 4 }, + subtype = "point", elements = 1 }, +}) do + local options = { operation = case.operation } + if case.lines then options.lines = case.lines end + if case.point then options.point = case.point end + local result = api.create_conic(new_model(), options) + assert(result.created, result.error) + assert(result.result.properties.subtype == case.subtype) + assert(result.element_count == case.elements) +end +local empty_model = new_model() +local empty = api.create_conic(empty_model, { operation = "degenerate_empty" }) +assert(empty.created == false and empty.status == "empty") +assert(#empty_model:page() == 0) + +assert(api.create_ellipse == api.create_ellipse_from_foci) +assert(api.create_parabola == api.create_parabolas) +''') + + def test_every_new_feature_has_a_public_workflow(self) -> None: + self.assert_lua_passes(r''' +local circle = { 1, 0, 1, 0, 0, -100 } +local outside_tangents = api.create_conic_features(new_model(), { + operation = "tangents_from_point", + definition = { coefficients = circle }, + feature_input = { point = { x = 20, y = 0 } }, + chord = true, marks = true, group_output = false, +}) +assert(outside_tangents.created, outside_tangents.error) +assert(outside_tangents.result.tangent_count == 2) +assert(outside_tangents.element_count == 5) + +local pole = api.create_conic_features(new_model(), { + operation = "pole", definition = { coefficients = circle }, + feature_input = { line = { a = 1, b = 0, c = -5 } }, + marks = true, labels = true, group_output = false, +}) +assert(pole.created and pole.result.finite, pole.error) +assert(approximate(pole.result.point.x, 20)) + +local focal = api.create_conic_features(new_model(), { + operation = "focal_chord", + definition = { coefficients = { 1 / 100, 0, 1 / 36, 0, 0, -1 } }, + feature_input = { point = { x = 10, y = 0 } }, marks = true, +}) +assert(focal.created, focal.error) +assert(#focal.result.endpoints == 2) + +local intersections = api.create_conic_features(new_model(), { + operation = "conic_intersections", definition = { coefficients = circle }, + feature_input = { + second_coefficients = { 1, 0, 1, -12, 0, -64 }, + }, + marks = false, +}) +assert(intersections.created == false and intersections.status == "computed") +assert(intersections.result.intersection_count == 2) + +local disjoint = api.create_conic_features(new_model(), { + operation = "conic_intersections", definition = { coefficients = circle }, + feature_input = { + second_coefficients = { 1, 0, 1, -60, 0, 800 }, + }, +}) +assert(disjoint.created == false and disjoint.status == "empty") + +local coincident = api.create_conic_features(new_model(), { + operation = "conic_intersections", definition = { coefficients = circle }, + feature_input = { second_coefficients = { -2, 0, -2, 0, 0, 200 } }, +}) +assert(coincident.created == false and coincident.status == "infinite") + +local parabola = { 1, 0, 0, 0, -4, 0 } +local parabola_arc = api.create_conic_features(new_model(), { + operation = "conic_arc", definition = { coefficients = parabola }, + feature_input = { points = { { x = -4, y = 4 }, { x = 4, y = 4 } } }, + replace_original = false, +}) +assert(parabola_arc.created, parabola_arc.error) +assert(parabola_arc.result.kind == "parabola") + +local hyperbola = { 1 / 25, 0, -1 / 9, 0, 0, -1 } +local hyperbola_arc = api.create_conic_features(new_model(), { + operation = "conic_arc", definition = { coefficients = hyperbola }, + feature_input = { points = { { x = 5, y = 0 }, { x = 10, y = 3 * math.sqrt(3) } } }, + replace_original = false, +}) +assert(hyperbola_arc.created, hyperbola_arc.error) +assert(hyperbola_arc.result.kind == "hyperbola") +''') + + def test_invalid_new_inputs_fail_cleanly_without_mutation(self) -> None: + self.assert_lua_passes(r''' +local invalid_calls = { + function() + return api.create_conic(new_model(), { + operation = "five_tangents", + lines = { { a = 1, b = 0, c = 0 } }, + }) + end, + function() + return api.create_conic(new_model(), { + operation = "five_conditions", + constraints = { { point = { x = 0, y = 0 } } }, + }) + end, + function() + return api.create_ellipse(new_model(), { + operation = "center_axes", center = { x = 0, y = 0 }, + first_endpoint = { x = 5, y = 0 }, second_endpoint = { x = 5, y = 5 }, + }) + end, + function() + return api.create_hyperbola(new_model(), { + operation = "asymptotes_point", + asymptote_a = { a = 1, b = 0, c = 0 }, + asymptote_b = { a = 1, b = 0, c = -1 }, + point = { x = 2, y = 3 }, + }) + end, + function() + return api.create_parabola(new_model(), { + operation = "vertex_focus", vertex = { x = 1, y = 1 }, focus = { x = 1, y = 1 }, + }) + end, +} +for _, callback in ipairs(invalid_calls) do + local model = new_model() + local result = callback(model) + assert(result.created == false and result.status == "error") + assert(type(result.error) == "string" and result.error ~= "") + assert(#model:page() == 0) +end + +local hyperbola = { 1 / 25, 0, -1 / 9, 0, 0, -1 } +local ok, message = pcall(api.conic_arc_definition, hyperbola, { x = 5, y = 0 }, { x = -5, y = 0 }) +assert(ok == false) +assert_contains(tostring(message), "same connected branch") +''') + + +if __name__ == "__main__": + unittest.main() diff --git a/conics/tests/test_geometry.py b/conics/tests/test_geometry.py new file mode 100644 index 0000000..3bfbb8a --- /dev/null +++ b/conics/tests/test_geometry.py @@ -0,0 +1,892 @@ +import json +import subprocess +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +CONICS = ROOT / "conics.lua" +RUNTIME = ROOT / "tests/conics_runtime.lua" + + +class ConicsContractTest(unittest.TestCase): + maxDiff = None + + def run_lua(self, body: str) -> subprocess.CompletedProcess[str]: + script = ( + f"CONICS_PATH={json.dumps(str(CONICS))}\n" + + RUNTIME.read_text(encoding="utf-8") + + "\n" + + body + ) + return subprocess.run( + ["lua5.4", "-e", script], + check=False, + text=True, + capture_output=True, + ) + + def assert_lua_passes(self, body: str) -> None: + completed = self.run_lua(body) + self.assertEqual(completed.returncode, 0, completed.stderr + completed.stdout) + + def test_standalone_api_surface_and_product_isolation(self): + self.assert_lua_passes(r''' +assert(_G.GEOMETRY == nil) +assert(_G.GEOMETRY_DIALOGS == nil) +assert(_G.IPE_MCP_BRIDGE_STATE == nil) +assert(api.api_version == 1) +assert(api.version == "1.1.0") +assert(api.is_compatible(1) == true) +assert(api.is_compatible(2) == false) +for _, name in ipairs(api.required_functions) do + assert(type(api[name]) == "function", name) +end +assert(type(_G.CONICS_DIALOGS) == "table") +assert(#methods == 7) +assert(methods[1].label == "Construct: conic") +assert(methods[2].label == "Construct: ellipse") +assert(methods[3].label == "Construct: hyperbola") +assert(methods[4].label == "Construct: parabola") +assert(methods[5].label == "Features: conic") +assert(methods[6].label == "Inspect: conic") +assert(methods[7].label == "Metadata: revalidate selected conic") +''') + source = CONICS.read_text(encoding="utf-8") + for forbidden in ( + "IPE_MCP_BRIDGE_STATE", + "IPE_GEOMETRY_API", + "IPE_GEOMETRY_DIALOGS", + "_G.GEOMETRY", + "/home/", + ".codex", + "mailbox", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, source) + self.assertIn("SPDX-License-Identifier: GPL-3.0-or-later", source) + self.assertIn("Standalone construction", source) + + def test_public_creators_validate_option_types_and_nested_schemas(self): + self.assert_lua_passes(r''' +local calls = { + function() return api.create_conic(new_model(), true) end, + function() return api.create_ellipse_from_foci(new_model(), "bad") end, + function() return api.create_hyperbola(new_model(), 42) end, + function() return api.create_parabolas(new_model(), false) end, + function() return api.create_conic_features(new_model(), "bad") end, + function() return api.inspect_conic(new_model(), 42) end, +} +for _, callback in ipairs(calls) do + local result = callback() + assert(result.created == false and result.status == "error") + assert_contains(result.error, "options must be a table") +end + +local unknown_top = api.create_hyperbola(new_model(), { + operation = "parameters", center = { x = 0, y = 0 }, a = 20, b = 10, + unrelated = true, +}) +assert(unknown_top.created == false) +assert_contains(unknown_top.error, "unsupported field") + +local unknown_nested = api.create_conic(new_model(), { + operation = "five_points", + definition = { + points = { + { x = 5, y = 0 }, { x = 0, y = 5 }, { x = -5, y = 0 }, + { x = 0, y = -5 }, { x = 3, y = 4 }, + }, + typo = true, + }, +}) +assert(unknown_nested.created == false) +assert_contains(unknown_nested.error, "conic definition contains unsupported field 'typo'") + +local unknown_input = api.create_conic_features(new_model(), { + operation = "tangent", + definition = { coefficients = { 1, 0, 1, 0, 0, -25 } }, + feature_input = { point = { x = 5, y = 0 }, typo = true }, +}) +assert(unknown_input.created == false) +assert_contains(unknown_input.error, "feature input contains unsupported field 'typo'") +''') + + def test_operation_specific_parsing_and_explicit_precedence(self): + self.assert_lua_passes(r''' +local selected_ellipse = ipe.Path({}, { + { type = "ellipse"; ipe.Matrix(2, 0, 0, 1, 0, 0) }, +}) +local stale_mark = mark(2, 0) +local model = new_model({ + { object = selected_ellipse, selected = 1, layer = "alpha" }, + { object = stale_mark, selected = 2, layer = "alpha" }, +}) +local explicit_circle_points = { + { x = 110, y = 0 }, { x = 100, y = 10 }, { x = 90, y = 0 }, + { x = 100, y = -10 }, { x = 106, y = 8 }, +} +local result = api.create_conic_features(model, { + operation = "tangent", + definition = { points = explicit_circle_points }, + feature_input = { point = { x = 110, y = 0 } }, + marks = false, +}) +assert(result.created == true, result.error) +assert(approximate(result.result.point.x, 110)) + +local valid_with_irrelevant_legacy_budget = api.create_conic(new_model(), { + operation = "five_points", + points = { + { x = 5, y = 0 }, { x = 0, y = 5 }, { x = -5, y = 0 }, + { x = 0, y = -5 }, { x = 3, y = 4 }, + }, + samples = "ignored-for-an-exact-ellipse", +}) +assert(valid_with_irrelevant_legacy_budget.created == true, valid_with_irrelevant_legacy_budget.error) + +local rejected_irrelevant_unknown = api.create_hyperbola(new_model(), { + operation = "foci_point", + focus_a = { x = -20, y = 0 }, focus_b = { x = 20, y = 0 }, + point = { x = 30, y = 10 }, points = { { x = "bad", y = 0 } }, +}) +assert(rejected_irrelevant_unknown.created == false) +assert_contains(rejected_irrelevant_unknown.error, "unsupported field 'points'") +''') + + def test_line_alias_and_strict_selection_contracts(self): + self.assert_lua_passes(r''' +local vertical = { p1 = { x = 0, y = -10 }, p2 = { x = 0, y = 10 } } +local by_line = api.create_hyperbola(new_model(), { + operation = "parameters", center = { x = 0, y = 0 }, line = vertical, + a = 20, b = 10, branch = "right", asymptotes = false, +}) +local by_axis = api.create_hyperbola(new_model(), { + operation = "parameters", center = { x = 0, y = 0 }, axis = vertical, + a = 20, b = 10, branch = "right", asymptotes = false, +}) +assert(by_line.created and by_axis.created) +for index = 1, 6 do + assert(approximate(by_line.result.coefficients[index], by_axis.result.coefficients[index])) +end + +local fake_model = new_model({ + { object = mark(0, 0, "symbol/not-a-mark"), selected = 1 }, + { object = mark(10, 0, "symbol/not-a-mark"), selected = 2 }, + { object = mark(0, 10, "symbol/not-a-mark"), selected = 2 }, +}) +local fake_result = api.create_conic(fake_model, { operation = "steiner" }) +assert(fake_result.created == false) +assert_contains(fake_result.error, "Select exactly three marks") + +local extra_model = new_model({ + { object = mark(0, 0), selected = 1 }, + { object = mark(10, 0), selected = 2 }, + { object = mark(0, 10), selected = 2 }, + { object = ipe.Text({}, "extra", { x = 5, y = 5 }), selected = 2 }, +}) +local extra_result = api.create_conic(extra_model, { operation = "steiner" }) +assert(extra_result.created == false) +assert_contains(extra_result.error, "Select exactly three marks") + +local directrix = segment(-20, -10, 20, -10) +local focus = mark(0, 10) +local point = mark(0, 0) +local correct = new_model({ + { object = focus, selected = 2 }, + { object = directrix, selected = 2 }, + { object = point, selected = 1 }, +}) +assert(api.create_conic(correct, { operation = "focus_directrix_point" }).created) +local wrong = new_model({ + { object = focus, selected = 1 }, + { object = directrix, selected = 2 }, + { object = point, selected = 2 }, +}) +local wrong_result = api.create_conic(wrong, { operation = "focus_directrix_point" }) +assert(wrong_result.created == true) +assert(math.abs(api.evaluate_conic(wrong_result.result.coefficients, { x = 0, y = 10 })) < 1e-8) +assert(math.abs(api.evaluate_conic(wrong_result.result.coefficients, { x = 0, y = 0 })) > 1e-3) +''') + + def test_active_layer_attributes_and_multi_output_selection_states(self): + self.assert_lua_passes(r''' +local attributes = { + stroke = "red", pen = "fat", dashstyle = "dotted", fill = "blue", + farrow = "arrow/normal(spx)", decoration = "something", + markshape = "mark/cross(sx)", symbolsize = "large", +} +local model = new_model(nil, { + attributes = attributes, + invisible_layers = { alpha = true }, +}) +local result = api.create_conic(model, { + operation = "steiner", group_output = false, + points = { { x = 0, y = 0 }, { x = 60, y = 0 }, { x = 0, y = 40 } }, +}) +assert(result.created == true) +assert(#model.entries == 2) +assert(model.entries[1].selected == 1) +assert(model.entries[2].selected == 2) +assert(model.entries[1].layer == "alpha" and model.entries[2].layer == "alpha") +local style = model.entries[1].object.attributes +assert(style.stroke == "red" and style.pen == "fat" and style.dashstyle == "dotted") +assert(style.fill == nil and style.farrow == nil and style.decoration == nil) +assert(model.last_warning and model.last_warning.title == "Active layer is invisible") + +local hyperbola_model = new_model() +local hyperbola = api.create_hyperbola(hyperbola_model, { + operation = "parameters", center = { x = 0, y = 0 }, a = 20, b = 10, + branch = "both", asymptotes = true, group_output = false, +}) +assert(hyperbola.created and #hyperbola_model.entries == 4) +assert(hyperbola_model.entries[1].selected == 1) +for index = 2, 4 do assert(hyperbola_model.entries[index].selected == 2) end + +local grouped_model = new_model() +local grouped = api.create_hyperbola(grouped_model, { + operation = "parameters", center = { x = 0, y = 0 }, a = 20, b = 10, + branch = "both", asymptotes = true, +}) +assert(grouped.created and #grouped_model.entries == 1) +assert(grouped_model.entries[1].object:type() == "group") +assert(grouped_model.entries[1].selected == 1) +''') + + def test_metadata_namespace_roles_corruption_and_legacy_compatibility(self): + self.assert_lua_passes(r''' +local model = new_model() +local result = api.create_hyperbola(model, { + operation = "parameters", center = { x = 0, y = 0 }, a = 20, b = 10, + branch = "both", asymptotes = true, group_output = false, +}) +assert(result.created and #model.entries == 4) +for index = 1, 2 do + local custom = model.entries[index].object.custom + assert(custom:find("conics:v1", 1, true)) + assert(custom:find("role=branch", 1, true)) + assert(custom:find("coefficients=", 1, true)) +end +for index = 3, 4 do + local custom = model.entries[index].object.custom + assert(custom:find("role=asymptote", 1, true)) + assert(not custom:find("coefficients=", 1, true)) + local coefficients, information = api.parse_conic_metadata(model.entries[index].object) + assert(coefficients == nil and information.status == "auxiliary") +end + +local foreign = segment(-10, 0, 10, 0) +foreign.custom = "other-plugin:data;coefficients=1,0,1,0,0,-25" +local foreign_model = new_model({ { object = foreign, selected = 1 } }) +local foreign_result = api.inspect_conic(foreign_model, {}) +assert(foreign_result.created == false and foreign_result.status == "error") +assert_contains(foreign_result.error, "not a conic curve") + +local corrupt = model.entries[1].object +corrupt.custom = corrupt.custom:gsub("coefficients=[^;]+", "coefficients=1,2") +model.entries[1].selected = 1 +for index = 2, #model.entries do model.entries[index].selected = nil end +local corrupt_result = api.inspect_conic(model, {}) +assert(corrupt_result.status == "error") +assert_contains(corrupt_result.error, "exactly six coefficients") + +local legacy = ipe.Path({}, { { type = "ellipse"; ipe.Matrix(5, 0, 0, 5, 0, 0) } }) +legacy.custom = "geometry:conic;coefficients=1,0,1,0,0,-25" +local coefficients, information = api.parse_conic_metadata(legacy) +assert(#coefficients == 6 and information.status == "legacy") +''') + + def test_stale_metadata_detection_group_support_and_revalidation(self): + self.assert_lua_passes(r''' +local model = new_model() +local created = api.create_ellipse_from_foci(model, { + focus_a = { x = -10, y = 0 }, focus_b = { x = 10, y = 0 }, + point = { x = 0, y = 15 }, +}) +assert(created.created) +local ellipse = model.entries[1].object +ellipse.data[1][1] = ipe.Matrix(18, 0, 0, 9, 0, 0) +model.entries[1].selected = 1 +local stale = api.inspect_conic(model, {}) +assert(stale.status == "error") +assert_contains(stale.error, "stale") +local repaired = api.revalidate_metadata(model, {}) +assert(repaired.status == "updated") +assert(repaired.result.updated_object_count == 1) +local inspected = api.inspect_conic(model, {}) +assert(inspected.status == "inspected") +assert(inspected.result.properties.kind == "ellipse") + +local point = mark(18, 0) +local group = ipe.Group({ ellipse }) +local group_model = new_model({ + { object = group, selected = 1 }, + { object = point, selected = 2 }, +}) +local feature = api.create_conic_features(group_model, { + operation = "tangent", line_length = 40, marks = false, +}) +assert(feature.created == true, feature.error) +''') + + def test_feature_results_cover_created_computed_empty_and_infinite_states(self): + self.assert_lua_passes(r''' +local circle = { 1, 0, 1, 0, 0, -25 } +local tangent = api.create_conic_features(new_model(), { + operation = "tangent", + definition = { coefficients = circle }, + feature_input = { point = { x = 5, y = 0 } }, + tangent = false, +}) +assert(tangent.status == "error") +assert_contains(tangent.error, "cannot disable its tangent") + +local neither = api.create_conic_features(new_model(), { + operation = "tangent_normal", + definition = { coefficients = circle }, + feature_input = { point = { x = 5, y = 0 } }, + tangent = false, normal = false, +}) +assert(neither.status == "error") +assert_contains(neither.error, "at least the tangent or the normal") + +local computed = api.create_conic_features(new_model(), { + operation = "line_intersections", + definition = { coefficients = circle }, + feature_input = { line = { p1 = { x = -10, y = 0 }, p2 = { x = 10, y = 0 } } }, + marks = false, +}) +assert(computed.created == false and computed.status == "computed") +assert(computed.result.intersection_count == 2) + +local empty = api.create_conic_features(new_model(), { + operation = "line_intersections", + definition = { coefficients = circle }, + feature_input = { line = { p1 = { x = -10, y = 10 }, p2 = { x = 10, y = 10 } } }, +}) +assert(empty.created == false and empty.status == "empty") +assert(empty.result.intersection_count == 0) + +local infinite = api.create_conic_features(new_model(), { + operation = "line_intersections", + definition = { coefficients = { 0, 0, 0, 0, 1, 0 } }, + feature_input = { line = { p1 = { x = -10, y = 0 }, p2 = { x = 10, y = 0 } } }, +}) +assert(infinite.created == false and infinite.status == "infinite") +assert(infinite.result.infinite == true) + +local partial = api.create_parabolas(new_model(), { focus = { x = 0, y = 10 } }) +assert(partial.status == "error") +assert_contains(partial.error, "directrix and at least one focus are required") + +local focus_on_directrix = api.create_parabolas(new_model(), { + directrix = { p1 = { x = -20, y = 0 }, p2 = { x = 20, y = 0 } }, + foci = { { x = 0, y = 0 } }, +}) +assert(focus_on_directrix.status == "error") +assert_contains(focus_on_directrix.error, "focus must not lie on the directrix") +''') + + def test_numerical_stability_across_extreme_scales(self): + self.assert_lua_passes(r''' +local roots = api.conic_line_intersections( + { 1, 0, 0, -1e16, 0, 1 }, + { p1 = { x = 0, y = 0 }, p2 = { x = 1, y = 0 } } +) +assert(#roots == 2) +assert(roots[1].x > 0 and approximate(roots[1].x, 1e-16, 1e-6)) +assert(approximate(roots[2].x, 1e16, 1e-12)) + +local distant_line = api.line_from_equation(1, 0, 1e300) +assert(distant_line.point.x == -1e300) +assert(distant_line.direction.y ~= 0) + +local tiny = api.ellipse_from_foci_point( + { x = -1e-10, y = 0 }, { x = 1e-10, y = 0 }, { x = 0, y = 2e-10 } +) +assert(tiny.major_radius > tiny.minor_radius and tiny.minor_radius > 0) + +local huge_model = new_model() +local huge = api.create_ellipse_from_foci(huge_model, { + focus_a = { x = -1e200, y = 0 }, focus_b = { x = 1e200, y = 0 }, + point = { x = 0, y = 2e200 }, +}) +assert(huge.created == true, huge.error) +assert(huge.result.coefficients_available == false) +assert_contains(huge_model.entries[1].object.custom, "coordinate_space=ellipse_shape") + +local near_directrix_ok, near_directrix_error = pcall( + api.focus_directrix_conic_coefficients, + { x = 0, y = 1 }, + { p1 = { x = -1, y = 0 }, p2 = { x = 1, y = 0 } }, + { x = 0, y = 1e-15 } +) +assert(near_directrix_ok == false) +assert_contains(tostring(near_directrix_error), "too close to the directrix") + +local distant = api.hyperbola_from_foci_point( + { x = -100, y = 0 }, { x = 100, y = 0 }, { x = 1e6, y = 2000 } +) +assert(distant.point_parameter > 9) +local cubics, t_max = api.adaptive_hyperbola_cubics(distant, 1, nil, 0.25, 512) +assert(#cubics > 0 and t_max >= distant.point_parameter) +local endpoint = cubics[#cubics][4] +assert(approximate(endpoint.x, 1e6, 1e-10)) +assert(approximate(endpoint.y, 2000, 1e-10)) +''') + + def test_five_point_solver_preserves_input_and_rejects_degenerate_data(self): + self.assert_lua_passes(r''' +local points = { + { x = 5, y = 0 }, { x = 0, y = 5 }, { x = -5, y = 0 }, + { x = 0, y = -5 }, { x = 3, y = 4 }, +} +local originals = { points[1], points[2], points[3], points[4], points[5] } +local coefficients = api.conic_coefficients_from_five_points(points) +assert(#coefficients == 6) +for index = 1, 5 do assert(points[index] == originals[index]) end + +local duplicate_ok, duplicate_error = pcall(api.conic_coefficients_from_five_points, { + { x = 0, y = 0 }, { x = 0, y = 0 }, { x = 1, y = 0 }, + { x = 0, y = 1 }, { x = 1, y = 1 }, +}) +assert(duplicate_ok == false) +assert_contains(tostring(duplicate_error), "distinct points") + +local degenerate_ok, degenerate_error = pcall(api.conic_coefficients_from_five_points, { + { x = -2, y = 0 }, { x = -1, y = 0 }, { x = 0, y = 0 }, + { x = 0, y = 1 }, { x = 0, y = 2 }, +}) +assert(degenerate_ok == false) +assert(tostring(degenerate_error):find("degenerate", 1, true) + or tostring(degenerate_error):find("stable", 1, true)) +''') + + def test_exact_and_adaptive_paths_are_compact_and_continuous(self): + self.assert_lua_passes(r''' +local ellipse_model = new_model() +local ellipse = api.create_conic(ellipse_model, { + operation = "five_points", + points = { + { x = 5, y = 0 }, { x = 0, y = 5 }, { x = -5, y = 0 }, + { x = 0, y = -5 }, { x = 3, y = 4 }, + }, +}) +assert(ellipse.created) +local ellipse_shape = ellipse_model.entries[1].object:shape() +assert(#ellipse_shape == 1 and ellipse_shape[1].type == "ellipse") + +local parabola_model = new_model() +local parabola = api.create_parabolas(parabola_model, { + directrix = { p1 = { x = -40, y = 0 }, p2 = { x = 40, y = 0 } }, + foci = { { x = 0, y = 20 } }, +}) +assert(parabola.created) +local parabola_curve = parabola_model.entries[1].object:shape()[1] +assert(#parabola_curve == 1) +assert(parabola_curve[1].type == "spline" and #parabola_curve[1] == 3) + +local hyperbola_model = new_model() +local hyperbola = api.create_hyperbola(hyperbola_model, { + operation = "parameters", center = { x = 0, y = 0 }, a = 20, b = 10, + branch = "right", asymptotes = false, tolerance = 0.1, +}) +assert(hyperbola.created) +local curve = hyperbola_model.entries[1].object:shape()[1] +assert(#curve >= 1 and #curve < 256) +for index, spline in ipairs(curve) do + assert(spline.type == "spline" and #spline == 4) + if index > 1 then + assert(approximate(curve[index - 1][4].x, spline[1].x)) + assert(approximate(curve[index - 1][4].y, spline[1].y)) + end +end + +local far_points = {} +for _, item in ipairs({ { 1, 0 }, { 1, 1 }, { 1, 3 }, { -1, -1 }, { -1, 2 } }) do + local branch, parameter = item[1], item[2] + far_points[#far_points + 1] = { + x = branch * 5 * math.cosh(parameter), + y = 2 * math.sinh(parameter), + } +end +local fitted_model = new_model() +local fitted = api.create_conic(fitted_model, { + operation = "five_points", points = far_points, group_output = false, +}) +assert(fitted.created == true, fitted.error) +assert(fitted.result.properties.kind == "hyperbola") +assert(fitted.result.properties.t_max >= 3) +local fitted_curve = fitted_model.entries[1].object:shape()[1] +assert(math.abs(fitted_curve[#fitted_curve][4].y) >= math.abs(far_points[3].y) - 1e-6) +''') + + def test_public_result_schema_and_classification_properties(self): + self.assert_lua_passes(r''' +local creators = { + api.create_conic(new_model(), { + operation = "steiner", points = { { x = 0, y = 0 }, { x = 40, y = 0 }, { x = 0, y = 30 } }, + }), + api.create_ellipse_from_foci(new_model(), { + focus_a = { x = -10, y = 0 }, focus_b = { x = 10, y = 0 }, point = { x = 0, y = 15 }, + }), + api.create_hyperbola(new_model(), { + operation = "parameters", center = { x = 0, y = 0 }, a = 20, b = 10, asymptotes = false, + }), + api.create_parabolas(new_model(), { + directrix = { p1 = { x = -20, y = 0 }, p2 = { x = 20, y = 0 } }, + foci = { { x = 0, y = 10 }, { x = 5, y = 15 } }, + }), +} +for _, result in ipairs(creators) do + assert(result.created == true and result.status == "created") + assert(type(result.operation) == "string") + assert(type(result.element_count) == "number") + assert(type(result.object_count) == "number") + assert(type(result.metadata) == "string") + assert(type(result.result) == "table") +end +assert(#creators[1].result.conics == 2) +assert(creators[2].result.kind == "ellipse") +assert(creators[3].result.properties.kind == "hyperbola") +assert(creators[4].result.parabola_count == 2) +assert(#creators[4].result.conics == 2) + +local circle = api.conic_properties({ 1, 0, 1, 0, 0, -25 }) +assert(circle.kind == "circle" and #circle.vertices == 2 and #circle.foci == 2) +local parabola = api.conic_properties({ 1, 0, 0, 0, -4, 0 }) +assert(parabola.kind == "parabola" and parabola.focus and #parabola.directrices == 1) +local hyperbola = api.conic_properties({ 1, 0, -1, 0, 0, -1 }) +assert(hyperbola.kind == "hyperbola" and #hyperbola.asymptotes == 2) + +local circle_guides_model = new_model() +local circle_guides = api.create_conic_features(circle_guides_model, { + operation = "guides", + definition = { coefficients = { 1, 0, 1, 0, 0, -25 } }, + marks = true, labels = true, axes = true, vertices = true, + foci = true, directrices = true, group_output = false, +}) +assert(circle_guides.created == true, circle_guides.error) +local reference_positions = {} +local text_positions = {} +for _, entry in ipairs(circle_guides_model.entries) do + local object = entry.object + if object:type() == "reference" or object:type() == "text" then + local point = object:position() + local key = string.format("%.12g,%.12g", point.x, point.y) + local positions = object:type() == "reference" and reference_positions or text_positions + assert(not positions[key], "circle property guides contain coincident labels or marks") + positions[key] = true + end +end + +local translated = api.create_hyperbola(new_model(), { + operation = "foci_point", + focus_a = { x = 270, y = 400 }, + focus_b = { x = 390, y = 400 }, + point = { x = 430, y = 455 }, + branch = "both", asymptotes = false, +}) +assert(translated.created == true, translated.error) +assert(translated.result.properties.kind == "hyperbola") +assert(approximate(translated.result.center.x, 330)) +assert(approximate(translated.result.center.y, 400)) +''') + + def test_previews_cover_all_creators_and_track_selection_geometry(self): + self.assert_lua_passes(r''' +local model = new_model() +local previews = { + api.preview_shape_data(model, "conic", { + operation = "steiner", + points = { { x = 0, y = 0 }, { x = 40, y = 0 }, { x = 0, y = 30 } }, + }), + api.preview_shape_data(model, "ellipse_from_foci", { + focus_a = { x = -10, y = 0 }, focus_b = { x = 10, y = 0 }, point = { x = 0, y = 15 }, + }), + api.preview_shape_data(model, "hyperbola", { + operation = "parameters", center = { x = 0, y = 0 }, a = 20, b = 10, asymptotes = true, + }), + api.preview_shape_data(model, "parabolas", { + directrix = { p1 = { x = -20, y = 0 }, p2 = { x = 20, y = 0 } }, + foci = { { x = 0, y = 10 } }, + }), + api.preview_shape_data(model, "conic_features", { + operation = "tangent_normal", + definition = { coefficients = { 1, 0, 1, 0, 0, -25 } }, + feature_input = { point = { x = 5, y = 0 } }, marks = true, + }), +} +for _, preview in ipairs(previews) do + assert(preview.created == true and preview.shape_count > 0) +end +assert(#model.entries == 0) + +local selected = mark(0, 0) +local signature_model = new_model({ { object = selected, selected = 1 } }) +local first = api.preview_signature(signature_model, "conic", { operation = "steiner" }) +selected.matrix_value = ipe.Matrix(1, 0, 0, 1, 10, 0) +local second = api.preview_signature(signature_model, "conic", { operation = "steiner" }) +assert(first ~= second) +''') + + def test_dialogs_are_dynamic_persistent_and_exception_safe(self): + self.assert_lua_passes(r''' +local execute_hook +local execute_error +local accepted = true +ipeui.Dialog = function(_, title) + local dialog = { title = title, values = {}, controls = {}, enabled = {}, buttons = {} } + function dialog:add(name, kind, options) + self.controls[name] = { kind = kind, options = options or {} } + end + function dialog:set(name, value) self.values[name] = value end + function dialog:get(name) return self.values[name] end + function dialog:setEnabled(name, value) self.enabled[name] = value end + function dialog:addButton(name, _, action) self.buttons[name] = action end + function dialog:execute() + if execute_hook then execute_hook(self) end + if execute_error then error(execute_error) end + return accepted + end + return dialog +end + +local selection = { + { object = mark(0, 0), selected = 1 }, + { object = mark(60, 0), selected = 2 }, + { object = mark(0, 40), selected = 2 }, +} +local model = new_model(selection) +execute_hook = function(dialog) + assert(dialog.title == "Construct conic") + dialog:set("operation", 2) + dialog.controls.operation.options.action(dialog) + assert(dialog.enabled.steiner == false) + assert(dialog.enabled.branch == true) + dialog:set("operation", 1) + dialog.controls.operation.options.action(dialog) + dialog:set("live_preview", false) + dialog.buttons.preview() +end +local result = _G.CONICS_DIALOGS.conic(model) +assert(result.created == true, result.error) +assert(api.dialog_state.conic.live_preview == false) +assert(model.ui.finished_tools == 1) + +local remembered_mode = api.dialog_state.conic.mode +local invalid_model = new_model() +execute_hook = nil +accepted = true +local invalid = _G.CONICS_DIALOGS.conic(invalid_model) +assert(invalid.created == false) +assert(api.dialog_state.conic.mode == remembered_mode) +assert(invalid_model.ui.finished_tools == 1) + +local exception_model = new_model({ + { object = mark(-10, 0), selected = 2 }, + { object = mark(10, 0), selected = 2 }, + { object = mark(0, 15), selected = 1 }, +}) +execute_error = "dialog exploded" +local failed = _G.CONICS_DIALOGS.ellipse_from_foci(exception_model) +assert(failed.created == false and failed.status == "error") +assert_contains(failed.error, "dialog exploded") +assert(exception_model.ui.finished_tools == 1) +execute_error = nil + +local cancel_model = new_model(selection) +accepted = false +local before = #cancel_model.entries +local cancelled = _G.CONICS_DIALOGS.conic(cancel_model) +assert(cancelled == false and #cancel_model.entries == before) +assert(cancel_model.ui.finished_tools == 1) +''') + + def test_aliases_are_unambiguous_and_irrelevant_values_are_not_parsed(self): + self.assert_lua_passes(r''' +local conflict = api.create_conic(new_model(), { + operation = "steiner", construction = "steiner", + points = { { x = 0, y = 0 }, { x = 10, y = 0 }, { x = 0, y = 10 } }, +}) +assert(conflict.status == "error") +assert_contains(conflict.error, "cannot contain both 'operation'") + +local mixed = api.create_ellipse_from_foci(new_model(), { + definition = { + focus_a = { x = -10, y = 0 }, focus_b = { x = 10, y = 0 }, point = { x = 0, y = 15 }, + }, + focus_a = { x = -20, y = 0 }, +}) +assert(mixed.status == "error") +assert_contains(mixed.error, "cannot mix nested 'definition'") + +local missing_center = api.create_hyperbola(new_model(), { + operation = "parameters", + axis = { p1 = { x = 0, y = 0 }, p2 = { x = 0, y = 10 } }, + a = 20, b = 10, +}) +assert(missing_center.status == "error") +assert_contains(missing_center.error, "center is required") + +local ambiguous_definition = api.create_conic_features(new_model(), { + operation = "tangent", + definition = { + coefficients = { 1, 0, 1, 0, 0, -25 }, + points = { + { x = 5, y = 0 }, { x = 0, y = 5 }, { x = -5, y = 0 }, + { x = 0, y = -5 }, { x = 3, y = 4 }, + }, + }, + feature_input = { point = { x = 5, y = 0 } }, +}) +assert(ambiguous_definition.status == "error") +assert_contains(ambiguous_definition.error, "cannot combine coefficients") + +local no_guides_model = new_model() +local no_guides = api.create_conic_features(no_guides_model, { + operation = "properties", + definition = { coefficients = { 1, 0, 1, 0, 0, -25 } }, + create_guides = false, + line_length = "irrelevant-and-invalid", +}) +assert(no_guides.status == "inspected" and #no_guides_model.entries == 0) + +local conflicting_guides = api.create_conic_features(new_model(), { + operation = "properties", + definition = { coefficients = { 1, 0, 1, 0, 0, -25 } }, + create_guides = false, guides = true, +}) +assert(conflicting_guides.status == "error") +assert_contains(conflicting_guides.error, "cannot contain both 'create_guides'") + +local extra_coefficient = api.inspect_conic(new_model(), { + coefficients = { 1, 0, 1, 0, 0, -25, 99 }, +}) +assert(extra_coefficient.status == "error") +assert_contains(extra_coefficient.error, "exactly six conic coefficients") +''') + + def test_metadata_schema_rejects_unknown_versions_roles_and_incomplete_auxiliaries(self): + self.assert_lua_passes(r''' +local future = segment(-1, 0, 1, 0) +future.custom = "conics:v2;role=curve" +local ok_future, future_error = pcall(api.parse_conic_metadata, future) +assert(ok_future == false) +assert_contains(tostring(future_error), "Unsupported Conics metadata version") + +local unknown_role = segment(-1, 0, 1, 0) +unknown_role.custom = "conics:v1;role=banana;id=c1;kind=ellipse;source=test;trusted=true" +local ok_role, role_error = pcall(api.parse_conic_metadata, unknown_role) +assert(ok_role == false) +assert_contains(tostring(role_error), "unsupported object role") + +local incomplete = segment(-1, 0, 1, 0) +incomplete.custom = "conics:v1;role=asymptote;id=c1;kind=hyperbola;source=test" +local ok_incomplete, incomplete_error = pcall(api.parse_conic_metadata, incomplete) +assert(ok_incomplete == false) +assert_contains(tostring(incomplete_error), "not marked as trusted") + +local grouped_model = new_model() +local grouped = api.create_hyperbola(grouped_model, { + operation = "parameters", center = { x = 0, y = 0 }, a = 20, b = 10, + branch = "both", asymptotes = true, +}) +assert(grouped.created and #grouped_model.entries == 1) +local coefficients, information = api.parse_conic_metadata(grouped_model.entries[1].object) +assert(coefficients == nil and information.status == "auxiliary") +assert(information.role == "group") +''') + + def test_bounds_padding_and_scaled_exact_ellipses(self): + self.assert_lua_passes(r''' +local far_points = {} +for _, item in ipairs({ { 1, 0 }, { 1, 1 }, { 1, 3 }, { -1, -1 }, { -1, 2 } }) do + far_points[#far_points + 1] = { + x = item[1] * 5 * math.cosh(item[2]), y = 2 * math.sinh(item[2]), + } +end +local plain = api.create_conic(new_model(), { + operation = "five_points", points = far_points, padding = 0, group_output = false, +}) +local padded = api.create_conic(new_model(), { + operation = "five_points", points = far_points, padding = 40, group_output = false, +}) +assert(plain.created and padded.created) +assert(padded.result.properties.t_max > plain.result.properties.t_max) + +local bounded = api.create_conic(new_model(), { + operation = "focus_directrix_point", + focus = { x = 0, y = 10 }, + directrix = { p1 = { x = -20, y = -10 }, p2 = { x = 20, y = -10 } }, + point_on_conic = { x = 0, y = 0 }, + bounds = { left = -200, right = 200, bottom = -50, top = 250 }, + padding = 5, +}) +assert(bounded.created == true, bounded.error) +assert(bounded.result.properties.kind == "parabola") +assert(bounded.result.properties.render_extent >= 205) + +local conflicting_extent = api.create_conic(new_model(), { + operation = "focus_directrix_point", + focus = { x = 0, y = 10 }, + directrix = { p1 = { x = -20, y = -10 }, p2 = { x = 20, y = -10 } }, + point_on_conic = { x = 0, y = 0 }, extent = 100, + bounds = { left = -200, right = 200, bottom = -50, top = 250 }, +}) +assert(conflicting_extent.status == "error") +assert_contains(conflicting_extent.error, "both 'extent' and 'bounds'") + +assert(api.stable_acosh(1e300) > 690) + +for _, magnitude in ipairs({ 1e-200, 1e200 }) do + local model = new_model() + local result = api.create_conic(model, { + operation = "steiner", mode = "circumellipse", + points = { + { x = -magnitude, y = 0 }, { x = magnitude, y = 0 }, + { x = 0, y = magnitude }, + }, + }) + assert(result.created == true, result.error) + assert(model.entries[1].object:shape()[1].type == "ellipse") + assert(result.result.conics[1].properties.major_radius > 0) +end +''') + + def test_single_transaction_undo_redo_and_affine_metadata_transform(self): + self.assert_lua_passes(r''' +local model, _, document = new_model() +local created = api.create_ellipse_from_foci(model, { + focus_a = { x = -10, y = 0 }, focus_b = { x = 10, y = 0 }, point = { x = 0, y = 15 }, +}) +assert(created.created and #model.registrations == 1 and #model.entries == 1) +model.registration:undo(document) +assert(#model.entries == 0) +model.registration:redo(document) +assert(#model.entries == 1 and model.entries[1].selected == 1) + +local ellipse = model.entries[1].object +ellipse.matrix_value = ipe.Matrix(2, 0, 0, 1, 10, 0) +local inspected = api.inspect_conic(model, {}) +assert(inspected.status == "inspected", inspected.error) +assert(approximate(inspected.result.properties.center.x, 10)) +local transformed_point = { x = 10, y = 15 } +local value, magnitude = api.evaluate_conic(inspected.result.coefficients, transformed_point) +assert(math.abs(value) <= 1e-8 * math.max(1, magnitude)) +''') + + def test_public_api_is_explicit_instead_of_exporting_runtime_internals(self): + self.assert_lua_passes(r''' +assert(type(api.public_functions) == "table") +for _, name in ipairs(api.public_functions) do assert(type(api[name]) == "function", name) end +for _, internal in ipairs({ + "clone_table", "make_segment", "register_creation", "selected_objects", + "start_dialog_preview", "object_preview_shapes", +}) do + assert(api[internal] == nil, internal) +end +''') + +if __name__ == "__main__": + unittest.main() diff --git a/conics/tests/test_package.py b/conics/tests/test_package.py new file mode 100644 index 0000000..3f37f8f --- /dev/null +++ b/conics/tests/test_package.py @@ -0,0 +1,119 @@ +import hashlib +import os +import posixpath +import re +import stat +import subprocess +import tempfile +import unittest +import zipfile +from pathlib import Path + + +CONICS_ROOT = Path(__file__).resolve().parents[1] +REPOSITORY_ROOT = CONICS_ROOT.parent +PACKAGE_SCRIPT = REPOSITORY_ROOT / "scripts" / "package.sh" +VERSION = (CONICS_ROOT / "VERSION").read_text(encoding="utf-8").strip() +ARCHIVE_ROOT = f"conics-v{VERSION}" +LINK_PATTERN = re.compile(r"!?\[[^]]*\]\(([^)]+)\)") + + +class ConicsPackageTest(unittest.TestCase): + def build_package(self, destination: Path) -> tuple[Path, Path]: + environment = os.environ.copy() + environment["IPELETS_DIST_DIR"] = str(destination) + subprocess.run( + [str(PACKAGE_SCRIPT), "conics"], + cwd=REPOSITORY_ROOT, + env=environment, + check=True, + capture_output=True, + text=True, + ) + archive = destination / f"conics-v{VERSION}.zip" + checksum = destination / f"conics-v{VERSION}.zip.sha256" + return archive, checksum + + def test_archive_is_self_contained_and_has_a_valid_checksum(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + archive, checksum = self.build_package(Path(temporary_directory)) + expected_digest = hashlib.sha256(archive.read_bytes()).hexdigest() + self.assertEqual( + checksum.read_text(encoding="ascii"), + f"{expected_digest} {archive.name}\n", + ) + + with zipfile.ZipFile(archive) as package: + names = set(package.namelist()) + for relative in ( + "conics.lua", + "README.md", + "README.pt-BR.md", + "CHANGELOG.md", + "VERSION", + "LICENSE", + "NOTICE.md", + "examples/README.md", + "examples/conics-overview.ipe", + "examples/conics-feature-gallery.ipe", + "docs/images/conics-overview.svg", + "docs/images/conics-overview.png", + "docs/images/conics-advanced-workflows.png", + "docs/images/conics-five-point-live-preview.png", + "docs/images/conics-property-guides.png", + "docs/images/conics-parabolas.png", + ): + self.assertIn(f"{ARCHIVE_ROOT}/{relative}", names, relative) + + for readme in ("README.md", "README.pt-BR.md"): + readme_path = f"{ARCHIVE_ROOT}/{readme}" + content = package.read(readme_path).decode("utf-8") + self.assertNotIn("](../LICENSE)", content) + self.assertNotIn("](../NOTICE.md)", content) + for target in LINK_PATTERN.findall(content): + target = target.strip().strip("<>") + if target.startswith("#") or re.match(r"^[a-z]+://", target): + continue + target = target.split("#", 1)[0] + resolved = posixpath.normpath( + posixpath.join(posixpath.dirname(readme_path), target) + ) + self.assertIn(resolved, names, f"{readme}: {target}") + + def test_archive_is_byte_reproducible(self) -> None: + with tempfile.TemporaryDirectory() as first_directory, tempfile.TemporaryDirectory() as second_directory: + first, _ = self.build_package(Path(first_directory)) + second, _ = self.build_package(Path(second_directory)) + self.assertEqual(first.read_bytes(), second.read_bytes()) + + def test_archive_excludes_development_private_and_executable_artifacts(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + archive, _ = self.build_package(Path(temporary_directory)) + with zipfile.ZipFile(archive) as package: + names = package.namelist() + for name in names: + self.assertNotIn("/tests/", name) + self.assertNotIn("__pycache__", name) + self.assertFalse(name.endswith((".py", ".pyc", ".so", ".o")), name) + info = package.getinfo(name) + mode = (info.external_attr >> 16) & 0o777 + self.assertEqual(mode, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) + self.assertEqual(info.date_time, (1980, 1, 1, 0, 0, 0)) + + for name in names: + if not name.endswith((".lua", ".md", ".ipe", ".svg", ".txt")): + continue + content = package.read(name).decode("utf-8", errors="strict") + for forbidden in ( + "/home/", + ".codex", + "IPE_MCP_BRIDGE_STATE", + "IPE_GEOMETRY_API", + "mailbox", + "__pycache__", + ): + self.assertNotIn(forbidden, content, f"{name}: {forbidden}") + + +if __name__ == "__main__": + unittest.main() diff --git a/conics/tests/test_regressions.py b/conics/tests/test_regressions.py new file mode 100644 index 0000000..1f9aebc --- /dev/null +++ b/conics/tests/test_regressions.py @@ -0,0 +1,75 @@ +import json +import subprocess +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "conics.lua" +RUNTIME = ROOT / "tests/conics_runtime.lua" + + +class ConicsNumericalRegressionTest(unittest.TestCase): + def assert_lua_passes(self, body: str) -> None: + script = ( + f"CONICS_PATH={json.dumps(str(SOURCE))}\n" + + RUNTIME.read_text(encoding="utf-8") + + "\n" + + body + ) + completed = subprocess.run( + ["lua5.4", "-e", script], + check=False, + text=True, + capture_output=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr + completed.stdout) + + def test_quadratic_roots_keep_widely_separated_intersections(self) -> None: + self.assert_lua_passes(r''' +local roots = api.conic_line_intersections( + { 1, 0, 0, -1e16, 0, 1 }, + { p1 = { x = 0, y = 0 }, p2 = { x = 1, y = 0 } } +) +assert(#roots == 2) +assert(roots[1].x > 0 and approximate(roots[1].x, 1e-16, 1e-6)) +assert(approximate(roots[2].x, 1e16, 1e-12)) +''') + + def test_open_conic_paths_are_compact_continuous_and_finite(self) -> None: + self.assert_lua_passes(r''' +local model = new_model() +local result = api.create_hyperbola(model, { + operation = "parameters", center = { x = 0, y = 0 }, a = 30, b = 12, + branch = "right", asymptotes = false, tolerance = 0.05, +}) +assert(result.created == true, result.error) +local curve = model.entries[1].object:shape()[1] +assert(#curve > 0 and #curve < 512) +for index, spline in ipairs(curve) do + assert(#spline == 4) + for _, point in ipairs(spline) do + assert(api.finite_number(point.x) and api.finite_number(point.y)) + end + if index > 1 then + assert(approximate(curve[index - 1][4].x, spline[1].x)) + assert(approximate(curve[index - 1][4].y, spline[1].y)) + end +end +''') + + def test_five_point_fit_rejects_nearly_collapsed_data(self) -> None: + self.assert_lua_passes(r''' +local ok, message = pcall(api.conic_coefficients_from_five_points, { + { x = 0, y = 0 }, { x = 1, y = 1e-14 }, { x = 2, y = 2e-14 }, + { x = 3, y = 3e-14 }, { x = 4, y = 4.000000000001e-14 }, +}) +assert(ok == false) +assert(tostring(message):find("stable", 1, true) + or tostring(message):find("degenerate", 1, true) + or tostring(message):find("ill-conditioned", 1, true)) +''') + + +if __name__ == "__main__": + unittest.main() diff --git a/conics/tests/test_release_contract.py b/conics/tests/test_release_contract.py new file mode 100644 index 0000000..0d9cd3c --- /dev/null +++ b/conics/tests/test_release_contract.py @@ -0,0 +1,70 @@ +import re +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "conics.lua" +VERSION = (ROOT / "VERSION").read_text(encoding="utf-8").strip() + + +class ConicsReleaseContractTest(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.source = SOURCE.read_text(encoding="utf-8") + + def test_release_metadata_is_present_and_synchronized(self) -> None: + self.assertEqual(VERSION, "1.1.0") + self.assertIn("-- Conics", self.source) + self.assertIn("Copyright (C) 2026 japbcoelho", self.source) + self.assertIn("SPDX-License-Identifier: GPL-3.0-or-later", self.source) + self.assertIn(f"Conics {VERSION}", self.source) + self.assertIn('label = "Conics"', self.source) + self.assertIn("goodies.lua", self.source) + + def test_runtime_source_is_standalone_and_has_no_local_infrastructure(self) -> None: + self.assertIsNone(re.search(r"\b(?:dofile|loadfile|require)\s*\(", self.source)) + for forbidden in ( + "_G.GEOMETRY", + "_G.GEOMETRY_DIALOGS", + "IPE_MCP", + ".codex", + "/home/", + "mailbox", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, self.source) + + def test_public_menu_contains_exactly_seven_tools(self) -> None: + menu = self.source[self.source.index("methods = {") :] + expected = [ + "Construct: conic", + "Construct: ellipse", + "Construct: hyperbola", + "Construct: parabola", + "Features: conic", + "Inspect: conic", + "Metadata: revalidate selected conic", + ] + labels = re.findall(r'\{ label = "([^"]+)", run = ', menu) + self.assertEqual(labels, expected) + + def test_documentation_assets_exist(self) -> None: + for relative in ( + "examples/conics-overview.ipe", + "examples/conics-feature-gallery.ipe", + "docs/images/conics-overview.svg", + "docs/images/conics-overview.png", + "docs/images/conics-advanced-workflows.png", + "docs/images/conics-five-point-live-preview.png", + "docs/images/conics-property-guides.png", + "docs/images/conics-parabolas.png", + ): + with self.subTest(relative=relative): + path = ROOT / relative + self.assertTrue(path.is_file(), relative) + self.assertGreater(path.stat().st_size, 0, relative) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/package.sh b/scripts/package.sh index 2518ced..75ce1c1 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -28,8 +28,10 @@ package_root="$stage_root/$ipelet_name-v$version" mkdir -p -- "$package_root/docs/images" "$package_root/examples" "$archive_dir" install -m 0644 -- "$ipelet_root/$ipelet_name.lua" "$package_root/$ipelet_name.lua" -sed 's#](../LICENSE)#](LICENSE)#g' "$ipelet_root/README.md" > "$package_root/README.md" -sed 's#](../LICENSE)#](LICENSE)#g' "$ipelet_root/README.pt-BR.md" > "$package_root/README.pt-BR.md" +sed -e 's#](../LICENSE)#](LICENSE)#g' -e 's#](../NOTICE.md)#](NOTICE.md)#g' \ + "$ipelet_root/README.md" > "$package_root/README.md" +sed -e 's#](../LICENSE)#](LICENSE)#g' -e 's#](../NOTICE.md)#](NOTICE.md)#g' \ + "$ipelet_root/README.pt-BR.md" > "$package_root/README.pt-BR.md" chmod 0644 "$package_root/README.md" "$package_root/README.pt-BR.md" install -m 0644 -- "$ipelet_root/CHANGELOG.md" "$package_root/CHANGELOG.md" install -m 0644 -- "$version_file" "$package_root/VERSION" @@ -41,7 +43,30 @@ install -m 0644 -- "$ipelet_root/examples/"*.ipe "$package_root/examples/" ( cd -- "$stage_root" - python3 -m zipfile -c "$archive" "$ipelet_name-v$version" + python3 - "$archive" "$ipelet_name-v$version" <<'PY' +import pathlib +import stat +import sys +import zipfile + +archive = pathlib.Path(sys.argv[1]) +package_root = pathlib.Path(sys.argv[2]) +files = sorted(path for path in package_root.rglob("*") if path.is_file()) + +with zipfile.ZipFile( + archive, + mode="x", + compression=zipfile.ZIP_DEFLATED, + compresslevel=9, +) as package: + for path in files: + info = zipfile.ZipInfo(path.as_posix(), date_time=(1980, 1, 1, 0, 0, 0)) + info.create_system = 3 + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = (stat.S_IFREG | 0o644) << 16 + package.writestr(info, path.read_bytes(), compress_type=zipfile.ZIP_DEFLATED, + compresslevel=9) +PY ) python3 - "$archive" "$checksum" <<'PY' diff --git a/scripts/validate.sh b/scripts/validate.sh index 2b0c750..d586a48 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -13,8 +13,28 @@ if ! command -v lua5.4 >/dev/null 2>&1; then exit 2 fi -luac5.4 -p "$repo_root/circles/circles.lua" -python3 -m unittest discover -s "$repo_root/circles/tests" -v +ipelets=() +if (( $# > 0 )); then + ipelets=("$@") +else + for version_file in "$repo_root"/*/VERSION; do + [[ -f "$version_file" ]] || continue + ipelets+=("$(basename -- "${version_file%/VERSION}")") + done +fi + +for ipelet_name in "${ipelets[@]}"; do + ipelet_root="$repo_root/$ipelet_name" + source_file="$ipelet_root/$ipelet_name.lua" + tests_dir="$ipelet_root/tests" + if [[ ! -f "$source_file" || ! -d "$tests_dir" ]]; then + printf 'Unknown or incomplete ipelet: %s\n' "$ipelet_name" >&2 + exit 3 + fi + printf 'Validating %s\n' "$ipelet_name" + luac5.4 -p "$source_file" + PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s "$tests_dir" -v +done if git -C "$repo_root" rev-parse --git-dir >/dev/null 2>&1; then git -C "$repo_root" diff --check