Something went wrong. Try again.
inkfinite (an infinite canvas/whiteboard) monorepo ink.stormlightlabs.org
svelte rust tauri svg canvas perfect-freehand
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646//! Generates the checked-in TypeScript bindings and schema projections.
use std::collections::BTreeMap;use std::error::Error;use std::fs;use std::io::Write;use std::path::{Path, PathBuf};use std::process::{Command, Stdio};
use inkfinite_core::editor::*;use inkfinite_core::path::*;use inkfinite_core::proto::*;use inkfinite_core::svg_import::*;use inkfinite_core::wasm::*;use inkfinite_core::*;use schemars::JsonSchema;use serde_json::{Value, json};use ts_rs::{Config, TS};
const RECTANGLE_KIND: &str = "rect";
const GENERATED_TS_HEADER: &str = concat!( "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n", "// Generated by Inkfinite's binding generator. Run `cargo run -p inkfinite-cli --bin generate-bindings`.\n\n",);
const GENERATED_JSON_COMMENT: &str = "Generated by Inkfinite's binding generator. Do not edit this file manually.";
fn main() { let check = match std::env::args().skip(1).collect::<Vec<_>>().as_slice() { [] => false, [flag] if flag == "--check" => true, [flag] if flag == "--help" || flag == "-h" => { println!("Usage: generate-bindings [--check]"); return; } _ => { eprintln!("usage: generate-bindings [--check]"); std::process::exit(2); } };
if let Err(error) = run(check) { eprintln!("binding generation failed: {error}"); std::process::exit(1); }}
fn run(check: bool) -> Result<(), Box<dyn Error>> { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); let artifacts = artifacts()? .into_iter() .map(|(relative_path, contents)| { let contents = if relative_path.extension().and_then(|extension| extension.to_str()) == Some("ts") { format_typescript(&root, &relative_path, &contents)? } else { contents }; Ok((relative_path, contents)) }) .collect::<Result<BTreeMap<_, _>, Box<dyn Error>>>()?; let mut stale = Vec::new();
for (relative_path, contents) in artifacts { let path = root.join(&relative_path); if check { match fs::read_to_string(&path) { Ok(existing) if existing == contents => {} Ok(_) => stale.push(relative_path), Err(error) if error.kind() == std::io::ErrorKind::NotFound => { stale.push(relative_path); } Err(error) => return Err(error.into()), } } else { write_if_changed(&path, &contents)?; } }
if stale.is_empty() { if check { println!("generated bindings are up to date"); } return Ok(()); }
eprintln!("generated files are stale:"); for path in stale { eprintln!(" {}", path.display()); } Err("run generate-bindings to refresh generated output".into())}
fn format_typescript(root: &Path, relative_path: &Path, source: &str) -> Result<String, Box<dyn Error>> { let prettier = root.join("node_modules/.bin/prettier"); if !prettier.is_file() { return Err(format!( "Prettier was not found at {}; install the workspace dependencies before generating bindings", prettier.display() ) .into()); }
let file_name = relative_path .to_str() .ok_or_else(|| format!("binding path is not valid UTF-8: {}", relative_path.display()))?; let mut child = Command::new(prettier) .args(["--config", ".prettierrc", "--stdin-filepath", file_name]) .current_dir(root) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; child .stdin .take() .ok_or("could not open Prettier stdin")? .write_all(source.as_bytes())?; let output = child.wait_with_output()?; if !output.status.success() { return Err(format!( "Prettier failed for {}: {}", relative_path.display(), String::from_utf8_lossy(&output.stderr).trim() ) .into()); } Ok(String::from_utf8(output.stdout)?)}
fn artifacts() -> Result<BTreeMap<PathBuf, String>, Box<dyn Error>> { let mut artifacts = BTreeMap::new(); insert_schema::<DocumentSnapshot>(&mut artifacts, "schemas/document-snapshot.schema.json")?; insert_schema::<TransactionDraft>(&mut artifacts, "schemas/transaction-draft.schema.json")?; insert_schema::<Request>(&mut artifacts, "schemas/protocol-request.schema.json")?; insert_schema::<Response>(&mut artifacts, "schemas/protocol-response.schema.json")?; insert_schema::<ProtocolError>(&mut artifacts, "schemas/protocol-error.schema.json")?;
artifacts.insert(PathBuf::from("fixtures/native/shape-registry.json"), fixture_json()?); artifacts.insert(PathBuf::from("packages/bindings/src/model.ts"), model_bindings()); artifacts.insert( PathBuf::from("packages/bindings/src/transaction.ts"), transaction_bindings(), ); artifacts.insert(PathBuf::from("packages/bindings/src/protocol.ts"), protocol_bindings()); artifacts.insert(PathBuf::from("packages/bindings/src/registry.ts"), registry_bindings()); artifacts.insert( PathBuf::from("packages/bindings/src/svg-import.ts"), svg_import_bindings(), ); artifacts.insert(PathBuf::from("packages/bindings/src/editor.ts"), editor_bindings()); artifacts.insert(PathBuf::from("packages/bindings/src/wasm.ts"), wasm_bindings()); artifacts.insert(PathBuf::from("packages/bindings/src/index.ts"), index_bindings()); Ok(artifacts)}
fn insert_schema<T: JsonSchema>( artifacts: &mut BTreeMap<PathBuf, String>, relative_path: &str,) -> Result<(), Box<dyn Error>> { let mut schema = serde_json::to_value(schemars::schema_for!(T))?; if let Some(object) = schema.as_object_mut() { object.insert("$comment".into(), Value::String(GENERATED_JSON_COMMENT.into())); } artifacts.insert( PathBuf::from(relative_path), format!("{}\n", serde_json::to_string_pretty(&schema)?), ); Ok(())}
fn model_bindings() -> String { let config = ts_config(); let mut output = GENERATED_TS_HEADER.to_owned(); append_declaration::<serde_json::Value>(&mut output, &config); output.push_str("export type ShapeProperties = { [key in string]: JsonValue };\n\n");
append_declaration::<DocumentId>(&mut output, &config); append_declaration::<PageId>(&mut output, &config); append_declaration::<LayerId>(&mut output, &config); append_declaration::<ShapeId>(&mut output, &config); append_declaration::<BindingId>(&mut output, &config); append_declaration::<AssetId>(&mut output, &config); append_declaration::<ActorId>(&mut output, &config); append_declaration::<ChangeHash>(&mut output, &config); append_declaration::<FormatId>(&mut output, &config); append_declaration::<ShapeKind>(&mut output, &config); append_declaration::<BindingKind>(&mut output, &config); append_declaration::<Timestamp>(&mut output, &config); append_declaration::<RecordVersion>(&mut output, &config); append_declaration::<Opacity>(&mut output, &config); append_declaration::<SiblingAnchor<ShapeId>>(&mut output, &config); append_declaration::<Vec2>(&mut output, &config); append_declaration::<GradientUnits>(&mut output, &config); append_declaration::<GradientSpread>(&mut output, &config); append_declaration::<GradientTransform>(&mut output, &config); append_declaration::<GradientStop>(&mut output, &config); append_declaration::<Paint>(&mut output, &config); append_declaration::<PaintValue>(&mut output, &config); append_declaration::<MaskMode>(&mut output, &config); append_declaration::<MaskEffect>(&mut output, &config); append_declaration::<FilterPrimitive>(&mut output, &config); append_declaration::<FilterEffect>(&mut output, &config); append_clean_declaration::<PathFillRule>(&mut output, &config); append_clean_declaration::<PathHandleMode>(&mut output, &config); append_clean_declaration::<PathSegment>(&mut output, &config); append_clean_declaration::<PathSubpath>(&mut output, &config); append_clean_declaration::<PathGeometry>(&mut output, &config); append_declaration::<ResolvedArrowGeometry>(&mut output, &config); append_declaration::<FlattenedPath>(&mut output, &config); append_declaration::<FlattenedSubpath>(&mut output, &config); append_declaration::<PathMetricPoint>(&mut output, &config); append_declaration::<PathNearestPoint>(&mut output, &config); append_declaration::<Transform>(&mut output, &config); append_declaration::<Origin>(&mut output, &config); append_declaration::<Provenance>(&mut output, &config); append_declaration::<SemanticMetadata>(&mut output, &config); append_declaration::<ShapeStyle>(&mut output, &config); append_declaration::<ShapeParent>(&mut output, &config); append_declaration::<StackDirection>(&mut output, &config); append_declaration::<LayoutAlignment>(&mut output, &config); append_declaration::<Insets>(&mut output, &config); append_declaration::<ContainerLayout>(&mut output, &config); append_declaration::<ShapeRecord>(&mut output, &config); append_declaration::<PageRecord>(&mut output, &config); append_declaration::<LayerRecord>(&mut output, &config); append_declaration::<BindingAnchor>(&mut output, &config); append_declaration::<BindingRecord>(&mut output, &config); append_declaration::<AssetSource>(&mut output, &config); append_declaration::<ReferenceKind>(&mut output, &config); append_declaration::<ReferenceProperties>(&mut output, &config); append_declaration::<ImageCrop>(&mut output, &config); append_declaration::<ImageMaskKind>(&mut output, &config); append_declaration::<ImageMask>(&mut output, &config); append_declaration::<AssetRecord>(&mut output, &config); append_declaration::<Document>(&mut output, &config); append_declaration::<DocumentSnapshot>(&mut output, &config); output}
fn transaction_bindings() -> String { let config = ts_config(); let mut output = GENERATED_TS_HEADER.to_owned(); output.push_str("import type {\n"); output.push_str(" ActorId, AssetId, AssetRecord, BindingId, BindingRecord, ChangeHash, ContainerLayout,\n"); output.push_str(" LayerId, LayerRecord, Opacity, Origin, PageId, PageRecord,\n"); output.push_str(" RecordVersion, SemanticMetadata, ShapeId, ShapeParent, ShapeProperties, ShapeRecord,\n"); output.push_str(" ShapeStyle, SiblingAnchor, Timestamp, Transform\n"); output.push_str("} from \"./model.js\";\n\n");
append_declaration::<TransactionId>(&mut output, &config); append_declaration::<TransactionDraft>(&mut output, &config); append_declaration::<DocumentPath>(&mut output, &config); append_declaration::<LayerContentsDisposition>(&mut output, &config); append_declaration::<LayerPatch>(&mut output, &config); append_declaration::<ShapePatch>(&mut output, &config); append_declaration::<AssetPatch>(&mut output, &config); append_declaration::<LayoutAxis>(&mut output, &config); append_declaration::<ShapeAlignment>(&mut output, &config); append_declaration::<GraphLayoutAlgorithm>(&mut output, &config); append_declaration::<GraphLayoutDirection>(&mut output, &config); append_declaration::<GraphLayoutOptions>(&mut output, &config); append_declaration::<Operation>(&mut output, &config); append_declaration::<RecordId>(&mut output, &config); append_clean_declaration::<QueryRecord>(&mut output, &config); append_declaration::<DocumentPatch>(&mut output, &config); append_declaration::<Bounds>(&mut output, &config); append_clean_declaration::<CameraState>(&mut output, &config); append_declaration::<AffectedRegion>(&mut output, &config); append_declaration::<InverseMetadata>(&mut output, &config); append_declaration::<Warning>(&mut output, &config); append_declaration::<CommitResult>(&mut output, &config); append_clean_declaration::<Query>(&mut output, &config); append_clean_declaration::<QueryResult>(&mut output, &config); append_declaration::<SaveResult>(&mut output, &config); append_declaration::<ProposalId>(&mut output, &config); append_clean_declaration::<ProposalOperationPreview>(&mut output, &config); append_declaration::<ProposalChangeKind>(&mut output, &config); append_clean_declaration::<ProposalObjectPreview>(&mut output, &config); append_clean_declaration::<Proposal>(&mut output, &config); output}
fn protocol_bindings() -> String { let config = ts_config(); let mut output = GENERATED_TS_HEADER.to_owned(); output.push_str( "import type { ActorId, ChangeHash, DocumentId, DocumentSnapshot, JsonValue, Origin, Timestamp } from \"./model.js\";\n", ); output.push_str( "import type { CommitResult, DocumentPath, Proposal, ProposalId, Query, QueryResult, SaveResult, TransactionDraft } from \"./transaction.js\";\n\n", ); append_declaration::<SessionId>(&mut output, &config); append_clean_declaration::<Request>(&mut output, &config); append_clean_declaration::<Response>(&mut output, &config); append_clean_declaration::<ProtocolError>(&mut output, &config); output}
fn registry_bindings() -> String { let kinds = builtin_shape_kinds() .iter() .map(|kind| format!("\"{kind}\"")) .collect::<Vec<_>>() .join(", "); format!( "{GENERATED_TS_HEADER}import type {{ JsonValue, PathGeometry, Transform }} from \"./model.js\";\nimport type {{ Bounds }} from \"./transaction.js\";\n\nexport const BUILTIN_SHAPE_KINDS = [{kinds}] as const;\nexport type BuiltinShapeKind = typeof BUILTIN_SHAPE_KINDS[number];\n\nexport const GEOMETRY_CONVENTION = {{\n coordinateSystem: \"{GEOMETRY_COORDINATE_SYSTEM}\",\n rotation: \"{GEOMETRY_ROTATION}\",\n bounds: \"{GEOMETRY_BOUNDS}\",\n}} as const;\n\nexport const SHAPE_REGISTRY = BUILTIN_SHAPE_KINDS.map((kind) => ({{\n kind,\n dimensions: [\"width\", \"height\"] as const,\n allowsChildren: kind === \"container\",\n}}));\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {{\n return typeof value === \"object\" && value !== null;\n}}\n\nfunction isFinitePoint(value: unknown): boolean {{\n if (!isRecord(value)) return false;\n return typeof value.x === \"number\" && Number.isFinite(value.x)\n && typeof value.y === \"number\" && Number.isFinite(value.y);\n}}\n\nfunction isPathSegment(value: unknown): boolean {{\n if (!isRecord(value) || typeof value.type !== \"string\") return false;\n switch (value.type) {{\n case \"move\":\n case \"line\":\n return isFinitePoint(value.to);\n case \"quadratic\":\n return isFinitePoint(value.control) && isFinitePoint(value.to);\n case \"cubic\":\n return isFinitePoint(value.control_1) && isFinitePoint(value.control_2) && isFinitePoint(value.to);\n default:\n return false;\n }}\n}}\n\nexport function validatePathGeometry(value: unknown): value is PathGeometry {{\n if (!isRecord(value) || !Array.isArray(value.subpaths) || value.subpaths.length === 0) return false;\n if (value.fill_rule !== \"nonzero\" && value.fill_rule !== \"evenodd\") return false;\n return value.subpaths.every((subpath) => {{\n if (!isRecord(subpath) || typeof subpath.closed !== \"boolean\" || !Array.isArray(subpath.segments) || subpath.segments.length === 0) return false;\n if (subpath.handle_modes !== undefined && subpath.handle_modes !== null\n && (!Array.isArray(subpath.handle_modes)\n || subpath.handle_modes.length !== subpath.segments.length\n || !subpath.handle_modes.every((mode) => mode === \"broken\" || mode === \"joined\"))) return false;\n const first = subpath.segments[0];\n if (!isRecord(first) || first.type !== \"move\") return false;\n return subpath.segments.every((segment, index) => (index === 0 || (isRecord(segment) && segment.type !== \"move\")) && isPathSegment(segment));\n }});\n}}\n\nfunction validatePaintValue(value: unknown): boolean {{\n if (typeof value === \"string\") return value.length > 0;\n if (!isRecord(value) || typeof value.kind !== \"string\") return false;\n if (value.kind === \"solid\") return typeof value.color === \"string\" && value.color.length > 0;\n if (value.kind !== \"linear_gradient\" && value.kind !== \"radial_gradient\") return false;\n if (!Array.isArray(value.stops) || value.stops.length === 0) return false;\n return value.stops.every((stop) => isRecord(stop)\n && typeof stop.offset === \"number\" && Number.isFinite(stop.offset) && stop.offset >= 0 && stop.offset <= 1\n && typeof stop.color === \"string\" && stop.color.length > 0\n && typeof stop.opacity === \"number\" && Number.isFinite(stop.opacity) && stop.opacity >= 0 && stop.opacity <= 1);\n}}\n\nfunction validateStrokeProperties(properties: Record<string, JsonValue>): boolean {{\n const points = properties.points;\n if (!Array.isArray(points) || points.length < 2) return false;\n if (!points.every((point) => Array.isArray(point)\n && (point.length === 2 || point.length === 3)\n && point.every((value, index) => typeof value === \"number\" && Number.isFinite(value)\n && (index < 2 || (value >= 0 && value <= 1))))) return false;\n const widthProfile = properties.widthProfile ?? properties.width_profile;\n if (widthProfile !== undefined) {{\n if (!Array.isArray(widthProfile) || !widthProfile.every((point) => isRecord(point)\n && typeof point.offset === \"number\" && Number.isFinite(point.offset) && point.offset >= 0 && point.offset <= 1\n && typeof point.width === \"number\" && Number.isFinite(point.width) && point.width > 0)) return false;\n const profilePoints = widthProfile as unknown as Array<Record<string, unknown>>;\n for (let index = 1; index < profilePoints.length; index += 1) {{\n const previousOffset = profilePoints[index - 1]?.offset;\n const nextOffset = profilePoints[index]?.offset;\n if (typeof previousOffset !== \"number\" || typeof nextOffset !== \"number\" || previousOffset >= nextOffset) return false;\n }}\n }}\n const style = properties.style;\n if (!isRecord(style) || !validatePaintValue(style.color)\n || typeof style.opacity !== \"number\" || !Number.isFinite(style.opacity)\n || style.opacity < 0 || style.opacity > 1) return false;\n const brush = properties.brush;\n if (!isRecord(brush) || typeof brush.size !== \"number\" || !Number.isFinite(brush.size)\n || brush.size <= 0 || typeof brush.thinning !== \"number\" || !Number.isFinite(brush.thinning)\n || typeof brush.smoothing !== \"number\" || !Number.isFinite(brush.smoothing)\n || typeof brush.streamline !== \"number\" || !Number.isFinite(brush.streamline)\n || typeof brush.simulatePressure !== \"boolean\") return false;\n return true;\n}}\n\nfunction numericProperty(properties: Record<string, JsonValue>, name: string): number {{\n const value = properties[name];\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}}\n\ntype PathPoint = {{ x: number; y: number }};\n\nfunction quadraticPoint(start: PathPoint, control: PathPoint, end: PathPoint, t: number): PathPoint {{\n const inverse = 1 - t;\n return {{\n x: inverse * inverse * start.x + 2 * inverse * t * control.x + t * t * end.x,\n y: inverse * inverse * start.y + 2 * inverse * t * control.y + t * t * end.y,\n }};\n}}\n\nfunction cubicPoint(start: PathPoint, control1: PathPoint, control2: PathPoint, end: PathPoint, t: number): PathPoint {{\n const inverse = 1 - t;\n return {{\n x: inverse ** 3 * start.x + 3 * inverse ** 2 * t * control1.x + 3 * inverse * t ** 2 * control2.x + t ** 3 * end.x,\n y: inverse ** 3 * start.y + 3 * inverse ** 2 * t * control1.y + 3 * inverse * t ** 2 * control2.y + t ** 3 * end.y,\n }};\n}}\n\nfunction quadraticRoots(a: number, b: number, c: number): number[] {{\n if (Math.abs(a) <= Number.EPSILON) return Math.abs(b) > Number.EPSILON ? [-c / b] : [];\n const discriminant = b * b - 4 * a * c;\n if (discriminant < 0) return [];\n const root = Math.sqrt(discriminant);\n return [(-b - root) / (2 * a), (-b + root) / (2 * a)];\n}}\n\n/** Returns exact local bounds, including Bézier derivative extrema. */\nexport function pathBounds(geometry: PathGeometry): Bounds {{\n const points: PathPoint[] = [];\n for (const subpath of geometry.subpaths) {{\n const first = subpath.segments[0];\n if (!first || first.type !== 'move') continue;\n const start = first.to;\n let current = start;\n points.push(current);\n for (const segment of subpath.segments.slice(1)) {{\n if (segment.type === 'move') {{\n current = segment.to;\n points.push(current);\n }} else if (segment.type === 'line') {{\n points.push(current, segment.to);\n current = segment.to;\n }} else if (segment.type === 'quadratic') {{\n points.push(current, segment.to);\n for (const value of [\n (current.x - segment.control.x) / (current.x - 2 * segment.control.x + segment.to.x),\n (current.y - segment.control.y) / (current.y - 2 * segment.control.y + segment.to.y),\n ]) {{\n if (Number.isFinite(value) && value > 0 && value < 1) points.push(quadraticPoint(current, segment.control, segment.to, value));\n }}\n current = segment.to;\n }} else {{\n points.push(current, segment.to);\n for (const [startValue, control1, control2, endValue] of [\n [current.x, segment.control_1.x, segment.control_2.x, segment.to.x],\n [current.y, segment.control_1.y, segment.control_2.y, segment.to.y],\n ]) {{\n const a = -startValue + 3 * control1 - 3 * control2 + endValue;\n const b = 2 * (startValue - 2 * control1 + control2);\n const c = control1 - startValue;\n for (const value of quadraticRoots(a, b, c)) {{\n if (value > 0 && value < 1) points.push(cubicPoint(current, segment.control_1, segment.control_2, segment.to, value));\n }}\n }}\n current = segment.to;\n }}\n }}\n if (subpath.closed) points.push(current, start);\n }}\n if (points.length === 0) return {{ x: 0, y: 0, width: 0, height: 0 }};\n const xs = points.map((point) => point.x);\n const ys = points.map((point) => point.y);\n const minX = Math.min(...xs);\n const maxX = Math.max(...xs);\n const minY = Math.min(...ys);\n const maxY = Math.max(...ys);\n return {{ x: minX, y: minY, width: maxX - minX, height: maxY - minY }};\n}}\n\nexport function validateShapeProperties(kind: string, properties: Record<string, JsonValue>): boolean {{\n if (!(BUILTIN_SHAPE_KINDS as readonly string[]).includes(kind)) return false;\n if (kind === \"path\" && !validatePathGeometry({{ subpaths: properties.subpaths, fill_rule: properties.fill_rule, handle_modes: properties.handle_modes }})) return false;\n if (kind === \"stroke\" && !validateStrokeProperties(properties)) return false;\n for (const name of [\"fill\", \"stroke\", \"background\", \"border\"]) {{\n if (properties[name] !== undefined && properties[name] !== null && !validatePaintValue(properties[name])) return false;\n }}\n const style = properties.style;\n if (isRecord(style)) {{\n for (const name of [\"stroke\", \"color\"]) {{\n if (style[name] !== undefined && style[name] !== null && !validatePaintValue(style[name])) return false;\n }}\n }}\n return [\"width\", \"height\"].every((name) => {{\n const value = properties[name];\n return value === undefined || (typeof value === \"number\" && Number.isFinite(value) && value >= 0);\n }});\n}}\n\nexport type RegistryShape = {{\n kind: string;\n properties: Record<string, JsonValue>;\n transform: Transform;\n}};\n\nexport function boundsForShape(shape: RegistryShape): Bounds {{\n const pathValue: PathGeometry = {{\n subpaths: shape.properties.subpaths as PathGeometry['subpaths'],\n fill_rule: shape.properties.fill_rule as PathGeometry['fill_rule'],\n }};\n const local = shape.kind === 'path' && validatePathGeometry(pathValue)\n ? pathBounds(pathValue)\n : {{ x: 0, y: 0, width: Math.abs(numericProperty(shape.properties, \"width\")), height: Math.abs(numericProperty(shape.properties, \"height\")) }};\n const {{ translation, rotation, scale_x: scaleX, scale_y: scaleY }} = shape.transform;\n const cos = Math.cos(rotation);\n const sin = Math.sin(rotation);\n const points = [[local.x, local.y], [local.x + local.width, local.y], [local.x, local.y + local.height], [local.x + local.width, local.y + local.height]].map(([x, y]) => [\n translation.x + x * scaleX * cos - y * scaleY * sin,\n translation.y + x * scaleX * sin + y * scaleY * cos,\n ]);\n const xs = points.map(([x]) => x);\n const ys = points.map(([, y]) => y);\n const minX = Math.min(...xs);\n const maxX = Math.max(...xs);\n const minY = Math.min(...ys);\n const maxY = Math.max(...ys);\n return {{ x: minX, y: minY, width: maxX - minX, height: maxY - minY }};\n}}\n" )}
fn svg_import_bindings() -> String { let config = ts_config(); let mut output = GENERATED_TS_HEADER.to_owned(); output.push_str("import type { AssetId, JsonValue, ShapeKind, ShapeStyle, Transform } from './model.js';\n\n"); append_declaration::<SvgUnsupportedFeature>(&mut output, &config); append_declaration::<SvgUnsupportedAction>(&mut output, &config); append_clean_declaration::<SvgImportWarning>(&mut output, &config); append_declaration::<SvgViewBox>(&mut output, &config); append_declaration::<SvgAsset>(&mut output, &config); append_declaration::<SvgShape>(&mut output, &config); append_declaration::<SvgGroup>(&mut output, &config); append_declaration::<SvgImage>(&mut output, &config); append_clean_declaration::<SvgImportNode>(&mut output, &config); append_declaration::<SvgImport>(&mut output, &config); output}
fn editor_bindings() -> String { let config = ts_config(); let mut output = GENERATED_TS_HEADER.to_owned(); output.push_str( "import type { ActorId, AssetId, AssetRecord, BindingAnchor, BindingId, BindingKind, BindingRecord, ContainerLayout, LayerId, LayerRecord, Opacity, Origin, PageId, PageRecord, ResolvedArrowGeometry, SemanticMetadata, ShapeId, ShapeKind, ShapeParent, ShapeProperties, ShapeStyle, SiblingAnchor, Timestamp } from './model.js';\n", ); output.push_str("import type { LayerContentsDisposition, LayerPatch, TransactionId } from './transaction.js';\n\n"); append_declaration::<EditorTransform>(&mut output, &config); append_declaration::<EditorShape>(&mut output, &config); append_declaration::<EditorShapeDraft>(&mut output, &config); append_declaration::<EditorPage>(&mut output, &config); append_declaration::<EditorLayer>(&mut output, &config); append_declaration::<EditorBinding>(&mut output, &config); append_declaration::<EditorOrder>(&mut output, &config); append_declaration::<EditorProjection>(&mut output, &config); append_declaration::<PathCurveKind>(&mut output, &config); append_declaration::<PathTopologyOperation>(&mut output, &config); append_declaration::<BooleanPathOperation>(&mut output, &config); append_clean_declaration::<EditorPatch>(&mut output, &config); append_declaration::<EditorReconciliationRequest>(&mut output, &config); output}
fn wasm_bindings() -> String { let config = ts_config(); let mut output = GENERATED_TS_HEADER.to_owned(); output.push_str("import type { Bounds, CommitResult, TransactionDraft } from './transaction.js';\n"); output.push_str( "import type { AssetId, DocumentSnapshot, FlattenedPath, ResolvedArrowGeometry, ShapeId } from './model.js';\n", ); output.push_str("import type { EditorProjection } from './editor.js';\n"); output.push_str("import type { SvgImport, SvgImportWarning } from './svg-import.js';\n\n"); append_declaration::<WasmDocumentSessionFailure>(&mut output, &config); append_declaration::<WasmDocumentSessionState>(&mut output, &config); append_clean_declaration::<WasmDocumentMutationResponse>(&mut output, &config); append_declaration::<WasmArrowGeometryFailure>(&mut output, &config); append_clean_declaration::<WasmArrowGeometryResponse>(&mut output, &config); append_declaration::<WasmPathMetricsFailure>(&mut output, &config); append_clean_declaration::<WasmPathMetricsResponse>(&mut output, &config); append_declaration::<WasmSvgRenderOptions>(&mut output, &config); append_declaration::<WasmSvgImportFailure>(&mut output, &config); append_clean_declaration::<WasmSvgImportResponse>(&mut output, &config); append_clean_declaration::<WasmSvgImportCommitResponse>(&mut output, &config); append_declaration::<WasmSvgRenderWarning>(&mut output, &config); append_declaration::<WasmSvgRenderFailure>(&mut output, &config); append_clean_declaration::<WasmSvgRenderResponse>(&mut output, &config); append_declaration::<WasmEditorProjectionFailure>(&mut output, &config); append_clean_declaration::<WasmEditorProjectionResponse>(&mut output, &config); append_declaration::<WasmEditorReconciliationFailure>(&mut output, &config); append_clean_declaration::<WasmEditorReconciliationResponse>(&mut output, &config); output}
fn index_bindings() -> String { format!( "{GENERATED_TS_HEADER}export * from './model.js';\nexport * from './protocol.js';\nexport * from './registry.js';\nexport * from './svg-import.js';\nexport * from './editor.js';\nexport * from './transaction.js';\nexport * from './wasm.js';\n" )}
fn ts_config() -> Config { Config::new().with_large_int("number")}
fn append_declaration<T: TS>(output: &mut String, config: &Config) { if let Some(docs) = T::docs() { output.push_str(&docs); } output.push_str("export "); output.push_str(&T::decl(config)); output.push_str("\n\n");}
fn append_clean_declaration<T: TS>(output: &mut String, config: &Config) { if let Some(docs) = T::docs() { output.push_str(&docs); } let declaration = format!("export {}", T::decl(config)); for line in declaration.lines() { output.push_str(line.trim_end()); output.push('\n'); } output.push('\n');}
fn fixture_json() -> Result<String, Box<dyn Error>> { let shape = fixture_shape(); let transaction = fixture_transaction(); let transform = shape.transform; let expected_bounds = transformed_bounds(40.0, 20.0, transform); let path_geometry = PathGeometry { subpaths: vec![PathSubpath { segments: vec![ PathSegment::Move { to: Vec2 { x: 0.0, y: 0.0 } }, PathSegment::Line { to: Vec2 { x: 40.0, y: 0.0 } }, PathSegment::Quadratic { control: Vec2 { x: 50.0, y: 10.0 }, to: Vec2 { x: 40.0, y: 20.0 } }, PathSegment::Cubic { control_1: Vec2 { x: 40.0, y: 30.0 }, control_2: Vec2 { x: 0.0, y: 30.0 }, to: Vec2 { x: 0.0, y: 20.0 }, }, ], closed: true, handle_modes: None, }], fill_rule: PathFillRule::EvenOdd, }; let path_properties = json!({ "subpaths": &path_geometry.subpaths, "fill_rule": path_geometry.fill_rule, }); let path_transform = Transform { translation: Vec2 { x: -5.0, y: 7.0 }, rotation: 0.3, scale_x: 1.2, scale_y: 0.8 }; let path_local_bounds = inkfinite_core::engine::geometry::path_bounds(&path_geometry); let path_expected_bounds = inkfinite_core::engine::geometry::Affine::from_transform(path_transform).transform_bounds(path_local_bounds); let invalid_path_cases = [ ("empty", json!({ "subpaths": [], "fill_rule": "nonzero" })), ( "empty_subpath", json!({ "subpaths": [{ "segments": [], "closed": false }], "fill_rule": "nonzero" }), ), ( "missing_move", json!({ "subpaths": [{ "segments": [{ "type": "line", "to": { "x": 1.0, "y": 2.0 } }], "closed": false }], "fill_rule": "nonzero" }), ), ( "move_not_first", json!({ "subpaths": [{ "segments": [ { "type": "move", "to": { "x": 0.0, "y": 0.0 } }, { "type": "move", "to": { "x": 1.0, "y": 1.0 } } ], "closed": false }], "fill_rule": "nonzero" }), ), ( "nonfinite_coordinate", json!({ "subpaths": [{ "segments": [{ "type": "move", "to": { "x": null, "y": 0.0 } }], "closed": false }], "fill_rule": "nonzero" }), ), ] .into_iter() .map(|(name, geometry)| json!({ "name": name, "geometry": geometry, "valid": false })) .collect::<Vec<_>>(); let stroke_properties = json!({ "points": [[0.0, 0.0], [20.0, 10.0]], "style": { "color": "#000000", "opacity": 1.0 }, "brush": { "size": 8.0, "thinning": 0.5, "smoothing": 0.5, "streamline": 0.5, "simulatePressure": true } }); let property_cases = [ (RECTANGLE_KIND, json!({"width": 40.0, "height": 20.0})), (inkfinite_core::PATH_KIND, path_properties.clone()), (inkfinite_core::STROKE_KIND, stroke_properties), ( inkfinite_core::STROKE_KIND, json!({ "points": [[0.0, 0.0]], "style": { "color": "#000000", "opacity": 1.0 }, "brush": { "size": 8.0, "thinning": 0.5, "smoothing": 0.5, "streamline": 0.5, "simulatePressure": true } }), ), (RECTANGLE_KIND, json!({"width": -1.0, "height": 20.0})), (RECTANGLE_KIND, json!({"width": "40", "height": 20.0})), ("unknown", json!({})), ] .into_iter() .map(|(kind, properties)| { let properties_map = serde_json::from_value(properties.clone()).expect("object properties"); json!({ "kind": kind, "properties": properties, "valid": validate_shape_properties(kind, &properties_map).is_ok(), }) }) .collect::<Vec<_>>();
let fixture = json!({ "_generated_by": "cargo run -p inkfinite-cli --bin generate-bindings", "_do_not_edit": true, "format": "inkfinite.shape-registry-fixture", "version": 1, "kind_names": builtin_shape_kinds(), "geometry": { "coordinate_system": GEOMETRY_COORDINATE_SYSTEM, "rotation": GEOMETRY_ROTATION, "bounds": GEOMETRY_BOUNDS, }, "path_geometry": serde_json::to_value(&path_geometry)?, "invalid_path_cases": invalid_path_cases, "property_cases": property_cases, "geometry_cases": [ { "kind": shape.kind, "properties": shape.properties, "transform": shape.transform, "expected_bounds": expected_bounds, }, { "kind": inkfinite_core::PATH_KIND, "properties": path_properties, "transform": path_transform, "expected_bounds": path_expected_bounds, } ], "serialization": { "shape": serde_json::to_value(shape)?, "transaction": serde_json::to_value(transaction)?, }, }); Ok(format!("{}\n", serde_json::to_string_pretty(&fixture)?))}
fn fixture_shape() -> inkfinite_core::ShapeRecord { inkfinite_core::ShapeRecord { id: ShapeId::from("shape:fixture"), kind: ShapeKind::from("rect"), parent: ShapeParent::Layer(LayerId::from("layer:fixture")), transform: Transform { translation: Vec2 { x: 10.0, y: 20.0 }, rotation: 0.5, scale_x: 2.0, scale_y: 1.5 }, child_ids: Vec::new(), layout: None, properties: BTreeMap::from([("width".into(), json!(40.0)), ("height".into(), json!(20.0))]), metadata: SemanticMetadata { name: Some("Fixture rectangle".into()), title: None, role: Some("fixture.shape".into()), description: None, body: None, tags: vec!["fixture".into()], source: None, link: None, custom_metadata: BTreeMap::new(), locked: false, agent_editable: true, provenance: Provenance { actor_id: ActorId::from("actor:fixture"), origin: Origin::System, timestamp: Timestamp(1_700_000_000_000), source: Some("native-fixture".into()), }, }, style: ShapeStyle { opacity: Opacity::OPAQUE, fill_opacity: None, stroke_opacity: None }, version: RecordVersion(1), }}
fn fixture_transaction() -> TransactionDraft { TransactionDraft { id: TransactionId("transaction:fixture".into()), actor_id: ActorId::from("actor:fixture"), origin: Origin::System, base_heads: vec![ChangeHash::from("head:fixture")], description: "Native shared binding fixture".into(), operations: vec![Operation::PatchShape { shape_id: ShapeId::from("shape:fixture"), patch: ShapePatch { transform: Some(Transform { translation: Vec2 { x: 10.0, y: 20.0 }, rotation: 0.5, scale_x: 2.0, scale_y: 1.5, }), properties: Some(BTreeMap::from([ ("width".into(), json!(40.0)), ("height".into(), json!(20.0)), ])), ..ShapePatch::default() }, expected_version: Some(RecordVersion(1)), }], timestamp: Timestamp(1_700_000_000_000), }}
fn transformed_bounds(width: f64, height: f64, transform: Transform) -> Bounds { let cos = transform.rotation.cos(); let sin = transform.rotation.sin(); let points = [(0.0, 0.0), (width, 0.0), (0.0, height), (width, height)].map(|(x, y)| { let x = x * transform.scale_x; let y = y * transform.scale_y; ( transform.translation.x + x * cos - y * sin, transform.translation.y + x * sin + y * cos, ) }); let min_x = points.iter().map(|point| point.0).fold(f64::INFINITY, f64::min); let max_x = points.iter().map(|point| point.0).fold(f64::NEG_INFINITY, f64::max); let min_y = points.iter().map(|point| point.1).fold(f64::INFINITY, f64::min); let max_y = points.iter().map(|point| point.1).fold(f64::NEG_INFINITY, f64::max); Bounds { x: min_x, y: min_y, width: max_x - min_x, height: max_y - min_y }}
fn write_if_changed(path: &Path, contents: &str) -> Result<(), Box<dyn Error>> { if let Ok(existing) = fs::read_to_string(path) && existing == contents { return Ok(()); } if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } fs::write(path, contents)?; Ok(())}