docs: sync wiki from docs/users (0037234)

gitea-actions
2026-08-23 11:42:22 +00:00
parent 602d21405c
commit 6a56908993
4 changed files with 810 additions and 213 deletions
+103
@@ -0,0 +1,103 @@
# Micro Markdown
Every bit of text on the map — station titles, station subtitles, and
free-standing text boxes — accepts a small set of formatting markers typed
directly into the text itself. No separate bold/italic controls to hunt for:
type the marker, the map renders it styled.
It's called "micro" markdown on purpose. It's not the full Markdown you'd
write a README in — no headings, links, or lists — just enough inline
emphasis to make a label pop.
## Syntax
| You type | You get |
|---|---|
| `**bold**` | **bold** |
| `*italic*` | *italic* |
| `__underline__` | underlined text |
| `~~strikethrough~~` | struck-through text |
| `[#ff0000]colored[/]` | text in that hex color |
| `[w300]weighted[/]` | text at an explicit font weight |
| `24^th^` | superscript |
| `H~2~O` | subscript |
A few examples:
```
**Oxford Circus**
Meet at *platform 3*
__Zone 1__ station
~~Closed~~ reopening soon
[#e32017]Central line[/] interchange
[w300]light[/] and [w900]heavy[/] in the same label
King's Cross 5^th^ entrance
```
## Nesting
Markers combine freely. Wrap one inside another to stack effects:
```
**bold [#ff0000]and red[/]**
```
renders as bold text where the words "and red" are also red.
## Color
Color tags take a hex value only — 6 digits (`[#ff0000]`) or the 3-digit
shorthand (`[#f00]`, same as `#ff0000`). There's no named-color list to
memorize; if you know the hex, you can use it. `[/]` closes the nearest
still-open color (or weight) tag.
## Explicit weight
`**bold**` is really just shorthand for `[w700]bold[/]` — the conventional
"bold" weight. If you want something lighter or heavier than that, set the
weight directly: any number from 1 to 1000, e.g. `[w300]` for a light weight
or `[w900]` for a very heavy one. Whether the difference is visible depends
on the font — see the font weight note in the
[prefab scripting guide](prefab_scripting_guide.md#text-fields) for how that
works.
## Superscript and subscript
`^` raises a bit of text, `~` lowers it — handy for ordinals (`24^th^ Street`),
footnote marks (`Baker Street^*^`), and the occasional formula (`H~2~O`). The
raised text shrinks and shifts automatically; you don't set a size for it.
These two have one extra rule the other markers don't: **no spaces inside**.
`^st^` works, `^a b^` doesn't. That's deliberate — carets and tildes turn up in
ordinary label text ("Terminal 2 ^ 3", "~5 mins to the pier"), and without the
rule every stray one would start swallowing words. If there's a space in
between, it's treated as plain text and left alone.
Note that `~~` is still strikethrough. A single `~` is a subscript, a double
`~~` strikes through — the same way `*` is italic and `**` is bold.
## Unclosed or malformed markers
If a marker never finds its match — a `**` with no closing `**` before the
text ends, a `[#ff0000]` with no `[/]` — it's left as plain text, asterisks,
brackets and all, rather than swallowing the rest of the label or breaking
the render. A typo shouldn't eat your text.
## Glyph shortcodes still work
If your map has custom glyphs imported, `:alias:` shortcodes work exactly as
before, and can appear inside or alongside markdown:
```
**Oxford Circus :interchange:**
```
## Where it applies
- Station titles and subtitles
- Free-standing text box objects (the "text" tool)
It works the same way whether you're typing directly into a text field on
the canvas, or setting `text.text` from a
[prefab script](prefab_scripting_guide.md) — the markup lives in the string
itself either way.
+471
@@ -0,0 +1,471 @@
# Writing Prefab Scripts
Scripted prefabs let you generate the contents of a prefab from a small Lua
script instead of hand-drawing everything once and copy-pasting it around the
map. Declare a few parameters at the top of your script, then use those
parameters to draw boxes and text labels. Every time you place a new instance
of the prefab, change one of its parameters, or edit the script itself, the
script re-runs and the instance's content is regenerated from scratch.
A quick note before you dive in: the editor UI for attaching a script to a
prefab and editing it in-app isn't wired up yet. This guide documents the
scripting language itself — the part that already works under the hood —
so you know what to write once that UI lands.
## What a script actually does
A prefab script is a normal Lua script. There's no `generate()` function or
special entry point — the whole file just runs top to bottom, every time.
Along the way it can:
1. Declare the parameters the prefab exposes (`declareParams{...}`).
2. Read the current values of those parameters (`params.<name>.value`).
3. Create canvas objects (`CanvasObject.new(...)`) and set their fields.
Whatever objects your script creates during that run become the instance's
content. There's no separate "commit" step — creating an object registers it
immediately.
The script re-runs automatically:
- When a new instance of the prefab is placed.
- Whenever that instance's parameter values change.
- Whenever the script's source is edited — this regenerates **every**
existing instance of the prefab, not just new ones.
## Declaring parameters
Parameters are declared once, with a call to `declareParams`:
```lua
params = declareParams{
{ name = "label_text", type = "string", default = "Interchange" },
}
```
Note it's a real function call (`declareParams{...}`, Lua's sugar for
`declareParams({...})`), not a bare table you assign to `params` yourself.
The table you pass in is a plain ordered list of field descriptors. What
`declareParams` hands back is different — a table keyed by name, so the rest
of your script reads values as `params.label_text.value`, not by position.
Five field types are supported, all flat — there's no `list` or `object`
type, so a param can't nest another param inside it:
| `type` | Extra keys | What `.value` looks like in Lua |
|---|---|---|
| `"string"` | `default` | a string |
| `"number"` | `default`, `min`, `max`, `decimals` | a number |
| `"boolean"` | `default` | `true` / `false` |
| `"color"` | `default` (a hex string) | a hex string, e.g. `"#da291c"` |
| `"select"` | `options` (a list of strings), `default` | a string, always one of `options` |
Every field descriptor also takes an optional `section` — a plain string.
Fields sharing the same `section` get grouped into one collapsible section in
the properties panel, in the order they first appear; a field with no
`section` just renders as its own row. There's currently no way to declare a
variable-length repeated row of fields where the row *contents* are
user-authored per instance (the old `list`-of-`object` pattern) — that
capability was dropped along with `list`/`object` and hasn't been replaced
yet. If the row's length and contents instead come from existing map data —
one card per metro line, say — you don't need a declared list at all: loop
over `Lines.all` directly (see "Reading metro lines" below and Full example
2).
**Naming convention:** capitalize `name` and `section` values —
`Label_Text` rather than `label_text`, `"Appearance"` rather than
`"appearance"`. `name` still doubles as the Lua accessor key
(`params.Label_Text.value`), so keep underscores instead of spaces there;
`section` is never accessed from Lua, so a plain title-case phrase like
`"Route Colors"` is fine.
### Select
`select` is a string param constrained to a fixed set of choices, rendered as
a dropdown in the properties panel instead of a free text field:
```lua
params = declareParams{
{ name = "shape", type = "select", options = {"circle", "square", "diamond"}, default = "circle" },
}
```
`options` is required and can't be empty — a script that declares a `select`
param without one fails the whole run, same as any other bad declaration.
`params.shape.value` reads back as a plain string, exactly like `string`. If
a `default` isn't one of `options` (or is missing), the value falls back to
the first entry in `options`.
`min`, `max`, and `decimals` on a `number` param are just metadata describing
how the value should be presented and edited — the script engine doesn't
clamp anything to them itself.
### Colors
Color values are always plain hex strings, both when you declare a default
and when you read `params.<name>.value` back in your script — `"#DA291C"`.
6 digits means fully opaque (`RRGGBB`); if you need to control opacity too
you can use 8 digits, alpha first (`AARRGGBB`), e.g. `"#80DA291C"`.
### `list` and `object` — nesting
`list` needs an `item` describing the schema of every element. `object`
needs a `fields` list, and each entry in that list is itself a full field
descriptor (name + type + whatever else that type needs) — which means a
`list` of `object`s, or an `object` with a `list` field, or deeper, all just
fall out of the same recursive shape. This is what a "row of colored route
badges" — a list of `{number, color}` pairs — looks like declared:
```lua
params = declareParams{
{ name = "routes", type = "list", item = {
type = "object",
fields = {
{ name = "number", type = "string" },
{ name = "color", type = "color" },
}
}
},
}
```
Reading it back is a normal Lua loop over a table of tables:
```lua
for i, route in ipairs(params.routes.value) do
-- route.number is a string, route.color is a hex string
end
```
## Creating canvas objects
```lua
local box = CanvasObject.new("box", {
x = -60,
y = -20,
width = 120,
height = 40,
})
```
`CanvasObject.new(kind, fields)` creates an object and optionally applies a
table of fields immediately. You can also set or change fields afterward:
```lua
local label = CanvasObject.new("text")
label.text = "Central"
label.textAlign = TextAlign.center
```
Right now only two kinds are supported:
### Shared fields
Every generated canvas object supports the same placement fields:
| Field | Meaning | Default if unset |
|---|---|---|
| `name` | object name | `""` |
| `x`, `y` | position, relative to the instance's anchor | `0`, `0` |
| `width`, `height` | layout box size | kind-specific |
| `anchorX`, `anchorY` | where the position sits inside the box, `0`-`1` | `0.5`, `0.5` |
| `lockAspectRatio` | preserve width:height while resizing | `false` |
| `rotation` | rotation, radians | `0` |
| `opacity` | `0`-`1` | `1` |
| `zIndex` | stacking order | `0` |
### `"box"` fields
| Field | Meaning | Default if unset |
|---|---|---|
| `width`, `height` | size | `160`, `80` |
| `cornerRadius` | corner rounding | `8` |
| `fillColor` | fill color, hex string | `"#ffffff"` |
| `color` | alias for `fillColor` | `"#ffffff"` |
| `borderColor` | border color, hex string | `"#333333"` |
| `borderWidth` | border thickness | `2` |
### `"text"` fields
| Field | Meaning | Default if unset |
|---|---|---|
| `width`, `height` | text layout box size | `160`, `40` |
| `text` | the label's text | `"Label"` |
| `fontFamily` | font family name | app default |
| `fontSize` | font size | `11` |
| `color` | text color, hex string | `"#444466"` |
| `textAlign` | `TextAlign.left`, `TextAlign.center`, `TextAlign.right`, etc. | `TextAlign.center` |
| `verticalAlign` | `TextVerticalAlign.top`, `TextVerticalAlign.center`, `TextVerticalAlign.bottom` | `TextVerticalAlign.top` |
| `labelPadding` | draw the white label halo behind text | `false` |
| `padding` | inset the text inside its layout box, an `EdgeInsets` (see below) | none |
Any field you don't set just falls back to that default — you never have to
set every field on every object.
`TextAlign` and `TextVerticalAlign` are declared enum tables. Use those enum
values instead of raw strings; an invalid enum value fails the whole
generation attempt so typos don't silently render with the wrong alignment.
### `EdgeInsets` — padding
`padding` doesn't take a bare number or a loose table of sides; it takes an
`EdgeInsets`, built with `EdgeInsets.new`. There are two ways to call it, and
any side you leave out is `0`:
```lua
EdgeInsets.new(20, 3, 0, 0) -- left, top, right, bottom
EdgeInsets.new{ left = 20, top = 3 } -- name only the sides you want
```
so a cell label inset 20 from the left edge of its box is:
```lua
CanvasObject.new("text", {
x = colX,
y = rowY,
width = colW,
height = rowH,
text = "7-13",
padding = EdgeInsets.new{ left = 20 },
})
```
Padding insets the text *within* the layout box you gave it — the box stays
where `x`/`y`/`width`/`height` put it. That matters when you're rebuilding a
hand-drawn panel: labels drawn in the editor usually carry their own padding
(a cell label is typically `left = 14`, `top = 14`), and `get_properties`
reports it, so you can copy those numbers across rather than trying to fake
the inset by nudging `x`.
Like the enum tables, `EdgeInsets.new` is strict: an unknown side name or a
non-number value fails the whole generation attempt rather than silently
resolving to zero.
There's no separate `fontWeight`/`italic` field anymore — bold, italic,
underline, strikethrough, inline color and explicit weight are all written
directly into `text` as micro-markdown instead, e.g.
`text.text = "**Oxford Circus** [w300]zone 1[/]"`. See
[Micro Markdown](micro_markdown_guide.md) for the full syntax.
You can also read fields back off an object you created earlier in the same
script (e.g. `box.width * 2`) — property reads and writes both go through
the same object, so this works fine, it's just not typically needed.
### Coordinate space
`x`/`y` on every object are **local to wherever the prefab instance gets
placed** — `(0, 0)` is the instance's own anchor point, not a fixed spot on
the map. The app adds the instance's actual position on top after your
script runs, so you never need to know or care where on the map the instance
actually ended up while you're writing the script.
## Instance geometry
Every instance also has its own width/height and anchor point — the same
placement box you'd get on any other canvas object, editable directly in its
properties panel. Inside a script, this is a plain global table called
`instance`:
```lua
box.width = instance.width
box.height = instance.height
box.anchorX = instance.anchorX
box.anchorY = instance.anchorY
```
- **Reading** `instance.width`/`instance.height` gives you the instance's
current stored size, so you can lay your generated content out relative to
it instead of hardcoding numbers.
- **Reading** `instance.anchorX`/`instance.anchorY` gives you the instance's
current anchor fractions, where `0.5, 0.5` is centered, `0, 0` is top-left,
and `1, 1` is bottom-right.
- **Writing** `instance.width`/`instance.height` during your script lets you
report a size back that the script computed itself — handy for anything
whose natural footprint depends on its parameters (a variable-length row of
items, say) rather than being fixed. Whatever you leave those fields as
when the script finishes is what gets saved as the instance's size.
- **Writing** `instance.anchorX`/`instance.anchorY` works the same way: the
values left at the end of the script are saved as the instance's anchor.
- If your script never touches `instance` at all, nothing changes — same
value in, same value out.
## Reading metro lines
Scripts get read-only access to the map's metro lines (the same lines you
see and edit in the Lines panel) through a global called `Lines`:
```lua
for i, line in ipairs(Lines.all) do
-- line.id, line.name, line.color, line.style, line.width, line.padding
end
local central = Lines.byId(3)
if central then
box.color = central.color
end
```
- `Lines.all` is every line on the map, in order, as a normal 1-indexed
table. `Lines.byId(id)` looks one up directly and gives you `nil` if that
id doesn't exist — handy for a prefab whose parameters reference a
specific line by id rather than looping over all of them.
- Each line's table has `id`, `name`, `color` (a hex string, same as every
other color field in this API), `style` (a string — `"single"`,
`"doubleTrack"`, `"road"`, or `"dashed"`), `width`, and `padding`.
- This is a snapshot handed to your script fresh every run — changing a
field on a table you got from `Lines` (`line.name = "whatever"`) only
affects your own local copy, it never writes back to the real map.
- It only covers line attributes, not which stations or segments belong to
a line — that's not available to scripts yet.
## Measuring text
`measureText(text, options)` tells you how much space a single line of text
would take up, without creating a `"text"` object for it — useful for sizing
a box around a label before you commit to laying it out:
```lua
local size = measureText("7-13", { fontFamily = "Inter", fontSize = 24 })
-- size.width, size.height
local badge = CanvasObject.new("box", {
x = 0, y = 0,
width = size.width + 20, -- pad the box out around the measured text
height = size.height + 10,
})
```
`options` is optional and takes the same font-ish fields a `"text"` object
does:
| Option | Meaning | Default if unset |
|---|---|---|
| `fontFamily` | font family name, or a custom font id off `CustomFonts` | app default |
| `fontSize` | font size | `11` |
| `weight` | font weight (`100`-`900`) | `400` |
An unknown option name fails the whole generation attempt, same as
`EdgeInsets.new`. This only measures a *single line* — it doesn't wrap, so
for multi-line text you'd measure a representative line rather than the
whole string.
## What's off-limits right now
- **Only `"box"` and `"text"` kinds exist.** `CanvasObject.new("image")` (or
anything else) fails — and because a script's output is all-or-nothing,
one bad object call fails the *entire* instance, not just that one object.
- **No way to read stations, segments, or other canvas objects from a
script.** A `canvas` global for reading the rest of the scene tree
(station names, other images, etc.) is planned but not available yet —
scripts can read metro line info (see above) and create new objects, but
can't inspect or react to anything else in the map.
- **Sandboxed.** No file access, no network access, no `os`, `io`,
`require`, `load`/`loadfile`/`dofile`, and no `coroutine`. The base Lua
library plus `math`, `string`, and `table` are all available, so ordinary
logic (loops, string formatting, `math.floor`, etc.) works normally, with
one exception: `#` gives you the length of a **table** only. On a string it
throws ("attempt to get length of a string value") and, because a failed run
produces nothing, takes the whole instance down with it. Use `string.len(s)`
or `s:len()` instead.
- **Bounded execution.** A script gets a time budget (2 seconds) and an
object-count cap (2000 objects) per run. A runaway loop trips one of these
and fails the run with an explanatory message rather than hanging.
## When something goes wrong
If your script has a syntax error, throws a runtime error (e.g. indexing
`nil`), creates an unsupported object kind, or blows past the time/object
budget, the whole generation attempt fails: the instance ends up with **no**
generated content for that run (whatever it had before is cleared, nothing
new takes its place), and the error message is kept on the instance so it's
visible while you're fixing the script. The error is also always printed to
the console. There's no partial output — either the script runs clean start
to finish, or the instance produces nothing.
## Full example 1 — a single labeled box
A minimal interchange badge: one box with a text label centered on it.
```lua
params = declareParams{
{ name = "Label_Text", type = "string", default = "Interchange", section = "Text" },
}
local box = CanvasObject.new("box")
box.x = -60
box.y = -20
box.width = 120
box.height = 40
box.cornerRadius = 6
box.color = "#ffffff"
box.borderColor = "#333333"
box.borderWidth = 2
local label = CanvasObject.new("text")
label.x = 0
label.y = 0
label.text = "**" .. params.Label_Text.value .. "**"
label.fontSize = 14
```
Change the `Label_Text` parameter on an instance and only that instance's
label text changes — the box shape stays as authored in the script.
## Full example 2 — a row of route cards
A variable-length row without a declared `list` param at all — the row's
length and content come from `Lines.all` (every metro line on the map, see
"Reading metro lines" above) instead. The card row resizes to fit the
instance's own width, splitting it evenly across however many lines exist:
```lua
params = declareParams{
{ name = "Gap", type = "number", default = 11, min = 0, decimals = 0 },
}
local cardHeight = instance.height
local gap = params.Gap.value
local routeCount = #Lines.all
local cardWidth = 0
if routeCount > 0 then
cardWidth = (instance.width - gap * (routeCount - 1)) / routeCount
end
for i, line in ipairs(Lines.all) do
local routeNumber = string.gsub(line.name, "^Route ", "")
local x = (i - 1) * (cardWidth + gap)
CanvasObject.new("box", {
x = x,
y = 0,
width = cardWidth,
height = cardHeight,
anchorX = 0,
anchorY = 0,
color = line.color,
})
CanvasObject.new("text", {
x = x,
y = 0,
width = cardWidth,
height = cardHeight,
anchorX = 0,
anchorY = 0,
text = "**" .. routeNumber .. "**",
fontFamily = "Inter",
fontSize = 42,
color = "#FFFFFF",
textAlign = TextAlign.center,
verticalAlign = TextVerticalAlign.center,
})
end
instance.anchorX = 0
instance.anchorY = 0
```
Add or remove a line on the map and the card row grows or shrinks
automatically, no instance param edit needed — the only declared param here
(`Gap`) controls spacing, not row count.
+35 -18
@@ -1,7 +1,7 @@
# Arcs & Angles Privacy Policy
**Effective Date:** March 6, 2026
**Last Updated:** March 6, 2026
**Last Updated:** August 16, 2026
This Privacy Policy explains how IMBENJI.NET LTD ("we", "us", "our") handles information when you use Arcs & Angles (the "Service").
@@ -14,23 +14,35 @@ This policy applies to the Arcs & Angles web and desktop applications and relate
Depending on how you use the Service, we may process:
- Content you create in the app (for example map files, labels, and exports).
- Technical and usage data needed to run core features (for example update checks and network requests required for app functionality).
- Technical and usage data needed to run core features (for example IP address, device/browser information, request timestamps, and application version checks).
- Local settings and recovery data stored on your device (for example preferences, recent files, and recovery drafts).
- Account or backend-related data processed through Supabase when those features are enabled (for example authentication identifiers and related metadata).
- Account or backend-related data processed through our backend services when those features are enabled (for example authentication identifiers and related metadata). This authentication infrastructure is operated by IMBENJI.NET LTD, not a third party.
- Content you send to or generate through the AI Assistant/agent feature when you use it (for example your prompts, relevant map content, and the Assistant's responses), routed through our backend to OpenRouter, our third-party AI model provider, under a zero data retention configuration (see Section 5).
## 3. Local Storage
## 3. Legal Bases for Processing (UK GDPR)
Where UK GDPR applies, we process personal data under one or more of the following legal bases:
- **Contract:** where processing is necessary to provide the Service you request.
- **Legitimate Interests:** where processing is necessary for service security, reliability, abuse prevention, and product improvement, and does not override your rights.
- **Legal Obligation:** where processing is needed to comply with applicable law.
- **Consent:** where required by law, including where you choose to enable optional features.
## 4. Local Storage
Arcs & Angles stores certain data locally on your device to provide core features, including save/recovery and app preferences.
You control your local files and may delete them from your device at any time.
## 4. Third-Party Services (Including Supabase)
## 5. Third-Party Services
The Service uses third-party services to support functionality, including Supabase for backend services and database/authentication capabilities, plus external links and font delivery for exports.
The Service uses third-party services to support functionality, including external links and font delivery for exports.
The optional AI Assistant/agent feature is powered by OpenRouter, a third-party AI model routing provider. We use OpenRouter with zero data retention (ZDR) enabled: for inference requests, OpenRouter routes only to model providers that do not store or train on your data. This ZDR guarantee covers the core chat/inference requests but does not extend to optional plugins or tools (for example, the Assistant's web search capability), which may be subject to those tools' own data handling. Account authentication and other backend services are operated directly by IMBENJI.NET LTD and are not third-party processing.
Those services are governed by their own privacy policies and terms.
## 5. How We Use Information
## 6. How We Use Information
We use information to:
@@ -38,42 +50,47 @@ We use information to:
- Maintain reliability, security, and performance.
- Support export, update, and recovery functionality.
- Improve and develop Service features.
- Improve the Service, including the AI Assistant/agent feature, using information from how the Assistant/agent is used.
## 6. Sharing
## 7. International Data Transfers
If personal data is transferred outside the United Kingdom, we use appropriate safeguards required by applicable law, which may include adequacy decisions or approved standard contractual clauses.
## 8. Sharing
We do not sell personal information.
We may share information only when necessary to operate the Service (including with Supabase as our backend processor/provider), comply with legal obligations, enforce our terms, or protect rights and safety.
We may share information only when necessary to operate the Service (including with third-party providers such as font delivery), comply with legal obligations, enforce our terms, or protect rights and safety. Processing by our own backend and authentication infrastructure (operated by IMBENJI.NET LTD) is not third-party sharing.
## 7. Data Retention
## 9. Data Retention
Retention depends on the data type and purpose.
Local app data remains on your device until removed, overwritten, or cleared by app actions.
## 8. Security
## 10. Security
We use reasonable technical and organizational measures to protect information we process. No method of transmission or storage is completely secure.
## 9. Children's Privacy
## 11. Children's Privacy
The Service is not directed to children under 13. If we become aware that personal data from a child has been collected in violation of applicable law, we will take appropriate steps to address it.
## 10. Your Rights
## 12. Your Rights
Depending on your location, you may have rights regarding your personal data, including access, correction, deletion, or objection.
To exercise rights requests, contact us using the details below.
To exercise rights requests, contact us using the details below with the subject line `Privacy Request` and a description of your request.
## 11. International Users
## 13. Cookies and Similar Technologies
If you access the Service from outside the United Kingdom, you understand your information may be processed in jurisdictions where we or our providers operate.
The Service may use essential local storage or equivalent browser/device storage to support core app features (for example preferences and recovery behavior). We do not use advertising cookies.
## 12. Changes to This Policy
## 14. Changes to This Policy
We may update this Privacy Policy from time to time. Material changes may be communicated through the Service or other reasonable means.
## 13. Contact
## 15. Contact
For privacy questions or requests, contact:
+12 -6
@@ -1,7 +1,7 @@
# Arcs & Angles Terms of Service
**Effective Date:** March 6, 2026
**Last Updated:** March 6, 2026
**Last Updated:** August 16, 2026
These Terms of Service ("Terms") govern your access to and use of Arcs & Angles (the "Service"), including our web app, desktop app, and related features.
@@ -17,7 +17,7 @@ If you use the Service on behalf of an organization, you represent that you have
Arcs & Angles is a design tool for creating stylized metro/subway maps.
Current and planned features may include local map editing, save/load, import/export, recovery features, and optional integrations with third-party services.
Current and planned features may include local map editing, save/load, import/export, recovery features, an optional AI Assistant/agent feature, and integrations with backend services operated by IMBENJI.NET LTD (including account authentication).
We may add, remove, or change features at any time.
@@ -35,9 +35,11 @@ We do not guarantee uninterrupted availability, error-free operation, or that an
You retain ownership of your User Content.
By using the free tier, you grant IMBENJI.NET LTD a worldwide, non-exclusive, royalty-free, transferable, sublicensable license to use, reproduce, adapt, publish, distribute, and display your User Content for operating, improving, and promoting the Service.
By using the free tier, you grant IMBENJI.NET LTD a worldwide, non-exclusive, royalty-free, transferable, sublicensable license to use, reproduce, adapt, publish, distribute, and display your User Content solely as needed to operate, improve, and promote the Service.
If you are on the free tier, you may create and redistribute your User Content, but you may not sell, license for a fee, or otherwise commercially monetize that User Content unless you have an active paid plan or separate written commercial permission from IMBENJI.NET LTD.
If you are on the free tier, you may create and redistribute your User Content, but you may not sell, license for a fee, or otherwise commercially monetize that User Content unless you have an active paid plan or separate written commercial permission from IMBENJI.NET LTD. This includes, for example, selling map files, selling prints or merchandise using exported maps, or bundling maps into paid products or services.
You may not create, distribute, or sell templates, asset packs, editable project files, or similar resources whose primary purpose is to enable third parties to create maps using the Service without requiring them to obtain their own license to the Service.
## 5. Local Storage, Recovery, and Data Handling
@@ -49,9 +51,11 @@ We are not responsible for loss or corruption of local files, drafts, or unsaved
## 6. Third-Party Services and Links
The Service may rely on or link to third-party services (for example, font delivery, external issue/release pages, optional backend services).
The Service may rely on or link to third-party services (for example, font delivery and external issue/release pages). Account authentication is handled by backend infrastructure operated by IMBENJI.NET LTD, not a third party.
Your use of third-party services is subject to those providers' own terms and privacy policies. We are not responsible for third-party services.
The optional AI Assistant/agent feature is powered by OpenRouter, a third-party AI model routing provider, used with zero data retention (ZDR) enabled for inference requests. This does not extend to optional plugins or tools available through the Assistant (for example, web search), which may be subject to those tools' own data handling. Do not submit content through the AI Assistant/agent feature that you are not permitted to share with a third-party AI provider.
Your use of third-party services is subject to those providers' own terms and privacy policies. We are not responsible for third-party services, including outputs generated by the AI Assistant/agent feature.
## 7. Acceptable Use
@@ -70,6 +74,8 @@ The Service, including software, design, branding, and documentation (excluding
No rights are granted except as expressly stated in these Terms.
If you believe content available through the Service infringes your copyright, contact `benjamin.watt@imbenji.net` with sufficient detail for us to review and respond.
## 9. Payments and Commercial Terms
If paid plans, subscriptions, or paid features are offered, additional commercial terms may apply at purchase.