diff --git a/apps/ui/AGENTS.md b/apps/ui/AGENTS.md index 7ccb23602..ceba1a655 100644 --- a/apps/ui/AGENTS.md +++ b/apps/ui/AGENTS.md @@ -1,5 +1,7 @@ # Particle Component Development Guide for @coss/ui +For blueprint illustration work, read [Plotter](skills/plotter/SKILL.md). It records the visual direction, reference images, and review feedback for the production heroes and the unlinked `/ui/plotter` showcase. + This guide provides comprehensive instructions for creating new particle components that match the existing library's patterns and best practices. Use this when creating equivalents of origin components or building new particles from scratch. --- @@ -1377,3 +1379,13 @@ const items = [ --- This guide should enable autonomous creation of particles that match the existing library's patterns, style, and best practices. Always refer to existing particles in the same category for consistency. + + + +## This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/apps/ui/app/plotter/README.md b/apps/ui/app/plotter/README.md new file mode 100644 index 000000000..97d095415 --- /dev/null +++ b/apps/ui/app/plotter/README.md @@ -0,0 +1,83 @@ +# Plotter illustrations + +Direct route: `/ui/plotter`. This showcase is intentionally absent from site navigation and the sitemap, and its metadata disables indexing. The former `/ui/illustration-lab` and `/ui/illustration-lab-prototype` URLs redirect here with their query parameters. + +The gallery presents flat monochrome illustrations, organized by subject with category filters and native-size inline drawings. Stable illustration IDs preserve links and feedback history; `?variant=` selects the category and scrolls to the drawing. Discarded drawings have been removed from the collection; their critique remains in the Plotter skill’s feedback ledger. Inclusion in this curated collection does not make every drawing an approved visual anchor. + +## Fresh-session examples + +The user selected the original versions without added background grids from the 2026-10-31 evaluation: + +| Code | Illustration | Study ID | +| --- | --- | --- | +| C04 | Shared frame | `particles-shared-frame` | +| K09 | Appointment buffers | `cal-appointment-buffers` | +| S15 | No matching results | `state-no-matching-results` | + +The original SVGs, prompts, and feedback are preserved in [`../../plotter-evals/2026-10-31`](../../plotter-evals/2026-10-31). These catalog additions retain their own review history; they do not change the skill’s earlier approved reference set. + +## Production selections + +| Illustration | Selected page | Showcase source | +| --- | --- | --- | +| P00 | Main | `./primitives-blueprint.tsx` | +| C03 | Particles | `./particles-blueprint.tsx` | +| E02 | 404 | `./not-found-blueprint.tsx` | + +The showcase owns these drawing snapshots and its `BlueprintDrawing` wrapper. It can ship independently of the production illustration integration in PR #912. Original C00 and E00 remain separate examples. When a selected production drawing changes, update its showcase snapshot explicitly if that revision should also appear in the gallery. + +## Expanded collection + +The initial expansion added 78 studies. After the user removed 36 entries during visual review, 42 additions remain alongside the earlier 58: **100 illustrations** in total. The user explicitly liked the revised registration, delegation, and iris drawings. Retaining the other additions does not approve them individually or change the 27 approved visual references. Existing codes and IDs are preserved without renumbering; removed sources remain in the [review archive](../../plotter-evals/2026-10-31/catalog-expansion/removed/). + +Editable sources and metadata live in [`drawings/`](drawings/). Preserve the IDs in [`drawings/manifest.json`](drawings/manifest.json); they also drive category filters and `?variant=` links. Construction and reference choices are recorded in the manifest and SVG metadata. Edit the SVG source, then regenerate the React components from the repository root: + +```sh +python3 apps/ui/scripts/build-plotter-drawings.py +``` + +The generated `expanded-studies.tsx` uses the existing drawing wrapper and gives every rendered instance unique definition IDs. Format the generated file with the repository formatter. Do not edit it directly. The [expansion notes](../../plotter-evals/2026-10-31/catalog-expansion/notes.md) record the feedback and visual review scope. + +The visual grammar uses the skill’s editable SVGs as its source. Hover, keyboard focus, or the touch toggle isolates the topic’s marks; `data-grammar-focus` identifies those elements in the source. After editing `skills/plotter/references/grammar/*.svg`, run `python3 apps/ui/scripts/build-plotter-grammar.py` from the repository root and format `visual-grammar-drawings.tsx`. The page explanations live in `visual-grammar-content.ts`. + +Isolation preserves the original stroke treatment. The grid’s missing shared segments use `data-grammar-isolation-only` and are hidden in the complete drawing, then revealed at the same grid weight when the surrounding geometry disappears. Callouts are not part of a focused element unless that topic explains callouts. + +## Plotter skill + +The visible Visual grammar section precedes the illustrations and is reachable from the top navigation at `/ui/plotter#visual-grammar`. + +The reusable style instructions, visual anchors, and feedback ledger live in [`../../skills/plotter`](../../skills/plotter). Keep specific user feedback separate from interpretations; a new revision is unreviewed until the user responds. Add illustrations to their subject collection and preserve existing IDs when revising them. + +## Shared foundation pilot + +AG01 and W05 opt into the [portable drafting foundation](../../skills/plotter/references/foundation.md). AG01 keeps its editable SVG; its transfer arrow and width dimension come from the shared helpers. W05 uses the same arrow helper and shares rectangular hatching between its static and animated drawings. Its timeline and playback controls remain local and unchanged. + +From the repository root, regenerate standard styles, AG01's marked fragments, and the SVG-to-JSX output after relevant source changes: + +```sh +python3 apps/ui/skills/plotter/scripts/build-standard.py +node apps/ui/scripts/build-plotter-registration.mjs +python3 apps/ui/scripts/build-plotter-drawings.py +``` + +The registration script and portable example both use the skill-owned [`renderRegistration` recipe](../../skills/plotter/assets/examples/render-registration.mjs). It reads its width witnesses from the SVG's `registry-record` rectangle and local unit, and changes only marked helper fragments and the embedded generated stylesheet. Preserve those markers when editing the surrounding composition. + +To produce the two self-contained static SVGs, run: + +```sh +bun apps/ui/scripts/export-plotter-pilots.tsx /tmp/plotter-exports +``` + +`BlueprintDrawing` enables generated role styles only when passed `roles`. Legacy drawings keep their existing inherited defaults. Keep `data-plotter-role` on the original styled groups, and avoid applying opacity twice. The rest of the catalogue and the approved snapshot manifest are outside this pilot. + +## Using Plotter from the page + +`#use-plotter` provides installation, copyable drawing/animation briefs, and editable output downloads. `#plotter-tokens` shows supported CSS overrides on a separate W05 instance and reads token defaults from the shared standard. Existing catalogue filters, deep links, animation controls, and grammar isolation retain their behavior. + +The page serves a complete local skill ZIP, two resolved SVGs, and a portable W05 HTML player from `public/plotter/downloads/`. Regenerate these artifacts after changing the packaged skill or the pilot drawings: + +```sh +bun apps/ui/scripts/build-plotter-downloads.tsx +``` + +The script packages the current sources rather than assuming a branch has been published. It does not run checks or tests. Download URLs use the app's configured base path. The HTML embeds the current W05 motion and static SVGs plus generated role styles; it needs no coss runtime or external font. New presentation code lives in `plotter-intro.tsx`, `plotter-usage.tsx`, `plotter-prompts.tsx`, and `plotter-token-guide.tsx`. diff --git a/apps/ui/app/plotter/appointment-buffers.tsx b/apps/ui/app/plotter/appointment-buffers.tsx new file mode 100644 index 000000000..9b8d3d887 --- /dev/null +++ b/apps/ui/app/plotter/appointment-buffers.tsx @@ -0,0 +1,97 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; + +export function AppointmentBuffersStudy() { + return ( + + + + + + + + + + + + 10 min + + + 30 min + + + 10 min + + + + + + + + + + + + + + Buffer before + + + Appointment + + + Buffer after + + + + + + + + 09:50 + + + 10:00 + + + 10:30 + + + 10:40 + + + + + + + + 50 min reserved + + + ); +} diff --git a/apps/ui/app/plotter/blueprint-drawing.tsx b/apps/ui/app/plotter/blueprint-drawing.tsx new file mode 100644 index 000000000..90dbb6f61 --- /dev/null +++ b/apps/ui/app/plotter/blueprint-drawing.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from "react"; +import { roleCss } from "../../skills/plotter/assets/foundation/index.mjs"; + +export function BlueprintDrawing({ + children, + roles = false, +}: { + children: ReactNode; + roles?: boolean; +}) { + return ( + + ); +} + +// Draws hero geometry at empty-state size. Strokes stay one pixel at any scale. +export function CompactBlueprintDrawing({ + center: [x, y], + scale, + children, +}: { + center: [number, number]; + scale: number; + children: ReactNode; +}) { + return ( + + ); +} diff --git a/apps/ui/app/plotter/cal-studies.tsx b/apps/ui/app/plotter/cal-studies.tsx new file mode 100644 index 000000000..b92795a4e --- /dev/null +++ b/apps/ui/app/plotter/cal-studies.tsx @@ -0,0 +1,187 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +function RegistrationMarks() { + return ( + + + + + ); +} + +function SharedAvailability() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A + + + B + + + + + + + + + + + 13:00 + + + 15:00 + + + 2h + + + Shared availability + + + + + ); +} + +function TwoTimeZones() { + return ( + + + + + + + + + + + {[104, 176, 248, 320, 392, 464].map((x) => ( + + ))} + + + + + + + + + + + + UTC + + + UTC+03 + + + 09:00 + + + 12:00 + + + + {["07", "08", "09", "10", "11", "12"].map((hour, index) => ( + + {hour} + + ))} + {["10", "11", "12", "13", "14", "15"].map((hour, index) => ( + + {hour} + + ))} + + 1h + + + Same booking + + + + + ); +} + +export const calStudies: IllustrationStudy[] = [ + { + id: "cal-availability", + code: "K02", + title: "Common ground", + description: + "Two independent schedules share one precisely aligned window. The overlap becomes the composition’s central column.", + category: "Scheduling", + illustration: , + }, + { + id: "cal-time-zones", + code: "K03", + title: "One moment, two places", + description: + "Two hour scales describe the same booking. An aligned one-hour interval stays fixed while the local time changes by three hours.", + category: "Scheduling", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/content-state-studies.tsx b/apps/ui/app/plotter/content-state-studies.tsx new file mode 100644 index 000000000..bb24c913f --- /dev/null +++ b/apps/ui/app/plotter/content-state-studies.tsx @@ -0,0 +1,561 @@ +import { BlueprintDrawing, CompactBlueprintDrawing } from "./blueprint-drawing"; +import { HorizontalDimension, VerticalDimension } from "./dimensions"; +import { NoMatchingResultsStudy } from "./no-matching-results"; +import type { IllustrationStudy } from "./study-types"; + +const secondaryLine = { + opacity: 0.5, + strokeWidth: 0.65, + strokeLinecap: "butt", + strokeLinejoin: "miter", +} as const; + +function Registration() { + return ( + + + + ); +} + +const indexRecords = [ + { x: 108, label: "A" }, + { x: 180, label: "B" }, + { x: 324, label: "D" }, + { x: 396, label: "E" }, +]; + +function SearchForm() { + return ( + <> + + + + {indexRecords.map(({ x }) => ( + + ))} + + ); +} + +function SearchGap({ dash = "4 5" }: { dash?: string }) { + return ( + + + + + + ); +} + +function SearchStudy() { + return ( + + + + + + + + + + + + + + + + + C + + {indexRecords.map(({ x, label }) => ( + + {label} + + ))} + + + + Index + + + 0 matches + + + + + ); +} + +function SearchCompact() { + return ( + + + + {indexRecords.map(({ x }) => ( + + ))} + + + ); +} + +const readNotifications = [132, 196, 260]; + +function NotificationStream({ dash = "3 5" }: { dash?: string }) { + return ( + <> + + {readNotifications.map((y) => ( + + + + + + + + ))} + + + + + + + ); +} + +function NotificationsStudy() { + return ( + + + + + + + + + + + + + + Read + + + 0 unread + + + + + The stream is caught up. + + + + + ); +} + +function NotificationsCompact() { + return ( + + + + ); +} + +const pageHeight = 200 * Math.sqrt(2); +const pageBottom = 80 + pageHeight; +const writingTop = 80 + pageHeight * 0.2; +const writingBottom = 80 + pageHeight * 0.8; + +function PageForm() { + return ( + <> + + + + + ); +} + +function WritingArea({ dash = "3 5" }: { dash?: string }) { + return ( + + ); +} + +function FilesStudy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + Begin here + + + + + 1 : √2 + + + + + ); +} + +function FilesCompact() { + return ( + + + + + + ); +} + +const plotColumns = [196, 244, 292, 340, 388, 436]; +const plotRows = [132, 180, 228, 276]; + +function EmptyPlot({ compact = false }: { compact?: boolean }) { + return ( + <> + + {plotRows.map((y) => ( + + ))} + {plotColumns.map((x) => ( + + ))} + + + + + ); +} + +function DataStudy() { + return ( + + + + + + + + + + + + + + No observations + + + + + 0 + + + x + + + y + + + An unoccupied coordinate field + + + + + ); +} + +function DataCompact() { + return ( + + + + ); +} + +const weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; + +function EmptyWeek() { + return ( + <> + + + + + + + ); +} + +function BookingsStudy() { + return ( + + + + + + + + + + + + + + + + + {weekdays.map((day, index) => ( + + {day} + + ))} + + + {["09:00", "10:00", "11:00", "12:00"].map((time, index) => ( + + {time} + + ))} + + + 0 bookings + + + + ); +} + +function BookingsCompact() { + return ( + + + {[160, 200, 280, 320, 360, 400].map((x) => ( + + ))} + + ); +} + +function ImageBounds({ dash = "4 6" }: { dash?: string }) { + return ( + <> + + + + + + ); +} + +function ImageStudy() { + return ( + + + + + + + + + + + + + + + + + + + Image / 4 : 3 + + + + + ); +} + +function ImageCompact() { + return ( + + + + ); +} + +export const contentStateStudies: IllustrationStudy[] = [ + { + id: "state-no-matching-results", + code: "S15", + title: "No matching results", + description: + "A lookup for key 03 finds no entry among 01, 02, and 04. A missing position interrupts the repeated index.", + category: "Empty states", + illustration: , + }, + { + id: "state-search", + code: "S03", + title: "A search, unanswered", + description: + "A query projects into the missing position of an ordered index. The surrounding records exist; the requested one does not.", + category: "Empty states", + illustration: , + emptyState: { + title: "No results found", + description: "Try a different search term or remove a filter.", + illustration: , + }, + }, + { + id: "state-notifications", + code: "S05", + title: "A stream, caught up", + description: + "Read entries occupy an evenly spaced notification stream. Its next position remains open, with no unread entry to continue it.", + category: "Empty states", + illustration: , + emptyState: { + title: "No notifications", + description: "You’re all caught up. New activity will appear here.", + illustration: , + }, + }, + { + id: "state-files", + code: "S06", + title: "A page, unwritten", + description: + "A blank writing area repeats the page’s 1 : √2 proportion. Equal side margins locate the cursor before the first line exists.", + category: "Empty states", + illustration: , + emptyState: { + title: "No documents yet", + description: "Create a document to start writing.", + illustration: , + }, + }, + { + id: "state-data", + code: "S07", + title: "A field without observations", + description: + "Calibrated axes establish a coordinate field. Its units are defined, but no observation occupies a coordinate.", + category: "Empty states", + illustration: , + emptyState: { + title: "No data yet", + description: "Data will appear once activity is recorded.", + illustration: , + }, + }, + { + id: "state-bookings", + code: "S13", + title: "A week, unbooked", + description: + "Seven days share a measured hour grid. Today has its place in the week; the time field contains no bookings.", + category: "Empty states", + illustration: , + emptyState: { + title: "No upcoming bookings", + description: "Share your booking link to start receiving meetings.", + illustration: , + }, + }, + { + id: "state-image", + code: "S14", + title: "An image, not yet placed", + description: + "Crop corners establish a 4 : 3 image area. The diagonal and unit measurements define its proportions while the image itself is absent.", + category: "Empty states", + illustration: , + emptyState: { + title: "No image", + description: "Drop an image here or browse your files.", + illustration: , + }, + }, +]; diff --git a/apps/ui/app/plotter/coss-concepts/common-interface-study.tsx b/apps/ui/app/plotter/coss-concepts/common-interface-study.tsx new file mode 100644 index 000000000..099693117 --- /dev/null +++ b/apps/ui/app/plotter/coss-concepts/common-interface-study.tsx @@ -0,0 +1,184 @@ +import { BlueprintDrawing } from "../blueprint-drawing"; + +// Conceptual assembly, not a physical part or infrastructure diagram. +// Four outer profiles stand for the catalog services. Their common circular +// opening stands for the shared SDK contract. The lifted quadrant is a planar +// translation (u, −u), never a rotation or a perspective/extrusion operation. +const unit = 32; +const center = { x: 264, y: 224 }; +const radius = 2 * unit; +const displaced = { x: center.x + unit, y: center.y - unit }; +const arrow = 6 / Math.SQRT2; + +type Interval = readonly [number, number]; + +function remaining(start: number, end: number, cuts: Interval[]) { + const segments: Interval[] = []; + let cursor = start; + for (const [from, to] of cuts.sort((a, b) => a[0] - b[0])) { + if (from > cursor) segments.push([cursor, Math.min(from, end)]); + cursor = Math.max(cursor, to); + if (cursor >= end) break; + } + if (cursor < end) segments.push([cursor, end]); + return segments.filter(([from, to]) => to > from); +} + +const labelBounds = [ + [162, 130, 216, 151], + [348, 89, 390, 110], + [164, 308, 237, 331], + [300, 308, 347, 331], +] as const; + +function ConstructionGrid() { + const paths: string[] = []; + // The grid measures the 32-unit profile steps. Clear the common opening, + // selected interface strip, text, and segments already owned by an outline. + for (const x of [168, 200, 232, 296, 328, 360]) { + const cuts: Interval[] = []; + const dx = x - center.x; + if (Math.abs(dx) < radius) { + const dy = Math.sqrt(radius ** 2 - dx ** 2); + cuts.push([center.y - dy, center.y + dy]); + } + if (dx <= 0 && dx > -80) { + cuts.push([center.y, center.y + Math.sqrt(80 ** 2 - dx ** 2)]); + } + const displacedX = x - displaced.x; + if (displacedX >= 0 && displacedX < radius) { + cuts.push([ + displaced.y - Math.sqrt(radius ** 2 - displacedX ** 2), + displaced.y, + ]); + } + for (const [left, top, right, bottom] of labelBounds) { + if (x >= left && x <= right) cuts.push([top, bottom]); + } + if (x === 168) cuts.push([352, 368]); + if (x === 296) cuts.push([64, 128]); + if (x === 360) cuts.push([288, 352]); + paths.push(...remaining(80, 368, cuts).map(([a, b]) => `M${x} ${a}V${b}`)); + } + for (const y of [128, 160, 192, 256, 288, 320]) { + const cuts: Interval[] = []; + const dy = y - center.y; + if (Math.abs(dy) < radius) { + const dx = Math.sqrt(radius ** 2 - dy ** 2); + cuts.push([center.x - dx, center.x + dx]); + } + if (dy >= 0 && dy < 80) { + cuts.push([center.x - Math.sqrt(80 ** 2 - dy ** 2), center.x]); + } + const displacedY = y - displaced.y; + if (displacedY <= 0 && displacedY > -radius) { + cuts.push([ + displaced.x, + displaced.x + Math.sqrt(radius ** 2 - displacedY ** 2), + ]); + } + for (const [left, top, right, bottom] of labelBounds) { + if (y >= top && y <= bottom) cuts.push([left, right]); + } + if (y === 192) cuts.push([360, 424]); + if (y === 288) cuts.push([360, 392]); + paths.push(...remaining(120, 440, cuts).map(([a, b]) => `M${a} ${y}H${b}`)); + } + return ; +} + +function InterfaceHatch() { + const paths: string[] = []; + // Selected common-profile strip: SW quadrant, 64 ≤ r ≤ 80. For x+y=k, + // circle intersections are solved analytically and the inner disk removed. + // This highlights the matching edge; it does not assert a cut material. + for (let sum = 408; sum <= 564; sum += 12) { + const offset = sum - center.x - center.y; + const outerRoot = Math.sqrt(2 * 80 ** 2 - offset ** 2); + const low = (offset - outerRoot) / 2; + const high = Math.min((offset + outerRoot) / 2, 0, offset); + const cuts: Interval[] = []; + if (Math.abs(offset) < radius * Math.SQRT2) { + const innerRoot = Math.sqrt(2 * radius ** 2 - offset ** 2); + cuts.push([(offset - innerRoot) / 2, (offset + innerRoot) / 2]); + } + for (const [a, b] of remaining(low, high, cuts)) { + paths.push( + `M${center.x + a} ${sum - center.x - a}L${center.x + b} ${sum - center.x - b}`, + ); + } + } + return ; +} + +export function CommonInterfaceStudy() { + return ( + + + + + + + + + {/* The unoccupied position is projected exactly, not a second part. */} + + + + + + + {/* One owner for every shared seam in the assembled three parts. */} + + + {/* The same NE quadrant translated by (32, −32). */} + + + + + + + + + + + + + + Email + + + SMS + + + Scheduling + + + Video + + + One interface + + + + + r + + + r + + + u + + + One SDK · One key per environment + + + + ); +} diff --git a/apps/ui/app/plotter/coss-concepts/index.tsx b/apps/ui/app/plotter/coss-concepts/index.tsx new file mode 100644 index 000000000..e17415cba --- /dev/null +++ b/apps/ui/app/plotter/coss-concepts/index.tsx @@ -0,0 +1,65 @@ +import { CommonInterfaceStudy } from "./common-interface-study"; +import { SharedSystemStudy } from "./shared-system-study"; + +const concepts = [ + { + code: "X01", + title: "One interface, many services", + description: + "Different parts, one shared opening. Email, SMS, video, and scheduling meet a common SDK contract.", + Drawing: CommonInterfaceStudy, + }, + { + code: "X07", + title: "A common boundary", + description: + "Two complementary profiles form one system. A construction study of the interface between an app and coss.", + Drawing: SharedSystemStudy, + }, +] as const; + +export function CossConcepts() { + return ( +
+
+

+ coss.com concepts +

+

+ Two studies of a shared interface: different services, designed to fit + together. +

+
+
+ {concepts.map(({ code, title, description, Drawing }) => ( +
+
+ +
+
+
+

{title}

+

+ {description} +

+
+ + {code} + +
+
+ ))} +
+
+ ); +} diff --git a/apps/ui/app/plotter/coss-concepts/shared-system-study.tsx b/apps/ui/app/plotter/coss-concepts/shared-system-study.tsx new file mode 100644 index 000000000..03a6a6455 --- /dev/null +++ b/apps/ui/app/plotter/coss-concepts/shared-system-study.tsx @@ -0,0 +1,100 @@ +import { BlueprintDrawing } from "../blueprint-drawing"; + +const unit = 60; +const frame = { left: 160, top: 88, size: 4 * unit }; +const seamX = frame.left + 2 * unit; +const upperCenterY = frame.top + unit; +const lowerCenterY = frame.top + 3 * unit; +const middleY = frame.top + 2 * unit; +const bottomY = frame.top + frame.size; +const rightX = frame.left + frame.size; +const radialOffset = unit / Math.SQRT2; +const radiusTip = { + x: seamX + radialOffset, + y: upperCenterY - radialOffset, +}; + +// A conceptual compatibility diagram, not a physical joint or API topology. +// The two regions are complementary: their common interface is owned once. +// Two equal tangent generating circles give a C1-continuous pair of arcs. +const interfaceProfile = `M${seamX} ${frame.top}A${unit} ${unit} 0 0 1 ${seamX} ${middleY}A${unit} ${unit} 0 0 0 ${seamX} ${bottomY}`; +const complementaryLoci = `M${seamX} ${frame.top}A${unit} ${unit} 0 0 0 ${seamX} ${middleY} M${seamX} ${middleY}A${unit} ${unit} 0 0 1 ${seamX} ${bottomY}`; + +// The half-unit field locates the circle centers, radii, and square edges. +// Shared axes/boundaries own their strokes; text zones are omitted in geometry. +const grid = [ + "M160 330V352 M400 330V352", + "M190 64V198 M190 218V352", + "M220 64V198 M220 218V352", + "M250 64V352", + "M310 64V102 M310 134V352", + "M340 64V198 M340 218V352", + "M370 64V198 M370 218V352", + "M136 88H158 M402 88H424", + "M136 118H284 M318 118H424", + "M136 178H424", + "M136 208H178 M230 208H330 M382 208H424", + "M136 238H424", + "M136 298H424", + "M136 328H158 M402 328H424", +].join(" "); + +export function SharedSystemStudy() { + return ( + + + + + + + + + + + + + + + + + + + 4u + + + 2u + + + r = u + + + App + + + coss + + + One shared interface + + + + ); +} diff --git a/apps/ui/app/plotter/dimensions.tsx b/apps/ui/app/plotter/dimensions.tsx new file mode 100644 index 000000000..062e57d5e --- /dev/null +++ b/apps/ui/app/plotter/dimensions.tsx @@ -0,0 +1,61 @@ +export function HorizontalDimension({ + x1, + x2, + y, + label, +}: { + x1: number; + x2: number; + y: number; + label: string; +}) { + return ( + <> + + + {label} + + + ); +} + +export function VerticalDimension({ + x, + y1, + y2, + label, + side = "end", +}: { + x: number; + y1: number; + y2: number; + label: string; + side?: "start" | "end"; +}) { + return ( + <> + + + {label} + + + ); +} diff --git a/apps/ui/app/plotter/disconnected-plug.tsx b/apps/ui/app/plotter/disconnected-plug.tsx new file mode 100644 index 000000000..9da10c182 --- /dev/null +++ b/apps/ui/app/plotter/disconnected-plug.tsx @@ -0,0 +1,92 @@ +import { BlueprintDrawing, CompactBlueprintDrawing } from "./blueprint-drawing"; +import { HorizontalDimension, VerticalDimension } from "./dimensions"; + +function PlugBody() { + return ( + <> + + + + ); +} + +// Conceptual section: the fixed support lies behind x356. A separate socket +// has a 12-unit flange in front of that plane and a body recessed to x400. +// Contact pockets enter at x344 and stop at x388, inside the socket body. +// The one-sided hatch denotes a support; it is not a material specification. +function SocketSection({ compact = false }: { compact?: boolean }) { + return ( + <> + + + + + + ); +} + +export function DisconnectedBlueprint() { + return ( + + + + + + + + + + + + + + {/* Gap witnesses: the pin ends at x264 and socket mouth at x344. */} + + + {/* Both pin and pocket centerlines are y196 and y244. */} + + + + + + + Disconnected + + + + + Socket + + + + ); +} + +export function DisconnectedCompact() { + return ( + + + + + + + ); +} diff --git a/apps/ui/app/plotter/drawings/agent-checkpoint.svg b/apps/ui/app/plotter/drawings/agent-checkpoint.svg new file mode 100644 index 000000000..68333dc53 --- /dev/null +++ b/apps/ui/app/plotter/drawings/agent-checkpoint.svg @@ -0,0 +1 @@ +A state worth keepingOne selected state is preserved as a checkpoint while the wider work continues.Revision 2, catalog candidate. Reference: C01, K07. Conceptual checkpoint. Unit u = 24. A 96-unit, 4u partition of a 336-unit, 14u record is repeated unchanged below. Hatching identifies the same saved state in both positions. This illustrates capture, not a claim of fault tolerance or storage durability.WorkState 02ContinueState 02Checkpoint14u4u diff --git a/apps/ui/app/plotter/drawings/agent-delegation.svg b/apps/ui/app/plotter/drawings/agent-delegation.svg new file mode 100644 index 000000000..cc0aeaeb8 --- /dev/null +++ b/apps/ui/app/plotter/drawings/agent-delegation.svg @@ -0,0 +1,46 @@ + + One task, two agents + A task is divided into research and drafting scopes. The two scopes retain their widths and profiles when separated and assigned to Agent 01 and Agent 02. + Study: agent-delegation, revision 2, explicitly liked by the user on 2026-10-31. Conceptual diagram. References: K07 part/whole proportions, C01 preserved fragments, C02 allocation. Unit u = 24 SVG units. The task is 14u wide; research and drafting allocations are 6u and 8u. Widths describe the illustrative partition, not effort, duration, cost, or an execution schedule. The two lower forms retain the source partition geometry, translated by (-12,156) and (12,156). The guide pairs connect the source's lower width witnesses to the assigned forms' upper width witnesses; the arrows indicate conceptual assignment, not measured trajectories. No background grid is needed. + + + + + + + + + + + + + + + + + + + + + One task + Research + Draft + Research + Draft + Agent 01 + Agent 02 + + + + + + + + + + + 14u + 6u + 8u + + diff --git a/apps/ui/app/plotter/drawings/agent-handoff.svg b/apps/ui/app/plotter/drawings/agent-handoff.svg new file mode 100644 index 000000000..a6d03de4f --- /dev/null +++ b/apps/ui/app/plotter/drawings/agent-handoff.svg @@ -0,0 +1 @@ +Context, carried forwardOne continuous context passes between two agents without changing its form.Revision 2, catalog candidate. Reference: E02, I06. Conceptual handoff. A 40-unit band has paired bend radii 20 and 60. The same 64 × 24 context capsule has 8-unit side clearances in both horizontal portions. Hatching marks the handoff section, not a physical material. No serialization, timing or persistence guarantee is asserted.ContextContextAgent 01Agent 02HandoffR2040336 diff --git a/apps/ui/app/plotter/drawings/agent-memory.svg b/apps/ui/app/plotter/drawings/agent-memory.svg new file mode 100644 index 000000000..c7e076e5e --- /dev/null +++ b/apps/ui/app/plotter/drawings/agent-memory.svg @@ -0,0 +1 @@ +One context, two readersTwo agents address the same findings row within one shared memory.Revision 2, catalog candidate. Reference: B09, C02. Conceptual shared access. The 192-unit record is 8u wide with 48-unit, 2u rows. Both connections terminate at the same row, not duplicated stores. Route returns are R24. Hatching identifies the selected row. No concurrent consistency guarantee is depicted.Shared memoryGoalFindingsNext stepAgent 01Agent 028u2uR24 diff --git a/apps/ui/app/plotter/drawings/agent-parallel.svg b/apps/ui/app/plotter/drawings/agent-parallel.svg new file mode 100644 index 000000000..0511f587d --- /dev/null +++ b/apps/ui/app/plotter/drawings/agent-parallel.svg @@ -0,0 +1 @@ +Separate work, one joinThree agents finish at different points and wait for the longest task before joining.Revision 2, catalog candidate. Reference: B04, K07. Conceptual synchronization. One illustrative time unit u = 72 SVG units. Work durations are 3u, 4u and 2u; the shared join is at 4u. Hatching denotes waiting intervals only. The shared join line owns all right-edge segments. This is not a scheduler performance claim.Agent 01Agent 02Agent 03StartJoinWait4u2u diff --git a/apps/ui/app/plotter/drawings/agent-permissions.svg b/apps/ui/app/plotter/drawings/agent-permissions.svg new file mode 100644 index 000000000..5691036d9 --- /dev/null +++ b/apps/ui/app/plotter/drawings/agent-permissions.svg @@ -0,0 +1 @@ +Only the shared scopeThe granted region is the intersection of what is requested and what is allowed.Revision 2, catalog candidate. Reference: K08, P00. Conceptual permission scope. Two 240 × 144 rounded fields overlap; the selected intersection is bounded by their actual edges, with no duplicate outlines. Unit u = 24; overlap width is 6u. Hatching means granted scope. This is an illustrative set relationship, not a policy engine or numerical permission measure.Requested scopeAllowed scopeGranted10u6u diff --git a/apps/ui/app/plotter/drawings/agent-registration.svg b/apps/ui/app/plotter/drawings/agent-registration.svg new file mode 100644 index 000000000..751b9710c --- /dev/null +++ b/apps/ui/app/plotter/drawings/agent-registration.svg @@ -0,0 +1,165 @@ + + + An agent, registered + An identity, name, and two tools align into one registry record. The identity and tool shapes retain their sizes and horizontal positions. The record receives the example identifier agent_07. + Study: agent-registration, revision 2, explicitly liked by the user on 2026-10-31. Mode: conceptual diagram. Registration is represented as the assembly of one discoverable record from an agent definition; this does not depict authentication, permission grants, or execution. References: C01 assembly, C02 allocation, I06 identity fragments. Local unit u = 24 SVG units. Record width 14u = 336; height 4u = 96; horizontal allocation 1u inset + 2u identity + 1u gap + 5u name + 1u gap + 3u tools + 1u inset. Source and assembled fragments differ only by a vertical translation of 148 units. The assigned identifier is new text within the name allocation. No background grid or hatch is needed. + + + + + + + + + + + + + + + + + + + + + + Research agent + Read + Search + + + Identity + Name + Tools + + + + + + + + + + Registered agent + + + + + + + + + + + + + Research agent + Read + Search + + agent_07 + + + + + + + + + + + + 14u + + u + u + + diff --git a/apps/ui/app/plotter/drawings/agent-tools.svg b/apps/ui/app/plotter/drawings/agent-tools.svg new file mode 100644 index 000000000..e720a31d0 --- /dev/null +++ b/apps/ui/app/plotter/drawings/agent-tools.svg @@ -0,0 +1 @@ +One matching interfaceA selected tool’s rounded end matches the agent’s common interface.Revision 2, catalog candidate. Reference: C02, P02. Conceptual tool selection. All tools have 144 × 48 footprints with R24 ends. The receiving semicircle has the same radius. The selected end reaches the receiving profile after a horizontal translation of 96 units. The drawing does not describe an actual API connector.ReadSearchWriteR24Agent 07Tool interface14432 diff --git a/apps/ui/app/plotter/drawings/arch-missing-keystone.svg b/apps/ui/app/plotter/drawings/arch-missing-keystone.svg new file mode 100644 index 000000000..def98b48b --- /dev/null +++ b/apps/ui/app/plotter/drawings/arch-missing-keystone.svg @@ -0,0 +1 @@ +The last piece of the archA lifted keystone preserves the geometry of the opening it will complete.Revision 2. Reference E06. Geometric assembly study. Concentric arch radii120 and84; missing20° sector spans-100° to-80°. The keystone is the exact annular sector translated upward52, with H1 identifying that part. Missing arcs show its destination; shared radial seams are not duplicated. No structural-stability assertion.R84R120Keystone240 diff --git a/apps/ui/app/plotter/drawings/arch-staircase.svg b/apps/ui/app/plotter/drawings/arch-staircase.svg new file mode 100644 index 000000000..b3afe9195 --- /dev/null +++ b/apps/ui/app/plotter/drawings/arch-staircase.svg @@ -0,0 +1 @@ +One rise, one runSix equal rise-and-run pairs construct a staircase along one repeated slope.Revision 2. Reference C02. Geometric staircase study. u=16; every run3u=48, rise2u=32, six runs span18u=288. The diagonal joins the repeated pitch vertices; H1 identifies one triangular unit cell, not a material section. The outline is a construction profile, not a staircase specification.3u2u18uOne step diff --git a/apps/ui/app/plotter/drawings/craft-dovetail.svg b/apps/ui/app/plotter/drawings/craft-dovetail.svg new file mode 100644 index 000000000..f8355dcca --- /dev/null +++ b/apps/ui/app/plotter/drawings/craft-dovetail.svg @@ -0,0 +1 @@ +A joint that holdsA wider tail and narrower neck create one complementary boundary between two pieces.Revision 2, catalog candidate. Reference: C01, P00. Illustrative dovetail in plan. Shared seam drawn once. Neck width 128, tail width 160, depth 48 SVG units; symmetric flank offset 16. Hatching identifies the projecting tongue region, not a material. No manufacturing tolerance or load claim is implied.Part APart B12816048 diff --git a/apps/ui/app/plotter/drawings/craft-paper-fold.svg b/apps/ui/app/plotter/drawings/craft-paper-fold.svg new file mode 100644 index 000000000..eca9741a3 --- /dev/null +++ b/apps/ui/app/plotter/drawings/craft-paper-fold.svg @@ -0,0 +1 @@ +Across the creaseOne corner reflects across its diagonal crease and becomes a triangular flap.Revision 2, catalog candidate. Reference: P00, E00. Flat paper-fold study. Square side 240; u=40. Crease x+y=380 joins (160,220) and (280,100); reflection maps the original corner (160,100) to (280,220). The crease is drawn only once as the visible boundary. Hatching identifies the folded flap, and dashed edges mark the original absent corner. Arrow indicates folding direction, not a physical 3D trajectory.45°3u6u diff --git a/apps/ui/app/plotter/drawings/craft-stitch.svg b/apps/ui/app/plotter/drawings/craft-stitch.svg new file mode 100644 index 000000000..8280b8f6c --- /dev/null +++ b/apps/ui/app/plotter/drawings/craft-stitch.svg @@ -0,0 +1 @@ +A seam, joinedRepeated crossed stitches bridge two edges on one shared hole pitch.Revision 2, catalog candidate. Reference: K07, C02. Conceptual front view of a stitched seam. Hole centers repeat every 48 units and sit 16 units from the joined edge. Thread segments end on the hole circumferences. One diagonal is interrupted at each crossing to preserve over/under meaning. The construction is illustrative, not a sewing specification.Edge AEdge B11248 diff --git a/apps/ui/app/plotter/drawings/craft-tiles.svg b/apps/ui/app/plotter/drawings/craft-tiles.svg new file mode 100644 index 000000000..3828a5c77 --- /dev/null +++ b/apps/ui/app/plotter/drawings/craft-tiles.svg @@ -0,0 +1 @@ +One shared cornerFour tiles meet around a common circular opening and a constant joint width.Revision 2, catalog candidate. Reference: P00, C02. Flat tiling construction. Four 128-unit square allocations surround a 32-unit cross joint. A shared R48 circle cuts their inner corners; its remaining arcs appear only within the joint, so no locus segment retraces a tile edge. Overall extent 288 × 288. The four quarter turns place tiles in one plane and do not introduce perspective.R4832288 diff --git a/apps/ui/app/plotter/drawings/craft-weave.svg b/apps/ui/app/plotter/drawings/craft-weave.svg new file mode 100644 index 000000000..9692c75a8 --- /dev/null +++ b/apps/ui/app/plotter/drawings/craft-weave.svg @@ -0,0 +1 @@ +Over and underOne warp and one weft reveal the alternating rule that holds a weave together.Revision 2, catalog candidate. Reference: C02, P00. Flat weave construction: 24-unit strands on 64-unit centers. Under-strand outlines stop 4 units before the crossing strand; the interruptions alternate by row and column. Hatching marks one selected crossing. This is an ordering diagram, not a fabric or strength specification.WarpWeft6424 diff --git a/apps/ui/app/plotter/drawings/geo-circle-rectangle.svg b/apps/ui/app/plotter/drawings/geo-circle-rectangle.svg new file mode 100644 index 000000000..7fc746277 --- /dev/null +++ b/apps/ui/app/plotter/drawings/geo-circle-rectangle.svg @@ -0,0 +1 @@ +A circle, extendedA circle and its wider rounded-rectangle state share one height while their corner radii change.Revision 2. Reference P00. Geometric construction. u=44. Initial circle radius2u=88; final rectangle6u×4u with corner radiusu. Dashed circle means previous form. The two arrows describe horizontal change, not scaling the height.R = u6u4uCircle / beforeA shared height diff --git a/apps/ui/app/plotter/drawings/geo-compound-radii.svg b/apps/ui/app/plotter/drawings/geo-compound-radii.svg new file mode 100644 index 000000000..0c2c8e329 --- /dev/null +++ b/apps/ui/app/plotter/drawings/geo-compound-radii.svg @@ -0,0 +1 @@ +A curve from three radiiThree circular arcs share tangent directions at their joins, producing one uninterrupted curve.Revision 2. Reference P00. Geometric construction. Arc centers(104,204),(280,204),(280,284); radii112,64,144. First arc90→0°, second180→270°, third270→360°. Only selected generating radii are drawn, avoiding coincident radius segments.R112R64R144TangentThree radii · one continuous curve diff --git a/apps/ui/app/plotter/drawings/geo-interrupted-pattern.svg b/apps/ui/app/plotter/drawings/geo-interrupted-pattern.svg new file mode 100644 index 000000000..b7f343944 --- /dev/null +++ b/apps/ui/app/plotter/drawings/geo-interrupted-pattern.svg @@ -0,0 +1 @@ +One interval left openA field of tangent circles keeps its repeated pitch while one missing member interrupts the lower row.Revision 2. Reference P00. Geometric construction. u=48. Circle radiusu; pitch2u=96 on both axes. The absent lower middle circle uses the absent-outline convention. Outer width6u=288. No full background grid; tangent contacts carry the pattern.6u2uInterruption diff --git a/apps/ui/app/plotter/drawings/geo-negative-space.svg b/apps/ui/app/plotter/drawings/geo-negative-space.svg new file mode 100644 index 000000000..dd0fbdffb --- /dev/null +++ b/apps/ui/app/plotter/drawings/geo-negative-space.svg @@ -0,0 +1 @@ +The shape between circlesTwo equal circular regions reveal an unhatched lens where their boundaries overlap.Revision 2. Reference P00. Geometric construction. u=100; two radius-u circles have centers separated byu. H1 occupies the symmetric difference of the two discs; the shared lens is clear. A single even-odd clipping path defines the two bounded regions.R = uuShared void diff --git a/apps/ui/app/plotter/drawings/interface-corner-rule.svg b/apps/ui/app/plotter/drawings/interface-corner-rule.svg new file mode 100644 index 000000000..f49764992 --- /dev/null +++ b/apps/ui/app/plotter/drawings/interface-corner-rule.svg @@ -0,0 +1 @@ +One corner ruleThree differently proportioned components retain a common corner radius.Revision 2. Reference: P00. Geometric construction. u=24; true circular R24 corners. Widths 4u,6u,3u; heights6u,4u,2u. Generating radii meet each top-left arc.R244uR246uR243uOne radius · different proportions diff --git a/apps/ui/app/plotter/drawings/interface-reflow.svg b/apps/ui/app/plotter/drawings/interface-reflow.svg new file mode 100644 index 000000000..3ac912336 --- /dev/null +++ b/apps/ui/app/plotter/drawings/interface-reflow.svg @@ -0,0 +1 @@ +The same modules, reflowedThree equal modules preserve their size when the available width wraps the last one onto another row.Revision 2. Reference: C02. Geometric UI construction. Each module96x56; all gaps and insets12. Outer frames336x80 and228x148. C is emphasized in both arrangements; no resizing of the modules.ABCABC336228WideNarrow diff --git a/apps/ui/app/plotter/drawings/interface-slider-value.svg b/apps/ui/app/plotter/drawings/interface-slider-value.svg new file mode 100644 index 000000000..22a70db15 --- /dev/null +++ b/apps/ui/app/plotter/drawings/interface-slider-value.svg @@ -0,0 +1 @@ +Position becomes valueThe thumb locates sixty percent of a shared travel interval; the track clears its circular outline.Revision 2. Reference: P00. Geometric UI construction. Travel from x120 to440 is320; thumb x312 is192/320=60%. Thumb R24. Rail y220/236 intersects it at x312±sqrt(24²−8²), and rail paths stop there.60%0100R2460 diff --git a/apps/ui/app/plotter/drawings/manifest.json b/apps/ui/app/plotter/drawings/manifest.json new file mode 100644 index 000000000..502e3ef0f --- /dev/null +++ b/apps/ui/app/plotter/drawings/manifest.json @@ -0,0 +1,416 @@ +[ + { + "id": "agent-registration", + "code": "AG01", + "title": "An agent, registered", + "description": "Identity, name, and tools keep their alignment as they become one registry record.", + "category": "Agents", + "file": "agent-registration.svg" + }, + { + "id": "agent-delegation", + "code": "AG02", + "title": "One task, two agents", + "description": "Research and drafting separate into complementary scopes, assigned to two agents.", + "category": "Agents", + "file": "agent-delegation.svg" + }, + { + "id": "agent-handoff", + "code": "AG03", + "title": "Context, carried forward", + "description": "One continuous context passes between two agents without changing its form.", + "category": "Agents", + "file": "agent-handoff.svg", + "reference": "E02, I06", + "construction": "Conceptual handoff. A 40-unit band has paired bend radii 20 and 60. The same 64 × 24 context capsule has 8-unit side clearances in both horizontal portions. Hatching marks the handoff section, not a physical material. No serialization, timing or persistence guarantee is asserted." + }, + { + "id": "agent-tools", + "code": "AG04", + "title": "One matching interface", + "description": "A selected tool’s rounded end matches the agent’s common interface.", + "category": "Agents", + "file": "agent-tools.svg", + "reference": "C02, P02", + "construction": "Conceptual tool selection. All tools have 144 × 48 footprints with R24 ends. The receiving semicircle has the same radius. The selected end reaches the receiving profile after a horizontal translation of 96 units. The drawing does not describe an actual API connector." + }, + { + "id": "agent-permissions", + "code": "AG05", + "title": "Only the shared scope", + "description": "The granted region is the intersection of what is requested and what is allowed.", + "category": "Agents", + "file": "agent-permissions.svg", + "reference": "K08, P00", + "construction": "Conceptual permission scope. Two 240 × 144 rounded fields overlap; the selected intersection is bounded by their actual edges, with no duplicate outlines. Unit u = 24; overlap width is 6u. Hatching means granted scope. This is an illustrative set relationship, not a policy engine or numerical permission measure." + }, + { + "id": "agent-memory", + "code": "AG06", + "title": "One context, two readers", + "description": "Two agents address the same findings row within one shared memory.", + "category": "Agents", + "file": "agent-memory.svg", + "reference": "B09, C02", + "construction": "Conceptual shared access. The 192-unit record is 8u wide with 48-unit, 2u rows. Both connections terminate at the same row, not duplicated stores. Route returns are R24. Hatching identifies the selected row. No concurrent consistency guarantee is depicted." + }, + { + "id": "agent-checkpoint", + "code": "AG07", + "title": "A state worth keeping", + "description": "One selected state is preserved as a checkpoint while the wider work continues.", + "category": "Agents", + "file": "agent-checkpoint.svg", + "reference": "C01, K07", + "construction": "Conceptual checkpoint. Unit u = 24. A 96-unit, 4u partition of a 336-unit, 14u record is repeated unchanged below. Hatching identifies the same saved state in both positions. This illustrates capture, not a claim of fault tolerance or storage durability." + }, + { + "id": "agent-parallel", + "code": "AG08", + "title": "Separate work, one join", + "description": "Three agents finish at different points and wait for the longest task before joining.", + "category": "Agents", + "file": "agent-parallel.svg", + "reference": "B04, K07", + "construction": "Conceptual synchronization. One illustrative time unit u = 72 SVG units. Work durations are 3u, 4u and 2u; the shared join is at 4u. Hatching denotes waiting intervals only. The shared join line owns all right-edge segments. This is not a scheduler performance claim." + }, + { + "id": "geo-circle-rectangle", + "code": "GE01", + "title": "A circle, extended", + "description": "A circle and its wider rounded-rectangle state share one height while their corner radii change.", + "category": "Foundations", + "file": "geo-circle-rectangle.svg", + "reference": "P00", + "construction": "Geometric construction. u=44. Initial circle radius2u=88; final rectangle6u×4u with corner radiusu. Dashed circle means previous form. The two arrows describe horizontal change, not scaling the height." + }, + { + "id": "geo-compound-radii", + "code": "GE03", + "title": "A curve from three radii", + "description": "Three circular arcs share tangent directions at their joins, producing one uninterrupted curve.", + "category": "Foundations", + "file": "geo-compound-radii.svg", + "reference": "P00", + "construction": "Geometric construction. Arc centers(104,204),(280,204),(280,284); radii112,64,144. First arc90→0°, second180→270°, third270→360°. Only selected generating radii are drawn, avoiding coincident radius segments." + }, + { + "id": "geo-negative-space", + "code": "GE04", + "title": "The shape between circles", + "description": "Two equal circular regions reveal an unhatched lens where their boundaries overlap.", + "category": "Foundations", + "file": "geo-negative-space.svg", + "reference": "P00", + "construction": "Geometric construction. u=100; two radius-u circles have centers separated byu. H1 occupies the symmetric difference of the two discs; the shared lens is clear. A single even-odd clipping path defines the two bounded regions." + }, + { + "id": "geo-interrupted-pattern", + "code": "GE05", + "title": "One interval left open", + "description": "A field of tangent circles keeps its repeated pitch while one missing member interrupts the lower row.", + "category": "Foundations", + "file": "geo-interrupted-pattern.svg", + "reference": "P00", + "construction": "Geometric construction. u=48. Circle radiusu; pitch2u=96 on both axes. The absent lower middle circle uses the absent-outline convention. Outer width6u=288. No full background grid; tangent contacts carry the pattern." + }, + { + "id": "mech-iris-aperture", + "code": "ME01", + "title": "Six blades, one aperture", + "description": "Six repeated edges shape a single opening. Hatching identifies one visible blade.", + "category": "Mechanisms", + "file": "mech-iris-aperture.svg" + }, + { + "id": "mech-pulley-ratio", + "code": "ME02", + "title": "One belt, two rotations", + "description": "A shared belt joins two pulley radii in a 2:1 relation.", + "category": "Mechanisms", + "file": "mech-pulley-ratio.svg", + "reference": "P00", + "construction": "Conceptual mechanism study. Contact radii72 and36, center distance208. Contact normals derive from(R-r)/distance. The belt boundary owns its circular contact arcs; no pulley outline duplicates them. Inner circles are subordinate details. Ideal no-slip ratio only; no hardware or load specification." + }, + { + "id": "mech-cam-follower", + "code": "ME04", + "title": "An offset becomes a lift", + "description": "An eccentric circular cam meets a guided roller, relating rotation to a vertical displacement.", + "category": "Mechanisms", + "file": "mech-cam-follower.svg", + "reference": "P00", + "construction": "Conceptual mechanism, one pose. Cam center(260,244),radius76; shaft(236,244), eccentricity24. Roller radius12 atx280; y derived from center distance88, so contact is tangent. Rotation is about the shaft, lift is guided vertically. No dynamic, contact-force, or manufacturability claim." + }, + { + "id": "mech-latch-receiver", + "code": "ME05", + "title": "A profile finds its receiver", + "description": "A rounded bolt approaches the matching receiver, with its destination and clearance visible.", + "category": "Mechanisms", + "file": "mech-latch-receiver.svg", + "reference": "P00", + "construction": "Conceptual fit study. Bolt24-radius nose; receiver28-radius cavity gives4 radial clearance. Source bolt nose center(304,220); proposed destination center(392,220), translation88. Absent lines show destination only. H1 identifies the receiver body, not a material section. No hardware specification." + }, + { + "id": "arch-staircase", + "code": "AR02", + "title": "One rise, one run", + "description": "Six equal rise-and-run pairs construct a staircase along one repeated slope.", + "category": "Architecture", + "file": "arch-staircase.svg", + "reference": "C02", + "construction": "Geometric staircase study. u=16; every run3u=48, rise2u=32, six runs span18u=288. The diagonal joins the repeated pitch vertices; H1 identifies one triangular unit cell, not a material section. The outline is a construction profile, not a staircase specification." + }, + { + "id": "arch-missing-keystone", + "code": "AR03", + "title": "The last piece of the arch", + "description": "A lifted keystone preserves the geometry of the opening it will complete.", + "category": "Architecture", + "file": "arch-missing-keystone.svg", + "reference": "E06", + "construction": "Geometric assembly study. Concentric arch radii120 and84; missing20° sector spans-100° to-80°. The keystone is the exact annular sector translated upward52, with H1 identifying that part. Missing arcs show its destination; shared radial seams are not duplicated. No structural-stability assertion." + }, + { + "id": "nature-leaf-veins", + "code": "NA01", + "title": "A branching leaf", + "description": "Paired veins follow a repeated pitch within a leaf formed by two intersecting arcs.", + "category": "Nature", + "file": "nature-leaf-veins.svg", + "reference": "P00", + "construction": "u=40. Lens centers (330,220) and (230,220), radii130, tips(280,100),(280,340); maximum width160=4u. Vein endpoints have vertical pitch40. The emphasized pair joins the same midrib. Stylized leaf, not a botanical specimen." + }, + { + "id": "nature-honeycomb", + "code": "NA02", + "title": "One edge, two cells", + "description": "A selected hexagonal cell shares each of its walls with the surrounding six cells.", + "category": "Nature", + "file": "nature-honeycomb.svg", + "reference": "P00", + "construction": "Seven regular hexagons of circumradius48. Neighbor centers lie at sqrt(3)*48. Shared edges are serialized once; central-cell edges own primary role. H1 denotes the selected cell. r=48, total width288=6r." + }, + { + "id": "music-chord", + "code": "MU02", + "title": "Three notes, one onset", + "description": "Three separately placed pitches align vertically on one shared onset to form a chord.", + "category": "Music", + "file": "music-chord.svg", + "reference": "C01", + "construction": "The source heads at y240,208,176 retain those staff positions in the final chord atx424. Staff spacing16. Every staff segment clears notehead interiors; the chord stem is drawn once. This is a pitch/alignment illustration, not a complete notated score or named harmony." + }, + { + "id": "map-summit-contours", + "code": "MP04", + "title": "The summit in contours", + "description": "Nested elevation lines tighten around one marked summit, with a consistent interval between levels.", + "category": "Maps", + "file": "map-summit-contours.svg", + "reference": "P00", + "construction": "Five illustrative polar contours carry levels100,125,150,175,200. Radii28,50,74,98,122 with deterministic cosine/sine modulation and small center shifts. A plus marks the summit inside the200 contour. Each path deliberately stops around its text label; no masking patch hides linework. These are invented elevation units, not a surveyed location." + }, + { + "id": "map-converging-paths", + "code": "MP05", + "title": "Different paths, one place", + "description": "Three routes join one final stretch before reaching the same destination.", + "category": "Maps", + "file": "map-converging-paths.svg", + "reference": "E02", + "construction": "The central route is primary; two secondary approaches use R48 outside bends and R24 merging bends. Shared final segment x288..408 at y220 is drawn once. The destination ring is reached at its left tangent x408. Conceptual map topology, not a surveyed road network." + }, + { + "id": "type-letter-strokes", + "code": "TY01", + "title": "A from three strokes", + "description": "Two parallel-sided diagonal strokes and one horizontal stroke make an outlined capital A; the crossbar is identified by hatching.", + "category": "Typography", + "file": "type-letter-strokes.svg", + "reference": "T01", + "construction": "Borrow T01’s outlined glyph, counter and named construction. Local u = 40 SVG units; cap height = 6u; each sloping stem has 40 units of horizontal width and constant normal width 40 / sqrt(1 + 0.45²). The crossbar spans y252–280. This is a constructed glyph, not a production font." + }, + { + "id": "type-shared-glyphs", + "code": "TY02", + "title": "One arch, three glyphs", + "description": "A shared circular shoulder and a constant stem width generate n, h and m, with the h selected to show the construction.", + "category": "Typography", + "file": "type-shared-glyphs.svg", + "reference": "T01", + "construction": "Borrow T01’s circular counter and stem relationship. Local u = 16 SVG units; outer shoulders R40 and inner shoulders R24 differ by u; ascender extension is 4u. The overlapping outer m shoulder arcs meet at their calculated intersection (400,212). The letters are conceptual geometric glyphs." + }, + { + "id": "type-ligature", + "code": "TY03", + "title": "The joining stroke", + "description": "The f and i share one continuous crossbar, joining the two glyphs into a single constructed ligature.", + "category": "Typography", + "file": "type-ligature.svg", + "reference": "T01", + "construction": "Borrow T01’s outlined letter and circular counter construction. Local u = 28 SVG units; the hook uses concentric R56 and R28 arcs, both vertical stems and the common crossbar are u wide, and the i dot has diameter u. Hatching identifies the bar that unites the forms." + }, + { + "id": "type-negative-monogram", + "code": "TY04", + "title": "A monogram in the void", + "description": "An M is defined by the transparent space within a hatched field; its straight stems and surrounding margin follow the same unit.", + "category": "Typography", + "file": "type-negative-monogram.svg", + "reference": "T02", + "construction": "Borrow T02’s mark, negative space and local construction unit. Local u = 32 SVG units; the outer field is 8u square; each straight stem and the nearest field inset are u. The parallel diagonal strokes are approximately one unit wide measured normally. Hatching belongs only to the surrounding field, never the letter." + }, + { + "id": "type-optical-alignment", + "code": "TY05", + "title": "The center you see", + "description": "A rightward shift brings a triangle’s area centroid to the center of its frame, providing a geometric starting point for optical alignment.", + "category": "Typography", + "file": "type-optical-alignment.svg", + "reference": "T02", + "construction": "Borrow T02’s bounded mark and annotated construction. Both frames are 144 units square; triangles are 90 units wide and 108 high. The first uses bounding-box alignment; the second moves right by 90 / 6 = 15 units so its area centroid is centered. Hatching denotes the measured area. Final optical balance remains a visual judgement." + }, + { + "id": "interface-corner-rule", + "code": "UI01", + "title": "One corner rule", + "description": "Three differently proportioned components retain a common corner radius.", + "category": "Interfaces", + "file": "interface-corner-rule.svg", + "reference": "P00", + "construction": "Geometric construction. u=24; true circular R24 corners. Widths 4u,6u,3u; heights6u,4u,2u. Generating radii meet each top-left arc." + }, + { + "id": "interface-reflow", + "code": "UI02", + "title": "The same modules, reflowed", + "description": "Three equal modules preserve their size when the available width wraps the last one onto another row.", + "category": "Interfaces", + "file": "interface-reflow.svg", + "reference": "C02", + "construction": "Geometric UI construction. Each module96x56; all gaps and insets12. Outer frames336x80 and228x148. C is emphasized in both arrangements; no resizing of the modules." + }, + { + "id": "interface-slider-value", + "code": "UI03", + "title": "Position becomes value", + "description": "The thumb locates sixty percent of a shared travel interval; the track clears its circular outline.", + "category": "Interfaces", + "file": "interface-slider-value.svg", + "reference": "P00", + "construction": "Geometric UI construction. Travel from x120 to440 is320; thumb x312 is192/320=60%. Thumb R24. Rail y220/236 intersects it at x312±sqrt(24²−8²), and rail paths stop there." + }, + { + "id": "software-cache-route", + "code": "SW02", + "title": "A shorter return", + "description": "A cached response turns back at the near bend while the longer path continues to the origin.", + "category": "Systems", + "file": "software-cache-route.svg", + "reference": "E02", + "construction": "Conceptual route, not network architecture. u=84; cache x280 is2u from x112, origin x448 is4u. All corners R24. Shared request/return segments are drawn once. Distances show the analogy only, not latency or network hops." + }, + { + "id": "software-branch-history", + "code": "SW03", + "title": "A history comes together", + "description": "One branch departs from a shared history, carries its own revision, and joins the main line again.", + "category": "Systems", + "file": "software-branch-history.svg", + "reference": "B04", + "construction": "Conceptual version history. u=24; routing corners R24, fork-to-merge span240. Circles are revision points; paths are ancestry. Main and branch intersect only at declared fork/merge. Spatial distances are illustrative, not time or commit count." + }, + { + "id": "time-appointment-buffers", + "code": "TM02", + "title": "Room around an appointment", + "description": "An hour-long appointment reserves fifteen minutes of buffer time on either side.", + "category": "Scheduling", + "file": "time-appointment-buffers.svg", + "reference": "K07", + "construction": "Conceptual schedule. Borrowed the user-liked K09/buffers hierarchy: primary appointment, subordinate buffers, boundary witnesses and total reserved interval. Scale4 SVG units/minute; widths60,240,60 correspond15,60,15 minutes. Example10:00–11:00 appointment; reserved09:45–11:15. Hatch means buffer time." + }, + { + "id": "time-common-recurrence", + "code": "TM03", + "title": "The same interval, again", + "description": "Four occurrences align to one repeated seven-day interval.", + "category": "Scheduling", + "file": "time-common-recurrence.svg", + "reference": "B04", + "construction": "Conceptual recurrence. Event centers136,232,328,424 share a96-unit pitch representing seven days. Tiles64x64. The common baseline is the time axis; stems locate each occurrence. No month or real calendar date is asserted." + }, + { + "id": "craft-weave", + "code": "CR01", + "title": "Over and under", + "description": "One warp and one weft reveal the alternating rule that holds a weave together.", + "category": "Craft", + "file": "craft-weave.svg", + "reference": "C02, P00", + "construction": "Flat weave construction: 24-unit strands on 64-unit centers. Under-strand outlines stop 4 units before the crossing strand; the interruptions alternate by row and column. Hatching marks one selected crossing. This is an ordering diagram, not a fabric or strength specification." + }, + { + "id": "craft-stitch", + "code": "CR02", + "title": "A seam, joined", + "description": "Repeated crossed stitches bridge two edges on one shared hole pitch.", + "category": "Craft", + "file": "craft-stitch.svg", + "reference": "K07, C02", + "construction": "Conceptual front view of a stitched seam. Hole centers repeat every 48 units and sit 16 units from the joined edge. Thread segments end on the hole circumferences. One diagonal is interrupted at each crossing to preserve over/under meaning. The construction is illustrative, not a sewing specification." + }, + { + "id": "craft-dovetail", + "code": "CR03", + "title": "A joint that holds", + "description": "A wider tail and narrower neck create one complementary boundary between two pieces.", + "category": "Craft", + "file": "craft-dovetail.svg", + "reference": "C01, P00", + "construction": "Illustrative dovetail in plan. Shared seam drawn once. Neck width 128, tail width 160, depth 48 SVG units; symmetric flank offset 16. Hatching identifies the projecting tongue region, not a material. No manufacturing tolerance or load claim is implied." + }, + { + "id": "craft-paper-fold", + "code": "CR04", + "title": "Across the crease", + "description": "One corner reflects across its diagonal crease and becomes a triangular flap.", + "category": "Craft", + "file": "craft-paper-fold.svg", + "reference": "P00, E00", + "construction": "Flat paper-fold study. Square side 240; u=40. Crease x+y=380 joins (160,220) and (280,100); reflection maps the original corner (160,100) to (280,220). The crease is drawn only once as the visible boundary. Hatching identifies the folded flap, and dashed edges mark the original absent corner. Arrow indicates folding direction, not a physical 3D trajectory." + }, + { + "id": "craft-tiles", + "code": "CR05", + "title": "One shared corner", + "description": "Four tiles meet around a common circular opening and a constant joint width.", + "category": "Craft", + "file": "craft-tiles.svg", + "reference": "P00, C02", + "construction": "Flat tiling construction. Four 128-unit square allocations surround a 32-unit cross joint. A shared R48 circle cuts their inner corners; its remaining arcs appear only within the joint, so no locus segment retraces a tile edge. Overall extent 288 × 288. The four quarter turns place tiles in one plane and do not introduce perspective." + }, + { + "id": "play-dominoes", + "code": "PL03", + "title": "One piece short", + "description": "The pip sequence continues across one absent domino, leaving its position unmistakable.", + "category": "Play", + "file": "play-dominoes.svg", + "reference": "E00, K07", + "construction": "Flat domino sequence. Each piece is48×96 with72-unit pitch; u=24. Filled circles are the declared domino-pip convention. Visible pairs are1/2,2/3,4/5,5/6; the missing3/4 piece is absent rather than hidden. No falling motion is depicted." + }, + { + "id": "play-balance", + "code": "PL04", + "title": "Unequal arms, equal moments", + "description": "Three mass units on the shorter arm balance two on the longer arm.", + "category": "Play", + "file": "play-balance.svg", + "reference": "P03, P00", + "construction": "Conceptual ideal lever. Fulcrum x248; weight centersx152 and392 give arms96 and144, withu=48. Identical72×24 blocks represent one mass unitm. Hatching identifies one equal unit on each side. Geometry illustrates equal moments only, not structural capacity or real-world calibration." + } +] diff --git a/apps/ui/app/plotter/drawings/map-converging-paths.svg b/apps/ui/app/plotter/drawings/map-converging-paths.svg new file mode 100644 index 000000000..a74a4a940 --- /dev/null +++ b/apps/ui/app/plotter/drawings/map-converging-paths.svg @@ -0,0 +1 @@ +Different paths, one placeThree routes join one final stretch before reaching the same destination.Mode: conceptual construction. Reference: E02. The central route is primary; two secondary approaches use R48 outside bends and R24 merging bends. Shared final segment x288..408 at y220 is drawn once. The destination ring is reached at its left tangent x408. Conceptual map topology, not a surveyed road network.DestinationR48Shared routeABC diff --git a/apps/ui/app/plotter/drawings/map-summit-contours.svg b/apps/ui/app/plotter/drawings/map-summit-contours.svg new file mode 100644 index 000000000..4f52b54e7 --- /dev/null +++ b/apps/ui/app/plotter/drawings/map-summit-contours.svg @@ -0,0 +1 @@ +The summit in contoursNested elevation lines tighten around one marked summit, with a consistent interval between levels.Mode: conceptual construction. Reference: P00. Five illustrative polar contours carry levels100,125,150,175,200. Radii28,50,74,98,122 with deterministic cosine/sine modulation and small center shifts. A plus marks the summit inside the200 contour. Each path deliberately stops around its text label; no masking patch hides linework. These are invented elevation units, not a surveyed location.200175150125100SummitContour interval / 25 diff --git a/apps/ui/app/plotter/drawings/mech-cam-follower.svg b/apps/ui/app/plotter/drawings/mech-cam-follower.svg new file mode 100644 index 000000000..7862730b3 --- /dev/null +++ b/apps/ui/app/plotter/drawings/mech-cam-follower.svg @@ -0,0 +1 @@ +An offset becomes a liftAn eccentric circular cam meets a guided roller, relating rotation to a vertical displacement.Revision 2. Reference P00. Conceptual mechanism, one pose. Cam center(260,244),radius76; shaft(236,244), eccentricity24. Roller radius12 atx280; y derived from center distance88, so contact is tangent. Rotation is about the shaft, lift is guided vertically. No dynamic, contact-force, or manufacturability claim.e = 24R76RotationLiftFollower diff --git a/apps/ui/app/plotter/drawings/mech-iris-aperture.svg b/apps/ui/app/plotter/drawings/mech-iris-aperture.svg new file mode 100644 index 000000000..0b778483a --- /dev/null +++ b/apps/ui/app/plotter/drawings/mech-iris-aperture.svg @@ -0,0 +1,35 @@ + + Six blades, one aperture + Six repeated blade edges are tangent to one generating circle, leaving a hexagonal aperture. Hatching identifies the visible region of one blade. + Study: mech-iris-aperture, revision 2, explicitly liked by the user on 2026-10-31. Mode: geometric construction inspired by an iris aperture. Reference: P00 circle, tangency, hierarchy, bounded hatch. C=(280,220); R=120; r=40; u=40 SVG units. For normals n at multiples of 60 degrees and tangent t=(-n.y,n.x), blade edges lie on n dot (P-C)=r. Their outer endpoints are C+r*n+sqrt(R*R-r*r)*t. The six opening vertices lie on radius r/cos(30 degrees), at angles 30+60i degrees. Each visible edge runs from the previous opening vertex to its outer endpoint. H1 hatching denotes one visible blade region, bounded by adjacent edges and the outer circle; it is not material or shading. This illustrative layout does not assert pivot positions, blade thickness, hidden overlap geometry, or verified mechanical motion. The axes and generating circle explain the repetition without a background grid. + + + + + + + + + + + + + + + + + + + + + + + + u + 6u + + + Aperture + Blade 01 + + diff --git a/apps/ui/app/plotter/drawings/mech-latch-receiver.svg b/apps/ui/app/plotter/drawings/mech-latch-receiver.svg new file mode 100644 index 000000000..4f8060791 --- /dev/null +++ b/apps/ui/app/plotter/drawings/mech-latch-receiver.svg @@ -0,0 +1 @@ +A profile finds its receiverA rounded bolt approaches the matching receiver, with its destination and clearance visible.Revision 2. Reference P00. Conceptual fit study. Bolt24-radius nose; receiver28-radius cavity gives4 radial clearance. Source bolt nose center(304,220); proposed destination center(392,220), translation88. Absent lines show destination only. H1 identifies the receiver body, not a material section. No hardware specification.R244 clearanceBoltReceiver216 diff --git a/apps/ui/app/plotter/drawings/mech-pulley-ratio.svg b/apps/ui/app/plotter/drawings/mech-pulley-ratio.svg new file mode 100644 index 000000000..208585725 --- /dev/null +++ b/apps/ui/app/plotter/drawings/mech-pulley-ratio.svg @@ -0,0 +1 @@ +One belt, two rotationsA shared belt joins two pulley radii in a 2:1 relation.Revision 2. Reference P00. Conceptual mechanism study. Contact radii72 and36, center distance208. Contact normals derive from(R-r)/distance. The belt boundary owns its circular contact arcs; no pulley outline duplicates them. Inner circles are subordinate details. Ideal no-slip ratio only; no hardware or load specification.1 turn2 turns2rr208 diff --git a/apps/ui/app/plotter/drawings/music-chord.svg b/apps/ui/app/plotter/drawings/music-chord.svg new file mode 100644 index 000000000..48d61be00 --- /dev/null +++ b/apps/ui/app/plotter/drawings/music-chord.svg @@ -0,0 +1 @@ +Three notes, one onsetThree separately placed pitches align vertically on one shared onset to form a chord.Mode: conceptual construction. Reference: C01. The source heads at y240,208,176 retain those staff positions in the final chord atx424. Staff spacing16. Every staff segment clears notehead interiors; the chord stem is drawn once. This is a pitch/alignment illustration, not a complete notated score or named harmony.SeparateTogetherThree pitches diff --git a/apps/ui/app/plotter/drawings/nature-honeycomb.svg b/apps/ui/app/plotter/drawings/nature-honeycomb.svg new file mode 100644 index 000000000..15aa73b1e --- /dev/null +++ b/apps/ui/app/plotter/drawings/nature-honeycomb.svg @@ -0,0 +1 @@ +One edge, two cellsA selected hexagonal cell shares each of its walls with the surrounding six cells.Mode: conceptual construction. Reference: P00. Seven regular hexagons of circumradius48. Neighbor centers lie at sqrt(3)*48. Shared edges are serialized once; central-cell edges own primary role. H1 denotes the selected cell. r=48, total width288=6r.Shared wall6rr diff --git a/apps/ui/app/plotter/drawings/nature-leaf-veins.svg b/apps/ui/app/plotter/drawings/nature-leaf-veins.svg new file mode 100644 index 000000000..e26da7c2f --- /dev/null +++ b/apps/ui/app/plotter/drawings/nature-leaf-veins.svg @@ -0,0 +1 @@ +A branching leafPaired veins follow a repeated pitch within a leaf formed by two intersecting arcs.Mode: conceptual construction. Reference: P00. u=40. Lens centers (330,220) and (230,220), radii130, tips(280,100),(280,340); maximum width160=4u. Vein endpoints have vertical pitch40. The emphasized pair joins the same midrib. Stylized leaf, not a botanical specimen.Midribu4u diff --git a/apps/ui/app/plotter/drawings/play-balance.svg b/apps/ui/app/plotter/drawings/play-balance.svg new file mode 100644 index 000000000..f280aca15 --- /dev/null +++ b/apps/ui/app/plotter/drawings/play-balance.svg @@ -0,0 +1 @@ +Unequal arms, equal momentsThree mass units on the shorter arm balance two on the longer arm.Revision 2, catalog candidate. Reference: P03, P00. Conceptual ideal lever. Fulcrum x248; weight centersx152 and392 give arms96 and144, withu=48. Identical72×24 blocks represent one mass unitm. Hatching identifies one equal unit on each side. Geometry illustrates equal moments only, not structural capacity or real-world calibration.2u3u3m2m3m × 2u = 2m × 3u diff --git a/apps/ui/app/plotter/drawings/play-dominoes.svg b/apps/ui/app/plotter/drawings/play-dominoes.svg new file mode 100644 index 000000000..faba03c80 --- /dev/null +++ b/apps/ui/app/plotter/drawings/play-dominoes.svg @@ -0,0 +1 @@ +One piece shortThe pip sequence continues across one absent domino, leaving its position unmistakable.Revision 2, catalog candidate. Reference: E00, K07. Flat domino sequence. Each piece is48×96 with72-unit pitch; u=24. Filled circles are the declared domino-pip convention. Visible pairs are1/2,2/3,4/5,5/6; the missing3/4 piece is absent rather than hidden. No falling motion is depicted.Missing2u3u diff --git a/apps/ui/app/plotter/drawings/software-branch-history.svg b/apps/ui/app/plotter/drawings/software-branch-history.svg new file mode 100644 index 000000000..2bbf8b9c1 --- /dev/null +++ b/apps/ui/app/plotter/drawings/software-branch-history.svg @@ -0,0 +1 @@ +A history comes togetherOne branch departs from a shared history, carries its own revision, and joins the main line again.Revision 2. Reference: B04. Conceptual version history. u=24; routing corners R24, fork-to-merge span240. Circles are revision points; paths are ancestry. Main and branch intersect only at declared fork/merge. Spatial distances are illustrative, not time or commit count.R24BranchMainForkMerge10u diff --git a/apps/ui/app/plotter/drawings/software-cache-route.svg b/apps/ui/app/plotter/drawings/software-cache-route.svg new file mode 100644 index 000000000..ffa07875e --- /dev/null +++ b/apps/ui/app/plotter/drawings/software-cache-route.svg @@ -0,0 +1 @@ +A shorter returnA cached response turns back at the near bend while the longer path continues to the origin.Revision 2. Reference: E02. Conceptual route, not network architecture. u=84; cache x280 is2u from x112, origin x448 is4u. All corners R24. Shared request/return segments are drawn once. Distances show the analogy only, not latency or network hops.CacheOriginR24RequestResponse2u4u diff --git a/apps/ui/app/plotter/drawings/time-appointment-buffers.svg b/apps/ui/app/plotter/drawings/time-appointment-buffers.svg new file mode 100644 index 000000000..a02e3f6e1 --- /dev/null +++ b/apps/ui/app/plotter/drawings/time-appointment-buffers.svg @@ -0,0 +1 @@ +Room around an appointmentAn hour-long appointment reserves fifteen minutes of buffer time on either side.Revision 2. Reference: K07. Conceptual schedule. Borrowed the user-liked K09/buffers hierarchy: primary appointment, subordinate buffers, boundary witnesses and total reserved interval. Scale4 SVG units/minute; widths60,240,60 correspond15,60,15 minutes. Example10:00–11:00 appointment; reserved09:45–11:15. Hatch means buffer time.15 min60 min15 minBuffer beforeAppointmentBuffer after09:4510:0011:0011:1590 min reserved diff --git a/apps/ui/app/plotter/drawings/time-common-recurrence.svg b/apps/ui/app/plotter/drawings/time-common-recurrence.svg new file mode 100644 index 000000000..9909a2a4c --- /dev/null +++ b/apps/ui/app/plotter/drawings/time-common-recurrence.svg @@ -0,0 +1 @@ +The same interval, againFour occurrences align to one repeated seven-day interval.Revision 2. Reference: B04. Conceptual recurrence. Event centers136,232,328,424 share a96-unit pitch representing seven days. Tiles64x64. The common baseline is the time axis; stems locate each occurrence. No month or real calendar date is asserted.Event01Event02Event03Event047 days7 days7 daysOne recurring interval diff --git a/apps/ui/app/plotter/drawings/type-letter-strokes.svg b/apps/ui/app/plotter/drawings/type-letter-strokes.svg new file mode 100644 index 000000000..d2d3a9a08 --- /dev/null +++ b/apps/ui/app/plotter/drawings/type-letter-strokes.svg @@ -0,0 +1 @@ +A from three strokesTwo parallel-sided diagonal strokes and one horizontal stroke make an outlined capital A; the crossbar is identified by hatching.Mode: conceptual construction. Reference: T01. Borrow T01’s outlined glyph, counter and named construction. Local u = 40 SVG units; cap height = 6u; each sloping stem has 40 units of horizontal width and constant normal width 40 / sqrt(1 + 0.45²). The crossbar spans y252–280. This is a constructed glyph, not a production font.6uuCrossbar diff --git a/apps/ui/app/plotter/drawings/type-ligature.svg b/apps/ui/app/plotter/drawings/type-ligature.svg new file mode 100644 index 000000000..db5daccd3 --- /dev/null +++ b/apps/ui/app/plotter/drawings/type-ligature.svg @@ -0,0 +1 @@ +The joining strokeThe f and i share one continuous crossbar, joining the two glyphs into a single constructed ligature.Mode: conceptual construction. Reference: T01. Borrow T01’s outlined letter and circular counter construction. Local u = 28 SVG units; the hook uses concentric R56 and R28 arcs, both vertical stems and the common crossbar are u wide, and the i dot has diameter u. Hatching identifies the bar that unites the forms.R56Shared strokeu diff --git a/apps/ui/app/plotter/drawings/type-negative-monogram.svg b/apps/ui/app/plotter/drawings/type-negative-monogram.svg new file mode 100644 index 000000000..ba762da40 --- /dev/null +++ b/apps/ui/app/plotter/drawings/type-negative-monogram.svg @@ -0,0 +1 @@ +A monogram in the voidAn M is defined by the transparent space within a hatched field; its straight stems and surrounding margin follow the same unit.Mode: conceptual construction. Reference: T02. Borrow T02’s mark, negative space and local construction unit. Local u = 32 SVG units; the outer field is 8u square; each straight stem and the nearest field inset are u. The parallel diagonal strokes are approximately one unit wide measured normally. Hatching belongs only to the surrounding field, never the letter.8uuu diff --git a/apps/ui/app/plotter/drawings/type-optical-alignment.svg b/apps/ui/app/plotter/drawings/type-optical-alignment.svg new file mode 100644 index 000000000..d078d7e93 --- /dev/null +++ b/apps/ui/app/plotter/drawings/type-optical-alignment.svg @@ -0,0 +1 @@ +The center you seeA rightward shift brings a triangle’s area centroid to the center of its frame, providing a geometric starting point for optical alignment.Mode: conceptual construction. Reference: T02. Borrow T02’s bounded mark and annotated construction. Both frames are 144 units square; triangles are 90 units wide and 108 high. The first uses bounding-box alignment; the second moves right by 90 / 6 = 15 units so its area centroid is centered. Hatching denotes the measured area. Final optical balance remains a visual judgement.BoundsAreaδδ = width / 6 diff --git a/apps/ui/app/plotter/drawings/type-shared-glyphs.svg b/apps/ui/app/plotter/drawings/type-shared-glyphs.svg new file mode 100644 index 000000000..e52a084c7 --- /dev/null +++ b/apps/ui/app/plotter/drawings/type-shared-glyphs.svg @@ -0,0 +1 @@ +One arch, three glyphsA shared circular shoulder and a constant stem width generate n, h and m, with the h selected to show the construction.Mode: conceptual construction. Reference: T01. Borrow T01’s circular counter and stem relationship. Local u = 16 SVG units; outer shoulders R40 and inner shoulders R24 differ by u; ascender extension is 4u. The overlapping outer m shoulder arcs meet at their calculated intersection (400,212). The letters are conceptual geometric glyphs.R40u4u diff --git a/apps/ui/app/plotter/engineering-motion-preview.module.css b/apps/ui/app/plotter/engineering-motion-preview.module.css new file mode 100644 index 000000000..3a33eeaf2 --- /dev/null +++ b/apps/ui/app/plotter/engineering-motion-preview.module.css @@ -0,0 +1,13 @@ +.animation { + display: none; +} + +@media (prefers-reduced-motion: no-preference) { + .drawing[data-live] .animation { + display: block; + } + + .drawing[data-live] .original { + visibility: hidden; + } +} diff --git a/apps/ui/app/plotter/engineering-motion-preview.tsx b/apps/ui/app/plotter/engineering-motion-preview.tsx new file mode 100644 index 000000000..11730b1e1 --- /dev/null +++ b/apps/ui/app/plotter/engineering-motion-preview.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { PauseIcon, PlayIcon, RotateCcwIcon } from "lucide-react"; +import { type ReactNode, useEffect, useRef, useState } from "react"; +import { useMediaQuery } from "@/registry/default/hooks/use-media-query"; +import { Button } from "@/registry/default/ui/button"; +import styles from "./engineering-motion-preview.module.css"; + +export function EngineeringMotionPreview({ + animated, + caption, + children, +}: { + animated: ReactNode; + caption: string; + children: ReactNode; +}) { + const drawing = useRef(null); + const animation = useRef(null); + const [live, setLive] = useState(true); + const [paused, setPaused] = useState(false); + const [inView, setInView] = useState(false); + const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); + + useEffect(() => { + if (!drawing.current) return; + const observer = new IntersectionObserver(([entry]) => { + if (entry) setInView(entry.isIntersecting); + }); + observer.observe(drawing.current); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const svg = animation.current?.querySelector("svg"); + if (!svg) return; + if (!live || paused || reducedMotion || !inView) svg.pauseAnimations(); + else svg.unpauseAnimations(); + }, [live, paused, reducedMotion, inView]); + + const togglePlayback = () => { + if (live) setPaused((value) => !value); + else { + setLive(true); + setPaused(false); + } + }; + + const replay = () => { + animation.current?.querySelector("svg")?.setCurrentTime(0); + setLive(true); + setPaused(false); + }; + + return ( +
+
+
{children}
+
+ {animated} +
+
+
+

{caption}

+
+ + + {live && ( + + )} +
+

+ Reduced motion · Static drawing +

+
+
+ ); +} diff --git a/apps/ui/app/plotter/engineering-studies.tsx b/apps/ui/app/plotter/engineering-studies.tsx new file mode 100644 index 000000000..f3214f660 --- /dev/null +++ b/apps/ui/app/plotter/engineering-studies.tsx @@ -0,0 +1,459 @@ +import { + arrow, + standard, +} from "../../skills/plotter/assets/foundation/index.mjs"; +import { BlueprintDrawing } from "./blueprint-drawing"; +import { ProductionChainHatching } from "./production-chain-hatching"; +import type { IllustrationStudy } from "./study-types"; + +type GridBoundary = { + left: number; + right: number; + top: number; + bottom: number; +}; + +// Trim the actual guide geometry around forms and labels. No cover or mask. +function gridSegments( + start: number, + end: number, + omissions: [number, number][], +) { + const segments: [number, number][] = []; + let cursor = start; + for (const [from, to] of omissions.sort((a, b) => a[0] - b[0])) { + if (from > cursor) segments.push([cursor, Math.min(from, end)]); + cursor = Math.max(cursor, to); + if (cursor >= end) break; + } + if (cursor < end) segments.push([cursor, end]); + return segments; +} + +function constructionGrid( + boundary: GridBoundary, + columns: readonly number[], + rows: readonly number[], + omissions: GridBoundary[], +) { + return [ + ...columns.flatMap((x) => + gridSegments( + boundary.top, + boundary.bottom, + omissions + .filter(({ left, right }) => x >= left && x <= right) + .map(({ top, bottom }) => [top, bottom]), + ).map(([from, to]) => `M${x} ${from}V${to}`), + ), + ...rows.flatMap((y) => + gridSegments( + boundary.left, + boundary.right, + omissions + .filter(({ top, bottom }) => y >= top && y <= bottom) + .map(({ left, right }) => [left, right]), + ).map(([from, to]) => `M${from} ${y}H${to}`), + ), + ].join(" "); +} + +const cpu = { x: 220, y: 144, size: 120, pinPitch: 12, pinLength: 12 }; +const cpuPinOffsets = Array.from( + { length: 8 }, + (_, i) => 18 + i * cpu.pinPitch, +); +const processorRoutes = [174, 234]; +const boardGrid = constructionGrid( + { left: 88, right: 472, top: 96, bottom: 336 }, + Array.from({ length: 15 }, (_, i) => 112 + i * 24).filter((x) => x !== 280), + Array.from({ length: 9 }, (_, i) => 120 + i * 24), + [ + { left: 206, right: 354, top: 130, bottom: 278 }, + { left: 400, right: 460, top: 150, bottom: 258 }, + ...processorRoutes.flatMap((y) => [ + { left: 118, right: 210, top: y - 6, bottom: y + 6 }, + { left: 350, right: 418, top: y - 6, bottom: y + 6 }, + ]), + { left: 112, right: 136, top: 139, bottom: 157 }, + { left: 236, right: 324, top: 108, bottom: 130 }, + ], +); + +// A hypothetical processor-board plan. Pads and trace endpoints agree, but +// the routes do not assert a functional CPU/RAM circuit or a vendor pinout. +function ProcessorBoardStudy() { + return ( + + + + {cpuPinOffsets.map((offset) => ( + + + + + ))} + {processorRoutes.map((y) => ( + + ))} + + + + + + {processorRoutes.map((y) => ( + + ))} + + + + {processorRoutes.map((y) => ( + + ))} + + + + {/* Center axes stop at the package envelope and never cross the die. */} + + + + + + 7p + + + CPU + + + RAM + + + I/O + + + Processor · planar construction + + + + ); +} + +// The 24-unit field locates routing and terminal centers, not physical sizes. +const rcGrid = constructionGrid( + { left: 88, right: 472, top: 112, bottom: 352 }, + Array.from({ length: 15 }, (_, i) => 112 + i * 24), + Array.from({ length: 10 }, (_, i) => 112 + i * 24), + [ + { left: 104, right: 456, top: 178, bottom: 190 }, + { left: 181, right: 259, top: 169, bottom: 199 }, + { left: 323, right: 333, top: 178, bottom: 346 }, + { left: 301, right: 355, top: 253, bottom: 271 }, + { left: 301, right: 355, top: 325, bottom: 346 }, + { left: 97, right: 128, top: 147, bottom: 165 }, + { left: 429, right: 468, top: 147, bottom: 165 }, + { left: 191, right: 249, top: 133, bottom: 150 }, + { left: 340, right: 360, top: 158, bottom: 176 }, + { left: 365, right: 414, top: 247, bottom: 283 }, + { left: 142, right: 260, top: 312, bottom: 330 }, + ], +); + +function RcFilterStudy() { + return ( + + + + + + + + + + + + + Vin + + + Vout + + + R1 · 1 kΩ + + + n₁ + + + C1 + + + 100 nF + + + τ = RC = 100 μs + + + RC low-pass · one shared node + + + + ); +} + +const assemblyStations = [144, 280, 416]; +const assemblyGrid = constructionGrid( + { left: 88, right: 472, top: 184, bottom: 320 }, + assemblyStations.flatMap((x) => [x - 40, x - 32, x, x + 32, x + 40]), + [200, 248, 264, 272, 304], + [ + ...assemblyStations.map((x) => ({ + left: x - 42, + right: x + 42, + top: 198, + bottom: 274, + })), + { left: 86, right: 474, top: 270, bottom: 306 }, + { left: 250, right: 310, top: 184, bottom: 196 }, + ...assemblyStations.map((x) => ({ + left: x - 2, + right: x + 2, + top: 312, + bottom: 320, + })), + ], +); + +const assemblyTransferArrow = arrow({ + kind: "line", + from: [256, 288], + to: [304, 288], +}); +const assemblyDropArrow = arrow({ + kind: "line", + from: [280, 182], + to: [280, 200], +}); + +export function ProductionChainStudy() { + return ( + + + + + + + + {assemblyStations.map((x) => ( + + {/* An open frame. The core shares its eventual side and bottom edges. */} + + + + ))} + + + + + + + + + + + + + + + + + + + Core + + + 3u + + + Pitch + + + Load + + + Assembled + + + One line · equal carrier pitch + + + + ); +} + +const cradle = { + centers: [216, 260, 304, 348, 392], + pivotY: 120, + length: 176, + radius: 22, + pivotRadius: 2.4, + angle: Math.PI / 6, +} as const; +const cradleRestY = cradle.pivotY + cradle.length; +const cradlePivotX = cradle.centers[0]; +const displacedBob = { + x: cradlePivotX - cradle.length * Math.sin(cradle.angle), + y: cradle.pivotY + cradle.length * Math.cos(cradle.angle), +}; +const displacedStringStart = { + x: cradlePivotX - cradle.pivotRadius * Math.sin(cradle.angle), + y: cradle.pivotY + cradle.pivotRadius * Math.cos(cradle.angle), +}; +const displacedStringEnd = { + x: cradlePivotX - (cradle.length - cradle.radius) * Math.sin(cradle.angle), + y: cradle.pivotY + (cradle.length - cradle.radius) * Math.cos(cradle.angle), +}; +// Trim the center trajectory exactly where it meets the two bob outlines. +const cradleArcInset = 2 * Math.asin(cradle.radius / (2 * cradle.length)); +const cradleArcStart = { + x: cradlePivotX - cradle.length * Math.sin(cradle.angle - cradleArcInset), + y: cradle.pivotY + cradle.length * Math.cos(cradle.angle - cradleArcInset), +}; +const cradleArcEnd = { + x: cradlePivotX - cradle.length * Math.sin(cradleArcInset), + y: cradle.pivotY + cradle.length * Math.cos(cradleArcInset), +}; +const cradleArcArrow = [-Math.PI / 4, Math.PI / 4] + .map( + (angle) => + `${cradleArcEnd.x - 6 * Math.cos(cradleArcInset + angle)} ${cradleArcEnd.y - 6 * Math.sin(cradleArcInset + angle)}`, + ) + .join(` ${cradleArcEnd.x} ${cradleArcEnd.y} `); +const cradleAngleRadius = 48; +const cradleRadiusOffset = cradle.radius / Math.SQRT2; + +export function NewtonsCradleStudy() { + return ( + + {/* S1 marks identify the fixed support, not a material cross-section. */} + + {Array.from({ length: 15 }, (_, i) => 176 + i * 16).map((x) => ( + + ))} + + + + + + + {cradle.centers.map((x) => ( + + + + + ))} + + + + + + + + + + + + {cradle.centers.slice(1).map((x) => ( + + + + + ))} + + + + + + + + + + + + + 30° + + + 8u + + + 2u + + + r = u + + + Equal lengths · tangent at rest + + + + ); +} + +export const engineeringStudies: IllustrationStudy[] = [ + { + id: "object-gpu-plan", + code: "O06", + title: "A processor, constructed", + description: + "Regular contacts connect a central CPU to I/O and memory. Dashed centerlines locate the package on an open construction grid. The layout is conceptual.", + category: "Objects", + illustration: , + }, + { + id: "workflow-rc-filter", + code: "W04", + title: "One node, two paths", + description: + "A first-order RC low-pass joins a series resistor and a shunt capacitor at one explicit junction.", + category: "Systems", + illustration: , + }, + { + id: "workflow-production-chain", + code: "W05", + title: "A part, made in sequence", + description: + "Equal carriers advance along one assembly line while a matching core enters an open frame at the central station.", + category: "Systems", + illustration: , + }, + { + id: "object-newtons-cradle", + code: "O07", + title: "Newton’s cradle, at release", + description: + "Five equal pendulums share a tangent rest position; one follows a true arc from a 30° release.", + category: "Objects", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/expanded-studies.tsx b/apps/ui/app/plotter/expanded-studies.tsx new file mode 100644 index 000000000..69372b7d8 --- /dev/null +++ b/apps/ui/app/plotter/expanded-studies.tsx @@ -0,0 +1,7480 @@ +// Generated from drawings/*.svg by scripts/build-plotter-drawings.py. Edit the SVG sources. +import { useId } from "react"; +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +function DrawingAgentRegistration() { + return ( + + + + + + + + + + + + + + + + + + + + + {"Research agent"} + + + {"Read"} + + + {"Search"} + + + + + {"Identity"} + + + {"Name"} + + + {"Tools"} + + + + + + + + {"Registered agent"} + + + + + + + + + + + + + + {"Research agent"} + + + {"Read"} + + + {"Search"} + + + + {"agent_07"} + + + + + + + + + {"14u"} + + + {"u"} + + + {"u"} + + + + + ); +} + +function DrawingAgentDelegation() { + return ( + + + + + + + + + + + + + + + + + + + {"One task"} + + + {"Research"} + + + {"Draft"} + + + {"Research"} + + + {"Draft"} + + + {"Agent 01"} + + + {"Agent 02"} + + + + + + + + + + + {"14u"} + + + {"6u"} + + + {"8u"} + + + + + ); +} + +function DrawingAgentHandoff() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Context"} + + + + {"Context"} + + + {"Agent 01"} + + + {"Agent 02"} + + + + {"Handoff"} + + + + + {"R20"} + + + + {"40"} + + + + + {"336"} + + + + + + + ); +} + +function DrawingAgentTools() { + return ( + + + + + + {"Read"} + + + + {"Search"} + + + + {"Write"} + + + + + + + + {"R24"} + + + {"Agent 07"} + + + {"Tool interface"} + + + + + {"144"} + + + + {"32"} + + + + ); +} + +function DrawingAgentPermissions() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Requested scope"} + + + {"Allowed scope"} + + + + {"Granted"} + + + + + {"10u"} + + + + {"6u"} + + + + + ); +} + +function DrawingAgentMemory() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Shared memory"} + + + {"Goal"} + + + {"Findings"} + + + {"Next step"} + + + + + {"Agent 01"} + + + + {"Agent 02"} + + + + + + + + {"8u"} + + + + {"2u"} + + + + {"R24"} + + + + + ); +} + +function DrawingAgentCheckpoint() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Work"} + + + {"State 02"} + + + {"Continue"} + + + {"State 02"} + + + {"Checkpoint"} + + + + + + + + {"14u"} + + + + {"4u"} + + + + ); +} + +function DrawingAgentParallel() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Agent 01"} + + + + {"Agent 02"} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Agent 03"} + + + + + {"Start"} + + + {"Join"} + + + + {"Wait"} + + + + + {"4u"} + + + + + {"2u"} + + + + ); +} + +function DrawingGeoCircleRectangle() { + return ( + + + + + + + + + {"R = u"} + + + + + + + + + + + + + + {"6u"} + + + + + + + + + {"4u"} + + + + {"Circle / before"} + + + {"A shared height"} + + + + ); +} + +function DrawingGeoCompoundRadii() { + return ( + + + + + + + + + + {"R112"} + + + + + + + + {"R64"} + + + + + + + + {"R144"} + + + + + + {"Tangent"} + + + {"Three radii · one continuous curve"} + + + + ); +} + +function DrawingGeoNegativeSpace() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + {"R = u"} + + + + + + + + + {"u"} + + + + {"Shared void"} + + + + ); +} + +function DrawingGeoInterruptedPattern() { + return ( + + + + + + + + + + + + + + + + + {"6u"} + + + + + + + + + {"2u"} + + + + {"Interruption"} + + + + ); +} + +function DrawingMechIrisAperture() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + {"u"} + + + {"6u"} + + + + + {"Aperture"} + + + {"Blade 01"} + + + + + ); +} + +function DrawingMechPulleyRatio() { + return ( + + + + + + + + + + + + + + {"1 turn"} + + + {"2 turns"} + + + + + {"2r"} + + + + + {"r"} + + + + + + + + + {"208"} + + + + ); +} + +function DrawingMechCamFollower() { + return ( + + + + + + + + + + + + {"e = 24"} + + + + + {"R76"} + + + + + + {"Rotation"} + + + + + {"Lift"} + + + + {"Follower"} + + + + ); +} + +function DrawingMechLatchReceiver() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + {"R24"} + + + + {"4 clearance"} + + + {"Bolt"} + + + {"Receiver"} + + + + + + + + + {"216"} + + + + ); +} + +function DrawingArchStaircase() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + {"3u"} + + + + + + + + + {"2u"} + + + + + + + + + {"18u"} + + + + {"One step"} + + + + ); +} + +function DrawingArchMissingKeystone() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + {"R84"} + + + + + {"R120"} + + + + + {"Keystone"} + + + + + + + + + {"240"} + + + + ); +} + +function DrawingNatureLeafVeins() { + return ( + + + + + + + + + + + + + {"Midrib"} + + + + {"u"} + + + + + + {"4u"} + + + + ); +} + +function DrawingNatureHoneycomb() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Shared wall"} + + + + + + {"6r"} + + + + + + {"r"} + + + + ); +} + +function DrawingMusicChord() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Separate"} + + + {"Together"} + + + + + {"Three pitches"} + + + + + ); +} + +function DrawingMapSummitContours() { + return ( + + + + + {"200"} + + + + {"175"} + + + + {"150"} + + + + {"125"} + + + + {"100"} + + + + + {"Summit"} + + + {"Contour interval / 25"} + + + + ); +} + +function DrawingMapConvergingPaths() { + return ( + + + + + + + + {"Destination"} + + + + + + {"R48"} + + + + + + {"Shared route"} + + + {"A"} + + + {"B"} + + + {"C"} + + + + ); +} + +function DrawingTypeLetterStrokes() { + const id = useId(); + return ( + + + + + + + + + + + + + + {"6u"} + + + + + {"u"} + + + + {"Crossbar"} + + + + + ); +} + +function DrawingTypeSharedGlyphs() { + return ( + + + + + + + + + + + + + + + + + + {"R40"} + + + + + {"u"} + + + + {"4u"} + + + + ); +} + +function DrawingTypeLigature() { + const id = useId(); + return ( + + + + + + + + + + + + + + + {"R56"} + + + + {"Shared stroke"} + + + + + {"u"} + + + + + ); +} + +function DrawingTypeNegativeMonogram() { + const id = useId(); + return ( + + + + + + + + + + + + + + + {"8u"} + + + + + {"u"} + + + + {"u"} + + + + + ); +} + +function DrawingTypeOpticalAlignment() { + const id = useId(); + return ( + + + + + + + + + + + + + + + {"Bounds"} + + + + + + + + + + + + + + + + {"Area"} + + + + {"δ"} + + + {"δ = width / 6"} + + + + ); +} + +function DrawingInterfaceCornerRule() { + return ( + + + + + + + + {"R24"} + + + + + {"4u"} + + + + + + + {"R24"} + + + + + {"6u"} + + + + + + + {"R24"} + + + + + {"3u"} + + + + {"One radius · different proportions"} + + + + ); +} + +function DrawingInterfaceReflow() { + return ( + + + + + + + {"A"} + + + + {"B"} + + + + {"C"} + + + + {"A"} + + + + {"B"} + + + + {"C"} + + + + {"336"} + + + + + {"228"} + + + + + {"Wide"} + + + {"Narrow"} + + + + ); +} + +function DrawingInterfaceSliderValue() { + return ( + + + + + + + + + {"60%"} + + + + {"0"} + + + {"100"} + + + + + {"R24"} + + + {"60"} + + + + ); +} + +function DrawingSoftwareCacheRoute() { + return ( + + + + + + + + {"Cache"} + + + {"Origin"} + + + + + + {"R24"} + + + {"Request"} + + + {"Response"} + + + + {"2u"} + + + + + {"4u"} + + + + ); +} + +function DrawingSoftwareBranchHistory() { + return ( + + + + + + + + + + + + {"R24"} + + + + {"Branch"} + + + {"Main"} + + + {"Fork"} + + + {"Merge"} + + + + {"10u"} + + + + + ); +} + +function DrawingTimeAppointmentBuffers() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + {"15 min"} + + + {"60 min"} + + + {"15 min"} + + + {"Buffer before"} + + + {"Appointment"} + + + {"Buffer after"} + + + + {"09:45"} + + + + {"10:00"} + + + + {"11:00"} + + + + {"11:15"} + + + + + {"90 min reserved"} + + + + ); +} + +function DrawingTimeCommonRecurrence() { + return ( + + + + + + {"Event"} + + + {"01"} + + + + + {"Event"} + + + {"02"} + + + + + {"Event"} + + + {"03"} + + + + + {"Event"} + + + {"04"} + + + + + + + + + {"7 days"} + + + + {"7 days"} + + + + {"7 days"} + + + {"One recurring interval"} + + + + ); +} + +function DrawingCraftWeave() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Warp"} + + + {"Weft"} + + + + + {"64"} + + + + {"24"} + + + + ); +} + +function DrawingCraftStitch() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Edge A"} + + + {"Edge B"} + + + + + {"112"} + + + + + {"48"} + + + + ); +} + +function DrawingCraftDovetail() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Part A"} + + + {"Part B"} + + + + + {"128"} + + + + {"160"} + + + + + {"48"} + + + + ); +} + +function DrawingCraftPaperFold() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"45°"} + + + + + {"3u"} + + + + {"6u"} + + + + ); +} + +function DrawingCraftTiles() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + {"R48"} + + + + {"32"} + + + + + {"288"} + + + + ); +} + +function DrawingPlayDominoes() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"Missing"} + + + + + + {"2u"} + + + + {"3u"} + + + + ); +} + +function DrawingPlayBalance() { + const id = useId(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"2u"} + + + {"3u"} + + + {"3m"} + + + {"2m"} + + + {"3m × 2u = 2m × 3u"} + + + + ); +} + +export const expandedStudies: IllustrationStudy[] = [ + { + id: "agent-registration", + code: "AG01", + title: "An agent, registered", + description: + "Identity, name, and tools keep their alignment as they become one registry record.", + category: "Agents", + illustration: , + }, + { + id: "agent-delegation", + code: "AG02", + title: "One task, two agents", + description: + "Research and drafting separate into complementary scopes, assigned to two agents.", + category: "Agents", + illustration: , + }, + { + id: "agent-handoff", + code: "AG03", + title: "Context, carried forward", + description: + "One continuous context passes between two agents without changing its form.", + category: "Agents", + illustration: , + }, + { + id: "agent-tools", + code: "AG04", + title: "One matching interface", + description: + "A selected tool’s rounded end matches the agent’s common interface.", + category: "Agents", + illustration: , + }, + { + id: "agent-permissions", + code: "AG05", + title: "Only the shared scope", + description: + "The granted region is the intersection of what is requested and what is allowed.", + category: "Agents", + illustration: , + }, + { + id: "agent-memory", + code: "AG06", + title: "One context, two readers", + description: + "Two agents address the same findings row within one shared memory.", + category: "Agents", + illustration: , + }, + { + id: "agent-checkpoint", + code: "AG07", + title: "A state worth keeping", + description: + "One selected state is preserved as a checkpoint while the wider work continues.", + category: "Agents", + illustration: , + }, + { + id: "agent-parallel", + code: "AG08", + title: "Separate work, one join", + description: + "Three agents finish at different points and wait for the longest task before joining.", + category: "Agents", + illustration: , + }, + { + id: "geo-circle-rectangle", + code: "GE01", + title: "A circle, extended", + description: + "A circle and its wider rounded-rectangle state share one height while their corner radii change.", + category: "Foundations", + illustration: , + }, + { + id: "geo-compound-radii", + code: "GE03", + title: "A curve from three radii", + description: + "Three circular arcs share tangent directions at their joins, producing one uninterrupted curve.", + category: "Foundations", + illustration: , + }, + { + id: "geo-negative-space", + code: "GE04", + title: "The shape between circles", + description: + "Two equal circular regions reveal an unhatched lens where their boundaries overlap.", + category: "Foundations", + illustration: , + }, + { + id: "geo-interrupted-pattern", + code: "GE05", + title: "One interval left open", + description: + "A field of tangent circles keeps its repeated pitch while one missing member interrupts the lower row.", + category: "Foundations", + illustration: , + }, + { + id: "mech-iris-aperture", + code: "ME01", + title: "Six blades, one aperture", + description: + "Six repeated edges shape a single opening. Hatching identifies one visible blade.", + category: "Mechanisms", + illustration: , + }, + { + id: "mech-pulley-ratio", + code: "ME02", + title: "One belt, two rotations", + description: "A shared belt joins two pulley radii in a 2:1 relation.", + category: "Mechanisms", + illustration: , + }, + { + id: "mech-cam-follower", + code: "ME04", + title: "An offset becomes a lift", + description: + "An eccentric circular cam meets a guided roller, relating rotation to a vertical displacement.", + category: "Mechanisms", + illustration: , + }, + { + id: "mech-latch-receiver", + code: "ME05", + title: "A profile finds its receiver", + description: + "A rounded bolt approaches the matching receiver, with its destination and clearance visible.", + category: "Mechanisms", + illustration: , + }, + { + id: "arch-staircase", + code: "AR02", + title: "One rise, one run", + description: + "Six equal rise-and-run pairs construct a staircase along one repeated slope.", + category: "Architecture", + illustration: , + }, + { + id: "arch-missing-keystone", + code: "AR03", + title: "The last piece of the arch", + description: + "A lifted keystone preserves the geometry of the opening it will complete.", + category: "Architecture", + illustration: , + }, + { + id: "nature-leaf-veins", + code: "NA01", + title: "A branching leaf", + description: + "Paired veins follow a repeated pitch within a leaf formed by two intersecting arcs.", + category: "Nature", + illustration: , + }, + { + id: "nature-honeycomb", + code: "NA02", + title: "One edge, two cells", + description: + "A selected hexagonal cell shares each of its walls with the surrounding six cells.", + category: "Nature", + illustration: , + }, + { + id: "music-chord", + code: "MU02", + title: "Three notes, one onset", + description: + "Three separately placed pitches align vertically on one shared onset to form a chord.", + category: "Music", + illustration: , + }, + { + id: "map-summit-contours", + code: "MP04", + title: "The summit in contours", + description: + "Nested elevation lines tighten around one marked summit, with a consistent interval between levels.", + category: "Maps", + illustration: , + }, + { + id: "map-converging-paths", + code: "MP05", + title: "Different paths, one place", + description: + "Three routes join one final stretch before reaching the same destination.", + category: "Maps", + illustration: , + }, + { + id: "type-letter-strokes", + code: "TY01", + title: "A from three strokes", + description: + "Two parallel-sided diagonal strokes and one horizontal stroke make an outlined capital A; the crossbar is identified by hatching.", + category: "Typography", + illustration: , + }, + { + id: "type-shared-glyphs", + code: "TY02", + title: "One arch, three glyphs", + description: + "A shared circular shoulder and a constant stem width generate n, h and m, with the h selected to show the construction.", + category: "Typography", + illustration: , + }, + { + id: "type-ligature", + code: "TY03", + title: "The joining stroke", + description: + "The f and i share one continuous crossbar, joining the two glyphs into a single constructed ligature.", + category: "Typography", + illustration: , + }, + { + id: "type-negative-monogram", + code: "TY04", + title: "A monogram in the void", + description: + "An M is defined by the transparent space within a hatched field; its straight stems and surrounding margin follow the same unit.", + category: "Typography", + illustration: , + }, + { + id: "type-optical-alignment", + code: "TY05", + title: "The center you see", + description: + "A rightward shift brings a triangle’s area centroid to the center of its frame, providing a geometric starting point for optical alignment.", + category: "Typography", + illustration: , + }, + { + id: "interface-corner-rule", + code: "UI01", + title: "One corner rule", + description: + "Three differently proportioned components retain a common corner radius.", + category: "Interfaces", + illustration: , + }, + { + id: "interface-reflow", + code: "UI02", + title: "The same modules, reflowed", + description: + "Three equal modules preserve their size when the available width wraps the last one onto another row.", + category: "Interfaces", + illustration: , + }, + { + id: "interface-slider-value", + code: "UI03", + title: "Position becomes value", + description: + "The thumb locates sixty percent of a shared travel interval; the track clears its circular outline.", + category: "Interfaces", + illustration: , + }, + { + id: "software-cache-route", + code: "SW02", + title: "A shorter return", + description: + "A cached response turns back at the near bend while the longer path continues to the origin.", + category: "Systems", + illustration: , + }, + { + id: "software-branch-history", + code: "SW03", + title: "A history comes together", + description: + "One branch departs from a shared history, carries its own revision, and joins the main line again.", + category: "Systems", + illustration: , + }, + { + id: "time-appointment-buffers", + code: "TM02", + title: "Room around an appointment", + description: + "An hour-long appointment reserves fifteen minutes of buffer time on either side.", + category: "Scheduling", + illustration: , + }, + { + id: "time-common-recurrence", + code: "TM03", + title: "The same interval, again", + description: "Four occurrences align to one repeated seven-day interval.", + category: "Scheduling", + illustration: , + }, + { + id: "craft-weave", + code: "CR01", + title: "Over and under", + description: + "One warp and one weft reveal the alternating rule that holds a weave together.", + category: "Craft", + illustration: , + }, + { + id: "craft-stitch", + code: "CR02", + title: "A seam, joined", + description: + "Repeated crossed stitches bridge two edges on one shared hole pitch.", + category: "Craft", + illustration: , + }, + { + id: "craft-dovetail", + code: "CR03", + title: "A joint that holds", + description: + "A wider tail and narrower neck create one complementary boundary between two pieces.", + category: "Craft", + illustration: , + }, + { + id: "craft-paper-fold", + code: "CR04", + title: "Across the crease", + description: + "One corner reflects across its diagonal crease and becomes a triangular flap.", + category: "Craft", + illustration: , + }, + { + id: "craft-tiles", + code: "CR05", + title: "One shared corner", + description: + "Four tiles meet around a common circular opening and a constant joint width.", + category: "Craft", + illustration: , + }, + { + id: "play-dominoes", + code: "PL03", + title: "One piece short", + description: + "The pip sequence continues across one absent domino, leaving its position unmistakable.", + category: "Play", + illustration: , + }, + { + id: "play-balance", + code: "PL04", + title: "Unequal arms, equal moments", + description: + "Three mass units on the shorter arm balance two on the longer arm.", + category: "Play", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/flat-cal-studies.tsx b/apps/ui/app/plotter/flat-cal-studies.tsx new file mode 100644 index 000000000..a03828f1b --- /dev/null +++ b/apps/ui/app/plotter/flat-cal-studies.tsx @@ -0,0 +1,267 @@ +import { AppointmentBuffersStudy } from "./appointment-buffers"; +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +const weekDays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; +const weekHours = ["09:00", "10:00", "11:00", "12:00", "13:00"]; + +function WeekConstruction() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {weekDays.map((day, index) => ( + + {day} + + ))} + + + {weekHours.map((hour, index) => ( + + {hour} + + ))} + + 7 days + + + 4 h + + + 1 day + + + Thu · 10:00–11:00 + + + + + + + ); +} + +function TimeSlotConstruction() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 15 min + + + 15 min + + + 30 min + + + + + u + + + u + + + u + + + R28 + + + + + + + ); +} + +function AvailabilityConstruction() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + u + + + 6u + + + 4u + + + + + Alex’s availability + + + Sam’s availability + + + Shared slot + + + + + 30 min meeting + + + + + + + ); +} + +export const flatCalStudies: IllustrationStudy[] = [ + { + id: "cal-week-grid", + code: "K06", + title: "A week, constructed", + description: + "Monday through Sunday on a measured hourly grid. One constructed circle locates a meeting on Thursday, from 10:00 to 11:00.", + category: "Scheduling", + illustration: , + }, + { + id: "cal-time-modules", + code: "K07", + title: "The shape of a time slot", + description: + "Two measured intervals combine into one reusable time slot. Duration becomes a rule of composition.", + category: "Scheduling", + illustration: , + }, + { + id: "cal-availability-plan", + code: "K08", + title: "Where schedules meet", + description: + "Alex’s and Sam’s availability meet in one shared slot. Named bands and a meeting callout make the overlap explicit.", + category: "Scheduling", + illustration: , + }, + { + id: "cal-appointment-buffers", + code: "K09", + title: "Appointment buffers", + description: + "A 30-minute appointment sits between two 10-minute buffers. The proportional intervals reserve 50 minutes in total.", + category: "Scheduling", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/flat-object-studies.tsx b/apps/ui/app/plotter/flat-object-studies.tsx new file mode 100644 index 000000000..b1549daab --- /dev/null +++ b/apps/ui/app/plotter/flat-object-studies.tsx @@ -0,0 +1,215 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +const keyboardKeys = [ + { x: 144, y: 140, label: "Q" }, + { x: 216, y: 140, label: "W" }, + { x: 288, y: 140, label: "E" }, + { x: 162, y: 212, label: "A" }, + { x: 234, y: 212, label: "S" }, + { x: 306, y: 212, label: "D" }, +]; + +function KeyboardPlanStudy() { + return ( + + + + + + + + + + + + + + {keyboardKeys.map(({ x, y, label }) => ( + + ))} + + + + + + + + + + {keyboardKeys.map(({ x, y, label }) => ( + + {label} + + ))} + + + + + + + + + Pitch / u + + + 64 + + + ¼u + + + Stagger / ¼u + + + + ); +} + +function LampElevationStudy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + 2r + + + r + + + a + + + a + + + Generating circle + + + + ); +} + +const folders = [ + { x: 104, width: 216, height: 176, opacity: 1 }, + { x: 352, width: 136, height: 104, opacity: 0.62 }, +]; + +function FolderPlanStudy() { + return ( + + + + + + + + + {folders.map(({ x, width, height, opacity }) => ( + + ))} + + + + + + + + + + + + + + + + + + Tab / 64 + + + Tab / 64 + + + R8 + + + w₁ + + + w₂ + + + One tab / two widths + + + + ); +} + +export const flatObjectStudies: IllustrationStudy[] = [ + { + id: "object-keyboard-plan", + code: "O03", + title: "A keyboard, in detail", + description: + "Two cropped rows reveal the keyboard’s construction: one key pitch and a quarter-unit stagger. Dashed continuations keep the focus on the spacing rule.", + category: "Objects", + illustration: , + }, + { + id: "object-lamp-elevation", + code: "O04", + title: "A lamp, drawn in elevation", + description: + "Half a circle becomes the shade. Its center establishes the support axis and divides the base into equal halves.", + category: "Objects", + illustration: , + }, + { + id: "object-folder-plan", + code: "O05", + title: "A family of folders", + description: + "One tab construction belongs to two folder widths. Paired corner radii and a shared tab dimension hold the family together.", + category: "Objects", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/flat-system-studies.tsx b/apps/ui/app/plotter/flat-system-studies.tsx new file mode 100644 index 000000000..8bebd8a7c --- /dev/null +++ b/apps/ui/app/plotter/flat-system-studies.tsx @@ -0,0 +1,266 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +function SpacingPlanStudy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + R16 + + + 3u + + + + u + + + u + + + u + + + u + + + 12u + + + + + + + + ); +} + +const switchInputs = [148, 212, 276]; +const switchOutputs = [216, 280, 344]; + +function SwitchPlanStudy() { + return ( + + + + + + + + + + + + + + {switchInputs.map((y) => ( + + ))} + {switchOutputs.map((x) => ( + + ))} + + + + {switchOutputs.flatMap((x) => + switchInputs.map((y) => ( + + )), + )} + + + + {switchInputs.map((y) => ( + + ))} + {switchOutputs.map((x) => ( + + ))} + + + + + + + + + u + + + u + + + u + + + u + + + B / 02 + + + + A + + + B + + + C + + + + + 01 + + + 02 + + + 03 + + + + + + + + ); +} + +const matrixColumns = [152, 184, 216, 248, 280, 312, 344, 376, 408]; +const matrixRows = [104, 136, 168, 200, 232, 264, 296]; +const distributionPoints = [ + [248, 168], + [280, 168], + [312, 168], + [248, 200], + [280, 200], + [312, 200], + [280, 232], + [312, 232], +] as const; + +function CoordinatePlanStudy() { + return ( + + + + + + + {matrixColumns.flatMap((x) => + matrixRows.map((y) => ( + + )), + )} + + + + + + + + + + + + + + {distributionPoints.map(([x, y]) => ( + + ))} + + + + + (6u, 5u) + + + 6u + + + 5u + + + 0 + + + x + + + y + + + + ); +} + +export const flatSystemStudies: IllustrationStudy[] = [ + { + id: "interface-spacing-plan", + code: "I03", + title: "From parts to a whole", + description: + "Three separate elements project into one control. Equal gaps, a common centerline, and a shared radius make the assembly rule visible.", + category: "Interfaces", + illustration: , + }, + { + id: "workflow-switch-plan", + code: "W03", + title: "A connection, located", + description: + "Three inputs meet three outputs on a regular switching grid. A single closed junction connects B to 02, turning an abstract matrix into a precise route.", + category: "Systems", + illustration: , + }, + { + id: "data-coordinate-plan", + code: "A03", + title: "A measured distribution", + description: + "A small cluster occupies a regular coordinate field. Two projected measurements locate one point and make the underlying rule visible.", + category: "Systems", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/grammar-comparison.module.css b/apps/ui/app/plotter/grammar-comparison.module.css new file mode 100644 index 000000000..2eadcbd2b --- /dev/null +++ b/apps/ui/app/plotter/grammar-comparison.module.css @@ -0,0 +1,63 @@ +.drawing:is([data-isolated], :focus-visible) + svg + :is( + path, + rect, + circle, + ellipse, + line, + polyline, + polygon, + text, + use, + image + ):not( + defs *, + clipPath *, + mask *, + pattern *, + marker *, + symbol *, + [data-grammar-focus="true"], + [data-grammar-focus="true"] * + ) { + opacity: 0; +} + +.drawing:is([data-isolated], :focus-visible) + svg + [data-grammar-isolation-only="true"] { + visibility: visible; +} + +@media (hover: hover) and (pointer: fine) { + .drawing:hover + svg + :is( + path, + rect, + circle, + ellipse, + line, + polyline, + polygon, + text, + use, + image + ):not( + defs *, + clipPath *, + mask *, + pattern *, + marker *, + symbol *, + [data-grammar-focus="true"], + [data-grammar-focus="true"] * + ) { + opacity: 0; + } + + .drawing:hover svg [data-grammar-isolation-only="true"] { + visibility: visible; + } +} diff --git a/apps/ui/app/plotter/grammar-comparison.tsx b/apps/ui/app/plotter/grammar-comparison.tsx new file mode 100644 index 000000000..e1781c783 --- /dev/null +++ b/apps/ui/app/plotter/grammar-comparison.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { type ReactNode, useId, useState } from "react"; +import { Button } from "@/registry/default/ui/button"; +import styles from "./grammar-comparison.module.css"; + +export function GrammarComparison({ + title, + children, + caption, +}: { + title: string; + children: ReactNode; + caption: string; +}) { + const [isolated, setIsolated] = useState(false); + const id = useId(); + const drawingId = `${id}-drawing`; + const instructionId = `${id}-instruction`; + + return ( +
+
+ +

+ Hover or focus to reveal the element. Tap to keep it isolated. +

+
+ +
+ {caption} +
+

+ Scroll sideways to see the full drawing. +

+
+ ); +} diff --git a/apps/ui/app/plotter/illustration-gallery.tsx b/apps/ui/app/plotter/illustration-gallery.tsx new file mode 100644 index 000000000..7162c7f6c --- /dev/null +++ b/apps/ui/app/plotter/illustration-gallery.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { useEffect } from "react"; +import { Button } from "@/registry/default/ui/button"; +import type { IllustrationStudy, StudyCategory } from "./study-types"; + +const categories: StudyCategory[] = [ + "Foundations", + "Interfaces", + "Scheduling", + "Services", + "Systems", + "Objects", + "Empty states", + "Agents", + "Mechanisms", + "Architecture", + "Nature", + "Music", + "Maps", + "Typography", + "Craft", + "Play", +]; + +export function IllustrationGallery({ + illustrations, +}: { + illustrations: IllustrationStudy[]; +}) { + const params = useSearchParams(); + const linkedIllustration = illustrations.find( + (item) => item.id === params.get("variant"), + ); + const category = + linkedIllustration?.category ?? + categories.find((item) => item === params.get("category")); + const visible = category + ? illustrations.filter((item) => item.category === category) + : illustrations; + const linkedId = linkedIllustration?.id; + + // Existing preview URLs now locate the drawing directly in the gallery. + useEffect(() => { + if (linkedId) document.getElementById(linkedId)?.scrollIntoView(); + }, [linkedId]); + + function updateParams(values: Record) { + const next = new URLSearchParams(window.location.search); + next.delete("view"); + next.delete("scope"); + for (const [key, value] of Object.entries(values)) { + if (value) next.set(key, value); + else next.delete(key); + } + const query = next.toString(); + window.history.replaceState( + null, + "", + `${window.location.pathname}${query ? `?${query}` : ""}#illustrations`, + ); + } + + return ( + <> +
+
+ {["All", ...categories].map((item) => ( + + ))} +
+

+ {visible.length} illustrations +

+
+ +
    + {visible.map((item) => ( +
  • +
    +
    +
    + {item.illustration} +
    +
    +
    +
    +

    {item.title}

    +

    + {item.category} +

    +

    + {item.description} +

    +
    + + {item.code} + +
    +
    +
  • + ))} +
+ + ); +} diff --git a/apps/ui/app/plotter/illustrations.tsx b/apps/ui/app/plotter/illustrations.tsx new file mode 100644 index 000000000..4f8f7c04a --- /dev/null +++ b/apps/ui/app/plotter/illustrations.tsx @@ -0,0 +1,77 @@ +import { calStudies } from "./cal-studies"; +import { contentStateStudies } from "./content-state-studies"; +import { engineeringStudies } from "./engineering-studies"; +import { expandedStudies } from "./expanded-studies"; +import { flatCalStudies } from "./flat-cal-studies"; +import { flatObjectStudies } from "./flat-object-studies"; +import { flatSystemStudies } from "./flat-system-studies"; +import { interfaceDetailStudies } from "./interface-detail-studies"; +import { networkStudies } from "./network-studies"; +import { notFoundArchitectureStudies } from "./not-found-architecture-studies"; +import { notFoundGeometryStudies } from "./not-found-geometry-studies"; +import { objectStudies } from "./object-studies"; +import { OriginalNotFoundBlueprint } from "./original-not-found-blueprint"; +import { OriginalParticlesBlueprint } from "./original-particles-blueprint"; +import { popularServiceStudies } from "./popular-service-studies"; +import { PrimitivesBlueprint } from "./primitives-blueprint"; +import { serviceStudies } from "./service-studies"; +import { stateStudies } from "./state-studies"; +import type { IllustrationStudy } from "./study-types"; +import { systemStateStudies } from "./system-state-studies"; +import { systemStudies } from "./system-studies"; + +const collection: IllustrationStudy[] = [ + { + id: "primitives-reference", + code: "P00", + title: "Circle, square, line", + description: + "Fundamental geometry, connected by radius, tangency, and a shared construction grid.", + category: "Foundations", + illustration: , + }, + { + id: "particles-reference", + code: "C00", + title: "The exploded interface", + description: + "A pill, row, and toggle projected onto a shared frame. Separate elements follow a common set of rules.", + category: "Interfaces", + illustration: , + }, + ...systemStudies, + ...flatCalStudies, + ...interfaceDetailStudies, + ...flatSystemStudies, + ...flatObjectStudies, + ...serviceStudies, + ...popularServiceStudies, + ...calStudies, + ...networkStudies, + ...objectStudies, + ...stateStudies, + ...contentStateStudies, + ...systemStateStudies, + ...engineeringStudies, + ...notFoundArchitectureStudies, + ...notFoundGeometryStudies, + ...expandedStudies, + { + id: "404-reference", + code: "E00", + title: "The missing corner", + description: + "A vacant module in an otherwise complete construction. Absence, described by the edges around it.", + category: "Empty states", + illustration: , + }, +]; + +// Open the collection with a cross-section of its subjects. Codes and deep links +// stay stable as the drawings evolve. +const opening = ["P00", "C03", "B04", "K06", "I06", "E02"]; + +export const illustrations = [ + ...opening.flatMap((code) => collection.filter((item) => item.code === code)), + ...collection.filter((item) => !opening.includes(item.code)), +]; diff --git a/apps/ui/app/plotter/interface-detail-studies.tsx b/apps/ui/app/plotter/interface-detail-studies.tsx new file mode 100644 index 000000000..4dc4047da --- /dev/null +++ b/apps/ui/app/plotter/interface-detail-studies.tsx @@ -0,0 +1,239 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +function BrowserPlanStudy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + coss.com + + + + + R24 + + + R12 + + + Content inset + + + 24 + + + + ); +} + +function PhonePlanStudy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Safe area + + + + + R32 + + + Offset / 8 + + + 10u + + + 3u + + + 12u + + + 3u + + + 1.5u + + + 7u + + + 1.5u + + + u = 16 + + + + ); +} + +function ChatPlanStudy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Can we build it? + + + One part at a time. + + + + + 216 + + + R16 + + + 24 + + + 24 + + + Inset / 24 + + + 48 + + + + ); +} + +export const interfaceDetailStudies: IllustrationStudy[] = [ + { + id: "interface-browser-plan", + code: "I04", + title: "A browser, in detail", + description: + "An enlarged browser corner relates the window radius to its tab radius and content inset. Cropped edges leave the rest of the screen outside the drawing.", + category: "Interfaces", + illustration: , + }, + { + id: "interface-phone-plan", + code: "I05", + title: "The screen within the frame", + description: + "Shared corner centers and an eight-pixel offset construct the frame. Symmetric vertical and horizontal margins locate the safe area within it.", + category: "Interfaces", + illustration: , + }, + { + id: "interface-chat-plan", + code: "I06", + title: "A conversation, constructed", + description: + "Two rounded message blocks pair with circular avatars. Shared radius, text inset, and avatar spacing construct the conversation without pointed tails.", + category: "Interfaces", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/missing-destination.tsx b/apps/ui/app/plotter/missing-destination.tsx new file mode 100644 index 000000000..685fe45df --- /dev/null +++ b/apps/ui/app/plotter/missing-destination.tsx @@ -0,0 +1,104 @@ +import { BlueprintDrawing, CompactBlueprintDrawing } from "./blueprint-drawing"; +import { HorizontalDimension } from "./dimensions"; + +function RequestRoute() { + return ( + <> + + + + ); +} + +function AbsentPage({ dash = "3 5" }: { dash?: string }) { + return ( + + + + + + ); +} + +// S01 is shared by the gallery and M03. The marker follows the true R32 +// centerline inside the 16-unit route; its 3.5-unit radius stays clear of the +// closed end at x272. No part of the request crosses the 32-unit address gap. +export function MissingDestinationBlueprint({ + animated = false, +}: { + animated?: boolean; +}) { + return ( + + + + + + + + + + + + + + {animated && ( + + + + )} + + + + + + + + + + + Request + + + 404 + + + + + R40 + + + No page at this address + + + + + + + ); +} + +export function MissingDestinationCompact() { + return ( + + + + + ); +} diff --git a/apps/ui/app/plotter/network-studies.tsx b/apps/ui/app/plotter/network-studies.tsx new file mode 100644 index 000000000..18021140b --- /dev/null +++ b/apps/ui/app/plotter/network-studies.tsx @@ -0,0 +1,79 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +function RegistrationMarks() { + return ( + + + + + ); +} + +function BranchingWorkflowStudy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + r + + + 4r + + + 4r + + + r + + + in + + + 01 + + + 02 + + + 03 + + + + + ); +} + +export const networkStudies: IllustrationStudy[] = [ + { + id: "workflow-branch", + code: "W02", + title: "A route, constructed", + description: + "One selected route and two alternatives share a junction. A repeated bend radius establishes the spacing between the three outlets.", + category: "Systems", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/newtons-cradle-motion-drawing.tsx b/apps/ui/app/plotter/newtons-cradle-motion-drawing.tsx new file mode 100644 index 000000000..54fbd5ab7 --- /dev/null +++ b/apps/ui/app/plotter/newtons-cradle-motion-drawing.tsx @@ -0,0 +1,227 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; + +const cradle = { + pivots: [216, 260, 304, 348, 392], + pivotY: 120, + length: 176, + radius: 22, + pivotRadius: 2.4, + amplitude: 30, + interiorAmplitude: 0.75, + duration: 2, + turnaroundEmphasis: 0.25, +} as const; +const restY = cradle.pivotY + cradle.length; + +// Dimensionless pendulum flight: θ″ + sin(θ) = 0. Substituting +// θ = 2 asin(k cos φ), k = sin(θmax/2), gives dt/dφ below. Simpson sampling +// supplies one descent; reflection supplies the remaining three quarters. +// The user’s video reference suggests a roughly 2.4 s cycle and 30° release. +// A smooth quarter-flight time remap gives the turn more breathing room while +// retaining the requested 2 s cycle. This is an illustration timing accent, +// not an unmodified gravity trajectory. Velocities include its chain rule. +// Contacts at ¼ and ¾ of each cycle transfer velocity without dwell. +// The complete bob sweep remains inside x = 106…502, clear of the witnesses. +// A small phase-locked sway gives the touching group a shared response. This +// remains a motion study, not a simulation of collision compliance or damping. +function swingSamples() { + const count = 96; + const step = Math.PI / (2 * count); + const k = Math.sin((cradle.amplitude * Math.PI) / 360); + const rate = (phase: number) => 1 / Math.sqrt(1 - (k * Math.cos(phase)) ** 2); + let elapsed = 0; + const descent = Array.from({ length: count + 1 }, (_, i) => { + const phase = i * step; + if (i) { + elapsed += + (step * + (rate(phase - step) + 4 * rate(phase - step / 2) + rate(phase))) / + 6; + } + return { + time: elapsed, + angle: i === count ? 0 : (360 * Math.asin(k * Math.cos(phase))) / Math.PI, + speed: Math.sin(phase), + }; + }); + const samples = [0, 1, 2, 3].flatMap((quarter) => { + const reverse = quarter % 2 === 1; + return (reverse ? [...descent].reverse() : descent) + .slice(quarter ? 1 : 0) + .map(({ time, angle, speed }) => { + const progress = time / elapsed; + const flightTime = + progress + + (cradle.turnaroundEmphasis * Math.sin(Math.PI * progress)) / Math.PI; + const timeRate = + 1 + cradle.turnaroundEmphasis * Math.cos(Math.PI * progress); + return { + time: (quarter + (reverse ? 1 - flightTime : flightTime)) / 4, + angle: (quarter === 1 || quarter === 2 ? -1 : 1) * angle, + velocity: + ((quarter < 2 ? -1 : 1) * 1440 * elapsed * k * speed) / + (Math.PI * timeRate), + }; + }); + }); + return { + samples, + duration: cradle.duration, + }; +} + +const { samples, duration } = swingSamples(); +const keyTimes = samples.map(({ time }) => time).join(";"); + +type PendulumRole = "left" | "middle" | "right"; + +function swingScale(side: PendulumRole, angle: number) { + const active = + (side === "left" && angle > 0) || (side === "right" && angle < 0); + return active ? 1 : cradle.interiorAmplitude / cradle.amplitude; +} + +function swingMotion(side: PendulumRole, pivotX: number) { + const frames = samples.map((sample) => ({ + ...sample, + angle: sample.angle * swingScale(side, sample.angle), + })); + return { + values: frames + .map(({ angle }) => `${angle.toFixed(9)} ${pivotX} ${cradle.pivotY}`) + .join(";"), + // Cubic Hermite segments preserve the calculated endpoint velocities: + // zero at each turnaround, continuous in flight, handed off at contact. + // Fixed x controls make spline time linear; only the angle is interpolated. + keySplines: frames + .flatMap((current, i) => { + const next = frames[i + 1]; + if (!next) return []; + const angle = next.angle - current.angle; + if (!angle) return ["0 0 1 1"]; + const scale = (next.time - current.time) / (3 * angle); + // Pick the segment's side of contact: the end bob exchanges the main + // and shared velocities there, while the middle three stay continuous. + const response = swingScale(side, current.angle + next.angle); + return [ + `0.333333333 ${(current.velocity * response * scale).toFixed(9)} 0.666666667 ${(1 - next.velocity * response * scale).toFixed(9)}`, + ]; + }) + .join(";"), + }; +} + +function Pendulum({ x }: { x: number }) { + return ( + <> + + + {x === 304 && ( + + )} + + ); +} + +function SwingingPendulum({ side, x }: { side: PendulumRole; x: number }) { + const motion = swingMotion(side, x); + return ( + + + + + ); +} + +const radiusOffset = cradle.radius / Math.SQRT2; + +function radiusLeader(angle: number) { + const radians = (angle * Math.PI) / 180; + const x = 304 - cradle.length * Math.sin(radians) - radiusOffset; + const y = cradle.pivotY + cradle.length * Math.cos(radians) + radiusOffset; + // Keep the label and horizontal landing fixed; the 45° tip tracks the bob. + return `M${x} ${y} ${x + y - 328} 328H248 M${x} ${y + 6}V${y}H${x - 6}`; +} + +const radiusLeaderValues = samples + .map(({ angle }) => radiusLeader(angle * swingScale("middle", angle))) + .join(";"); + +export function NewtonsCradleMotionDrawing() { + return ( + + {/* S1 support marks stay fixed. No moving guide or ghost bob is needed. */} + + {Array.from({ length: 15 }, (_, i) => 176 + i * 16).map((x) => ( + + ))} + + + + + + + {cradle.pivots.map((x) => ( + + + + + ))} + + + + {cradle.pivots.slice(1, -1).map((x) => ( + + ))} + + + + + {/* Both length witnesses lie beyond the complete swept bob envelope. */} + + + + + + + + + 8u + + + 2u + + + r = u + + + Equal lengths · Tangent at rest + + + + ); +} diff --git a/apps/ui/app/plotter/no-matching-results.tsx b/apps/ui/app/plotter/no-matching-results.tsx new file mode 100644 index 000000000..d46fc34cd --- /dev/null +++ b/apps/ui/app/plotter/no-matching-results.tsx @@ -0,0 +1,90 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; + +export function NoMatchingResultsStudy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Index + + + Query + + + 03 + + + 01 + + + 02 + + + 04 + + + 0 matches + + + + u + + + ); +} diff --git a/apps/ui/app/plotter/not-found-architecture-studies.tsx b/apps/ui/app/plotter/not-found-architecture-studies.tsx new file mode 100644 index 000000000..3ed154d6f --- /dev/null +++ b/apps/ui/app/plotter/not-found-architecture-studies.tsx @@ -0,0 +1,111 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import { NotFoundBlueprint } from "./not-found-blueprint"; +import type { IllustrationStudy } from "./study-types"; + +function UnopenedDoor() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4u + + + r = 4u + + + 90° + + + 01 + + + Threshold / plan + + + + + + + + + ); +} + +export const notFoundArchitectureStudies: IllustrationStudy[] = [ + { + id: "404-doorway", + code: "E01", + title: "An empty threshold", + description: + "A precise architectural threshold opens onto empty space, suggesting a destination that was never built.", + category: "Empty states", + illustration: , + }, + { + id: "404-interrupted-path", + code: "E02", + title: "An interrupted path", + description: + "A measured route stops at a missing link. Its paired outlines and unresolved destination make the interruption the central gesture.", + category: "Empty states", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/not-found-blueprint.tsx b/apps/ui/app/plotter/not-found-blueprint.tsx new file mode 100644 index 000000000..db9281762 --- /dev/null +++ b/apps/ui/app/plotter/not-found-blueprint.tsx @@ -0,0 +1,67 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; + +export function NotFoundBlueprint() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Missing link + + + Unresolved + + + Request + + + + + u + + + R60 + + + 16 + + + + ); +} diff --git a/apps/ui/app/plotter/not-found-geometry-studies.tsx b/apps/ui/app/plotter/not-found-geometry-studies.tsx new file mode 100644 index 000000000..8dab4e62a --- /dev/null +++ b/apps/ui/app/plotter/not-found-geometry-studies.tsx @@ -0,0 +1,78 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +function UnbuiltArch() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Missing keystone + + + R126 + + + R90 + + + 7u + + + h + + + Unbuilt / Section A + + + 1 : 1 + + + + ); +} + +export const notFoundGeometryStudies: IllustrationStudy[] = [ + { + id: "404-unbuilt-arch", + code: "E06", + title: "An unbuilt arch", + description: + "An arch awaits its central keystone, turning the missing page into an unfinished architectural connection.", + category: "Empty states", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/not-found-motion.module.css b/apps/ui/app/plotter/not-found-motion.module.css new file mode 100644 index 000000000..3a33eeaf2 --- /dev/null +++ b/apps/ui/app/plotter/not-found-motion.module.css @@ -0,0 +1,13 @@ +.animation { + display: none; +} + +@media (prefers-reduced-motion: no-preference) { + .drawing[data-live] .animation { + display: block; + } + + .drawing[data-live] .original { + visibility: hidden; + } +} diff --git a/apps/ui/app/plotter/not-found-motion.tsx b/apps/ui/app/plotter/not-found-motion.tsx new file mode 100644 index 000000000..fcd9d76a2 --- /dev/null +++ b/apps/ui/app/plotter/not-found-motion.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { PauseIcon, PlayIcon, RotateCcwIcon } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { useMediaQuery } from "@/registry/default/hooks/use-media-query"; +import { Button } from "@/registry/default/ui/button"; +import { MissingDestinationBlueprint } from "./missing-destination"; +import styles from "./not-found-motion.module.css"; + +export function NotFoundMotion({ + controls = "preview", +}: { + controls?: "hero" | "preview"; +}) { + const drawing = useRef(null); + const animation = useRef(null); + const [live, setLive] = useState(true); + const [paused, setPaused] = useState(false); + const [inView, setInView] = useState(false); + const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); + + useEffect(() => { + if (!drawing.current) return; + const observer = new IntersectionObserver(([entry]) => { + if (entry) setInView(entry.isIntersecting); + }); + observer.observe(drawing.current); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const svg = animation.current?.querySelector("svg"); + if (!svg) return; + if (!live || reducedMotion || paused || !inView) svg.pauseAnimations(); + else svg.unpauseAnimations(); + }, [live, reducedMotion, paused, inView]); + + const togglePlayback = () => { + if (live) setPaused((value) => !value); + else { + setLive(true); + setPaused(false); + } + }; + + const replay = () => { + const svg = animation.current?.querySelector("svg"); + svg?.setCurrentTime(0); + if (inView && !reducedMotion) svg?.unpauseAnimations(); + setLive(true); + setPaused(false); + }; + + return ( +
+
+
+ +
+
+ +
+ {controls === "hero" && !reducedMotion && ( + + )} +
+ {controls === "preview" && ( +
+

+ A request approaches · The gap remains +

+
+ + + {live && ( + + )} +
+

+ Reduced motion · Static drawing +

+
+ )} +
+ ); +} diff --git a/apps/ui/app/plotter/object-studies.tsx b/apps/ui/app/plotter/object-studies.tsx new file mode 100644 index 000000000..d5b1454e3 --- /dev/null +++ b/apps/ui/app/plotter/object-studies.tsx @@ -0,0 +1,140 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import type { IllustrationStudy } from "./study-types"; + +function LetterAnatomy() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + x-height + + + Stem + + + r + + + Overshoot + + + Anatomy of a letter + + + + ); +} + +function MarkInMaking() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 90° + + + R + + + 1u + + + Terminal + + + A mark in the making + + + + ); +} + +export const objectStudies: IllustrationStudy[] = [ + { + id: "type-letter", + code: "T01", + title: "Anatomy of a letter", + description: + "A custom lowercase a exposes its circular bowl, counter, stem, and optical overshoot.", + category: "Foundations", + illustration: , + }, + { + id: "type-monogram", + code: "T02", + title: "A mark in the making", + description: + "Two concentric arcs and a deliberate opening establish the logic of a simple geometric C.", + category: "Foundations", + illustration: , + }, +]; diff --git a/apps/ui/app/plotter/orbits-motion-drawing.tsx b/apps/ui/app/plotter/orbits-motion-drawing.tsx new file mode 100644 index 000000000..d286e9e34 --- /dev/null +++ b/apps/ui/app/plotter/orbits-motion-drawing.tsx @@ -0,0 +1,292 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; + +// One cycle holds 4, 2 and 1 orbits. Radii follow Kepler’s third law, +// T² ∝ r³, so the orbit spacing agrees with the periods rather than being +// chosen by eye. Planet sizes only help the eye keep the three apart. +const center = { x: 280, y: 212 } as const; +const cycle = 12; +const unit = 52; +const sunRadius = 22; +const planets = [ + { orbits: 4, radius: 6 }, + { orbits: 2, radius: 8 }, + { orbits: 1, radius: 10 }, +].map((planet) => { + const period = cycle / planet.orbits; + return { + ...planet, + period, + orbit: unit * (period / (cycle / 4)) ** (2 / 3), + }; +}); +const outerOrbit = Math.max(...planets.map((p) => p.orbit)); +// The angular scale sits just outside the swept envelope of the outermost planet. +const scaleRadius = 152; +const arrowRadius = 172; +const labelRadius = 184; + +// Counter-clockwise angle in degrees from the start axis; SVG y points down. +function polar(radius: number, degrees: number) { + const angle = (degrees * Math.PI) / 180; + return { + x: center.x + radius * Math.cos(angle), + y: center.y - radius * Math.sin(angle), + }; +} + +function ray(from: number, to: number, degrees: number) { + const a = polar(from, degrees); + const b = polar(to, degrees); + return `M${a.x.toFixed(3)} ${a.y.toFixed(3)}L${b.x.toFixed(3)} ${b.y.toFixed(3)}`; +} + +// 24 divisions: long at quarter turns, medium at 30°, short at 15°. +const scaleTicks = Array.from({ length: 24 }, (_, i) => i * 15).reduce( + (groups, degrees) => { + const length = degrees % 90 === 0 ? 12 : degrees % 30 === 0 ? 8 : 4; + const group = length === 12 ? "quarter" : length === 8 ? "third" : "step"; + groups[group] += `${ray(scaleRadius, scaleRadius + length, degrees)} `; + return groups; + }, + { quarter: "", third: "", step: "" }, +); + +// Dashed travel arrow ahead of the planets, drawn from its tip so the open +// head meets the first dash. It follows the same counter-clockwise direction. +const arrowTip = polar(arrowRadius, 72); +const arrowTail = polar(arrowRadius, 24); +const arrowTangent = { + x: -Math.sin((72 * Math.PI) / 180), + y: -Math.cos((72 * Math.PI) / 180), +}; +const arrowHead = [-1, 1] + .map((side) => { + const turn = (side * Math.PI) / 4; + const x = arrowTangent.x * Math.cos(turn) - arrowTangent.y * Math.sin(turn); + const y = arrowTangent.x * Math.sin(turn) + arrowTangent.y * Math.cos(turn); + return `${(arrowTip.x - 6 * x).toFixed(3)} ${(arrowTip.y - 6 * y).toFixed(3)}`; + }) + .join(` ${arrowTip.x.toFixed(3)} ${arrowTip.y.toFixed(3)} `); + +const quarterLabels = [ + { degrees: 0, text: "0° · Start", anchor: "start" }, + { degrees: 90, text: "90°", anchor: "middle" }, +] as const; + +function Sun() { + return ( + <> + + + + + ); +} + +// An orbit is a theoretical generating path, so it takes the house centerline +// pattern for loci. Like the pitch circle in the grammar’s centerline study, it +// passes through the transparent body that travels on it. +function Orbits() { + return ( + + {planets.map(({ orbit }) => ( + + ))} + + ); +} + +function AngularScale() { + return ( + <> + + + + + + + + + + ); +} + +// Planets leave dashed outlines on the start axis and return into them once +// per cycle. The state is named by the “Start” label at 0°. +function StartPositions() { + return ( + + {planets.map(({ orbit, radius }) => ( + + ))} + + ); +} + +// The outer orbit’s diameter, measured between its top and bottom tangents. +// Witnesses start beyond the planet’s swept band and the dimension line breaks +// around its label, so nothing crosses a moving part. +const dimension = { x: 84, from: 196 } as const; +const dimensionTop = center.y - outerOrbit; +const dimensionBottom = center.y + outerOrbit; + +function Dimension() { + return ( + <> + + + + ); +} + +function Labels() { + const labelFont = { fontSize: 10, opacity: 0.58 } as const; + return ( + + {quarterLabels.map(({ degrees, text, anchor }) => { + const { x, y } = polar(labelRadius, degrees); + return ( + + {text} + + ); + })} + + T² ∝ r³ + + + {`⌀${Math.round(2 * outerOrbit)}`} + + + ); +} + +const keyEntries = [ + { left: 92, planet: planets[0], text: "4 orbits · 3 s" }, + { left: 232, planet: planets[1], text: "2 orbits · 6 s" }, + { left: 372, planet: planets[2], text: "1 orbit · 12 s" }, +] as const; + +function Key() { + return ( + <> + + {keyEntries.map(({ left, planet }) => ( + + ))} + + + {keyEntries.map(({ left, text }) => ( + + {text} + + ))} + + + ); +} + +function Planet({ + animated, + orbit, + period, + radius, +}: { + animated: boolean; + orbit: number; + period: number; + radius: number; +}) { + const start = polar(orbit - radius, 0); + return ( + + {animated && ( + + )} + + + + ); +} + +function OrbitDrawing({ animated }: { animated: boolean }) { + return ( + + + + + + {planets.map((planet) => ( + + ))} + + + + + ); +} + +export function OrbitsStudy() { + return ; +} + +export function OrbitsMotionDrawing() { + return ; +} diff --git a/apps/ui/app/plotter/original-not-found-blueprint.tsx b/apps/ui/app/plotter/original-not-found-blueprint.tsx new file mode 100644 index 000000000..2cd5064c3 --- /dev/null +++ b/apps/ui/app/plotter/original-not-found-blueprint.tsx @@ -0,0 +1,90 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; + +export function OriginalNotFoundBlueprint() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 6u + + + 6u + + + 3u + + + 03 + + + + + 404 + + + + + + + + ); +} diff --git a/apps/ui/app/plotter/original-particles-blueprint.tsx b/apps/ui/app/plotter/original-particles-blueprint.tsx new file mode 100644 index 000000000..42049125c --- /dev/null +++ b/apps/ui/app/plotter/original-particles-blueprint.tsx @@ -0,0 +1,80 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; + +export function OriginalParticlesBlueprint() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ready + + + Workspace + + + Available + + + + + R16 + + + u + + + u + + + u + + + 10u + + + Shared inset / 24 + + + + ); +} diff --git a/apps/ui/app/plotter/page.tsx b/apps/ui/app/plotter/page.tsx new file mode 100644 index 000000000..44c501f71 --- /dev/null +++ b/apps/ui/app/plotter/page.tsx @@ -0,0 +1,259 @@ +import { SiteFooter } from "@coss/ui/shared/site-footer"; +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { CossConcepts } from "./coss-concepts"; +import { EngineeringMotionPreview } from "./engineering-motion-preview"; +import { + NewtonsCradleStudy, + ProductionChainStudy, +} from "./engineering-studies"; +import { IllustrationGallery } from "./illustration-gallery"; +import { illustrations } from "./illustrations"; +import { NewtonsCradleMotionDrawing } from "./newtons-cradle-motion-drawing"; +import { NotFoundMotion } from "./not-found-motion"; +import { OrbitsMotionDrawing, OrbitsStudy } from "./orbits-motion-drawing"; +import { ParticlesBlueprint } from "./particles-blueprint"; +import { ParticlesMotion } from "./particles-motion"; +import { PlotterIntro } from "./plotter-intro"; +import { PlotterTokenGuide } from "./plotter-token-guide"; +import { PlotterUsage } from "./plotter-usage"; +import { PrimitivesBlueprint } from "./primitives-blueprint"; +import { PrimitivesMotionPreview } from "./primitives-motion-preview"; +import { ProductionChainMotionDrawing } from "./production-chain-motion-drawing"; +import { VisualGrammar } from "./visual-grammar"; + +export const metadata: Metadata = { + title: "Plotter illustrations – coss ui", + description: + "Draw and animate editable monochrome illustrations with Plotter. Install the skill, copy a brief, explore the catalogue, and learn the visual grammar.", + robots: { index: false, follow: false }, +}; + +export default function Page() { + return ( + <> +
+ + + + + +
+
+

+ Animations +

+

6 examples

+
+
+
+

Foundations / M01

+

+ Changing geometry +

+

+ Avatar, pill, switch, button, card, slider. Six familiar UI + elements, one continuous loop, with measurements that follow the + geometry. +

+
+
+ + + +
+
+
+
+

Particles / C03

+

+ Three assemblies +

+

+ Workspace, booking, activity. Three sets of independent elements + assemble into three different interfaces in a repeating + sequence. +

+
+
+ + + +
+
+
+
+

+ Empty states / M03 +

+

+ An unreachable destination +

+

+ A request follows a curved route, reaches the closed end, and + returns. The destination stays absent, one unit beyond its + reach. +

+
+
+ +
+
+
+
+

Systems / W05

+

+ An assembly line in motion +

+

+ A core lowers into its carrier. The line advances, the completed + part moves on, and the next assembly begins. +

+
+
+ } + caption="Load · Seat · Advance" + > + + +
+
+
+
+

Objects / O07

+

+ One swing, passed along +

+

+ One end swings in and the other swings out. Equal lengths and + fixed pivots carry the motion through a continuous rhythm. +

+
+
+ } + caption="One swing · Alternating ends" + > + + +
+
+
+
+

Nature / M04

+

+ Three orbits, three speeds +

+

+ Three planets leave one line together. The inner one circles + four times for every turn of the outer one, and all three meet + again at the start. +

+
+
+ } + caption="Inner · Middle · Outer" + > + + +
+
+
+ + +
+

+ Catalogue +

+

+ Choose a construction relationship to borrow, then{" "} + + give Plotter your own brief + + . The{" "} + + visual grammar + {" "} + explains the marks behind these drawings. +

+ + Loading illustrations… +

+ } + > + +
+
+
+
+

Make a drawing of your own.

+

+ Start with a relationship you found here. The skill includes the + references and the workflow to develop it. +

+
+ + Install and use Plotter ↑ + +
+
+ + + ); +} diff --git a/apps/ui/app/plotter/particles-assembly-content.tsx b/apps/ui/app/plotter/particles-assembly-content.tsx new file mode 100644 index 000000000..fc1232553 --- /dev/null +++ b/apps/ui/app/plotter/particles-assembly-content.tsx @@ -0,0 +1,109 @@ +import type { ReactNode } from "react"; + +export type AssemblyScene = "workspace" | "booking" | "activity"; + +export function AssemblyContent({ + scene, + part, + trailingMotion, + thumbMotion, +}: { + scene: AssemblyScene; + part: 0 | 1 | 2; + trailingMotion?: ReactNode; + thumbMotion?: ReactNode; +}) { + const label = { + workspace: ["Studio", "Public", "Open"], + booking: ["Wed 28", "10:30", "Meet"], + activity: ["", "", "Ready"], + }[scene][part]; + const labelX = part === 0 || (scene === "booking" && part === 1) ? 40 : 14; + + return ( + + + {scene === "workspace" && part === 0 && ( + + )} + {scene === "workspace" && part === 1 && ( + + {trailingMotion} + + + {thumbMotion} + + + )} + {scene === "booking" && part === 0 && ( + + )} + {scene === "booking" && part === 1 && } + {scene === "booking" && part === 2 && ( + <> + + + + )} + {scene === "activity" && part === 0 && ( + + )} + + + {scene === "workspace" && part === 0 && } + {scene === "workspace" && part === 2 && ( + + {trailingMotion} + + + )} + {scene === "booking" && part === 0 && ( + + )} + {scene === "booking" && part === 1 && } + {scene === "activity" && part === 0 && ( + <> + + + + )} + {scene === "activity" && part === 2 && } + + {scene === "activity" ? ( + + {part === 1 && ( + <> + + New reply + + + Maya · just now + + + )} + {part === 2 && ( + + Ready + + )} + + ) : ( + + {label} + + )} + + ); +} diff --git a/apps/ui/app/plotter/particles-assembly-drawing.tsx b/apps/ui/app/plotter/particles-assembly-drawing.tsx new file mode 100644 index 000000000..53cfbeacb --- /dev/null +++ b/apps/ui/app/plotter/particles-assembly-drawing.tsx @@ -0,0 +1,513 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; +import { + AssemblyContent, + type AssemblyScene, +} from "./particles-assembly-content"; +import { + type AssemblyAxis, + type AssemblyPart, + type Corner, + type Edge, + jointDuration, + jointNeighbor, + jointTime, + type ModuleGeometry, + outlineAt, + outlineTimes, + travel, +} from "./particles-assembly-geometry"; +import { secondaryLine } from "./primitives-morph-style"; + +export const particlesSequenceDuration = 16.2; +const chapterDuration = particlesSequenceDuration / 3; +const parts = [0, 1, 2] as const; +type Part = AssemblyPart; +type Frame = readonly [number, string | number]; +const edges: Edge[] = ["top", "right", "bottom", "left"]; +const corners: Corner[] = ["topLeft", "topRight", "bottomRight", "bottomLeft"]; +type Assembly = { + scene: AssemblyScene; + axis: AssemblyAxis; + unit: number; + labels: readonly [string, string, string]; + modules: readonly [ModuleGeometry, ModuleGeometry, ModuleGeometry]; + frame: { + x: number; + y: number; + width: number; + height: number; + radius: number; + }; +}; +const assemblies: Assembly[] = [ + { + scene: "workspace", + axis: "vertical", + unit: 40, + labels: ["Identity", "Visibility", "Action"], + modules: [ + { width: 120, targetWidth: 240, height: 40, radius: 8, x: 160, y: 224 }, + { width: 120, targetWidth: 240, height: 40, radius: 8, x: 160, y: 264 }, + { width: 120, targetWidth: 240, height: 40, radius: 8, x: 160, y: 304 }, + ], + frame: { x: 160, y: 224, width: 240, height: 120, radius: 8 }, + }, + { + scene: "booking", + axis: "horizontal", + unit: 40, + labels: ["Date", "Time", "Meeting"], + modules: [ + { width: 120, height: 40, radius: 8, x: 100, y: 264 }, + { width: 120, height: 40, radius: 8, x: 220, y: 264 }, + { width: 120, height: 40, radius: 8, x: 340, y: 264 }, + ], + frame: { x: 100, y: 264, width: 360, height: 40, radius: 8 }, + }, + { + scene: "activity", + axis: "horizontal", + unit: 24, + labels: ["Avatar", "Message", "Status"], + modules: [ + { width: 48, height: 48, radius: 8, x: 148, y: 260 }, + { width: 144, height: 48, radius: 8, x: 196, y: 260 }, + { width: 72, height: 32, targetHeight: 48, radius: 16, x: 340, y: 260 }, + ], + frame: { x: 148, y: 260, width: 264, height: 48, radius: 8 }, + }, +]; + +function Motion({ + index, + attributeName, + frames, + mode = "spline", +}: { + index: number; + attributeName: string; + frames: readonly Frame[]; + mode?: "spline" | "linear" | "discrete"; +}) { + const timeline = frames.map( + ([time, value]) => [time + index * chapterDuration, value] as const, + ); + const first = timeline[0]; + const last = timeline[timeline.length - 1]; + if (!first || !last) return null; + if (first[0] > 0) timeline.unshift([0, first[1]]); + if (last[0] < particlesSequenceDuration) + timeline.push([particlesSequenceDuration, last[1]]); + const timing = { + attributeName, + begin: "0s", + dur: `${particlesSequenceDuration}s`, + repeatCount: "indefinite", + calcMode: mode, + keyTimes: timeline + .map(([time]) => time / particlesSequenceDuration) + .join(";"), + keySplines: + mode === "spline" + ? timeline + .slice(1) + .map(() => "0.4 0 0.2 1") + .join(";") + : undefined, + values: timeline.map(([, value]) => value).join(";"), + }; + return attributeName === "transform" ? ( + + ) : ( + + ); +} + +function FragmentOutline({ + assembly, + index, + part, +}: { + assembly: Assembly; + index: number; + part: Part; +}) { + const times = outlineTimes(assembly.modules, assembly.axis, part); + const geometry = times.map((time) => + outlineAt(assembly.modules, assembly.axis, part, time), + ); + const initial = outlineAt(assembly.modules, assembly.axis, part, 0); + return ( + + {edges.map((edge) => { + const neighbor = jointNeighbor(assembly.axis, part, edge); + // Only the facing strokes merge. The exterior corner arcs keep their weight. + const ownsDivider = edge === "bottom" || edge === "right"; + const contact = + neighbor === undefined ? undefined : jointTime(part, neighbor); + return ( + + [ + time, + geometry[i]?.[edge] ?? initial[edge], + ])} + /> + {contact !== undefined && ( + <> + + + + )} + + ); + })} + {corners.map((corner) => ( + + [ + time, + geometry[i]?.[corner] ?? initial[corner], + ])} + /> + + ))} + + ); +} + +function Fragment({ + assembly, + index, + part, +}: { + assembly: Assembly; + index: number; + part: Part; +}) { + const { scene } = assembly; + const module = assembly.modules[part]; + const sourceX = 140 + part * 140 - module.width / 2; + const sourceY = 124 - module.height / 2; + const { start, end } = travel(part); + const grows = module.targetWidth !== undefined; + return ( + <> + + + + + + + + {module.targetHeight !== undefined && ( + + )} + + ) : undefined + } + thumbMotion={ + scene === "workspace" ? ( + + ) : undefined + } + /> + + + + ); +} + +function Receiver({ assembly, index }: { assembly: Assembly; index: number }) { + return ( + + {parts.map((part) => { + const module = assembly.modules[part]; + const final = outlineAt( + assembly.modules, + assembly.axis, + part, + chapterDuration, + ); + const { end } = travel(part); + return ( + + + {edges + .filter( + (edge) => + jointNeighbor(assembly.axis, part, edge) === undefined, + ) + .map((edge) => ( + + ))} + {corners.map((corner) => ( + + ))} + + ); + })} + + ); +} + +function widthDimension(left: number, right: number, y: number) { + const center = (left + right) / 2; + return `M${left} ${y - 8}V${y + 8} M${right} ${y - 8}V${y + 8} M${left} ${y}H${center - 16} M${center + 16} ${y}H${right} M${left - 4} ${y + 4}l8-8 M${right - 4} ${y + 4}l8-8`; +} + +function Measurements({ + assembly, + index, +}: { + assembly: Assembly; + index: number; +}) { + const { frame } = assembly; + const right = frame.x + frame.width; + const bottom = frame.y + frame.height; + const cx = frame.x + frame.width / 2; + const cy = frame.y + frame.height / 2; + const dy = bottom + 32; + return ( + + + + + + + {frame.height / assembly.unit}u + + + {frame.width / assembly.unit}u + + + A + B + C + + + + ); +} + +function Construction({ assembly }: { assembly: Assembly }) { + const { frame, scene, modules } = assembly; + const left = scene === "workspace" ? 140 : frame.x - 20; + const right = scene === "workspace" ? 420 : frame.x + frame.width + 20; + const top = frame.y - 20; + const bottom = frame.y + frame.height + 20; + return ( + <> + + + {modules.flatMap((module, part) => { + const x = 140 + part * 140 - module.width / 2; + return [x, x + module.width / 2, x + module.width].map((column) => ( + + )); + })} + + {/* Each field is fixed; all guides stop outside the assembled content. */} + + + {Array.from( + { length: Math.floor(frame.width / assembly.unit) + 1 }, + (_, i) => frame.x + i * assembly.unit, + ).map((x) => ( + + ))} + + + + {[140, 280, 420].map((x) => ( + + + + + ))} + + + + {assembly.labels.map((label, part) => ( + + {String.fromCharCode(65 + part)} / {label} + + ))} + + + ); +} + +export function ParticlesAssemblyDrawing() { + return ( + + {assemblies.map((assembly, index) => ( + + + + + {parts.map((part) => ( + + ))} + + + ))} + + ); +} diff --git a/apps/ui/app/plotter/particles-assembly-geometry.ts b/apps/ui/app/plotter/particles-assembly-geometry.ts new file mode 100644 index 000000000..51a08f4f1 --- /dev/null +++ b/apps/ui/app/plotter/particles-assembly-geometry.ts @@ -0,0 +1,178 @@ +export type AssemblyPart = 0 | 1 | 2; +export type Edge = "top" | "right" | "bottom" | "left"; +export type Corner = "topLeft" | "topRight" | "bottomRight" | "bottomLeft"; +export type AssemblyAxis = "vertical" | "horizontal"; +export type ModuleGeometry = { + width: number; + height: number; + radius: number; + x: number; + y: number; + targetWidth?: number; + targetHeight?: number; +}; + +const circleControl = 0.5522847498; +const samplingRate = 60; +export const jointDuration = 0.22; + +export function travel(part: AssemblyPart) { + const start = 0.45 + (2 - part) * 0.32; + return { start, end: start + 0.9 }; +} + +export function easing(progress: number) { + if (progress <= 0) return 0; + if (progress >= 1) return 1; + + let low = 0; + let high = 1; + for (let iteration = 0; iteration < 32; iteration++) { + const position = (low + high) / 2; + const inverse = 1 - position; + const x = + 3 * inverse * inverse * position * 0.4 + + 3 * inverse * position * position * 0.2 + + position * position * position; + if (x < progress) low = position; + else high = position; + } + + const position = (low + high) / 2; + return 3 * (1 - position) * position * position + position ** 3; +} + +export function jointNeighbor( + axis: AssemblyAxis, + part: AssemblyPart, + edge: Edge, +): AssemblyPart | undefined { + const previous = axis === "vertical" ? "top" : "left"; + const next = axis === "vertical" ? "bottom" : "right"; + if (edge === previous && part > 0) return (part - 1) as AssemblyPart; + if (edge === next && part < 2) return (part + 1) as AssemblyPart; + return undefined; +} + +export function jointTime(part: AssemblyPart, neighbor: AssemblyPart) { + return Math.max(travel(part).end, travel(neighbor).end); +} + +function flightProgress(part: AssemblyPart, time: number) { + const { start, end } = travel(part); + return easing((time - start) / (end - start)); +} + +export function sizeAt( + module: ModuleGeometry, + part: AssemblyPart, + time: number, +) { + const progress = flightProgress(part, time); + return { + width: + module.width + + ((module.targetWidth ?? module.width) - module.width) * progress, + height: + module.height + + ((module.targetHeight ?? module.height) - module.height) * progress, + radius: module.radius + (8 - module.radius) * progress, + progress, + }; +} + +export function positionAt( + modules: readonly ModuleGeometry[], + _axis: AssemblyAxis, + part: AssemblyPart, + time: number, +) { + const module = modules[part]; + if (!module) throw new Error(`Missing assembly module ${part}`); + const progress = flightProgress(part, time); + const sourceX = 140 + part * 140 - module.width / 2; + const sourceY = 124 - module.height / 2; + return { + x: sourceX + (module.x - sourceX) * progress, + y: sourceY + (module.y - sourceY) * progress, + }; +} + +function coordinate(value: number) { + return Number(value.toFixed(4)); +} + +export function outlineAt( + modules: readonly ModuleGeometry[], + axis: AssemblyAxis, + part: AssemblyPart, + time: number, +): Record { + const module = modules[part]; + if (!module) throw new Error(`Missing assembly module ${part}`); + const { width, height, radius } = sizeAt(module, part, time); + + function corner(edge: Edge) { + const neighbor = jointNeighbor(axis, part, edge); + if (neighbor === undefined) return radius; + const contact = jointTime(part, neighbor); + return radius * (1 - easing((time - contact) / jointDuration)); + } + + const topLeft = corner(axis === "vertical" ? "top" : "left"); + const topRight = corner(axis === "vertical" ? "top" : "right"); + const bottomRight = corner(axis === "vertical" ? "bottom" : "right"); + const bottomLeft = corner(axis === "vertical" ? "bottom" : "left"); + const point = (x: number, y: number) => `${coordinate(x)} ${coordinate(y)}`; + + // Separate arcs stay primary while only the straight mating edges become dividers. + return { + top: `M${point(topLeft, 0)} L${point(width - topRight, 0)}`, + right: `M${point(width, topRight)} L${point(width, height - bottomRight)}`, + bottom: `M${point(width - bottomRight, height)} L${point(bottomLeft, height)}`, + left: `M${point(0, height - bottomLeft)} L${point(0, topLeft)}`, + topLeft: `M${point(0, topLeft)} C${point(0, topLeft * (1 - circleControl))} ${point(topLeft * (1 - circleControl), 0)} ${point(topLeft, 0)}`, + topRight: `M${point(width - topRight, 0)} C${point(width - topRight * (1 - circleControl), 0)} ${point(width, topRight * (1 - circleControl))} ${point(width, topRight)}`, + bottomRight: `M${point(width, height - bottomRight)} C${point(width, height - bottomRight * (1 - circleControl))} ${point(width - bottomRight * (1 - circleControl), height)} ${point(width - bottomRight, height)}`, + bottomLeft: `M${point(bottomLeft, height)} C${point(bottomLeft * (1 - circleControl), height)} ${point(0, height - bottomLeft * (1 - circleControl))} ${point(0, height - bottomLeft)}`, + }; +} + +function addSamples(times: Set, start: number, end: number) { + times.add(Number(start.toFixed(9))); + for (let sample = 1; start + sample / samplingRate < end; sample++) { + times.add(Number((start + sample / samplingRate).toFixed(9))); + } + times.add(Number(end.toFixed(9))); +} + +export function sampleTimes(parts: readonly AssemblyPart[]) { + const times = new Set([0, 5.4]); + for (const part of parts) { + const { start, end } = travel(part); + addSamples(times, start, end); + } + return [...times].sort((a, b) => a - b); +} + +export function outlineTimes( + _modules: readonly ModuleGeometry[], + axis: AssemblyAxis, + part: AssemblyPart, +): number[] { + const participating: AssemblyPart[] = [part]; + const edges: Edge[] = + axis === "vertical" ? ["top", "bottom"] : ["left", "right"]; + for (const edge of edges) { + const neighbor = jointNeighbor(axis, part, edge); + if (neighbor !== undefined) participating.push(neighbor); + } + const times = new Set(sampleTimes(participating)); + for (const edge of edges) { + const neighbor = jointNeighbor(axis, part, edge); + if (neighbor === undefined) continue; + const contact = jointTime(part, neighbor); + addSamples(times, contact, contact + jointDuration); + } + return [...times].sort((a, b) => a - b); +} diff --git a/apps/ui/app/plotter/particles-blueprint.tsx b/apps/ui/app/plotter/particles-blueprint.tsx new file mode 100644 index 000000000..ffe2c4818 --- /dev/null +++ b/apps/ui/app/plotter/particles-blueprint.tsx @@ -0,0 +1,113 @@ +import { BlueprintDrawing } from "./blueprint-drawing"; + +function ParticleContent({ + kind, + x, + y, +}: { + kind: "identity" | "visibility" | "action"; + x: number; + y: number; +}) { + return ( + + + {kind === "identity" && ( + <> + + + + )} + {kind === "action" && } + + {kind === "visibility" && ( + + + + + )} + + + {kind === "identity" + ? "Studio" + : kind === "visibility" + ? "Public" + : "Open"} + + + + ); +} + +export function ParticlesBlueprint() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A + + + B + + + C + + + Parts + + + Composition + + + 3u + + + 3u + + + u + + + A + B + C + + + + + + + ); +} diff --git a/apps/ui/app/plotter/particles-motion.module.css b/apps/ui/app/plotter/particles-motion.module.css new file mode 100644 index 000000000..3a33eeaf2 --- /dev/null +++ b/apps/ui/app/plotter/particles-motion.module.css @@ -0,0 +1,13 @@ +.animation { + display: none; +} + +@media (prefers-reduced-motion: no-preference) { + .drawing[data-live] .animation { + display: block; + } + + .drawing[data-live] .original { + visibility: hidden; + } +} diff --git a/apps/ui/app/plotter/particles-motion.tsx b/apps/ui/app/plotter/particles-motion.tsx new file mode 100644 index 000000000..fb4f53caa --- /dev/null +++ b/apps/ui/app/plotter/particles-motion.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { PauseIcon, PlayIcon, RotateCcwIcon } from "lucide-react"; +import { type ReactNode, useEffect, useRef, useState } from "react"; +import { useMediaQuery } from "@/registry/default/hooks/use-media-query"; +import { Button } from "@/registry/default/ui/button"; +import { + ParticlesAssemblyDrawing, + particlesSequenceDuration, +} from "./particles-assembly-drawing"; +import styles from "./particles-motion.module.css"; + +const chapters = ["Workspace", "Booking", "Activity"]; +const chapterDuration = particlesSequenceDuration / chapters.length; + +export function ParticlesMotion({ + children, + controls = "hero", +}: { + children: ReactNode; + controls?: "hero" | "preview"; +}) { + const drawing = useRef(null); + const animation = useRef(null); + const [live, setLive] = useState(true); + const [paused, setPaused] = useState(false); + const [inView, setInView] = useState(false); + const [stage, setStage] = useState(0); + const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); + + useEffect(() => { + if (!drawing.current) return; + const observer = new IntersectionObserver(([entry]) => { + if (entry) setInView(entry.isIntersecting); + }); + observer.observe(drawing.current); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const svg = animation.current?.querySelector("svg"); + if (!svg) return; + if (!live || reducedMotion || paused || !inView) { + svg.pauseAnimations(); + return; + } + + svg.unpauseAnimations(); + const updateStage = () => { + const time = svg.getCurrentTime() % particlesSequenceDuration; + // SVG clock precision can place a chapter seek just before its boundary. + setStage( + Math.max( + 0, + Math.min( + chapters.length - 1, + Math.floor((time + 0.0001) / chapterDuration), + ), + ), + ); + }; + updateStage(); + const timer = window.setInterval(updateStage, 100); + return () => window.clearInterval(timer); + }, [paused, inView, live, reducedMotion]); + + const seek = (chapter: number) => { + const svg = animation.current?.querySelector("svg"); + svg?.setCurrentTime(chapter * chapterDuration); + if (inView && !reducedMotion) svg?.unpauseAnimations(); + setStage(chapter); + setLive(true); + setPaused(false); + }; + + const togglePlayback = () => { + if (!live) { + setLive(true); + setPaused(false); + } else setPaused((value) => !value); + }; + + return ( +
+
+
+ {children} +
+
+ +
+ {controls === "hero" && !reducedMotion && ( + + )} +
+ {controls === "preview" && ( +
+
+ {chapters.map((chapter, index) => ( + + ))} +
+
+ {live && ( + + )} + + {live && ( + + )} +
+

+ Reduced motion · Static drawing +

+
+ )} +
+ ); +} diff --git a/apps/ui/app/plotter/plotter-intro.tsx b/apps/ui/app/plotter/plotter-intro.tsx new file mode 100644 index 000000000..29e5cde12 --- /dev/null +++ b/apps/ui/app/plotter/plotter-intro.tsx @@ -0,0 +1,54 @@ +import { ArrowDownIcon } from "lucide-react"; +import { Button } from "@/registry/default/ui/button"; + +export function PlotterIntro() { + return ( +
+

+ Plotter illustrations +

+

+ Simple forms. Fine lines. A skill for drawing and animating editable + illustrations, with a shared visual language behind every mark. +

+ +
+ ); +} diff --git a/apps/ui/app/plotter/plotter-prompts.tsx b/apps/ui/app/plotter/plotter-prompts.tsx new file mode 100644 index 000000000..758b8ab1d --- /dev/null +++ b/apps/ui/app/plotter/plotter-prompts.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { CheckIcon, CopyIcon, DownloadIcon } from "lucide-react"; +import { type ReactNode, useEffect, useRef, useState } from "react"; +import { Button } from "@/registry/default/ui/button"; +import { Tabs, TabsList, TabsPanel, TabsTab } from "@/registry/default/ui/tabs"; +import { + animationPrompt, + drawingPrompt, + plotterDownload, +} from "./plotter-usage-content"; + +function Prompt({ text }: { text: string }) { + const [status, setStatus] = useState<"idle" | "copied" | "failed">("idle"); + const timer = useRef | null>(null); + useEffect( + () => () => { + if (timer.current) clearTimeout(timer.current); + }, + [], + ); + + async function copy() { + if (timer.current) clearTimeout(timer.current); + try { + if (!navigator.clipboard?.writeText) + throw new Error("Clipboard unavailable"); + await navigator.clipboard.writeText(text); + setStatus("copied"); + timer.current = setTimeout(() => setStatus("idle"), 2000); + } catch { + setStatus("failed"); + } + } + + return ( +
+

+ {text} +

+
+ + + {status === "failed" + ? "Select the prompt text and copy it manually." + : status === "copied" + ? "Ready to paste into your agent." + : "Adapt the subject and placement to your brief."} + +
+
+ ); +} + +export function PlotterPrompts({ + drawing, + animation, +}: { + drawing: ReactNode; + animation: ReactNode; +}) { + return ( +
+ + + Draw an illustration + Animate a drawing + + +
+
+

+ Start with one relationship. +

+

+ Give your agent a subject, a placement, and an output format. + Plotter supplies the construction references and drawing + workflow. +

+ +

+ Looking for a direction? Browse the{" "} + + catalogue + + , then use the{" "} + + visual grammar + {" "} + to choose supporting marks. +

+
+
+
+
{drawing}
+
+
+
+

+ Agent registration{" "} + + AG01 + +

+

+ Editable paths and labels · Self-contained SVG +

+
+ +
+
+
+
+ +
+
+

+ Give the drawing a reason to move. +

+

+ Start from an editable still.{" "} + + Download the assembly-line SVG + {" "} + and attach it with this prompt. +

+ +

+ One skill, a separate motion workflow. Explore more{" "} + + animation examples + + . +

+
+
+
+
{animation}
+
+
+
+

+ An assembly line{" "} + + W05 + +

+

+ Editable SVG + player · Open the HTML in a browser +

+
+ +
+
+
+
+
+

+ Scroll sideways to see the full drawing. +

+

+ These are editable examples to study and adapt. Downloads need no coss + runtime. Labels use Cal Sans when available, with a sans-serif fallback. +

+
+ ); +} diff --git a/apps/ui/app/plotter/plotter-token-guide.tsx b/apps/ui/app/plotter/plotter-token-guide.tsx new file mode 100644 index 000000000..a21115d2b --- /dev/null +++ b/apps/ui/app/plotter/plotter-token-guide.tsx @@ -0,0 +1,294 @@ +"use client"; + +import { + type CSSProperties, + type ReactNode, + useEffect, + useId, + useRef, + useState, +} from "react"; +import { Button } from "@/registry/default/ui/button"; +import standard from "../../skills/plotter/assets/foundation/standard.json"; + +const controls = [ + { + token: "--plotter-stroke-primary", + label: "Primary stroke", + hint: "The outlines of the carriers and cores.", + max: standard.tokens["--plotter-stroke-primary"] * 2, + }, + { + token: "--plotter-opacity-guide", + label: "Guide opacity", + hint: "The quiet alignment and extension lines.", + max: 1, + }, + { + token: "--plotter-opacity-hatch", + label: "Hatch opacity", + hint: "Only the diagonal lines on the core faces.", + max: 1, + }, +] as const; + +type ControlToken = (typeof controls)[number]["token"]; +type Values = Record; + +const defaults: Values = { + "--plotter-stroke-primary": standard.tokens["--plotter-stroke-primary"], + "--plotter-opacity-guide": standard.tokens["--plotter-opacity-guide"], + "--plotter-opacity-hatch": standard.tokens["--plotter-opacity-hatch"], +}; + +export function PlotterTokenGuide({ children }: { children: ReactNode }) { + const id = useId(); + const cssRef = useRef(null); + const [values, setValues] = useState(defaults); + const [copyState, setCopyState] = useState<"idle" | "copied" | "manual">( + "idle", + ); + const css = `.illustration {\n --plotter-ink: currentColor;\n${controls + .map(({ token }) => ` ${token}: ${values[token]};`) + .join("\n")}\n}`; + const previewStyle: CSSProperties & Values = values; + + useEffect(() => { + if (copyState !== "copied") return; + const timeout = window.setTimeout(() => setCopyState("idle"), 1600); + return () => window.clearTimeout(timeout); + }, [copyState]); + + async function copyCss() { + try { + await navigator.clipboard.writeText(css); + setCopyState("copied"); + } catch { + cssRef.current?.focus(); + cssRef.current?.select(); + setCopyState("manual"); + } + } + + return ( +
+
+

+ Make it fit your page +

+

+ Adjust ink, stroke widths, and opacity without changing the drawing. + Try the shared tokens on W05, then copy the CSS to a wrapper around + your inline SVG. +

+
+ +
+
+
+ {controls.map(({ token, label, hint, max }) => ( +
+
+ + + {values[token].toFixed(2)} + +
+ { + const value = event.currentTarget.valueAsNumber; + setValues((current) => ({ + ...current, + [token]: value, + })); + setCopyState("idle"); + }} + step={0.01} + type="range" + value={values[token]} + /> +

+ {hint} +

+
+ ))} +
+ +
+
+ +
+ + +
+
+