diff --git a/ROADMAP.md b/ROADMAP.md index 7a0895a..8c0f80c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -70,13 +70,16 @@ other platform effects. ### Document and editor model boundaries -Make the distinction between the canonical Rust document, the generated Rust -editor projection, and the ergonomic TypeScript editor model explicit in names, -modules, and documentation. - -Keep conversion between canonical and interactive representations behind a -small projection/reconciliation boundary. Generated bindings remain generated -contracts rather than a second hand-maintained model. +The canonical Rust records and generated TypeScript contracts are distinct from +the interactive `EditorDocument` model in `@inkfinite/core`. The editor-facing +records use explicit `Editor*` names, while Rust-owned records retain their +serialized names. + +`@inkfinite/core/src/persistence/canonical.ts` is the projection and +reconciliation boundary. It is the path between generated Rust contracts and +interactive editor state; browser and desktop adapters do not translate records +independently. Generated bindings remain generated contracts rather than a +second hand-maintained native model. ### Core package boundaries diff --git a/TODO.md b/TODO.md index 646bacd..a705227 100644 --- a/TODO.md +++ b/TODO.md @@ -56,15 +56,15 @@ ### Clarify model ownership -- [ ] Document the dependency direction between the canonical Rust document, +- [x] Document the dependency direction between the canonical Rust document, generated bindings, TypeScript editor model, editor runtime, UI, and apps -- [ ] Rename or reorganize TypeScript editor-model types so they cannot be +- [x] Rename or reorganize TypeScript editor-model types so they cannot be confused with canonical Rust records; update affected tests and public imports without changing serialized document behavior -- [ ] Put canonical-to-editor projection and editor-to-canonical reconciliation +- [x] Put canonical-to-editor projection and editor-to-canonical reconciliation adapters behind an explicit TypeScript module boundary, with round-trip coverage for projection -> edit -> canonical transaction -- [ ] Keep generated bindings generated; remove hand-maintained duplicates of +- [x] Keep generated bindings generated; remove hand-maintained duplicates of Rust-owned contract types where unnecessary and verify binding generation remains reproducible diff --git a/apps/desktop/src/lib/persistence/desktop-session.test.ts b/apps/desktop/src/lib/persistence/desktop-session.test.ts index e41b9db..5bceb2f 100644 --- a/apps/desktop/src/lib/persistence/desktop-session.test.ts +++ b/apps/desktop/src/lib/persistence/desktop-session.test.ts @@ -1,4 +1,4 @@ -import { PageRecord, ShapeRecord, type BoardExport, type DesktopFileOps, type FileHandle } from '@inkfinite/core'; +import { EditorPageRecord, EditorShapeRecord, type BoardExport, type DesktopFileOps, type FileHandle } from '@inkfinite/core'; import type { ChangeHash, DocumentSnapshot, Proposal, ShapeProperties, TransactionDraft } from '@inkfinite/bindings'; import { beforeEach, describe, expect, it } from 'vitest'; import { createDesktopSessionRepo } from '$lib/persistence/desktop-session'; @@ -721,8 +721,8 @@ describe('Rust-backed desktop session repository', () => { it('imports editable canvas content into the Rust-backed canonical document', async () => { fileOps.setSavePath('/tmp/imported.inkfinite'); - const page = PageRecord.create('Imported canvas', 'page:imported'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Imported canvas', 'page:imported'); + const shape = EditorShapeRecord.createRect( page.id, 12, 24, diff --git a/apps/desktop/src/lib/persistence/desktop-session.ts b/apps/desktop/src/lib/persistence/desktop-session.ts index 9f7662f..4881ad4 100644 --- a/apps/desktop/src/lib/persistence/desktop-session.ts +++ b/apps/desktop/src/lib/persistence/desktop-session.ts @@ -1,6 +1,11 @@ import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; -import { createEditorReconciliationRequest, createId } from '@inkfinite/core'; +import { + createEditorReconciliationRequest, + createId, + fromCanonicalDocumentSnapshot, + fromEditorProjection +} from '@inkfinite/core'; import type { BoardExport, BoardMeta, @@ -8,26 +13,22 @@ import type { DocPatch, FileHandle, LoadedDoc, - LayerRecord as EditorLayerRecord, - PageRecord as EditorPageRecord, - BindingRecord as EditorBindingRecord, + EditorLayerRecord, + EditorPageRecord, PersistenceSink, PersistenceStatus, - PersistentDocRepo, - ShapeRecord as EditorShapeRecord + PersistentDocRepo } from '@inkfinite/core'; import type { ChangeHash, CommitResult, DocumentSnapshot, - JsonValue, Query, QueryResult, Proposal, - ShapeRecord, TransactionDraft } from '@inkfinite/bindings'; -import type { EditorPatch, EditorProjection, EditorTransform } from '@inkfinite/bindings/editor'; +import type { EditorPatch, EditorProjection } from '@inkfinite/bindings/editor'; const ACTOR_ID = 'actor:desktop'; @@ -438,8 +439,8 @@ export function createDesktopSessionRepo(fileOps: DesktopFileOps, opts: { api?: updatedAt: modifiedAt ?? currentBoard?.updatedAt ?? Date.now() }; currentDoc = status.editor_projection - ? loadedDocFromProjection(status.editor_projection, status.snapshot) - : loadedDocFromSnapshot(status.snapshot); + ? fromEditorProjection(status.editor_projection, status.snapshot) + : fromCanonicalDocumentSnapshot(status.snapshot); if (!isDraft) { boardFiles.set(currentBoard.id, currentFile); boardFiles.set(boardIdForPath(status.path), currentFile); @@ -451,8 +452,8 @@ export function createDesktopSessionRepo(fileOps: DesktopFileOps, opts: { api?: currentStatus = status; currentFile = { path: status.path, name: fileName(status.path), modifiedAt: Date.now() }; currentDoc = status.editor_projection - ? loadedDocFromProjection(status.editor_projection, status.snapshot) - : loadedDocFromSnapshot(status.snapshot); + ? fromEditorProjection(status.editor_projection, status.snapshot) + : fromCanonicalDocumentSnapshot(status.snapshot); if (currentBoard && currentFile && !currentIsDraft) { currentBoard = { ...currentBoard, updatedAt: Date.now() }; boardFiles.set(currentBoard.id, currentFile); @@ -1031,277 +1032,6 @@ async function listDocumentEntries(fileOps: DesktopFileOps, directory: string) { }); } -function loadedDocFromProjection(projection: EditorProjection, snapshot?: DocumentSnapshot): LoadedDoc { - const pages: Record = {}; - const layers: Record = {}; - const shapes: Record = {}; - const bindings: Record = {}; - - for (const pageId of projection.order.page_ids) { - const page = projection.pages[pageId]; - if (!page) continue; - pages[page.id] = { id: page.id, name: page.name, shapeIds: [...page.shape_ids], layerIds: [...page.layer_ids] }; - } - for (const layer of Object.values(projection.layers)) { - layers[layer.id] = { - id: layer.id, - pageId: layer.page_id, - name: layer.name, - shapeIds: [...layer.shape_ids], - visible: layer.visible, - locked: layer.locked, - opacity: layer.opacity - }; - } - for (const shape of Object.values(projection.shapes)) { - shapes[shape.id] = { - id: shape.id, - type: shape.type as EditorShapeRecord['type'], - pageId: shape.page_id, - x: shape.x, - y: shape.y, - rot: shape.rot, - editorTransform: shape.transform, - opacity: shape.opacity, - ...(shape.fill_opacity !== null ? { fillOpacity: shape.fill_opacity } : {}), - ...(shape.stroke_opacity !== null ? { strokeOpacity: shape.stroke_opacity } : {}), - ...(shape.group_id ? { groupId: shape.group_id } : {}), - layerId: shape.layer_id, - locked: shape.locked, - agentEditable: shape.agent_editable, - props: shape.props as EditorShapeRecord['props'] - } as EditorShapeRecord; - } - for (const binding of Object.values(projection.bindings)) { - bindings[binding.id] = { - id: binding.id, - type: binding.type as 'arrow-end', - fromShapeId: binding.from_shape_id, - toShapeId: binding.to_shape_id, - handle: binding.handle as 'start' | 'end', - anchor: - binding.anchor.kind === 'center' - ? { kind: 'center' } - : { kind: 'edge', nx: binding.anchor.x, ny: binding.anchor.y } - }; - } - const assets = Object.fromEntries( - Object.values(snapshot?.document.assets ?? {}).flatMap((asset) => - asset.source.kind === 'embedded' - ? [ - [ - asset.id, - { - id: asset.id, - name: asset.name, - mediaType: asset.media_type, - digest: asset.digest, - bytes: [...asset.source.bytes] - } - ] - ] - : [] - ) - ); - return { - pages, - layers, - shapes, - bindings, - ...(Object.keys(assets).length > 0 ? { assets } : {}), - order: { - pageIds: [...projection.order.page_ids], - shapeOrder: Object.fromEntries( - Object.entries(projection.order.shape_order).map(([pageId, shapeIds]) => [pageId, [...shapeIds]]) - ), - layers - } - }; -} - -function loadedDocFromSnapshot(snapshot: DocumentSnapshot): LoadedDoc { - const pages: Record = {}; - const layers: Record = {}; - const shapes: Record = {}; - const bindings: Record = {}; - const shapeOrder: Record = {}; - - for (const pageId of snapshot.document.page_ids) { - const page = snapshot.document.pages[pageId]; - if (!page) continue; - const flattened: string[] = []; - for (const layerId of page.layer_ids) { - const layer = snapshot.document.layers[layerId]; - if (!layer) continue; - const layerShapeIds: string[] = []; - for (const shapeId of layer.shape_ids) { - flattenShape( - snapshot, - page.id, - layer.id, - shapeId, - undefined, - identityEditorTransform(), - layerShapeIds, - shapes - ); - } - flattened.push(...layerShapeIds); - layers[layer.id] = { - id: layer.id, - pageId: page.id, - name: layer.name, - shapeIds: layerShapeIds, - visible: layer.visible, - locked: layer.locked, - opacity: layer.opacity - }; - } - pages[page.id] = { id: page.id, name: page.name, shapeIds: flattened, layerIds: [...page.layer_ids] }; - shapeOrder[page.id] = [...flattened]; - } - - for (const binding of Object.values(snapshot.document.bindings)) { - bindings[binding.id] = { - id: binding.id, - type: binding.kind as 'arrow-end', - fromShapeId: binding.source_shape_id, - toShapeId: binding.target_shape_id, - handle: binding.source_handle as 'start' | 'end', - anchor: - binding.anchor.kind === 'center' - ? { kind: 'center' } - : { kind: 'edge', nx: binding.anchor.x, ny: binding.anchor.y } - }; - } - - const assets = Object.fromEntries( - Object.values(snapshot.document.assets).flatMap((asset) => - asset.source.kind === 'embedded' - ? [ - [ - asset.id, - { - id: asset.id, - name: asset.name, - mediaType: asset.media_type, - digest: asset.digest, - bytes: [...asset.source.bytes] - } - ] - ] - : [] - ) - ); - return { - pages, - layers, - shapes, - bindings, - ...(Object.keys(assets).length > 0 ? { assets } : {}), - order: { pageIds: [...snapshot.document.page_ids], shapeOrder, layers } - }; -} - -function flattenShape( - snapshot: DocumentSnapshot, - pageId: string, - layerId: string, - shapeId: string, - groupId: string | undefined, - parentTransform: EditorTransform, - flattened: string[], - shapes: Record -) { - const shape = snapshot.document.shapes[shapeId]; - if (!shape) return; - const worldTransform = multiplyEditorTransforms(parentTransform, nativeEditorTransform(shape)); - flattened.push(shape.id); - shapes[shape.id] = { ...editorShapeFromSnapshot(shape, pageId, groupId, worldTransform), layerId }; - for (const childId of shape.child_ids) { - flattenShape( - snapshot, - pageId, - layerId, - childId, - shape.kind === 'container' ? shape.id : groupId, - worldTransform, - flattened, - shapes - ); - } -} - -function editorShapeFromSnapshot( - shape: ShapeRecord, - pageId: string, - groupId: string | undefined, - worldTransform = nativeEditorTransform(shape) -): EditorShapeRecord { - const properties = { ...(shape.properties as Record) }; - if ('width' in properties) { - properties.w = properties.width; - delete properties.width; - } - if ('height' in properties) { - properties.h = properties.height; - delete properties.height; - } - if (shape.kind === 'stroke' && shape.style.stroke_opacity !== null) { - const strokeStyle = properties.style; - properties.style = { - ...(typeof strokeStyle === 'object' && strokeStyle !== null && !Array.isArray(strokeStyle) - ? strokeStyle - : {}), - opacity: shape.style.stroke_opacity - }; - } - return { - id: shape.id, - type: shape.kind as EditorShapeRecord['type'], - pageId, - x: worldTransform.e, - y: worldTransform.f, - rot: Math.atan2(worldTransform.b, worldTransform.a), - editorTransform: worldTransform, - opacity: shape.style.opacity, - ...(shape.style.fill_opacity !== null ? { fillOpacity: shape.style.fill_opacity } : {}), - ...(shape.style.stroke_opacity !== null ? { strokeOpacity: shape.style.stroke_opacity } : {}), - ...(groupId ? { groupId } : {}), - locked: shape.metadata.locked, - agentEditable: shape.metadata.agent_editable, - props: properties as EditorShapeRecord['props'] - } as EditorShapeRecord; -} - -function identityEditorTransform(): EditorTransform { - return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }; -} - -function nativeEditorTransform(shape: ShapeRecord): EditorTransform { - const cos = Math.cos(shape.transform.rotation); - const sin = Math.sin(shape.transform.rotation); - return { - a: cos * shape.transform.scale_x, - b: sin * shape.transform.scale_x, - c: -sin * shape.transform.scale_y, - d: cos * shape.transform.scale_y, - e: shape.transform.translation.x, - f: shape.transform.translation.y - }; -} - -function multiplyEditorTransforms(parent: EditorTransform, child: EditorTransform): EditorTransform { - return { - a: parent.a * child.a + parent.c * child.b, - b: parent.b * child.a + parent.d * child.b, - c: parent.a * child.c + parent.c * child.d, - d: parent.b * child.c + parent.d * child.d, - e: parent.a * child.e + parent.c * child.f + parent.e, - f: parent.b * child.e + parent.d * child.f + parent.f - }; -} - function applyPatch(doc: LoadedDoc, patch: DocPatch): LoadedDoc { const next = structuredClone(doc); for (const id of patch.deletes?.pageIds ?? []) delete next.pages[id]; diff --git a/apps/web/src/content/docs/development/architecture.md b/apps/web/src/content/docs/development/architecture.md index f9ebea7..202f0b9 100644 --- a/apps/web/src/content/docs/development/architecture.md +++ b/apps/web/src/content/docs/development/architecture.md @@ -68,30 +68,49 @@ back through Tauri. See [Web editor](/docs/platforms/web/) and [Desktop editor](/docs/platforms/desktop/) for the application-specific behavior. -## Document model and editor projection - -The most important internal distinction is between the Rust document contract and the TypeScript -editor model. - -`inkfinite-core` owns the native document session. Its shapes have a registry `kind`, a parent -relation, a parent-relative transform, ordered container children, kind-specific properties, -semantic metadata, common style, and a record version. Semantic metadata includes optional names, -roles, descriptions, sources, links, tags, custom JSON fields, and provenance. Binding records can -also carry an optional relation type with source and target shape IDs. The query API filters those -records by type and direction. -Pages own ordered layers. Layers own ordered root shapes. -Containers can own nested shapes and optionally apply free, stack, or grid layout. - -`@inkfinite/core` is the interactive projection used by tools and Canvas rendering. It keeps the -shape data in the form the current editor expects: page and layer draw-order arrays, shape `x`/`y` -coordinates, rotation, optional grouping and layer IDs, and shape-specific properties. It also owns -editor geometry, actions, tools, stencils, interchange helpers, and browser-side persistence -utilities. - -These are not two canonical file formats. The desktop and browser persistence adapters translate -between the generated Rust contracts in `@inkfinite/bindings` and the editor projection returned by -Rust sessions. The frontend keeps the projected state for low-latency interaction. Rust remains -authoritative for the native session and `.inkfinite` file. +## Dependency direction and model ownership + +Rust owns the canonical document and all operations that can change it. `inkfinite-core` defines +`Document`, `ShapeRecord`, `PageRecord`, `LayerRecord`, and `BindingRecord`, validates them, applies +transactions, and owns Automerge state, native files, sessions, and headless rendering. + +The binding generator is the only path from those Rust contracts to TypeScript contract types: + +```text +inkfinite-core Rust model and services + │ + └── generate-bindings ──> @inkfinite/bindings + │ + └── generated snapshots, projections, patches, and protocols +``` + +`@inkfinite/core/src/editor-model.ts` owns the interactive model. Its public records are named +`EditorDocument`, `EditorShapeRecord`, `EditorPageRecord`, `EditorLayerRecord`, and +`EditorBindingRecord` so they cannot be mistaken for the generated Rust records. These values use +editor property names, world-space transforms, flat draw order, and the mutable shape union needed +by tools and Canvas rendering. They are not another serialized document contract. + +`@inkfinite/core/src/persistence/canonical.ts` is the TypeScript adapter boundary. It converts Rust +snapshots and generated Rust editor projections into the interactive model, and turns completed +editor changes into generated `EditorPatch` requests. Browser and desktop adapters call this module +instead of translating records themselves. The adapter preserves native property names and +hierarchy only at the Rust boundary; it does not write `.inkfinite` bytes. + +The package dependency direction is: + +```text +@inkfinite/bindings ──> @inkfinite/core/editor-model + canonical adapter + │ + └──> @inkfinite/editor ──> @inkfinite/ui ──> web and desktop apps + +Rust inkfinite-wasm ──> web app persistence adapter +Rust Tauri commands ──> desktop app persistence adapter +``` + +`@inkfinite/editor` owns normalized input, interaction state, commands, and Canvas rendering. The +UI package owns Svelte presentation and inspector controls. Applications own browser storage, +filesystem access, Tauri or WASM calls, and composition. No UI, editor runtime, or application code +owns canonical records or applies native transactions directly. For the record structure, see [Document model](/docs/concepts/document-model/). The [native path geometry guide](/docs/development/native-path-geometry/) documents the path representation used by @@ -204,4 +223,5 @@ Rust types are serialized with Serde, described with Schemars, and exported to T verifies that checked-in generated contracts still match Rust. This generated boundary is used for document, transaction, protocol, and browser WASM payloads. The -hand-written `@inkfinite/core` editor types remain a separate interaction-oriented representation. +hand-written `@inkfinite/core` `Editor*` types remain a separate interaction-oriented +representation; `persistence/canonical.ts` is the only adapter between the two. diff --git a/apps/web/src/dexie-repository.test.ts b/apps/web/src/dexie-repository.test.ts index ad7fc5a..807dd8f 100644 --- a/apps/web/src/dexie-repository.test.ts +++ b/apps/web/src/dexie-repository.test.ts @@ -4,10 +4,10 @@ import { createDexieDocRepo, createPersistenceSink } from '$lib/persistence/repo import { CreateShapeCommand, diffDoc, - Document as DocumentOps, - PageRecord, + EditorDocument as EditorDocumentOps, + EditorPageRecord, SetSelectionCommand, - ShapeRecord, + EditorShapeRecord, Store, type CanonicalDocumentState } from '@inkfinite/core'; @@ -62,8 +62,8 @@ describe('DocRepo (Dexie)', () => { const repo = createDexieDocRepo(database); const sourceId = await repo.createBoard('Source'); - const page = PageRecord.create('Canvas'); - const rect = ShapeRecord.createRect(page.id, 0, 0, { + const page = EditorPageRecord.create('Canvas'); + const rect = EditorShapeRecord.createRect(page.id, 0, 0, { w: 20, h: 20, fill: '#000', @@ -71,10 +71,10 @@ describe('DocRepo (Dexie)', () => { radius: 0 }); page.shapeIds.push(rect.id); - const doc = DocumentOps.create(); + const doc = EditorDocumentOps.create(); doc.pages[page.id] = page; doc.shapes[rect.id] = rect; - await repo.applyDocPatch(sourceId, diffDoc(DocumentOps.create(), doc)); + await repo.applyDocPatch(sourceId, diffDoc(EditorDocumentOps.create(), doc)); const duplicateId = await repo.duplicateBoard(sourceId); expect(duplicateId).not.toBe(sourceId); @@ -89,8 +89,8 @@ describe('DocRepo (Dexie)', () => { const repo = createDexieDocRepo(database); const boardId = await repo.createBoard('Round trip'); - const page = PageRecord.create('Canvas'); - const rect = ShapeRecord.createRect(page.id, 0, 0, { + const page = EditorPageRecord.create('Canvas'); + const rect = EditorShapeRecord.createRect(page.id, 0, 0, { w: 100, h: 80, fill: '#000', @@ -99,11 +99,11 @@ describe('DocRepo (Dexie)', () => { }); page.shapeIds.push(rect.id); - const doc = DocumentOps.create(); + const doc = EditorDocumentOps.create(); doc.pages[page.id] = page; doc.shapes[rect.id] = rect; - await repo.applyDocPatch(boardId, diffDoc(DocumentOps.create(), doc)); + await repo.applyDocPatch(boardId, diffDoc(EditorDocumentOps.create(), doc)); const loaded = await repo.loadDoc(boardId); expect(loaded.pages[page.id]).toEqual(page); @@ -116,8 +116,8 @@ describe('DocRepo (Dexie)', () => { const repo = createDexieDocRepo(database); const boardId = await repo.createBoard('Tx board'); - const page = PageRecord.create('Tx Page'); - const rect = ShapeRecord.createRect(page.id, 10, 10, { + const page = EditorPageRecord.create('Tx Page'); + const rect = EditorShapeRecord.createRect(page.id, 10, 10, { w: 50, h: 50, fill: '#ccc', @@ -126,12 +126,12 @@ describe('DocRepo (Dexie)', () => { }); page.shapeIds.push(rect.id); - const doc = DocumentOps.create(); + const doc = EditorDocumentOps.create(); doc.pages[page.id] = page; doc.shapes[rect.id] = rect; const transactionSpy = vi.spyOn(database, 'transaction'); - await repo.applyDocPatch(boardId, diffDoc(DocumentOps.create(), doc)); + await repo.applyDocPatch(boardId, diffDoc(EditorDocumentOps.create(), doc)); expect(transactionSpy).toHaveBeenCalledTimes(1); }); @@ -140,8 +140,8 @@ describe('DocRepo (Dexie)', () => { const repo = createDexieDocRepo(database); const boardId = await repo.createBoard('Delete board'); - const page = PageRecord.create('Delete Page'); - const rect = ShapeRecord.createRect(page.id, 0, 0, { + const page = EditorPageRecord.create('Delete Page'); + const rect = EditorShapeRecord.createRect(page.id, 0, 0, { w: 40, h: 40, fill: '#f00', @@ -150,11 +150,11 @@ describe('DocRepo (Dexie)', () => { }); page.shapeIds.push(rect.id); - const doc = DocumentOps.create(); + const doc = EditorDocumentOps.create(); doc.pages[page.id] = page; doc.shapes[rect.id] = rect; - await repo.applyDocPatch(boardId, diffDoc(DocumentOps.create(), doc)); + await repo.applyDocPatch(boardId, diffDoc(EditorDocumentOps.create(), doc)); await repo.deleteBoard(boardId); expect(await database.table('boards').toArray()).toHaveLength(0); @@ -214,8 +214,8 @@ describe('DocRepo (Dexie)', () => { const repo = createDexieDocRepo(db); const boardId = await repo.createBoard('Source'); - const page = PageRecord.create('Canvas'); - const rect = ShapeRecord.createRect(page.id, 5, 5, { + const page = EditorPageRecord.create('Canvas'); + const rect = EditorShapeRecord.createRect(page.id, 5, 5, { w: 20, h: 10, fill: '#123', @@ -224,11 +224,11 @@ describe('DocRepo (Dexie)', () => { }); page.shapeIds.push(rect.id); - const doc = DocumentOps.create(); + const doc = EditorDocumentOps.create(); doc.pages[page.id] = page; doc.shapes[rect.id] = rect; - await repo.applyDocPatch(boardId, diffDoc(DocumentOps.create(), doc)); + await repo.applyDocPatch(boardId, diffDoc(EditorDocumentOps.create(), doc)); const snapshot = await repo.exportBoard(boardId); const importedId = await repo.importBoard({ @@ -255,7 +255,7 @@ describe('History persistence sink', () => { it('doc command triggers exactly one persistence flush', async () => { const { store, sink, applySpy, pageId } = await createStoreWithSink(repo, boardId); - const rect = ShapeRecord.createRect(pageId, 0, 0, { + const rect = EditorShapeRecord.createRect(pageId, 0, 0, { w: 10, h: 10, fill: '#222', @@ -276,7 +276,7 @@ describe('History persistence sink', () => { it('undo and redo both persist document changes', async () => { const { store, sink, applySpy, pageId } = await createStoreWithSink(repo, boardId); - const rect = ShapeRecord.createRect(pageId, 0, 0, { + const rect = EditorShapeRecord.createRect(pageId, 0, 0, { w: 25, h: 25, fill: '#0f0', @@ -305,7 +305,7 @@ describe('History persistence sink', () => { it('ui-only commands never hit persistence', async () => { const { store, sink, applySpy, pageId } = await createStoreWithSink(repo, boardId); - const rect = ShapeRecord.createRect(pageId, 0, 0, { + const rect = EditorShapeRecord.createRect(pageId, 0, 0, { w: 15, h: 15, fill: '#aaa', @@ -327,7 +327,7 @@ describe('History persistence sink', () => { const { store, sink, applySpy, pageId } = await createStoreWithSink(repo, boardId); for (let i = 0; i < 10; i++) { - const rect = ShapeRecord.createRect(pageId, i * 5, 0, { + const rect = EditorShapeRecord.createRect(pageId, i * 5, 0, { w: 5, h: 5, fill: '#444', diff --git a/apps/web/src/lib/persistence/repository.ts b/apps/web/src/lib/persistence/repository.ts index 8cd6280..68a6ebb 100644 --- a/apps/web/src/lib/persistence/repository.ts +++ b/apps/web/src/lib/persistence/repository.ts @@ -1,15 +1,15 @@ import { - BindingRecord as BindingOps, + EditorBindingRecord as BindingOps, BoardStatsOps, createId, fromCanonicalDocumentSnapshot, fromEditorProjection, - LayerRecord as LayerOps, - PageRecord as PageOps, - ShapeRecord as ShapeOps + EditorLayerRecord as LayerOps, + EditorPageRecord as PageOps, + EditorShapeRecord as ShapeOps } from '@inkfinite/core'; import type { - BindingRecord, + EditorBindingRecord, BoardExport, BoardInspectorData, BoardMeta, @@ -17,27 +17,27 @@ import type { CanonicalDocumentState, DocOrder, DocPatch, - Document, + EditorDocument, LoadedDoc, - LayerRecord, + EditorLayerRecord, ImportedAsset, - PageRecord, + EditorPageRecord, PersistenceSink, PersistentDocRepo, SchemaInfo, - ShapeRecord, + EditorShapeRecord, Timestamp } from '@inkfinite/core'; import Dexie from 'dexie'; /** IndexedDB row for a page scoped to its board. */ -export type PageRow = PageRecord & { boardId: string; updatedAt: Timestamp }; +export type PageRow = EditorPageRecord & { boardId: string; updatedAt: Timestamp }; /** IndexedDB row for a shape scoped to its board. */ -export type ShapeRow = ShapeRecord & { boardId: string; updatedAt: Timestamp }; +export type ShapeRow = EditorShapeRecord & { boardId: string; updatedAt: Timestamp }; /** IndexedDB row for a binding scoped to its board. */ -export type BindingRow = BindingRecord & { boardId: string; updatedAt: Timestamp }; +export type BindingRow = EditorBindingRecord & { boardId: string; updatedAt: Timestamp }; /** Canonical Rust document bytes and its derived materialized cache. */ export type CanonicalRow = { @@ -234,17 +234,17 @@ export function createDexieDocRepo( meta().get(assetsKey(boardId)) ]); - const docPages: Record = {}; + const docPages: Record = {}; for (const row of pageRows) { docPages[row.id] = clonePageRow(row); } - const docShapes: Record = {}; + const docShapes: Record = {}; for (const row of shapeRows) { docShapes[row.id] = cloneShapeRow(row); } - const docBindings: Record = {}; + const docBindings: Record = {}; for (const row of bindingRows) { docBindings[row.id] = cloneBindingRow(row); } @@ -271,7 +271,7 @@ export function createDexieDocRepo( shapeOrder: (shapeOrderRow?.value as Record | undefined) ?? fallbackShapeOrder, - layers: layersRow?.value as Record | undefined + layers: layersRow?.value as Record | undefined }; } @@ -363,7 +363,7 @@ export function createDexieDocRepo( } const { pages, layers, shapes, bindings, assets, order } = await loadDoc(boardId); - const doc: Document = { + const doc: EditorDocument = { pages, ...(layers ? { layers } : {}), ...(assets ? { assets } : {}), @@ -523,22 +523,22 @@ export function createPersistenceSink( return { enqueueDocPatch, flush }; } -function clonePageRow(row: PageRow): PageRecord { +function clonePageRow(row: PageRow): EditorPageRecord { const { boardId: _boardId, updatedAt: _updatedAt, ...rest } = row; return PageOps.clone(rest); } -function cloneShapeRow(row: ShapeRow): ShapeRecord { +function cloneShapeRow(row: ShapeRow): EditorShapeRecord { const { boardId: _boardId, updatedAt: _updatedAt, ...rest } = row; - return ShapeOps.clone(rest as ShapeRecord); + return ShapeOps.clone(rest as EditorShapeRecord); } -function cloneBindingRow(row: BindingRow): BindingRecord { +function cloneBindingRow(row: BindingRow): EditorBindingRecord { const { boardId: _boardId, updatedAt: _updatedAt, ...rest } = row; return BindingOps.clone(rest); } -function deriveDocOrderFromDocument(doc: Document): DocOrder { +function deriveDocOrderFromDocument(doc: EditorDocument): DocOrder { return { pageIds: Object.keys(doc.pages), shapeOrder: shapeOrderFromPagesRecords(doc.pages), @@ -546,7 +546,9 @@ function deriveDocOrderFromDocument(doc: Document): DocOrder { }; } -function shapeOrderFromPagesRecords(pages: Record): Record { +function shapeOrderFromPagesRecords( + pages: Record +): Record { return Object.fromEntries(Object.values(pages).map((page) => [page.id, [...page.shapeIds]])); } diff --git a/apps/web/src/lib/tests/canvas.integration.test.ts b/apps/web/src/lib/tests/canvas.integration.test.ts index 650b43a..7078944 100644 --- a/apps/web/src/lib/tests/canvas.integration.test.ts +++ b/apps/web/src/lib/tests/canvas.integration.test.ts @@ -4,9 +4,9 @@ import { duplicateAndConnectSelection, EditorState, Modifiers, - PageRecord, + EditorPageRecord, PointerButtons, - ShapeRecord, + EditorShapeRecord, Store, SelectTool } from '@inkfinite/core'; @@ -18,15 +18,15 @@ const down = PointerButtons.create(true, false, false); const up = PointerButtons.create(); function overlappingState(): EditorState { - const page = PageRecord.create('Canvas quality', 'page:quality'); - const back = ShapeRecord.createRect( + const page = EditorPageRecord.create('Canvas quality', 'page:quality'); + const back = EditorShapeRecord.createRect( page.id, 0, 0, { w: 80, h: 50, fill: '#fff', stroke: '#111', radius: 4 }, 'shape:back' ); - const front = ShapeRecord.createRect( + const front = EditorShapeRecord.createRect( page.id, 0, 0, diff --git a/apps/web/src/lib/tests/document-engine.worker.test.ts b/apps/web/src/lib/tests/document-engine.worker.test.ts index 9918d93..f0d833b 100644 --- a/apps/web/src/lib/tests/document-engine.worker.test.ts +++ b/apps/web/src/lib/tests/document-engine.worker.test.ts @@ -1,4 +1,9 @@ import type { DocumentSnapshot, EditorReconciliationRequest } from '@inkfinite/wasm'; +import { + createEditorReconciliationRequest, + fromEditorProjection, + type EditorDocument +} from '@inkfinite/core'; import { afterEach, describe, expect, it } from 'vitest'; import { getSharedDocumentEngineWorker, @@ -69,6 +74,33 @@ describe('compiled document engine worker', () => { expect(state.editor_projection.shapes['shape:rect']).toBeDefined(); expect(state.can_undo).toBe(true); + const projected = fromEditorProjection(state.editor_projection, state.snapshot); + const beforeEdit: EditorDocument = { + pages: projected.pages, + layers: projected.layers, + shapes: projected.shapes, + bindings: projected.bindings, + ...(projected.assets ? { assets: projected.assets } : {}) + }; + const shape = beforeEdit.shapes['shape:rect']; + if (!shape) throw new Error('Expected the created rectangle in the editor projection'); + const afterEdit: EditorDocument = { + ...beforeEdit, + shapes: { ...beforeEdit.shapes, [shape.id]: { ...shape, x: 14, y: 15 } } + }; + const moveRequest = createEditorReconciliationRequest(beforeEdit, afterEdit, { + actor_id: 'browser', + origin: 'human', + transaction_id: 'transaction:move', + description: 'Move rectangle', + timestamp: 2 + }); + state = await worker.applyEditorPatches(moveRequest); + expect(state.snapshot.document.shapes['shape:rect']?.transform.translation).toEqual({ + x: 14, + y: 15 + }); + state = await worker.applyEditorPatches({ ...request, transaction_id: 'transaction:convert', diff --git a/apps/web/src/lib/tests/markdown-editor.test.ts b/apps/web/src/lib/tests/markdown-editor.test.ts index e0be0dc..05c9e5c 100644 --- a/apps/web/src/lib/tests/markdown-editor.test.ts +++ b/apps/web/src/lib/tests/markdown-editor.test.ts @@ -1,4 +1,4 @@ -import { EditorState, PageRecord, ShapeRecord, Store } from '@inkfinite/core'; +import { EditorState, EditorPageRecord, EditorShapeRecord, Store } from '@inkfinite/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { MarkdownEditorController } from '$editor/canvas/controllers/markdown-controller.svelte'; @@ -16,8 +16,8 @@ describe('MarkdownEditorController', () => { describe('start', () => { it('should start editing a markdown shape', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, @@ -47,8 +47,8 @@ describe('MarkdownEditorController', () => { }); it('should not start editing if shape is not markdown', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createRect( 'page1', 100, 200, @@ -82,8 +82,8 @@ describe('MarkdownEditorController', () => { }); it('should compute layout when editing', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, @@ -116,8 +116,8 @@ describe('MarkdownEditorController', () => { }); it('should handle auto-computed height', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, @@ -141,8 +141,8 @@ describe('MarkdownEditorController', () => { describe('handleInput', () => { it('should update current value on input', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, @@ -187,8 +187,8 @@ describe('MarkdownEditorController', () => { describe('handleKeyDown', () => { beforeEach(() => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, @@ -311,8 +311,8 @@ describe('MarkdownEditorController', () => { describe('commit', () => { it('should update markdown content and create history entry', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, @@ -348,8 +348,8 @@ describe('MarkdownEditorController', () => { }); it('should not update if value is unchanged', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, @@ -390,8 +390,8 @@ describe('MarkdownEditorController', () => { describe('cancel', () => { it('should stop editing without saving', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, @@ -428,8 +428,8 @@ describe('MarkdownEditorController', () => { describe('handleBlur', () => { it('should commit on blur', () => { - const page = PageRecord.create('Test Page', 'page1'); - const shape = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create('Test Page', 'page1'); + const shape = EditorShapeRecord.createMarkdown( 'page1', 100, 200, diff --git a/apps/web/src/lib/tests/runtime.integration.test.ts b/apps/web/src/lib/tests/runtime.integration.test.ts index 050f9a0..99b6323 100644 --- a/apps/web/src/lib/tests/runtime.integration.test.ts +++ b/apps/web/src/lib/tests/runtime.integration.test.ts @@ -2,8 +2,8 @@ import { Action, DirectSelectTool, EditorState, - PageRecord, - ShapeRecord, + EditorPageRecord, + EditorShapeRecord, SnapshotCommand, Store, type PathProps, @@ -58,7 +58,7 @@ class DragTool implements Tool { describe('editor runtime Rust commit boundary', () => { it('commits one direct-edit gesture as one undoable transaction', () => { - const page = PageRecord.create('Page', 'page:direct-runtime'); + const page = EditorPageRecord.create('Page', 'page:direct-runtime'); const geometry: PathProps = { subpaths: [ { @@ -74,7 +74,7 @@ describe('editor runtime Rust commit boundary', () => { fill_rule: 'nonzero', fill: '#fff' }; - const path = ShapeRecord.createPath(page.id, 0, 0, geometry, 'path:direct-runtime'); + const path = EditorShapeRecord.createPath(page.id, 0, 0, geometry, 'path:direct-runtime'); page.shapeIds = [path.id]; const store = new Store({ doc: { pages: { [page.id]: page }, shapes: { [path.id]: path }, bindings: {} }, diff --git a/apps/web/src/lib/tests/status.test.ts b/apps/web/src/lib/tests/status.test.ts index 08e1276..92d5029 100644 --- a/apps/web/src/lib/tests/status.test.ts +++ b/apps/web/src/lib/tests/status.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { Observable, Observer, Subscription } from 'dexie'; -import type { DocPatch, PageRecord, PersistentDocRepo } from '@inkfinite/core'; +import type { DocPatch, EditorPageRecord, PersistentDocRepo } from '@inkfinite/core'; import { describe, expect, it, vi } from 'vitest'; import type { InkfiniteDB } from '$lib/persistence/database'; import { createDexieSession, type DexieAdapterOptions } from '$lib/persistence/dexie'; @@ -97,7 +97,9 @@ function createStatusTracker(overrides?: { } function buildPatch(): DocPatch { - return { upserts: { pages: [{ id: 'page:1', name: 'Page 1', shapeIds: [] } as PageRecord] } }; + return { + upserts: { pages: [{ id: 'page:1', name: 'Page 1', shapeIds: [] } as EditorPageRecord] } + }; } describe('Dexie editor adapter', () => { diff --git a/apps/web/src/lib/tests/toolbar-fixtures.ts b/apps/web/src/lib/tests/toolbar-fixtures.ts index 73618a1..74a1302 100644 --- a/apps/web/src/lib/tests/toolbar-fixtures.ts +++ b/apps/web/src/lib/tests/toolbar-fixtures.ts @@ -1,10 +1,10 @@ -import { EditorState, ShapeRecord, Store } from '@inkfinite/core'; +import { EditorState, EditorShapeRecord, Store } from '@inkfinite/core'; export function createStoreWithRect(): Store { const store = new Store(); const base = EditorState.create(); const pageId = 'page:rect'; - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( pageId, 0, 0, @@ -27,7 +27,7 @@ export function createStoreWithLine(): Store { const store = new Store(); const base = EditorState.create(); const pageId = 'page:line'; - const line = ShapeRecord.createLine( + const line = EditorShapeRecord.createLine( pageId, 0, 0, diff --git a/packages/core/src/arrow-geometry.ts b/packages/core/src/arrow-geometry.ts index 10f3db6..0366805 100644 --- a/packages/core/src/arrow-geometry.ts +++ b/packages/core/src/arrow-geometry.ts @@ -1,4 +1,4 @@ -import type { ArrowLabel, ArrowStyle, PathGeometry } from './model'; +import type { ArrowLabel, ArrowStyle, PathGeometry } from './editor-model'; import type { Vec2 } from './math'; import { pathLength, pointAtPathDistance, trimPathGeometry } from './path-metrics'; diff --git a/packages/core/src/boolean-paths.ts b/packages/core/src/boolean-paths.ts index 55c3828..e6bf509 100644 --- a/packages/core/src/boolean-paths.ts +++ b/packages/core/src/boolean-paths.ts @@ -2,8 +2,8 @@ import polygonClipping, { type MultiPolygon, type Pair, type Ring } from 'polygo import { flattenPath, transformPathGeometry } from './path-metrics'; import { Mat3 } from './math'; import type { Mat3 as Mat3Type } from './math'; -import { ensureDocumentLayers, ShapeRecord } from './model'; -import type { Document, PathGeometry, PathShape, ShapeRecord as Shape } from './model'; +import { ensureDocumentLayers, EditorShapeRecord } from './editor-model'; +import type { EditorDocument, PathGeometry, PathShape, EditorShapeRecord as Shape } from './editor-model'; import type { EditorState } from './reactivity'; import { shapeTransform } from './geom'; @@ -51,7 +51,7 @@ export function applyBooleanPathOperation( .filter((subpath): subpath is NonNullable => subpath !== null); if (subpaths.length === 0) return null; - const first = ShapeRecord.clone(paths[0]!) as PathShape; + const first = EditorShapeRecord.clone(paths[0]!) as PathShape; first.props = { ...first.props, subpaths }; const removed = new Set(paths.slice(1).map((path) => path.id)); const shapes = Object.fromEntries( @@ -78,7 +78,7 @@ export function applyBooleanPathOperation( ([, binding]) => !removed.has(binding.fromShapeId) && !removed.has(binding.toShapeId) ) ); - const document: Document = { ...state.doc, pages, shapes, bindings, ...(layers ? { layers } : {}) }; + const document: EditorDocument = { ...state.doc, pages, shapes, bindings, ...(layers ? { layers } : {}) }; return { ...state, doc: ensureDocumentLayers(document), diff --git a/packages/core/src/cards.ts b/packages/core/src/cards.ts index 5aef4f8..02f07ee 100644 --- a/packages/core/src/cards.ts +++ b/packages/core/src/cards.ts @@ -1,11 +1,11 @@ import { createId, - ShapeRecord, + EditorShapeRecord, type ContainerShape, - type Document, + type EditorDocument, type ShapeMetadata, - type ShapeRecord as Shape -} from './model'; + type EditorShapeRecord as Shape +} from './editor-model'; import { creationStylePolicy, type CanvasAppearance } from './style-policy'; /** User-editable fields carried by a card container. */ @@ -59,19 +59,19 @@ export function createCardShapes( appearance: CanvasAppearance = 'light' ): Shape[] { const styles = creationStylePolicy(appearance); - const container = ShapeRecord.createContainer( + const container = EditorShapeRecord.createContainer( pageId, x, y, { w: 320, h: 220, ...styles.card.container }, id ); - const title = ShapeRecord.createText(pageId, x + 16, y + 16, { + const title = EditorShapeRecord.createText(pageId, x + 16, y + 16, { text: fields.title, ...styles.card.title, w: 288 }); - const body = ShapeRecord.createMarkdown(pageId, x + 16, y + 58, { + const body = EditorShapeRecord.createMarkdown(pageId, x + 16, y + 58, { md: fields.body, w: 288, h: 140, @@ -85,7 +85,7 @@ export function createCardShapes( } /** Returns card fields from a container, or `null` for another frame. */ -export function cardToContentObject(shape: Shape, document?: Document): ContentObject | null { +export function cardToContentObject(shape: Shape, document?: EditorDocument): ContentObject | null { if (shape.type !== 'container' || !shape.metadata) return null; const metadata = shape.metadata; if (metadata.title === null && metadata.body === null) return null; @@ -104,7 +104,7 @@ export function cardToContentObject(shape: Shape, document?: Document): ContentO } /** Returns the ordinary title and body records that make up a card. */ -export function cardChildren(shape: ContainerShape, document: Document): Shape[] { +export function cardChildren(shape: ContainerShape, document: EditorDocument): Shape[] { return Object.values(document.shapes) .filter((candidate) => candidate.groupId === shape.id) .sort( diff --git a/packages/core/src/model.ts b/packages/core/src/editor-model.ts similarity index 93% rename from packages/core/src/model.ts rename to packages/core/src/editor-model.ts index bc546f3..1e7973f 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/editor-model.ts @@ -1,8 +1,17 @@ +/** + * Interactive editor state owned by `@inkfinite/core`. + * + * These records deliberately use editor-facing names and coordinate fields. They + * are not the Rust-owned records from `@inkfinite/bindings/model`; conversion + * between the two representations lives in `persistence/canonical.ts`. + */ + import type { EditorTransform as GeneratedEditorTransform, PathCurveKind, PathTopologyOperation } from '@inkfinite/bindings/editor'; +import type { BuiltinShapeKind } from '@inkfinite/bindings'; import type { FilterEffect as NativeFilterEffect, FilterPrimitive as NativeFilterPrimitive, @@ -29,7 +38,7 @@ export function createId(prefix?: string): string { return prefix ? `${prefix}:${id}` : id; } -export type PageRecord = { +export type EditorPageRecord = { id: string; name: string; /** Flat draw-order projection. Layer order and each layer's shape order are authoritative. */ @@ -38,18 +47,18 @@ export type PageRecord = { layerIds?: string[]; }; -export const PageRecord = { +export const EditorPageRecord = { /** * Create a new page record */ - create(name: string, id?: string): PageRecord { + create(name: string, id?: string): EditorPageRecord { return { id: id ?? createId('page'), name, shapeIds: [], layerIds: [] }; }, /** * Clone a page record */ - clone(page: PageRecord): PageRecord { + clone(page: EditorPageRecord): EditorPageRecord { return { id: page.id, name: page.name, @@ -60,7 +69,7 @@ export const PageRecord = { }; /** Ordered visual layer owned by one page. */ -export type LayerRecord = { +export type EditorLayerRecord = { id: string; pageId: string; name: string; @@ -70,14 +79,14 @@ export type LayerRecord = { opacity: number; }; -export const LayerRecord = { +export const EditorLayerRecord = { /** Creates an empty, visible layer. */ - create(pageId: string, name = 'Layer', id?: string): LayerRecord { + create(pageId: string, name = 'Layer', id?: string): EditorLayerRecord { return { id: id ?? createId('layer'), pageId, name, shapeIds: [], visible: true, locked: false, opacity: 1 }; }, /** Clones a layer without sharing its child-order array. */ - clone(layer: LayerRecord): LayerRecord { + clone(layer: EditorLayerRecord): EditorLayerRecord { return { ...layer, shapeIds: [...layer.shapeIds] }; } }; @@ -345,18 +354,8 @@ export type ShapeMetadata = { }; }; -export type ShapeType = - | 'rect' - | 'ellipse' - | 'line' - | 'arrow' - | 'text' - | 'stroke' - | 'path' - | 'markdown' - | 'image' - | 'reference' - | 'container'; +/** Built-in shape kinds accepted by the interactive editor model. */ +export type ShapeType = BuiltinShapeKind; /** Full projected transform shared with the Rust editor projection. */ export type EditorTransform = GeneratedEditorTransform; @@ -403,7 +402,7 @@ export type PathShape = BaseShape & { type: 'path'; props: PathProps }; export type MarkdownShape = BaseShape & { type: 'markdown'; props: MarkdownProps }; export type ContainerShape = BaseShape & { type: 'container'; props: ContainerProps }; -export type ShapeRecord = +export type EditorShapeRecord = | RectShape | EllipseShape | LineShape @@ -416,7 +415,7 @@ export type ShapeRecord = | MarkdownShape | ContainerShape; -export const ShapeRecord = { +export const EditorShapeRecord = { /** * Create a rectangle shape */ @@ -489,7 +488,7 @@ export const ShapeRecord = { /** * Clone a shape record */ - clone(shape: ShapeRecord): ShapeRecord { + clone(shape: EditorShapeRecord): EditorShapeRecord { const metadata = shape.metadata ? { ...shape.metadata, @@ -604,7 +603,7 @@ export const ShapeRecord = { ...shape, ...(metadata ? { metadata } : {}), props: { ...shape.props, ...cloneShapeEffects(shape.props) } - } as ShapeRecord; + } as EditorShapeRecord; } }; @@ -618,7 +617,7 @@ export type BindingHandle = 'start' | 'end'; */ export type BindingAnchor = { kind: 'center' } | { kind: 'edge'; nx: number; ny: number }; -export type BindingRecord = { +export type EditorBindingRecord = { id: string; type: BindingType; fromShapeId: string; @@ -629,7 +628,7 @@ export type BindingRecord = { relationType?: string; }; -export const BindingRecord = { +export const EditorBindingRecord = { /** * Create a binding record for arrow endpoints */ @@ -640,7 +639,7 @@ export const BindingRecord = { anchor?: BindingAnchor, id?: string, relationType?: string - ): BindingRecord { + ): EditorBindingRecord { if (!anchor) { anchor = { kind: 'center' }; } @@ -656,7 +655,7 @@ export const BindingRecord = { }, /** Creates a typed relationship that does not participate in arrow routing. */ - createRelation(fromShapeId: string, toShapeId: string, relationType: string, id?: string): BindingRecord { + createRelation(fromShapeId: string, toShapeId: string, relationType: string, id?: string): EditorBindingRecord { return { id: id ?? createId('binding'), type: 'relation', @@ -671,7 +670,7 @@ export const BindingRecord = { /** * Clone a binding record */ - clone(binding: BindingRecord): BindingRecord { + clone(binding: EditorBindingRecord): EditorBindingRecord { return { ...binding, anchor: binding.anchor.kind === 'edge' ? { ...binding.anchor } : { kind: 'center' } }; } }; @@ -679,34 +678,42 @@ export const BindingRecord = { /** A retained binary asset imported from an external document. */ export type ImportedAsset = { id: string; name: string; mediaType: string; digest: string; bytes: number[] }; -export type Document = { - pages: Record; +/** + * Mutable document state consumed by editor tools and renderers. + * + * This is an interactive projection, not the serialized Rust document. Use the + * canonical persistence adapter when crossing into generated native contracts. + */ +export type EditorDocument = { + pages: Record; /** Layers indexed by stable ID. */ - layers?: Record; + layers?: Record; /** Binary assets retained by interchange imports. */ assets?: Record; - shapes: Record; - bindings: Record; + shapes: Record; + bindings: Record; }; -export const Document = { +export const EditorDocument = { /** * Create an empty document */ - create(): Document { + create(): EditorDocument { return { pages: {}, layers: {}, shapes: {}, bindings: {} }; }, /** * Clone a document */ - clone(document: Document): Document { + clone(document: EditorDocument): EditorDocument { return { - pages: Object.fromEntries(Object.entries(document.pages).map(([id, page]) => [id, PageRecord.clone(page)])), + pages: Object.fromEntries( + Object.entries(document.pages).map(([id, page]) => [id, EditorPageRecord.clone(page)]) + ), ...(document.layers ? { layers: Object.fromEntries( - Object.entries(document.layers).map(([id, layer]) => [id, LayerRecord.clone(layer)]) + Object.entries(document.layers).map(([id, layer]) => [id, EditorLayerRecord.clone(layer)]) ) } : {}), @@ -721,10 +728,10 @@ export const Document = { } : {}), shapes: Object.fromEntries( - Object.entries(document.shapes).map(([id, shape]) => [id, ShapeRecord.clone(shape)]) + Object.entries(document.shapes).map(([id, shape]) => [id, EditorShapeRecord.clone(shape)]) ), bindings: Object.fromEntries( - Object.entries(document.bindings).map(([id, binding]) => [id, BindingRecord.clone(binding)]) + Object.entries(document.bindings).map(([id, binding]) => [id, EditorBindingRecord.clone(binding)]) ) }; } @@ -737,13 +744,15 @@ export const Document = { * preserved exactly in a stable default layer, while layered documents retain * their layer and child order. */ -export function ensureDocumentLayers(document: Document): Document { - const pages = Object.fromEntries(Object.entries(document.pages).map(([id, page]) => [id, PageRecord.clone(page)])); +export function ensureDocumentLayers(document: EditorDocument): EditorDocument { + const pages = Object.fromEntries( + Object.entries(document.pages).map(([id, page]) => [id, EditorPageRecord.clone(page)]) + ); const layers = Object.fromEntries( - Object.entries(document.layers ?? {}).map(([id, layer]) => [id, LayerRecord.clone(layer)]) + Object.entries(document.layers ?? {}).map(([id, layer]) => [id, EditorLayerRecord.clone(layer)]) ); const shapes = Object.fromEntries( - Object.entries(document.shapes).map(([id, shape]) => [id, ShapeRecord.clone(shape)]) + Object.entries(document.shapes).map(([id, shape]) => [id, EditorShapeRecord.clone(shape)]) ); for (const page of Object.values(pages)) { @@ -805,7 +814,7 @@ export type ValidationResult = { ok: true } | { ok: false; errors: string[] }; * @param doc - The document to validate * @returns ValidationResult with ok status and any errors found */ -export function validateDoc(document: Document): ValidationResult { +export function validateDoc(document: EditorDocument): ValidationResult { const errors: string[] = []; if (Object.keys(document.pages).length === 0 && Object.keys(document.shapes).length > 0) { diff --git a/packages/core/src/export.ts b/packages/core/src/export.ts index 9a63d2b..3b87748 100644 --- a/packages/core/src/export.ts +++ b/packages/core/src/export.ts @@ -12,9 +12,9 @@ import type { PathGeometry, PathShape, RectShape, - ShapeRecord, + EditorShapeRecord, TextShape -} from './model'; +} from './editor-model'; import type { EditorState } from './reactivity'; import { getSelectedShapes, getShapesOnCurrentPage } from './reactivity'; @@ -71,7 +71,7 @@ export async function exportViewportToPNG(canvas: HTMLCanvasElement): Promise void + renderFunction: (context: CanvasRenderingContext2D, shapes: EditorShapeRecord[], bounds: Box2) => void ): Promise { const shapes = getSelectedShapes(state); if (shapes.length === 0) { @@ -171,7 +171,7 @@ export function exportToSVG(state: EditorState, options: ExportOptions = {}): st /** * Convert a single shape to SVG markup. */ -function shapeToSVG(shape: ShapeRecord, state: EditorState, definitions: string[]): string | null { +function shapeToSVG(shape: EditorShapeRecord, state: EditorState, definitions: string[]): string | null { const transform = `translate(${shape.x},${shape.y})${ shape.rot === 0 ? '' : ` rotate(${(shape.rot * 180) / Math.PI})` }`; @@ -243,7 +243,7 @@ function shapeToSVG(shape: ShapeRecord, state: EditorState, definitions: string[ } } -function withSvgEffects(shape: ShapeRecord, content: string, transform: string, definitions: string[]): string { +function withSvgEffects(shape: EditorShapeRecord, content: string, transform: string, definitions: string[]): string { const props = shape.props; const safeId = shape.id.replace(/[^a-zA-Z0-9_-]/g, '-'); const attributes: string[] = []; @@ -273,7 +273,7 @@ function withSvgEffects(shape: ShapeRecord, content: string, transform: string, } function filterPrimitiveToSvg( - primitive: NonNullable['primitives'][number], + primitive: NonNullable['primitives'][number], index: number, filterId: string ): string { @@ -418,7 +418,7 @@ function pathGeometryToSVG(geometry: PathGeometry): string { .join(' '); } -function wrapSemanticMetadata(shape: ShapeRecord, content: string): string { +function wrapSemanticMetadata(shape: EditorShapeRecord, content: string): string { const metadata = shape.metadata; if (!metadata) return content; const attributes = [ @@ -472,7 +472,7 @@ function textToSVG(shape: TextShape, transform: string, state: EditorState, defi return `${escapeXML(text)}`; } -function shapeTransformToSvg(shape: ShapeRecord): string { +function shapeTransformToSvg(shape: EditorShapeRecord): string { return `translate(${svgNumber(shape.x)},${svgNumber(shape.y)})${shape.rot === 0 ? '' : ` rotate(${svgNumber((shape.rot * 180) / Math.PI)})`}`; } @@ -499,7 +499,7 @@ function reversePathGeometry(geometry: PathGeometry): PathGeometry { } function strokeToSVG( - shape: Extract, + shape: Extract, transform: string, definitions: string[] ): string { @@ -596,7 +596,7 @@ function escapeXML(string_: string): string { .replaceAll("'", '''); } -function exportBounds(state: EditorState, shape: ShapeRecord): Box2 { +function exportBounds(state: EditorState, shape: EditorShapeRecord): Box2 { if (shape.type !== 'arrow') return shapeBoundsForState(state, shape); const geometry = arrowGeometryForShape(state, shape); if (!geometry) return shapeBoundsForState(state, shape); @@ -625,7 +625,7 @@ function exportBounds(state: EditorState, shape: ShapeRecord): Box2 { return Box2Ops.fromPoints(points.map((point) => localToWorld(shape, point))); } -function getExportSelection(state: EditorState): ShapeRecord[] { +function getExportSelection(state: EditorState): EditorShapeRecord[] { const selected = new Set(state.ui.selectionIds); for (const shape of getShapesOnCurrentPage(state)) { if (shape.type === 'text' && shape.props.textPath && selected.has(shape.id)) @@ -636,7 +636,7 @@ function getExportSelection(state: EditorState): ShapeRecord[] { ); } -function hasSelectedAncestor(shape: ShapeRecord, selected: ReadonlySet, state: EditorState): boolean { +function hasSelectedAncestor(shape: EditorShapeRecord, selected: ReadonlySet, state: EditorState): boolean { let parentId = shape.groupId; while (parentId) { if (selected.has(parentId)) return true; diff --git a/packages/core/src/geom.ts b/packages/core/src/geom.ts index 2130ccf..1296af9 100644 --- a/packages/core/src/geom.ts +++ b/packages/core/src/geom.ts @@ -4,7 +4,7 @@ import { Box2 as Box2Ops, Mat3, Vec2 as Vec2Ops } from './math'; import { arrowHeadGeometry, arrowLabelPlacement } from './arrow-geometry'; import type { ArrowShape, - BindingRecord, + EditorBindingRecord, BrushConfig, EllipseShape, LineShape, @@ -17,12 +17,12 @@ import type { PathShape, RectShape, ResolvedArrowGeometry, - ShapeRecord, + EditorShapeRecord, StrokePoint, StrokeShape, StrokeWidthPoint, TextShape -} from './model'; +} from './editor-model'; import type { EditorState } from './reactivity'; import { getInteractiveShapesOnCurrentPage, getShapesOnCurrentPage } from './reactivity'; import { @@ -36,7 +36,7 @@ import { const strokeOutlineCache = new WeakMap(); /** Return the affine matrix that maps a shape's local geometry to world space. */ -export function shapeTransform(shape: ShapeRecord): Mat3 { +export function shapeTransform(shape: EditorShapeRecord): Mat3 { if (shape.editorTransform) { return [ shape.editorTransform.a, @@ -54,18 +54,18 @@ export function shapeTransform(shape: ShapeRecord): Mat3 { } /** Transform one local point through a shape's complete world transform. */ -export function localToWorld(shape: ShapeRecord, point: Vec2): Vec2 { +export function localToWorld(shape: EditorShapeRecord, point: Vec2): Vec2 { return Mat3.transformPoint(shapeTransform(shape), point); } /** Transform one world point into shape-local coordinates. */ -export function worldToLocal(point: Vec2, shape: ShapeRecord): Vec2 { +export function worldToLocal(point: Vec2, shape: EditorShapeRecord): Vec2 { const inverse = Mat3.invert(shapeTransform(shape)); return inverse ? Mat3.transformPoint(inverse, point) : { x: point.x - shape.x, y: point.y - shape.y }; } /** Returns local geometry bounds without applying the shape transform. */ -export function localShapeBounds(shape: ShapeRecord): Box2 { +export function localShapeBounds(shape: EditorShapeRecord): Box2 { switch (shape.type) { case 'rect': case 'ellipse': @@ -130,7 +130,7 @@ export function localShapeBounds(shape: ShapeRecord): Box2 { } } -function transformLocalBounds(shape: ShapeRecord, bounds: Box2): Box2 { +function transformLocalBounds(shape: EditorShapeRecord, bounds: Box2): Box2 { const corners = [ bounds.min, { x: bounds.max.x, y: bounds.min.y }, @@ -141,7 +141,7 @@ function transformLocalBounds(shape: ShapeRecord, bounds: Box2): Box2 { } /** Get the axis-aligned bounding box of a shape in world coordinates. */ -export function shapeBounds(shape: ShapeRecord): Box2 { +export function shapeBounds(shape: EditorShapeRecord): Box2 { return transformLocalBounds(shape, localShapeBounds(shape)); } @@ -168,7 +168,7 @@ export function textPathLayoutForShape( } /** Return the world-space bounds of a shape, resolving attached text through its path. */ -export function shapeBoundsForState(state: EditorState, shape: ShapeRecord): Box2 { +export function shapeBoundsForState(state: EditorState, shape: EditorShapeRecord): Box2 { if (shape.type !== 'text') return shapeBounds(shape); const attached = textPathLayoutForShape(state, shape); if (!attached) return shapeBounds(shape); @@ -797,7 +797,7 @@ export function hitTestPoint(state: EditorState, worldPoint: Vec2, tolerance = 5 return hitTestPoints(state, worldPoint, tolerance)[0] ?? null; } -function hitTestShape(state: EditorState, shape: ShapeRecord, worldPoint: Vec2, tolerance: number): boolean { +function hitTestShape(state: EditorState, shape: EditorShapeRecord, worldPoint: Vec2, tolerance: number): boolean { const localPoint = worldToLocal(worldPoint, shape); if (shape.props.clipPath && !pointInPath(localPoint, shape.props.clipPath)) return false; if (shape.props.maskEffect && !pointInPath(localPoint, shape.props.maskEffect.geometry)) return false; @@ -866,7 +866,7 @@ function hitTestShape(state: EditorState, shape: ShapeRecord, worldPoint: Vec2, * @param shape - The shape to get center for * @returns Center point in world coordinates */ -export function shapeCenter(shape: ShapeRecord): Vec2 { +export function shapeCenter(shape: EditorShapeRecord): Vec2 { const bounds = shapeBounds(shape); return { x: (bounds.min.x + bounds.max.x) / 2, y: (bounds.min.y + bounds.max.y) / 2 }; } @@ -880,7 +880,7 @@ export function shapeCenter(shape: ShapeRecord): Vec2 { * @param offset - Optional offset distance to push the anchor point away from the shape (default: 0) * @returns World coordinates of the anchor point */ -export function computeEdgeAnchor(shape: ShapeRecord, nx: number, ny: number, offset = 0): Vec2 { +export function computeEdgeAnchor(shape: EditorShapeRecord, nx: number, ny: number, offset = 0): Vec2 { const bounds = shapeBounds(shape); const centerX = (bounds.min.x + bounds.max.x) / 2; const centerY = (bounds.min.y + bounds.max.y) / 2; @@ -914,7 +914,7 @@ export function computeEdgeAnchor(shape: ShapeRecord, nx: number, ny: number, of * @param shape - Target shape to anchor to * @returns Normalized coordinates {nx, ny} in [-1, 1] */ -export function computeNormalizedAnchor(point: Vec2, shape: ShapeRecord): { nx: number; ny: number } { +export function computeNormalizedAnchor(point: Vec2, shape: EditorShapeRecord): { nx: number; ny: number } { const bounds = shapeBounds(shape); const centerX = (bounds.min.x + bounds.max.x) / 2; const centerY = (bounds.min.y + bounds.max.y) / 2; @@ -928,7 +928,7 @@ export function computeNormalizedAnchor(point: Vec2, shape: ShapeRecord): { nx: } /** Pre-indexed bindings keyed by their source shape ID for repeated geometry work. */ -export type BindingIndex = ReadonlyMap; +export type BindingIndex = ReadonlyMap; /** * Resolve arrow endpoints considering bindings diff --git a/packages/core/src/history.ts b/packages/core/src/history.ts index 542ee6f..a514d1b 100644 --- a/packages/core/src/history.ts +++ b/packages/core/src/history.ts @@ -1,5 +1,5 @@ import type { Camera } from "./camera"; -import type { PathTopologyEdit, ShapeRecord } from "./model"; +import type { PathTopologyEdit, EditorShapeRecord } from "./editor-model"; import type { EditorState } from "./reactivity"; export type CommandKind = "doc" | "ui" | "camera"; @@ -85,7 +85,7 @@ export class CreateShapeCommand implements Command { readonly name: string; readonly kind = "doc" as const; - constructor(private readonly shape: ShapeRecord, private readonly pageId: string) { + constructor(private readonly shape: EditorShapeRecord, private readonly pageId: string) { this.name = `Create ${shape.type}`; } @@ -136,8 +136,8 @@ export class UpdateShapeCommand implements Command { constructor( private readonly shapeId: string, - private readonly before: ShapeRecord, - private readonly after: ShapeRecord, + private readonly before: EditorShapeRecord, + private readonly after: EditorShapeRecord, ) { this.name = `Update ${after.type}`; } @@ -158,7 +158,7 @@ export class DeleteShapesCommand implements Command { readonly name: string; readonly kind = "doc" as const; - constructor(private readonly shapes: ShapeRecord[], private readonly pageId: string) { + constructor(private readonly shapes: EditorShapeRecord[], private readonly pageId: string) { this.name = shapes.length === 1 ? `Delete ${shapes[0].type}` : `Delete ${shapes.length} shapes`; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1ff7ddb..e91ea90 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,7 +11,7 @@ export * from './interchange'; export * from './layers'; export * from './layout'; export * from './math'; -export * from './model'; +export * from './editor-model'; export type { GradientSpread, GradientStop, diff --git a/packages/core/src/interchange/diagram.ts b/packages/core/src/interchange/diagram.ts index c860698..9e1565c 100644 --- a/packages/core/src/interchange/diagram.ts +++ b/packages/core/src/interchange/diagram.ts @@ -1,7 +1,7 @@ import { graphLayout } from '../layout'; import { shapeBounds } from '../geom'; import { EditorState } from '../reactivity'; -import { BindingRecord, ShapeRecord, type ArrowShape, type MarkdownProps, type ShapeMetadata } from '../model'; +import { EditorBindingRecord, EditorShapeRecord, type ArrowShape, type MarkdownProps, type ShapeMetadata } from '../editor-model'; import type { BoardExport } from '../persistence/document'; import type { InterchangeImport } from '../interchange'; import { addShape, blankSnapshot, inkId, WarningCollector } from './shared'; @@ -664,7 +664,7 @@ function materializeDiagram(parsed: ParsedDiagram, fileName: string): Interchang for (const group of groupsInOrder) { const id = inkId(`${parsed.format}-group`, group.key); groupIds.set(group.key, id); - const shape = ShapeRecord.createContainer( + const shape = EditorShapeRecord.createContainer( pageId, 0, 0, @@ -697,7 +697,7 @@ function materializeDiagram(parsed: ParsedDiagram, fileName: string): Interchang bg: node.style.fill ?? DEFAULT_NODE_FILL, border: node.style.stroke ?? DEFAULT_NODE_STROKE }; - const shape = ShapeRecord.createMarkdown(pageId, 0, 0, props, id); + const shape = EditorShapeRecord.createMarkdown(pageId, 0, 0, props, id); shape.layerId = layerId; const groupId = node.groupPath.length > 0 ? groupIds.get(node.groupPath.join('.')) : undefined; if (groupId) shape.groupId = groupId; @@ -722,9 +722,9 @@ function materializeDiagram(parsed: ParsedDiagram, fileName: string): Interchang `${parsed.format}-edge`, `${Object.keys(snapshot.doc.shapes).length + edgesForDocument(snapshot.doc).length}` ); - const startBinding = BindingRecord.create(edgeId, sourceId, 'start', { kind: 'center' }, `${edgeId}:start`); - const endBinding = BindingRecord.create(edgeId, targetId, 'end', { kind: 'center' }, `${edgeId}:end`); - const arrow = ShapeRecord.createArrow( + const startBinding = EditorBindingRecord.create(edgeId, sourceId, 'start', { kind: 'center' }, `${edgeId}:start`); + const endBinding = EditorBindingRecord.create(edgeId, targetId, 'end', { kind: 'center' }, `${edgeId}:end`); + const arrow = EditorShapeRecord.createArrow( pageId, 0, 0, diff --git a/packages/core/src/interchange/excalidraw.ts b/packages/core/src/interchange/excalidraw.ts index 88e985b..b68345b 100644 --- a/packages/core/src/interchange/excalidraw.ts +++ b/packages/core/src/interchange/excalidraw.ts @@ -1,7 +1,7 @@ import { shapeBounds } from '../geom'; import { paintColor } from '../paint'; import { clamp, Vec2 } from '../math'; -import { BindingRecord, ensureDocumentLayers, ShapeRecord, type ArrowShape, type ShapeRecord as Shape } from '../model'; +import { EditorBindingRecord, ensureDocumentLayers, EditorShapeRecord, type ArrowShape, type EditorShapeRecord as Shape } from '../editor-model'; import type { BoardExport } from '../persistence/document'; import type { InterchangeExport, InterchangeImport } from '../interchange'; import { @@ -67,7 +67,7 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang switch (type) { case 'rectangle': - shape = ShapeRecord.createRect( + shape = EditorShapeRecord.createRect( pageId, origin.x, origin.y, @@ -82,7 +82,7 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang ); break; case 'ellipse': - shape = ShapeRecord.createEllipse( + shape = EditorShapeRecord.createEllipse( pageId, origin.x, origin.y, @@ -92,7 +92,7 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang break; case 'line': { const points = excalidrawPoints(element.points, `element ${id}.points`); - shape = ShapeRecord.createLine( + shape = EditorShapeRecord.createLine( pageId, origin.x, origin.y, @@ -113,7 +113,7 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang } case 'arrow': { const points = excalidrawPoints(element.points, `element ${id}.points`); - shape = ShapeRecord.createArrow( + shape = EditorShapeRecord.createArrow( pageId, origin.x, origin.y, @@ -161,11 +161,11 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang digest: assetId, bytes: data.bytes }; - shape = ShapeRecord.createImage(pageId, origin.x, origin.y, { w: width, h: height, assetId }, shapeId); + shape = EditorShapeRecord.createImage(pageId, origin.x, origin.y, { w: width, h: height, assetId }, shapeId); break; } case 'text': - shape = ShapeRecord.createText( + shape = EditorShapeRecord.createText( pageId, origin.x, origin.y, @@ -185,7 +185,7 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang case 'freedraw': { const points = excalidrawPointTuples(element.points, `element ${id}.points`); const pressures = Array.isArray(element.pressures) ? element.pressures : []; - shape = ShapeRecord.createStroke( + shape = EditorShapeRecord.createStroke( pageId, origin.x, origin.y, @@ -212,7 +212,7 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang } case 'embeddable': { const url = typeof element.link === 'string' ? element.link : ''; - shape = ShapeRecord.createMarkdown( + shape = EditorShapeRecord.createMarkdown( pageId, origin.x, origin.y, @@ -233,7 +233,7 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang } case 'frame': case 'magicframe': - shape = ShapeRecord.createContainer( + shape = EditorShapeRecord.createContainer( pageId, origin.x, origin.y, @@ -283,7 +283,7 @@ export function importExcalidraw(root: JsonObject, fileName: string): Interchang warnings.add('excalidraw-dangling-binding', 'Bindings to omitted or missing elements were removed.'); continue; } - const binding = BindingRecord.create( + const binding = EditorBindingRecord.create( shape.id, target.id, handle, diff --git a/packages/core/src/interchange/json-canvas.ts b/packages/core/src/interchange/json-canvas.ts index 952b39d..73560f4 100644 --- a/packages/core/src/interchange/json-canvas.ts +++ b/packages/core/src/interchange/json-canvas.ts @@ -1,12 +1,12 @@ import { shapeBounds } from '../geom'; import { paintColor } from '../paint'; import { - BindingRecord, + EditorBindingRecord, ensureDocumentLayers, - ShapeRecord, + EditorShapeRecord, type BindingAnchor, - type ShapeRecord as Shape -} from '../model'; + type EditorShapeRecord as Shape +} from '../editor-model'; import type { BoardExport } from '../persistence/document'; import type { InterchangeExport, InterchangeImport } from '../interchange'; import { @@ -99,7 +99,7 @@ export function importJsonCanvas(root: JsonObject, fileName: string): Interchang switch (type) { case 'text': { if (typeof node.text !== 'string') throw new Error(`nodes[${index}].text must be a string.`); - shape = ShapeRecord.createMarkdown( + shape = EditorShapeRecord.createMarkdown( pageId, x, y, @@ -113,7 +113,7 @@ export function importJsonCanvas(root: JsonObject, fileName: string): Interchang const subpath = typeof node.subpath === 'string' ? node.subpath : ''; if (subpath && !subpath.startsWith('#')) warnings.add('json-canvas-file-subpath', 'File subpaths must start with # and were ignored.'); - shape = ShapeRecord.createReference( + shape = EditorShapeRecord.createReference( pageId, x, y, @@ -139,7 +139,7 @@ export function importJsonCanvas(root: JsonObject, fileName: string): Interchang 'json-canvas-link-card', 'Non-http JSON Canvas links were imported as Markdown cards.' ); - shape = ShapeRecord.createMarkdown( + shape = EditorShapeRecord.createMarkdown( pageId, x, y, @@ -147,7 +147,7 @@ export function importJsonCanvas(root: JsonObject, fileName: string): Interchang inkId('json-canvas', id) ); } else { - shape = ShapeRecord.createReference( + shape = EditorShapeRecord.createReference( pageId, x, y, @@ -167,7 +167,7 @@ export function importJsonCanvas(root: JsonObject, fileName: string): Interchang } for (const group of groupNodes) { - const frame = ShapeRecord.createContainer( + const frame = EditorShapeRecord.createContainer( pageId, group.x, group.y, @@ -242,21 +242,21 @@ export function importJsonCanvas(root: JsonObject, fileName: string): Interchang } const start = sidePoint(from, optionalSide(edge.fromSide)); const end = sidePoint(to, optionalSide(edge.toSide)); - const startBinding = BindingRecord.create( + const startBinding = EditorBindingRecord.create( inkId('json-canvas-edge', id), from.id, 'start', anchorForSide(optionalSide(edge.fromSide)), inkId('json-canvas-binding-start', id) ); - const endBinding = BindingRecord.create( + const endBinding = EditorBindingRecord.create( inkId('json-canvas-edge', id), to.id, 'end', anchorForSide(optionalSide(edge.toSide)), inkId('json-canvas-binding-end', id) ); - const arrow = ShapeRecord.createArrow( + const arrow = EditorShapeRecord.createArrow( pageId, start.x, start.y, diff --git a/packages/core/src/interchange/shared.ts b/packages/core/src/interchange/shared.ts index c74e9c7..1505c33 100644 --- a/packages/core/src/interchange/shared.ts +++ b/packages/core/src/interchange/shared.ts @@ -1,4 +1,4 @@ -import { createId, LayerRecord, PageRecord, type ArrowShape, type Document, type ShapeRecord as Shape } from '../model'; +import { createId, EditorLayerRecord, EditorPageRecord, type ArrowShape, type EditorDocument, type EditorShapeRecord as Shape } from '../editor-model'; import type { BoardExport } from '../persistence/document'; import type { InterchangeWarning } from '../interchange'; @@ -8,11 +8,11 @@ export type JsonObject = Record; /** Creates the single-page document that receives imported shapes. */ export function blankSnapshot(fileName: string) { const boardId = createId('board'); - const page = PageRecord.create('Page 1'); - const layer = LayerRecord.create(page.id, 'Imported'); + const page = EditorPageRecord.create('Page 1'); + const layer = EditorLayerRecord.create(page.id, 'Imported'); page.layerIds = [layer.id]; const timestamp = Date.now(); - const doc: Document = { pages: { [page.id]: page }, layers: { [layer.id]: layer }, shapes: {}, bindings: {} }; + const doc: EditorDocument = { pages: { [page.id]: page }, layers: { [layer.id]: layer }, shapes: {}, bindings: {} }; const snapshot: BoardExport = { board: { id: boardId, @@ -27,7 +27,7 @@ export function blankSnapshot(fileName: string) { } /** Adds an imported shape to its document, page, and layer indexes. */ -export function addShape(document: Document, pageId: string, layerId: string, shape: Shape) { +export function addShape(document: EditorDocument, pageId: string, layerId: string, shape: Shape) { document.shapes[shape.id] = shape; document.pages[pageId].shapeIds.push(shape.id); document.layers![layerId].shapeIds.push(shape.id); @@ -35,7 +35,7 @@ export function addShape(document: Document, pageId: string, layerId: string, sh /** Resolves the requested export page and records multi-page loss. */ export function selectPage( - document: Document, + document: EditorDocument, pageOrder: string[], requestedPageId: string | undefined, warnings: WarningCollector @@ -49,7 +49,7 @@ export function selectPage( } /** Resolves one bound endpoint from an arrow. */ -export function bindingFor(document: Document, arrow: ArrowShape, handle: 'start' | 'end') { +export function bindingFor(document: EditorDocument, arrow: ArrowShape, handle: 'start' | 'end') { const endpoint = arrow.props[handle]; if (endpoint.kind !== 'bound' || !endpoint.bindingId) return undefined; return document.bindings[endpoint.bindingId]; diff --git a/packages/core/src/layers.ts b/packages/core/src/layers.ts index 3a0b2b4..65af6f1 100644 --- a/packages/core/src/layers.ts +++ b/packages/core/src/layers.ts @@ -1,11 +1,11 @@ import type { EditorState } from './reactivity'; -import { createId, type LayerRecord } from './model'; +import { createId, type EditorLayerRecord } from './editor-model'; /** Required handling for shapes when deleting a non-empty layer. */ export type LayerDeleteDisposition = { kind: 'move'; destinationLayerId: string } | { kind: 'delete' }; /** Returns whether a layer can receive newly created or moved shapes. */ -export function isWritableLayer(layer: LayerRecord | undefined): layer is LayerRecord { +export function isWritableLayer(layer: EditorLayerRecord | undefined): layer is EditorLayerRecord { return Boolean(layer?.visible && !layer.locked); } @@ -30,7 +30,7 @@ export function createLayer(state: EditorState, name = 'Layer'): EditorState { if (!pageId) return state; const page = state.doc.pages[pageId]; if (!page) return state; - const layer: LayerRecord = { + const layer: EditorLayerRecord = { id: createId('layer'), pageId, name: name.trim() || 'Layer', @@ -54,7 +54,7 @@ export function createLayer(state: EditorState, name = 'Layer'): EditorState { export function patchLayer( state: EditorState, layerId: string, - patch: Partial> + patch: Partial> ): EditorState { const layer = state.doc.layers?.[layerId]; if (!layer) return state; diff --git a/packages/core/src/layout.test.ts b/packages/core/src/layout.test.ts index c2a4917..33735c4 100644 --- a/packages/core/src/layout.test.ts +++ b/packages/core/src/layout.test.ts @@ -2,15 +2,15 @@ import { describe, expect, it } from 'vitest'; import { graphLayout, gridShapes } from './layout'; import { shapeBounds } from './geom'; import { EditorState } from './reactivity'; -import { BindingRecord, PageRecord, ShapeRecord } from './model'; +import { EditorBindingRecord, EditorPageRecord, EditorShapeRecord } from './editor-model'; describe('gridShapes', () => { it('places selected objects in stable rows and columns', () => { - const page = PageRecord.create('Grid test', 'page:grid'); + const page = EditorPageRecord.create('Grid test', 'page:grid'); const shapes = [ - ShapeRecord.createRect(page.id, 120, 80, { w: 40, h: 20, fill: '#fff', stroke: '#000', radius: 0 }, 'a'), - ShapeRecord.createRect(page.id, 0, 0, { w: 80, h: 30, fill: '#fff', stroke: '#000', radius: 0 }, 'b'), - ShapeRecord.createRect(page.id, 300, 200, { w: 20, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'c') + EditorShapeRecord.createRect(page.id, 120, 80, { w: 40, h: 20, fill: '#fff', stroke: '#000', radius: 0 }, 'a'), + EditorShapeRecord.createRect(page.id, 0, 0, { w: 80, h: 30, fill: '#fff', stroke: '#000', radius: 0 }, 'b'), + EditorShapeRecord.createRect(page.id, 300, 200, { w: 20, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'c') ]; page.shapeIds = shapes.map((shape) => shape.id); const state = EditorState.create(); @@ -34,18 +34,18 @@ describe('gridShapes', () => { describe('graphLayout', () => { function graphState() { - const page = PageRecord.create('Graph test', 'page:graph'); + const page = EditorPageRecord.create('Graph test', 'page:graph'); const shapes = [ - ShapeRecord.createRect(page.id, 300, 200, { w: 40, h: 20, fill: '#fff', stroke: '#000', radius: 0 }, 'a'), - ShapeRecord.createRect(page.id, 0, 0, { w: 60, h: 30, fill: '#fff', stroke: '#000', radius: 0 }, 'b'), - ShapeRecord.createRect(page.id, 150, 300, { w: 20, h: 40, fill: '#fff', stroke: '#000', radius: 0 }, 'c') + EditorShapeRecord.createRect(page.id, 300, 200, { w: 40, h: 20, fill: '#fff', stroke: '#000', radius: 0 }, 'a'), + EditorShapeRecord.createRect(page.id, 0, 0, { w: 60, h: 30, fill: '#fff', stroke: '#000', radius: 0 }, 'b'), + EditorShapeRecord.createRect(page.id, 150, 300, { w: 20, h: 40, fill: '#fff', stroke: '#000', radius: 0 }, 'c') ]; page.shapeIds = shapes.map((shape) => shape.id); const state = EditorState.create(); state.doc.pages[page.id] = page; for (const shape of shapes) state.doc.shapes[shape.id] = shape; - state.doc.bindings.ab = BindingRecord.createRelation('a', 'b', 'depends_on', 'ab'); - state.doc.bindings.bc = BindingRecord.createRelation('b', 'c', 'depends_on', 'bc'); + state.doc.bindings.ab = EditorBindingRecord.createRelation('a', 'b', 'depends_on', 'ab'); + state.doc.bindings.bc = EditorBindingRecord.createRelation('b', 'c', 'depends_on', 'bc'); state.ui.currentPageId = page.id; return { state, shapes }; } @@ -67,7 +67,7 @@ describe('graphLayout', () => { const leftToRight = graphLayout(state, shapes.map((shape) => shape.id), 'tree', 'left-to-right'); expect(shapeBounds(leftToRight.doc.shapes.a!).min.x).toBeLessThan(shapeBounds(leftToRight.doc.shapes.b!).min.x); expect(shapeBounds(leftToRight.doc.shapes.b!).min.x).toBeLessThan(shapeBounds(leftToRight.doc.shapes.c!).min.x); - state.doc.bindings.ca = BindingRecord.createRelation('c', 'a', 'depends_on', 'ca'); + state.doc.bindings.ca = EditorBindingRecord.createRelation('c', 'a', 'depends_on', 'ca'); const first = graphLayout(state, shapes.map((shape) => shape.id), 'radial'); const second = graphLayout(state, shapes.map((shape) => shape.id), 'radial'); expect(first.doc.shapes).toEqual(second.doc.shapes); diff --git a/packages/core/src/layout.ts b/packages/core/src/layout.ts index 8273ef9..d35c84f 100644 --- a/packages/core/src/layout.ts +++ b/packages/core/src/layout.ts @@ -1,6 +1,6 @@ import { shapeBoundsForState } from './geom'; import { Box2, type Box2 as Box2Type, type Vec2 } from './math'; -import { createId, ShapeRecord, type ContainerShape, type ShapeRecord as Shape } from './model'; +import { createId, EditorShapeRecord, type ContainerShape, type EditorShapeRecord as Shape } from './editor-model'; import type { EditorState } from './reactivity'; /** Axis used to distribute selected shapes. */ @@ -224,7 +224,7 @@ export function groupShapes(state: EditorState, shapeIds: readonly string[]): Ed const containerId = createId('shape'); const firstLayerId = roots.find((shape) => shape.layerId)?.layerId; const layerId = firstLayerId ?? state.ui.activeLayerId ?? page.layerIds?.[0]; - const container = ShapeRecord.createContainer( + const container = EditorShapeRecord.createContainer( pageId, bounds.min.x, bounds.min.y, diff --git a/packages/core/src/path-metrics.ts b/packages/core/src/path-metrics.ts index ebe06fd..b3c1c9d 100644 --- a/packages/core/src/path-metrics.ts +++ b/packages/core/src/path-metrics.ts @@ -1,6 +1,6 @@ import type { Mat3, Box2 } from './math'; import { Box2 as Box2Ops, Mat3 as Mat3Ops } from './math'; -import type { PathGeometry, PathSegment, PathSubpath, TextPath } from './model'; +import type { PathGeometry, PathSegment, PathSubpath, TextPath } from './editor-model'; import type { Vec2 } from './math'; /** Default geometric error used by interactive path measurements. */ diff --git a/packages/core/src/path-topology.ts b/packages/core/src/path-topology.ts index c80f3b5..6a0baf4 100644 --- a/packages/core/src/path-topology.ts +++ b/packages/core/src/path-topology.ts @@ -1,6 +1,6 @@ import { validatePathGeometry } from '@inkfinite/bindings'; -import type { PathCurveKind, PathHandleMode, PathTopologyOperation } from './model'; -import { ShapeRecord, type PathShape, type PathSegment, type PathSubpath } from './model'; +import type { PathCurveKind, PathHandleMode, PathTopologyOperation } from './editor-model'; +import { EditorShapeRecord, type PathShape, type PathSegment, type PathSubpath } from './editor-model'; import type { Vec2 } from './math'; /** @@ -14,7 +14,7 @@ export function applyPathTopologyOperations( operations: readonly PathTopologyOperation[] ): PathShape | null { if (!validatePathGeometry(shape.props)) return null; - const preview = ShapeRecord.clone(shape) as PathShape; + const preview = EditorShapeRecord.clone(shape) as PathShape; for (const operation of operations) { if (!applyPathTopologyOperation(preview, operation)) return null; } diff --git a/packages/core/src/persistence/canonical.test.ts b/packages/core/src/persistence/canonical.test.ts index 75085fd..ab5b645 100644 --- a/packages/core/src/persistence/canonical.test.ts +++ b/packages/core/src/persistence/canonical.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { LayerRecord, PageRecord, ShapeRecord } from '../model'; +import { EditorLayerRecord, EditorPageRecord, EditorShapeRecord } from '../editor-model'; import { fromCanonicalDocumentSnapshot, fromEditorProjection, toCanonicalDocumentSnapshot } from './canonical'; import type { EditorProjection } from '@inkfinite/bindings/editor'; @@ -78,12 +78,12 @@ describe('canonical editor projection', () => { it('traverses imported root containers and retains independently addressable descendants', () => { const pageId = 'page:svg'; const layerId = 'layer:svg'; - const page = PageRecord.create('SVG', pageId); - const layer = LayerRecord.create(pageId, 'Imported', layerId); + const page = EditorPageRecord.create('SVG', pageId); + const layer = EditorLayerRecord.create(pageId, 'Imported', layerId); page.layerIds = [layerId]; - const root = ShapeRecord.createContainer(pageId, 10, 20, { w: 100, h: 80 }, 'shape:svg:root'); - const group = ShapeRecord.createContainer(pageId, 5, 6, { w: 40, h: 30 }, 'shape:svg:group'); - const child = ShapeRecord.createRect( + const root = EditorShapeRecord.createContainer(pageId, 10, 20, { w: 100, h: 80 }, 'shape:svg:root'); + const group = EditorShapeRecord.createContainer(pageId, 5, 6, { w: 40, h: 30 }, 'shape:svg:group'); + const child = EditorShapeRecord.createRect( pageId, 2, 3, diff --git a/packages/core/src/persistence/canonical.ts b/packages/core/src/persistence/canonical.ts index 0476349..392b049 100644 --- a/packages/core/src/persistence/canonical.ts +++ b/packages/core/src/persistence/canonical.ts @@ -1,3 +1,12 @@ +/** + * The only TypeScript adapter boundary between generated Rust contracts and + * the interactive editor document. + * + * The adapter reads canonical snapshots and Rust editor projections, materializes + * `EditorDocument` values for tools, and emits generated reconciliation patches. + * It never defines or serializes a second native document contract. + */ + import type { EditorPatch, EditorProjection, @@ -22,12 +31,12 @@ import type { import type { BoardExport, DocOrder, LoadedDoc } from './document'; import { ensureDocumentLayers, - type Document, - type LayerRecord, + type EditorDocument, + type EditorLayerRecord, type PathTopologyEdit, type ShapeMetadata, - type ShapeRecord -} from '../model'; + type EditorShapeRecord +} from '../editor-model'; const FORMAT_ID = 'inkfinite.document'; const FORMAT_VERSION = 2; @@ -45,11 +54,11 @@ export type CanonicalSnapshotOptions = { documentId: string; heads?: readonly st * rebuild this hierarchy in TypeScript. */ export function toCanonicalDocumentSnapshot( - input: BoardExport | Document, + input: BoardExport | EditorDocument, options?: CanonicalSnapshotOptions ): NativeDocumentSnapshot { const board = isBoardExport(input) ? input : undefined; - const source: Document = isBoardExport(input) ? input.doc : input; + const source: EditorDocument = isBoardExport(input) ? input.doc : input; const document = ensureDocumentLayers(source); const documentId = options?.documentId ?? board?.board.id ?? 'document:browser'; const order = options?.order ?? board?.order; @@ -143,10 +152,10 @@ export function toCanonicalDocumentSnapshot( * drawable records and ordering, not binary content. */ export function fromEditorProjection(projection: EditorProjection, snapshot?: NativeDocumentSnapshot): LoadedDoc { - const pages: Record = {}; - const layers: Record = {}; - const shapes: Record = {}; - const bindings: Record = {}; + const pages: Record = {}; + const layers: Record = {}; + const shapes: Record = {}; + const bindings: Record = {}; for (const pageId of projection.order.page_ids) { const page = projection.pages[pageId]; @@ -167,7 +176,7 @@ export function fromEditorProjection(projection: EditorProjection, snapshot?: Na for (const shape of Object.values(projection.shapes)) { shapes[shape.id] = { id: shape.id, - type: shape.type as ShapeRecord['type'], + type: shape.type as EditorShapeRecord['type'], pageId: shape.page_id, x: shape.x, y: shape.y, @@ -181,17 +190,17 @@ export function fromEditorProjection(projection: EditorProjection, snapshot?: Na locked: shape.locked, agentEditable: shape.agent_editable, metadata: fromNativeMetadata(shape.metadata), - props: editorProperties(shape.props as ShapeProperties) as ShapeRecord['props'], + props: editorProperties(shape.props as ShapeProperties) as EditorShapeRecord['props'], ...(shape.resolved_geometry ? { resolvedGeometry: shape.resolved_geometry } : {}) - } as ShapeRecord; + } as EditorShapeRecord; } for (const binding of Object.values(projection.bindings)) { bindings[binding.id] = { id: binding.id, - type: binding.type as import('../model').BindingType, + type: binding.type as import('../editor-model').BindingType, fromShapeId: binding.from_shape_id, toShapeId: binding.to_shape_id, - handle: binding.handle as import('../model').BindingHandle, + handle: binding.handle as import('../editor-model').BindingHandle, anchor: binding.anchor.kind === 'center' ? { kind: 'center' } @@ -236,10 +245,10 @@ export function fromEditorProjection(projection: EditorProjection, snapshot?: Na } export function fromCanonicalDocumentSnapshot(snapshot: NativeDocumentSnapshot): LoadedDoc { - const pages: Record = {}; - const layers: Record = {}; - const shapes: Record = {}; - const bindings: Record = {}; + const pages: Record = {}; + const layers: Record = {}; + const shapes: Record = {}; + const bindings: Record = {}; for (const pageId of snapshot.document.page_ids) { const page = snapshot.document.pages[pageId]; @@ -276,10 +285,10 @@ export function fromCanonicalDocumentSnapshot(snapshot: NativeDocumentSnapshot): for (const binding of Object.values(snapshot.document.bindings)) { bindings[binding.id] = { id: binding.id, - type: binding.kind as import('../model').BindingType, + type: binding.kind as import('../editor-model').BindingType, fromShapeId: binding.source_shape_id, toShapeId: binding.target_shape_id, - handle: binding.source_handle as import('../model').BindingHandle, + handle: binding.source_handle as import('../editor-model').BindingHandle, anchor: binding.anchor.kind === 'center' ? { kind: 'center' } @@ -318,8 +327,8 @@ export function fromCanonicalDocumentSnapshot(snapshot: NativeDocumentSnapshot): /** Builds one semantic Rust reconciliation request from an editor document change. */ export function createEditorReconciliationRequest( - before: Document, - after: Document, + before: EditorDocument, + after: EditorDocument, options: Omit & { topologyEdits?: PathTopologyEdit[] } ): EditorReconciliationRequest { const { topologyEdits = [], ...requestOptions } = options; @@ -489,10 +498,10 @@ function editorShape( layerId: string, groupId: string | undefined, transform: Affine -): ShapeRecord { +): EditorShapeRecord { return { id: native.id, - type: native.kind as ShapeRecord['type'], + type: native.kind as EditorShapeRecord['type'], pageId, x: transform.e, y: transform.f, @@ -507,7 +516,7 @@ function editorShape( agentEditable: native.metadata.agent_editable, metadata: fromNativeMetadata(native.metadata), props: editorProperties(native.properties) - } as ShapeRecord; + } as EditorShapeRecord; } function fromNativeMetadata(metadata: SemanticMetadata): ShapeMetadata { @@ -532,7 +541,7 @@ function fromNativeMetadata(metadata: SemanticMetadata): ShapeMetadata { }; } -function shapeMetadata(shape: ShapeRecord): ShapeMetadata { +function shapeMetadata(shape: EditorShapeRecord): ShapeMetadata { if (shape.metadata) { return { ...shape.metadata, @@ -619,11 +628,11 @@ function editorProperties(properties: ShapeProperties): ShapeProperties { return result; } -function cloneProperties(properties: ShapeRecord['props']): ShapeProperties { +function cloneProperties(properties: EditorShapeRecord['props']): ShapeProperties { return JSON.parse(JSON.stringify(properties)) as ShapeProperties; } -function shapeStyle(shape: ShapeRecord): ShapeStyle { +function shapeStyle(shape: EditorShapeRecord): ShapeStyle { return { opacity: clampOpacity(shape.opacity), fill_opacity: shape.fillOpacity === undefined ? null : clampOpacity(shape.fillOpacity), @@ -631,20 +640,20 @@ function shapeStyle(shape: ShapeRecord): ShapeStyle { }; } -function shapeParent(shape: ShapeRecord, document: Document): ShapeParent { +function shapeParent(shape: EditorShapeRecord, document: EditorDocument): ShapeParent { return shape.groupId ? { kind: 'shape', id: shape.groupId } : { kind: 'layer', id: shape.layerId ?? findShapeLayer(shape.id, document) }; } -function findShapeLayer(shapeId: string, document: Document): string { +function findShapeLayer(shapeId: string, document: EditorDocument): string { for (const layer of Object.values(document.layers ?? {})) { if (layer.shapeIds.includes(shapeId)) return layer.id; } throw new Error(`Shape ${shapeId} has no owning layer`); } -function nativeLayer(layer: LayerRecord): NativeLayerRecord { +function nativeLayer(layer: EditorLayerRecord): NativeLayerRecord { return { id: layer.id, page_id: layer.pageId, @@ -657,7 +666,7 @@ function nativeLayer(layer: LayerRecord): NativeLayerRecord { }; } -function deletedLayerMoves(before: Document, after: Document): Map { +function deletedLayerMoves(before: EditorDocument, after: EditorDocument): Map { const destinations = new Map(); for (const layer of Object.values(before.layers ?? {})) { if (after.layers?.[layer.id]) continue; @@ -679,10 +688,10 @@ function orderAnchorFor(id: string, ids: string[]) { } function shapePatch( - before: ShapeRecord, - after: ShapeRecord, - beforeDocument: Document, - afterDocument: Document, + before: EditorShapeRecord, + after: EditorShapeRecord, + beforeDocument: EditorDocument, + afterDocument: EditorDocument, skipDeletedLayerParent: boolean, skipProperties: boolean ): EditorPatch | null { @@ -748,7 +757,7 @@ function shapePatch( }; } -function editorTransform(shape: ShapeRecord, previous?: ShapeRecord): EditorTransform { +function editorTransform(shape: EditorShapeRecord, previous?: EditorShapeRecord): EditorTransform { const current = shape.editorTransform ? { ...shape.editorTransform } : transformFromRotation(shape.rot, 1, 1); if (previous && !shape.editorTransform && Math.abs(shape.rot - previous.rot) > 1e-9) { const previousTransform = previous.editorTransform ?? transformFromRotation(previous.rot, 1, 1); @@ -772,8 +781,8 @@ function siblingOrderChanged( shapeId: string, beforeParent: ShapeParent, afterParent: ShapeParent, - before: Document, - after: Document + before: EditorDocument, + after: EditorDocument ): boolean { if (JSON.stringify(beforeParent) !== JSON.stringify(afterParent)) return true; const beforeSiblings = siblings(beforeParent, before); @@ -781,14 +790,14 @@ function siblingOrderChanged( return JSON.stringify(beforeSiblings) !== JSON.stringify(afterSiblings) && afterSiblings.includes(shapeId); } -function siblings(parent: ShapeParent, document: Document): string[] { +function siblings(parent: ShapeParent, document: EditorDocument): string[] { if (parent.kind === 'layer') return document.layers?.[parent.id]?.shapeIds ?? []; return Object.values(document.shapes) .filter((shape) => shape.groupId === parent.id) .map((shape) => shape.id); } -function orderAnchor(shapeId: string, parent: ShapeParent, document: Document) { +function orderAnchor(shapeId: string, parent: ShapeParent, document: EditorDocument) { const ids = siblings(parent, document).filter((id) => id !== shapeId); const index = siblings(parent, document).indexOf(shapeId); return index <= 0 @@ -796,7 +805,7 @@ function orderAnchor(shapeId: string, parent: ShapeParent, document: Document) { : { position: 'after' as const, sibling_id: ids[index - 1] ?? ids.at(-1)! }; } -function nativePropertiesForShape(shape: ShapeRecord): ShapeProperties { +function nativePropertiesForShape(shape: EditorShapeRecord): ShapeProperties { const properties = JSON.parse(JSON.stringify(shape.props)) as ShapeProperties; if ('clipPath' in properties) { properties.clip_path = properties.clipPath; @@ -822,7 +831,7 @@ function nativePropertiesForShape(shape: ShapeRecord): ShapeProperties { return properties; } -function nativeTransform(shape: ShapeRecord, document: Document): Transform { +function nativeTransform(shape: EditorShapeRecord, document: EditorDocument): Transform { const world = editorTransform(shape); const parent = shape.groupId ? document.shapes[shape.groupId] : undefined; const parentWorld = parent ? editorTransform(parent) : identityAffine(); @@ -862,7 +871,7 @@ function sameAffine(left: Affine, right: Affine): boolean { ].every(([a, b]) => Math.abs(a - b) <= 1e-9 * (1 + Math.max(Math.abs(a), Math.abs(b)))); } -function orderedChildren(shape: ShapeRecord, document: Document): string[] { +function orderedChildren(shape: EditorShapeRecord, document: EditorDocument): string[] { const layer = shape.layerId ? document.layers?.[shape.layerId] : undefined; const order = layer?.shapeIds ?? document.pages[shape.pageId]?.shapeIds ?? []; const children = order.filter((id) => document.shapes[id]?.groupId === shape.id); @@ -872,7 +881,7 @@ function orderedChildren(shape: ShapeRecord, document: Document): string[] { .map((candidate) => candidate.id); } -function nativeAsset(asset: import('../model').ImportedAsset): NativeAssetRecord { +function nativeAsset(asset: import('../editor-model').ImportedAsset): NativeAssetRecord { return { id: asset.id, name: asset.name, @@ -884,7 +893,7 @@ function nativeAsset(asset: import('../model').ImportedAsset): NativeAssetRecord }; } -function nativeBinding(binding: import('../model').BindingRecord): NativeBindingRecord { +function nativeBinding(binding: import('../editor-model').EditorBindingRecord): NativeBindingRecord { return { id: binding.id, kind: binding.type, @@ -900,7 +909,7 @@ function nativeBinding(binding: import('../model').BindingRecord): NativeBinding }; } -function nativeShape(shape: ShapeRecord, layerId: string, document: Document): NativeShapeRecord { +function nativeShape(shape: EditorShapeRecord, layerId: string, document: EditorDocument): NativeShapeRecord { const metadata = toNativeMetadata(shape.metadata, { locked: shape.locked ?? false, agentEditable: shape.agentEditable ?? true @@ -928,7 +937,7 @@ function provenance(): Provenance { return { actor_id: BROWSER_ACTOR, origin: 'human', timestamp: 0, source: null }; } -function layerOwners(layers: Record): Map { +function layerOwners(layers: Record): Map { const owners = new Map(); for (const layer of Object.values(layers)) { for (const shapeId of layer.shapeIds) owners.set(shapeId, layer.id); @@ -951,6 +960,6 @@ function optionalOpacity(value: number | undefined): number | null { return value === undefined ? null : clampOpacity(value); } -function isBoardExport(input: BoardExport | Document): input is BoardExport { +function isBoardExport(input: BoardExport | EditorDocument): input is BoardExport { return 'board' in input && 'doc' in input; } diff --git a/packages/core/src/persistence/document.ts b/packages/core/src/persistence/document.ts index 60d5f48..d35aa85 100644 --- a/packages/core/src/persistence/document.ts +++ b/packages/core/src/persistence/document.ts @@ -1,15 +1,15 @@ import { - type BindingRecord, - BindingRecord as BindingOps, - type Document, - type LayerRecord, - type PageRecord, - PageRecord as PageOps, - type ShapeRecord, - ShapeRecord as ShapeOps, + type EditorBindingRecord, + EditorBindingRecord as BindingOps, + type EditorDocument, + type EditorLayerRecord, + type EditorPageRecord, + EditorPageRecord as PageOps, + type EditorShapeRecord, + EditorShapeRecord as ShapeOps, type ImportedAsset, type PathTopologyEdit -} from '../model'; +} from '../editor-model'; import type { EditorProjection } from '@inkfinite/bindings/editor'; import type { DocumentSnapshot as NativeDocumentSnapshot } from '@inkfinite/bindings/model'; import type { BoardMeta, DocRepo } from './repo'; @@ -20,12 +20,12 @@ export type DocOrder = { /** Optional per-page shape order overrides. */ shapeOrder?: Record; /** Complete layer records, stored with ordering metadata by editor adapters. */ - layers?: Record; + layers?: Record; }; /** Incremental document changes accepted by persistent repositories. */ export type DocPatch = { - upserts?: { pages?: PageRecord[]; shapes?: ShapeRecord[]; bindings?: BindingRecord[]; assets?: ImportedAsset[] }; + upserts?: { pages?: EditorPageRecord[]; shapes?: EditorShapeRecord[]; bindings?: EditorBindingRecord[]; assets?: ImportedAsset[] }; deletes?: { pageIds?: string[]; shapeIds?: string[]; bindingIds?: string[]; assetIds?: string[] }; order?: Partial; /** Canonical path operations associated with this document change. */ @@ -34,16 +34,16 @@ export type DocPatch = { /** A complete document loaded from persistence. */ export type LoadedDoc = { - pages: Record; - layers?: Record; - shapes: Record; - bindings: Record; + pages: Record; + layers?: Record; + shapes: Record; + bindings: Record; assets?: Record; order: DocOrder; }; /** Portable board snapshot used by import and export flows. */ -export type BoardExport = { board: BoardMeta; doc: Document; order: DocOrder }; +export type BoardExport = { board: BoardMeta; doc: EditorDocument; order: DocOrder }; /** Canonical bytes and their materialized cache stored by browser adapters. */ export type CanonicalDocumentState = { @@ -56,8 +56,8 @@ export type CanonicalDocumentState = { /** One editor document change handed to a Rust-backed browser persistence adapter. */ export type EditorDocumentChange = { boardId: string; - before: Document; - after: Document; + before: EditorDocument; + after: EditorDocument; op: 'do' | 'undo' | 'redo'; description: string; topologyEdits?: PathTopologyEdit[]; @@ -89,7 +89,7 @@ export interface PersistentDocRepo extends DocRepo { * identifiers explicitly. Repository adapters remain responsible for applying the * patch atomically. */ -export function diffDoc(before: Document, after: Document): DocPatch { +export function diffDoc(before: EditorDocument, after: EditorDocument): DocPatch { const patch: DocPatch = {}; const deletedPages = difference(Object.keys(before.pages), Object.keys(after.pages)); const deletedShapes = difference(Object.keys(before.shapes), Object.keys(after.shapes)); diff --git a/packages/core/src/reactivity.ts b/packages/core/src/reactivity.ts index d6fa5d4..a1011b9 100644 --- a/packages/core/src/reactivity.ts +++ b/packages/core/src/reactivity.ts @@ -2,8 +2,8 @@ import type { Camera } from './camera'; import { Camera as CameraOps } from './camera'; import { History } from './history'; import type { Command, HistoryAppliedEvent, HistoryEntry, HistoryOperation, HistoryState } from './history'; -import type { Document, LayerRecord, PageRecord, PathSelection, ShapeRecord } from './model'; -import { Document as DocumentOps, ensureDocumentLayers } from './model'; +import type { EditorDocument, EditorLayerRecord, EditorPageRecord, PathSelection, EditorShapeRecord } from './editor-model'; +import { EditorDocument as DocumentOps, ensureDocumentLayers } from './editor-model'; type Listener = (value: Value) => void; @@ -63,7 +63,7 @@ export type UIState = { bindingPreview?: BindingPreview; }; -export type EditorState = { doc: Document; ui: UIState; camera: Camera }; +export type EditorState = { doc: EditorDocument; ui: UIState; camera: Camera }; export const EditorState = { /** @@ -371,7 +371,7 @@ function enforceInvariants(state: EditorState): EditorState { * @param state - Editor state * @returns Current page or null if no page is selected */ -export function getCurrentPage(state: EditorState): PageRecord | null { +export function getCurrentPage(state: EditorState): EditorPageRecord | null { if (state.ui.currentPageId === null) { return null; } @@ -394,7 +394,7 @@ export function canCreateShapeOnActiveLayer(state: EditorState): boolean { * @param state - Editor state * @returns Array of shapes on current page (empty if no page selected) */ -export function getShapesOnCurrentPage(state: EditorState): ShapeRecord[] { +export function getShapesOnCurrentPage(state: EditorState): EditorShapeRecord[] { const currentPage = getCurrentPage(state); if (!currentPage) { return []; @@ -404,19 +404,19 @@ export function getShapesOnCurrentPage(state: EditorState): ShapeRecord[] { if (!layers || !currentPage.layerIds?.length) { return currentPage.shapeIds .map((id) => state.doc.shapes[id]) - .filter((shape): shape is ShapeRecord => shape !== undefined); + .filter((shape): shape is EditorShapeRecord => shape !== undefined); } return currentPage.layerIds.flatMap((layerId) => { const layer = layers[layerId]; if (!layer?.visible) return []; return layer.shapeIds .map((id) => state.doc.shapes[id]) - .filter((shape): shape is ShapeRecord => shape !== undefined); + .filter((shape): shape is EditorShapeRecord => shape !== undefined); }); } /** Returns visible, unlocked shapes in draw order for hit testing and selection. */ -export function getInteractiveShapesOnCurrentPage(state: EditorState): ShapeRecord[] { +export function getInteractiveShapesOnCurrentPage(state: EditorState): EditorShapeRecord[] { const currentPage = getCurrentPage(state); if (!currentPage) return []; const shapes = getShapesOnCurrentPage(state); @@ -424,7 +424,7 @@ export function getInteractiveShapesOnCurrentPage(state: EditorState): ShapeReco } /** Returns the direct children of the active container selection scope. */ -export function getSelectionScopeShapes(state: EditorState): ShapeRecord[] { +export function getSelectionScopeShapes(state: EditorState): EditorShapeRecord[] { const path = getContainerPath(state); const parentId = path.at(-1); const shapes = getInteractiveShapesOnCurrentPage(state); @@ -454,11 +454,11 @@ export function selectionTarget(state: EditorState, shapeId: string): string | n } /** Returns whether a shape and all of its ancestors can participate in editing. */ -function isShapeInteractive(state: EditorState, shape: ShapeRecord): boolean { +function isShapeInteractive(state: EditorState, shape: EditorShapeRecord): boolean { return isShapeInteractiveInDocument(state.doc, shape); } -function isShapeInteractiveInDocument(document: Document, shape: ShapeRecord): boolean { +function isShapeInteractiveInDocument(document: EditorDocument, shape: EditorShapeRecord): boolean { if (shape.locked) return false; if (shape.layerId) { const layer = document.layers?.[shape.layerId]; @@ -475,7 +475,7 @@ function isShapeInteractiveInDocument(document: Document, shape: ShapeRecord): b function normalizePathSelection( selection: PathSelection | undefined, - document: Document, + document: EditorDocument, pageId: string | null ): PathSelection | undefined { if (!selection || !pageId) return undefined; @@ -490,7 +490,7 @@ function normalizePathSelection( return { pathId: selection.pathId, anchors }; } -function normalizeContainerPath(state: EditorState, document: Document, pageId: string | null): string[] { +function normalizeContainerPath(state: EditorState, document: EditorDocument, pageId: string | null): string[] { if (!pageId) return []; const path: string[] = []; for (const id of state.ui.containerPath ?? []) { @@ -504,15 +504,15 @@ function normalizeContainerPath(state: EditorState, document: Document, pageId: } /** Returns the current page's layers in back-to-front order. */ -export function getLayersOnCurrentPage(state: EditorState): LayerRecord[] { +export function getLayersOnCurrentPage(state: EditorState): EditorLayerRecord[] { const page = getCurrentPage(state); if (!page || !state.doc.layers) return []; return (page.layerIds ?? []) .map((id) => state.doc.layers?.[id]) - .filter((layer): layer is LayerRecord => Boolean(layer)); + .filter((layer): layer is EditorLayerRecord => Boolean(layer)); } -function getInteractiveShapeIds(document: Document, page: PageRecord): string[] { +function getInteractiveShapeIds(document: EditorDocument, page: EditorPageRecord): string[] { const ids = !document.layers || !page.layerIds?.length ? page.shapeIds @@ -532,10 +532,10 @@ function getInteractiveShapeIds(document: Document, page: PageRecord): string[] * @param state - Editor state * @returns Array of selected shapes (empty if no selection) */ -export function getSelectedShapes(state: EditorState): ShapeRecord[] { +export function getSelectedShapes(state: EditorState): EditorShapeRecord[] { return state.ui.selectionIds .map((id) => state.doc.shapes[id]) - .filter((shape): shape is ShapeRecord => shape !== undefined); + .filter((shape): shape is EditorShapeRecord => shape !== undefined); } /** @@ -555,7 +555,7 @@ export function isShapeSelected(state: EditorState, shapeId: string): boolean { * @param state - Editor state * @returns Array of all pages */ -export function getAllPages(state: EditorState): PageRecord[] { +export function getAllPages(state: EditorState): EditorPageRecord[] { return Object.values(state.doc.pages); } @@ -566,6 +566,6 @@ export function getAllPages(state: EditorState): PageRecord[] { * @param shapeId - Shape ID * @returns Shape or undefined if not found */ -export function getShape(state: EditorState, shapeId: string): ShapeRecord | undefined { +export function getShape(state: EditorState, shapeId: string): EditorShapeRecord | undefined { return state.doc.shapes[shapeId]; } diff --git a/packages/core/src/selection.ts b/packages/core/src/selection.ts index b98089a..4980bd0 100644 --- a/packages/core/src/selection.ts +++ b/packages/core/src/selection.ts @@ -1,5 +1,5 @@ import { computeNormalizedAnchor, shapeCenter } from './geom'; -import { BindingRecord, createId, ShapeRecord, type ShapeRecord as ShapeRecordType } from './model'; +import { EditorBindingRecord, createId, EditorShapeRecord, type EditorShapeRecord as ShapeRecordType } from './editor-model'; import type { EditorState } from './reactivity'; /** World-space offset used when a selection is duplicated and connected. */ @@ -81,14 +81,14 @@ export function duplicateAndConnectSelection( const copyCenter = shapeCenter(copy); const arrowId = createId('shape'); const startAnchor = computeNormalizedAnchor(copyCenter, source); - const startBinding = BindingRecord.create(arrowId, source.id, 'start', { kind: 'edge', ...startAnchor }); + const startBinding = EditorBindingRecord.create(arrowId, source.id, 'start', { kind: 'edge', ...startAnchor }); const endAnchor = computeNormalizedAnchor(sourceCenter, copy); - const endBinding = BindingRecord.create(arrowId, copy.id, 'end', { kind: 'edge', ...endAnchor }); + const endBinding = EditorBindingRecord.create(arrowId, copy.id, 'end', { kind: 'edge', ...endAnchor }); const style = source.type === 'arrow' ? { ...source.props.style, headStart: false, headEnd: true } : { stroke: '#2563eb', width: 2, headEnd: true }; - const arrow = ShapeRecord.createArrow( + const arrow = EditorShapeRecord.createArrow( source.pageId, 0, 0, @@ -123,7 +123,7 @@ function duplicateState(state: EditorState, offset: DuplicateConnectOffset): Dup const shapes = { ...state.doc.shapes }; for (const source of included) { - const copy = ShapeRecord.clone(source); + const copy = EditorShapeRecord.clone(source); const id = mapping.get(source.id)!; const parentId = source.groupId ? mapping.get(source.groupId) : undefined; const copied = { @@ -167,7 +167,7 @@ function duplicateState(state: EditorState, offset: DuplicateConnectOffset): Dup if (!fromShapeId) continue; const id = createId('binding'); const toShapeId = mapping.get(binding.toShapeId) ?? binding.toShapeId; - bindings[id] = { ...BindingRecord.clone(binding), id, fromShapeId, toShapeId }; + bindings[id] = { ...EditorBindingRecord.clone(binding), id, fromShapeId, toShapeId }; } for (const source of included) { const id = mapping.get(source.id); diff --git a/packages/core/src/snapping.ts b/packages/core/src/snapping.ts index 19e4a43..7b5e29b 100644 --- a/packages/core/src/snapping.ts +++ b/packages/core/src/snapping.ts @@ -2,7 +2,7 @@ import { shapeBounds } from './geom'; import type { Box2, Vec2 } from './math'; import type { EditorState } from './reactivity'; import { getInteractiveShapesOnCurrentPage } from './reactivity'; -import type { ShapeRecord } from './model'; +import type { EditorShapeRecord } from './editor-model'; /** A line shown while an object is aligned to another object. */ export type SnapGuide = { @@ -86,7 +86,7 @@ export function snapPoint( */ export function snapTranslation( state: EditorState, - movingShapes: Iterable, + movingShapes: Iterable, leadPosition: Vec2, delta: Vec2, options: SnapOptions @@ -168,7 +168,7 @@ export function snapAngle(start: Vec2, point: Vec2, stepDeg = 15): Vec2 { return { x: start.x + Math.cos(snapped) * length, y: start.y + Math.sin(snapped) * length }; } -function closestFeature(value: number, shapes: ShapeRecord[], axis: 'x' | 'y', distance: number): Feature | null { +function closestFeature(value: number, shapes: EditorShapeRecord[], axis: 'x' | 'y', distance: number): Feature | null { let match: Feature | null = null; let best = distance + Number.EPSILON; for (const shape of shapes) { @@ -188,7 +188,7 @@ function closestFeature(value: number, shapes: ShapeRecord[], axis: 'x' | 'y', d function closestTranslationMatch( moving: Box2, delta: number, - shapes: ShapeRecord[], + shapes: EditorShapeRecord[], axis: 'x' | 'y', distance: number ): { delta: number; position: number; kind: SnapGuide['kind'] } | null { @@ -212,7 +212,7 @@ function closestTranslationMatch( function equalGapMatch( moving: Box2, delta: number, - shapes: ShapeRecord[], + shapes: EditorShapeRecord[], axis: 'x' | 'y', distance: number ): { delta: number; position: number } | null { diff --git a/packages/core/src/stencils/definitions.ts b/packages/core/src/stencils/definitions.ts index b1a2fce..eca535f 100644 --- a/packages/core/src/stencils/definitions.ts +++ b/packages/core/src/stencils/definitions.ts @@ -1,6 +1,6 @@ import type { Vec2 } from '../math'; import { createCardShapes } from '../cards'; -import { ShapeRecord, type ShapeRecord as Shape } from '../model'; +import { EditorShapeRecord, type EditorShapeRecord as Shape } from '../editor-model'; import { registry } from './registry'; import type { Stencil, StencilCategory } from './types'; @@ -10,15 +10,15 @@ const PLACEHOLDER_PAGE_ID = 'placeholder_page'; type StencilDefinition = Omit & { spawn: (at: Vec2) => Shape[] }; function rect(at: Vec2, width: number, height: number, radius = 0, fill = '#ffffff', stroke = '#1f2937') { - return ShapeRecord.createRect(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: width, h: height, fill, stroke, radius }); + return EditorShapeRecord.createRect(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: width, h: height, fill, stroke, radius }); } function ellipse(at: Vec2, width: number, height: number, fill = '#ffffff', stroke = '#1f2937') { - return ShapeRecord.createEllipse(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: width, h: height, fill, stroke }); + return EditorShapeRecord.createEllipse(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: width, h: height, fill, stroke }); } function line(at: Vec2, x: number, y: number, width: number, height: number, stroke = '#1f2937') { - return ShapeRecord.createLine(PLACEHOLDER_PAGE_ID, at.x + x, at.y + y, { + return EditorShapeRecord.createLine(PLACEHOLDER_PAGE_ID, at.x + x, at.y + y, { a: { x: 0, y: 0 }, b: { x: width, y: height }, stroke, @@ -27,7 +27,7 @@ function line(at: Vec2, x: number, y: number, width: number, height: number, str } function text(at: Vec2, x: number, y: number, value: string, width: number, fontSize = 16) { - return ShapeRecord.createText(PLACEHOLDER_PAGE_ID, at.x + x, at.y + y, { + return EditorShapeRecord.createText(PLACEHOLDER_PAGE_ID, at.x + x, at.y + y, { text: value, fontSize, fontFamily: 'sans-serif', @@ -37,7 +37,7 @@ function text(at: Vec2, x: number, y: number, value: string, width: number, font } function reference(at: Vec2, referenceType: 'url' | 'file' | 'page', value: string, label: string) { - return ShapeRecord.createReference(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: 280, h: 72, referenceType, value, label }); + return EditorShapeRecord.createReference(PLACEHOLDER_PAGE_ID, at.x, at.y, { w: 280, h: 72, referenceType, value, label }); } function stencil( diff --git a/packages/core/src/stencils/insertion.ts b/packages/core/src/stencils/insertion.ts index ee10de3..a2a3689 100644 --- a/packages/core/src/stencils/insertion.ts +++ b/packages/core/src/stencils/insertion.ts @@ -1,4 +1,4 @@ -import { createId, type ShapeRecord } from '../model'; +import { createId, type EditorShapeRecord } from '../editor-model'; import { canCreateShapeOnActiveLayer, type EditorState } from '../reactivity'; import type { Stencil } from './types'; @@ -43,7 +43,7 @@ export function insertStencil( const insertedIds: string[] = []; const selectionIds: string[] = []; for (const spawnedShape of spawned) { - const shape: ShapeRecord = { + const shape: EditorShapeRecord = { ...spawnedShape, pageId, ...(activeLayerId ? { layerId: activeLayerId } : {}), diff --git a/packages/core/src/stencils/types.ts b/packages/core/src/stencils/types.ts index 2f81bf0..32c63f3 100644 --- a/packages/core/src/stencils/types.ts +++ b/packages/core/src/stencils/types.ts @@ -1,5 +1,5 @@ import { Vec2 } from "../math"; -import { ShapeRecord } from "../model"; +import { EditorShapeRecord } from "../editor-model"; export type StencilCategory = "Flowchart" | "Diagrams" | "UI" | "Content" | "Etc"; @@ -13,5 +13,5 @@ export interface Stencil { * Create the shapes for this stencil at the given position. * If multiple shapes are returned, they should ideally share a groupId. */ - spawn: (atPoint: Vec2) => ShapeRecord[]; + spawn: (atPoint: Vec2) => EditorShapeRecord[]; } diff --git a/packages/core/src/style-policy.ts b/packages/core/src/style-policy.ts index f7f00ae..4657928 100644 --- a/packages/core/src/style-policy.ts +++ b/packages/core/src/style-policy.ts @@ -8,7 +8,7 @@ import type { RectProps, StrokeStyle, TextProps -} from './model'; +} from './editor-model'; /** Canvas appearance used to resolve explicit creation-time document colors. */ export type CanvasAppearance = 'light' | 'dark'; diff --git a/packages/core/src/text-path.ts b/packages/core/src/text-path.ts index 33158b6..4e311ef 100644 --- a/packages/core/src/text-path.ts +++ b/packages/core/src/text-path.ts @@ -1,7 +1,7 @@ import { pathLength } from './path-metrics'; import type { EditorState } from './reactivity'; import { getSelectedShapes } from './reactivity'; -import type { PathShape, TextShape } from './model'; +import type { PathShape, TextShape } from './editor-model'; /** Return the selected text and supporting path when the attachment command applies. */ export function textPathSelectionTargets(state: EditorState): { text: TextShape; path: PathShape } | null { diff --git a/packages/core/src/tools.test.ts b/packages/core/src/tools.test.ts index 585daca..6095a27 100644 --- a/packages/core/src/tools.test.ts +++ b/packages/core/src/tools.test.ts @@ -5,28 +5,28 @@ import { type ArrowProps, type EllipseProps, type LineProps, - PageRecord, + EditorPageRecord, type RectProps, - ShapeRecord, + EditorShapeRecord, type TextProps, -} from "./model"; +} from "./editor-model"; import { EditorState } from "./reactivity"; import { ArrowTool, EllipseTool, LineTool, RectTool, SelectTool, TextTool } from "./tools"; describe("SelectTool", () => { let tool: SelectTool; let initialState: EditorState; - let page: PageRecord; - let shape1: ShapeRecord; - let shape2: ShapeRecord; - let shape3: ShapeRecord; + let page: EditorPageRecord; + let shape1: EditorShapeRecord; + let shape2: EditorShapeRecord; + let shape3: EditorShapeRecord; beforeEach(() => { tool = new SelectTool(); - page = PageRecord.create("Test Page"); - shape1 = ShapeRecord.createRect(page.id, 0, 0, { w: 100, h: 100, fill: "#ff0000", stroke: "#000000", radius: 0 }); - shape2 = ShapeRecord.createRect(page.id, 200, 0, { w: 100, h: 100, fill: "#00ff00", stroke: "#000000", radius: 0 }); - shape3 = ShapeRecord.createEllipse(page.id, 0, 200, { w: 80, h: 80, fill: "#0000ff", stroke: "#000000" }); + page = EditorPageRecord.create("Test Page"); + shape1 = EditorShapeRecord.createRect(page.id, 0, 0, { w: 100, h: 100, fill: "#ff0000", stroke: "#000000", radius: 0 }); + shape2 = EditorShapeRecord.createRect(page.id, 200, 0, { w: 100, h: 100, fill: "#00ff00", stroke: "#000000", radius: 0 }); + shape3 = EditorShapeRecord.createEllipse(page.id, 0, 200, { w: 80, h: 80, fill: "#0000ff", stroke: "#000000" }); page.shapeIds = [shape1.id, shape2.id, shape3.id]; @@ -484,11 +484,11 @@ describe("SelectTool", () => { describe("RectTool", () => { let tool: RectTool; let initialState: EditorState; - let page: PageRecord; + let page: EditorPageRecord; beforeEach(() => { tool = new RectTool(); - page = PageRecord.create("Test Page"); + page = EditorPageRecord.create("Test Page"); initialState = { ...EditorState.create(), @@ -700,11 +700,11 @@ describe("RectTool", () => { describe("EllipseTool", () => { let tool: EllipseTool; let initialState: EditorState; - let page: PageRecord; + let page: EditorPageRecord; beforeEach(() => { tool = new EllipseTool(); - page = PageRecord.create("Test Page"); + page = EditorPageRecord.create("Test Page"); initialState = { ...EditorState.create(), @@ -838,11 +838,11 @@ describe("EllipseTool", () => { describe("LineTool", () => { let tool: LineTool; let initialState: EditorState; - let page: PageRecord; + let page: EditorPageRecord; beforeEach(() => { tool = new LineTool(); - page = PageRecord.create("Test Page"); + page = EditorPageRecord.create("Test Page"); initialState = { ...EditorState.create(), @@ -980,11 +980,11 @@ describe("LineTool", () => { describe("ArrowTool", () => { let tool: ArrowTool; let initialState: EditorState; - let page: PageRecord; + let page: EditorPageRecord; beforeEach(() => { tool = new ArrowTool(); - page = PageRecord.create("Test Page"); + page = EditorPageRecord.create("Test Page"); initialState = { ...EditorState.create(), @@ -1117,11 +1117,11 @@ describe("ArrowTool", () => { describe("TextTool", () => { let tool: TextTool; let initialState: EditorState; - let page: PageRecord; + let page: EditorPageRecord; beforeEach(() => { tool = new TextTool(); - page = PageRecord.create("Test Page"); + page = EditorPageRecord.create("Test Page"); initialState = { ...EditorState.create(), @@ -1202,14 +1202,14 @@ describe("TextTool", () => { describe("Arrow Bindings", () => { let tool: ArrowTool; let initialState: EditorState; - let page: PageRecord; - let targetShape: ShapeRecord; + let page: EditorPageRecord; + let targetShape: EditorShapeRecord; beforeEach(() => { tool = new ArrowTool(); - page = PageRecord.create("Test Page"); + page = EditorPageRecord.create("Test Page"); - targetShape = ShapeRecord.createRect(page.id, 100, 100, { + targetShape = EditorShapeRecord.createRect(page.id, 100, 100, { w: 100, h: 100, fill: "#ff0000", @@ -1305,7 +1305,7 @@ describe("Arrow Bindings", () => { }); it("should create bindings for both ends when both hit shapes", () => { - const targetShape2 = ShapeRecord.createRect(page.id, 300, 300, { + const targetShape2 = EditorShapeRecord.createRect(page.id, 300, 300, { w: 100, h: 100, fill: "#00ff00", diff --git a/packages/core/src/tools/base.ts b/packages/core/src/tools/base.ts index e512e69..fb27057 100644 --- a/packages/core/src/tools/base.ts +++ b/packages/core/src/tools/base.ts @@ -1,5 +1,5 @@ import type { Action } from "../actions"; -import type { PathTopologyEdit } from "../model"; +import type { PathTopologyEdit } from "../editor-model"; import type { EditorState, ToolId } from "../reactivity"; /** diff --git a/packages/core/src/tools/direct.ts b/packages/core/src/tools/direct.ts index 19c6919..4b39dbd 100644 --- a/packages/core/src/tools/direct.ts +++ b/packages/core/src/tools/direct.ts @@ -15,14 +15,14 @@ import { import type { Vec2 } from '../math'; import { applyPathTopologyOperations } from '../path-topology'; import { - ShapeRecord, + EditorShapeRecord, type PathAnchorRef, type PathControlRef, type PathShape, type StrokeShape, type PathTopologyEdit, type PathTopologyOperation -} from '../model'; +} from '../editor-model'; import { EditorState, getSelectionScopeShapes, selectionTarget, type ToolId } from '../reactivity'; import type { Tool } from './base'; @@ -524,7 +524,7 @@ export class DirectSelectTool implements Tool { activeHandle: kind === 'anchor' ? { kind, ref: ref as PathAnchorRef } : { kind, ref: ref as PathControlRef }, dragStartWorld: point, - initialShape: ShapeRecord.clone(path) as PathShape, + initialShape: EditorShapeRecord.clone(path) as PathShape, initialStroke: null }; } @@ -536,7 +536,7 @@ export class DirectSelectTool implements Tool { activeHandle: { kind: 'width', index }, dragStartWorld: point, initialShape: null, - initialStroke: ShapeRecord.clone(stroke) as StrokeShape + initialStroke: EditorShapeRecord.clone(stroke) as StrokeShape }; } @@ -606,7 +606,7 @@ function moveAnchors( : { x: 0, y: rawDelta.y } : rawDelta; const selected = new Set(anchors.map(anchorKey)); - const updated = ShapeRecord.clone(initial) as PathShape; + const updated = EditorShapeRecord.clone(initial) as PathShape; for (const anchor of anchors) { const segment = updated.props.subpaths[anchor.subpathIndex]?.segments[anchor.segmentIndex]; @@ -630,7 +630,7 @@ function moveAnchors( } function moveControl(initial: PathShape, control: PathControlRef, point: Vec2): PathShape | null { - const updated = ShapeRecord.clone(initial) as PathShape; + const updated = EditorShapeRecord.clone(initial) as PathShape; const segment = updated.props.subpaths[control.subpathIndex]?.segments[control.segmentIndex]; if (!segment || segment.type === 'move' || segment.type === 'line') return null; const position = worldToLocal(point, initial); diff --git a/packages/core/src/tools/markdown.ts b/packages/core/src/tools/markdown.ts index 0347f74..ae2e014 100644 --- a/packages/core/src/tools/markdown.ts +++ b/packages/core/src/tools/markdown.ts @@ -1,5 +1,5 @@ import type { Action } from "../actions"; -import { createId, ShapeRecord } from "../model"; +import { createId, EditorShapeRecord } from "../editor-model"; import type { EditorState, ToolId } from "../reactivity"; import { canCreateShapeOnActiveLayer, getCurrentPage } from "../reactivity"; import type { Tool } from "./base"; @@ -38,7 +38,7 @@ export class MarkdownTool implements Tool { const shapeId = createId("shape"); - const shape = ShapeRecord.createMarkdown(currentPage.id, action.world.x, action.world.y, { + const shape = EditorShapeRecord.createMarkdown(currentPage.id, action.world.x, action.world.y, { md: "# Markdown\n\nEdit me...", w: 300, h: 200, diff --git a/packages/core/src/tools/pen.ts b/packages/core/src/tools/pen.ts index ed78e7f..875bde9 100644 --- a/packages/core/src/tools/pen.ts +++ b/packages/core/src/tools/pen.ts @@ -1,6 +1,6 @@ import type { Action } from "../actions"; -import type { BrushConfig, StrokePoint, StrokeStyle } from "../model"; -import { createId, ShapeRecord } from "../model"; +import type { BrushConfig, StrokePoint, StrokeStyle } from "../editor-model"; +import { createId, EditorShapeRecord } from "../editor-model"; import type { EditorState, ToolId } from "../reactivity"; import { canCreateShapeOnActiveLayer, getCurrentPage } from "../reactivity"; import type { Tool } from "../tools/base"; @@ -114,7 +114,7 @@ export class PenTool implements Tool { const strokeStyle = { ...this.getStrokeStyle() }; - const shape = ShapeRecord.createStroke(currentPage.id, 0, 0, { + const shape = EditorShapeRecord.createStroke(currentPage.id, 0, 0, { points: [firstPoint], brush: this.getBrush(), style: strokeStyle, diff --git a/packages/core/src/tools/select.ts b/packages/core/src/tools/select.ts index c7df455..077ac42 100644 --- a/packages/core/src/tools/select.ts +++ b/packages/core/src/tools/select.ts @@ -20,7 +20,7 @@ import { import { nearestPointOnPath, pathLength } from '../path-metrics'; import { Box2, clamp, Mat3, type Vec2, Vec2 as Vec2Ops } from '../math'; import { duplicateAndConnectSelection } from '../selection'; -import { BindingRecord, createId, ShapeRecord } from '../model'; +import { EditorBindingRecord, createId, EditorShapeRecord } from '../editor-model'; import { EditorState, getCurrentPage, getSelectionScopeShapes, selectionTarget, type ToolId } from '../reactivity'; import { snapAngle, type SnapResult } from '../snapping'; import type { Tool } from './base'; @@ -36,7 +36,7 @@ type SelectToolState = { /** Initial positions of shapes being dragged (shape id -> {x, y}) */ initialShapePositions: Map; /** Full shape snapshots used to preview nested affine movement. */ - initialShapes: Map; + initialShapes: Map; /** Marquee selection start point in world coordinates */ marqueeStart: Vec2 | null; /** Marquee selection end point in world coordinates */ @@ -48,7 +48,7 @@ type SelectToolState = { /** Bounds snapshot at the time handle drag started */ handleStartBounds: Box2 | null; /** Initial shapes snapshot for handle drags */ - handleInitialShapes: Map; + handleInitialShapes: Map; /** Rotation pivot in world coordinates */ rotationCenter: Vec2 | null; /** Starting angle for rotation handle */ @@ -79,7 +79,7 @@ type HandleKind = export type SelectSnapContext = { state: EditorState; selectionIds: string[]; - initialShapes: ReadonlyMap; + initialShapes: ReadonlyMap; leadPosition: Vec2; delta: Vec2; }; @@ -204,7 +204,7 @@ export class SelectTool implements Tool { return hitShapeId ? this.handleShapeClick(state, hitShapeId, action) : this.handleEmptyClick(state, action); } - private hitTestHandle(state: EditorState, point: Vec2): { handle: HandleKind; shape: ShapeRecord } | null { + private hitTestHandle(state: EditorState, point: Vec2): { handle: HandleKind; shape: EditorShapeRecord } | null { if (state.ui.selectionIds.length !== 1) { return null; } @@ -278,15 +278,15 @@ export class SelectTool implements Tool { this.toolState.cyclePendingTarget = null; } - private beginHandleDrag(state: EditorState, shape: ShapeRecord, handle: HandleKind, point: Vec2): EditorState { + private beginHandleDrag(state: EditorState, shape: EditorShapeRecord, handle: HandleKind, point: Vec2): EditorState { this.toolState.activeHandle = handle; this.toolState.handleShapeId = shape.id; this.toolState.handleStartBounds = shapeBounds(shape); this.toolState.handleInitialShapes.clear(); - this.toolState.handleInitialShapes.set(shape.id, ShapeRecord.clone(shape)); + this.toolState.handleInitialShapes.set(shape.id, EditorShapeRecord.clone(shape)); for (const descendant of Object.values(state.doc.shapes)) { if (hasSelectedAncestor(descendant, [shape.id], state)) { - this.toolState.handleInitialShapes.set(descendant.id, ShapeRecord.clone(descendant)); + this.toolState.handleInitialShapes.set(descendant.id, EditorShapeRecord.clone(descendant)); } } this.toolState.isDragging = false; @@ -352,13 +352,13 @@ export class SelectTool implements Tool { const shape = selectionState.doc.shapes[id]; if (shape) { this.toolState.initialShapePositions.set(id, { x: shape.x, y: shape.y }); - this.toolState.initialShapes.set(id, ShapeRecord.clone(shape)); + this.toolState.initialShapes.set(id, EditorShapeRecord.clone(shape)); } } for (const shape of Object.values(selectionState.doc.shapes)) { if (!selectionIds.includes(shape.id) && hasSelectedAncestor(shape, selectionIds, selectionState)) { this.toolState.initialShapePositions.set(shape.id, { x: shape.x, y: shape.y }); - this.toolState.initialShapes.set(shape.id, ShapeRecord.clone(shape)); + this.toolState.initialShapes.set(shape.id, EditorShapeRecord.clone(shape)); } } @@ -428,7 +428,7 @@ export class SelectTool implements Tool { this.toolState.activeHandle === 'rotate' ? action.world : this.snapHandlePoint(action.world, state, [shapeId]); - let updated: ShapeRecord | null = null; + let updated: EditorShapeRecord | null = null; if (this.toolState.activeHandle === 'rotate') { updated = this.rotateShape(state, initialShape, snappedPoint, action.modifiers.shift); } else if (this.toolState.activeHandle === 'arrow-label') { @@ -826,7 +826,7 @@ export class SelectTool implements Tool { return snapped; } - private getHandlePositions(state: EditorState, shape: ShapeRecord): Array<{ id: HandleKind; position: Vec2 }> { + private getHandlePositions(state: EditorState, shape: EditorShapeRecord): Array<{ id: HandleKind; position: Vec2 }> { const handles: Array<{ id: HandleKind; position: Vec2 }> = []; if (shape.type === 'text' && shape.props.textPath) { const position = textPathAnchorForShape(state, shape); @@ -893,12 +893,12 @@ export class SelectTool implements Tool { } private resizeRectLikeShape( - initial: ShapeRecord, + initial: EditorShapeRecord, _bounds: Box2, pointer: Vec2, handle: HandleKind, modifiers: { shift: boolean; alt: boolean } - ): ShapeRecord | null { + ): EditorShapeRecord | null { if ( initial.type !== 'rect' && initial.type !== 'ellipse' && @@ -987,15 +987,15 @@ export class SelectTool implements Tool { const translated = [...matrix] as Mat3; translated[6] += matrix[0] * minX + matrix[3] * minY; translated[7] += matrix[1] * minX + matrix[4] * minY; - let resized: ShapeRecord; + let resized: EditorShapeRecord; if (initial.type === 'text') resized = { ...initial, props: { ...initial.props, w: width } }; else if (initial.type === 'markdown') resized = { ...initial, props: { ...initial.props, w: width, h: height } }; - else resized = { ...initial, props: { ...initial.props, w: width, h: height } } as ShapeRecord; + else resized = { ...initial, props: { ...initial.props, w: width, h: height } } as EditorShapeRecord; return updateShapeTransform(resized, translated); } - private adjustArrowLabel(state: EditorState, initial: ShapeRecord, pointer: Vec2): ShapeRecord | null { + private adjustArrowLabel(state: EditorState, initial: EditorShapeRecord, pointer: Vec2): EditorShapeRecord | null { if ( initial.type !== 'arrow' || !initial.props.points || @@ -1023,7 +1023,7 @@ export class SelectTool implements Tool { }; } - private adjustTextPath(state: EditorState, initial: ShapeRecord, pointer: Vec2): ShapeRecord | null { + private adjustTextPath(state: EditorState, initial: EditorShapeRecord, pointer: Vec2): EditorShapeRecord | null { if (initial.type !== 'text' || !initial.props.textPath) return null; const path = supportingPathForText(state, initial); if (!path) return null; @@ -1038,11 +1038,11 @@ export class SelectTool implements Tool { private resizeLineShape( state: EditorState, - initial: ShapeRecord, + initial: EditorShapeRecord, pointer: Vec2, handle: HandleKind, constrainAngle: boolean - ): ShapeRecord | null { + ): EditorShapeRecord | null { if (initial.type !== 'line' && initial.type !== 'arrow') { return null; } @@ -1113,10 +1113,10 @@ export class SelectTool implements Tool { private rotateShape( state: EditorState, - initial: ShapeRecord, + initial: EditorShapeRecord, pointer: Vec2, constrainAngle: boolean - ): ShapeRecord | null { + ): EditorShapeRecord | null { if (!this.toolState.rotationCenter || this.toolState.rotationStartAngle === null) return null; const currentAngle = Math.atan2( pointer.y - this.toolState.rotationCenter.y, @@ -1169,7 +1169,7 @@ export class SelectTool implements Tool { * Try to add a point to an arrow segment at the clicked location * Returns updated state if successful, null otherwise */ - private tryAddPointToArrowSegment(state: EditorState, arrow: ShapeRecord, clickWorld: Vec2): EditorState | null { + private tryAddPointToArrowSegment(state: EditorState, arrow: EditorShapeRecord, clickWorld: Vec2): EditorState | null { if (arrow.type !== 'arrow' || !arrow.props.points || arrow.props.points.length < 2) { return null; } @@ -1241,7 +1241,7 @@ export class SelectTool implements Tool { const targetShape = state.doc.shapes[hitShapeId]; if (targetShape) { const anchor = computeNormalizedAnchor(endpointWorld, targetShape); - const binding = BindingRecord.create(arrowId, hitShapeId, handle, { + const binding = EditorBindingRecord.create(arrowId, hitShapeId, handle, { kind: 'edge', nx: anchor.nx, ny: anchor.ny @@ -1268,9 +1268,9 @@ function duplicateSelectionForDrag(state: EditorState): EditorState { for (const [oldId, newId] of mapping) { const original = state.doc.shapes[oldId]; if (!original) continue; - const copy = ShapeRecord.clone(original); + const copy = EditorShapeRecord.clone(original); const nextGroupId = copy.groupId ? mapping.get(copy.groupId) : undefined; - const cloned: ShapeRecord = { ...copy, id: newId, ...(nextGroupId ? { groupId: nextGroupId } : {}) }; + const cloned: EditorShapeRecord = { ...copy, id: newId, ...(nextGroupId ? { groupId: nextGroupId } : {}) }; if (cloned.type === 'arrow') { cloned.props = { ...cloned.props, start: { ...cloned.props.start }, end: { ...cloned.props.end } }; } @@ -1293,7 +1293,7 @@ function duplicateSelectionForDrag(state: EditorState): EditorState { const id = createId('binding'); bindingMapping.set(binding.id, id); bindings[id] = { - ...BindingRecord.clone(binding), + ...EditorBindingRecord.clone(binding), id, fromShapeId, toShapeId: mapping.get(binding.toShapeId) ?? binding.toShapeId @@ -1337,7 +1337,7 @@ function constrainAxis(delta: Vec2): Vec2 { return { x: 0, y: delta.y }; } -function hasSelectedAncestor(shape: ShapeRecord, selectedIds: string[], state: EditorState): boolean { +function hasSelectedAncestor(shape: EditorShapeRecord, selectedIds: string[], state: EditorState): boolean { let parentId = shape.groupId; while (parentId) { if (selectedIds.includes(parentId)) return true; @@ -1353,7 +1353,7 @@ function removeSelectedDescendants(state: EditorState, ids: string[]): string[] }); } -function updateShapeTransform(shape: ShapeRecord, matrix: Mat3): ShapeRecord { +function updateShapeTransform(shape: EditorShapeRecord, matrix: Mat3): EditorShapeRecord { return { ...shape, x: matrix[6], @@ -1363,7 +1363,7 @@ function updateShapeTransform(shape: ShapeRecord, matrix: Mat3): ShapeRecord { }; } -function translateShape(shape: ShapeRecord, delta: Vec2): ShapeRecord { +function translateShape(shape: EditorShapeRecord, delta: Vec2): EditorShapeRecord { const matrix = [...shapeTransform(shape)] as Mat3; matrix[6] += delta.x; matrix[7] += delta.y; diff --git a/packages/core/src/tools/shape.ts b/packages/core/src/tools/shape.ts index b5a86a2..c929bbc 100644 --- a/packages/core/src/tools/shape.ts +++ b/packages/core/src/tools/shape.ts @@ -2,7 +2,7 @@ import type { Action } from '../actions'; import { computeNormalizedAnchor, hitTestPoint, shapeBounds } from '../geom'; import { Vec2 } from '../math'; import { snapAngle } from '../snapping'; -import { BindingRecord, createId, ShapeRecord } from '../model'; +import { EditorBindingRecord, createId, EditorShapeRecord } from '../editor-model'; import type { EditorState, ToolId } from '../reactivity'; import { canCreateShapeOnActiveLayer, getCurrentPage } from '../reactivity'; import type { Tool } from '../tools/base'; @@ -148,7 +148,7 @@ export class RectTool implements Tool { const shapeId = createId('shape'); - const shape = ShapeRecord.createRect( + const shape = EditorShapeRecord.createRect( currentPage.id, action.world.x, action.world.y, @@ -292,7 +292,7 @@ export class FrameTool implements Tool { const page = getCurrentPage(state); if (!page) return state; const id = createId('shape'); - const shape = ShapeRecord.createContainer( + const shape = EditorShapeRecord.createContainer( page.id, action.world.x, action.world.y, @@ -435,7 +435,7 @@ export class EllipseTool implements Tool { const shapeId = createId('shape'); - const shape = ShapeRecord.createEllipse( + const shape = EditorShapeRecord.createEllipse( currentPage.id, action.world.x, action.world.y, @@ -597,7 +597,7 @@ export class LineTool implements Tool { const shapeId = createId('shape'); - const shape = ShapeRecord.createLine( + const shape = EditorShapeRecord.createLine( currentPage.id, action.world.x, action.world.y, @@ -756,7 +756,7 @@ export class ArrowTool implements Tool { const shapeId = createId('shape'); - const shape = ShapeRecord.createArrow( + const shape = EditorShapeRecord.createArrow( currentPage.id, action.world.x, action.world.y, @@ -919,7 +919,7 @@ export class ArrowTool implements Tool { const targetShape = state.doc.shapes[startHitId]; if (targetShape) { const anchor = computeNormalizedAnchor(startWorld, targetShape); - const binding = BindingRecord.create(arrowId, startHitId, 'start', { + const binding = EditorBindingRecord.create(arrowId, startHitId, 'start', { kind: 'edge', nx: anchor.nx, ny: anchor.ny @@ -937,7 +937,7 @@ export class ArrowTool implements Tool { const targetShape = state.doc.shapes[endHitId]; if (targetShape) { const anchor = computeNormalizedAnchor(endWorld, targetShape); - const binding = BindingRecord.create(arrowId, endHitId, 'end', { + const binding = EditorBindingRecord.create(arrowId, endHitId, 'end', { kind: 'edge', nx: anchor.nx, ny: anchor.ny diff --git a/packages/core/src/tools/text.ts b/packages/core/src/tools/text.ts index 223592d..5d47573 100644 --- a/packages/core/src/tools/text.ts +++ b/packages/core/src/tools/text.ts @@ -1,5 +1,5 @@ import type { Action } from "../actions"; -import { createId, ShapeRecord } from "../model"; +import { createId, EditorShapeRecord } from "../editor-model"; import type { EditorState, ToolId } from "../reactivity"; import { canCreateShapeOnActiveLayer, getCurrentPage } from "../reactivity"; import type { Tool } from "./base"; @@ -46,7 +46,7 @@ export class TextTool implements Tool { const shapeId = createId("shape"); - const shape = ShapeRecord.createText(currentPage.id, action.world.x, action.world.y, { + const shape = EditorShapeRecord.createText(currentPage.id, action.world.x, action.world.y, { text: "Text", ...creationStylePolicy(this.getAppearance()).text, }, shapeId); diff --git a/packages/core/src/vector-effects.ts b/packages/core/src/vector-effects.ts index 6b53477..7ef23c4 100644 --- a/packages/core/src/vector-effects.ts +++ b/packages/core/src/vector-effects.ts @@ -1,6 +1,6 @@ import { localToWorld, worldToLocal } from './geom'; import type { EditorState } from './reactivity'; -import type { PathGeometry, PathSegment, ShapeRecord } from './model'; +import type { PathGeometry, PathSegment, EditorShapeRecord } from './editor-model'; import type { Vec2 } from './math'; /** Returns whether the current selection can make one path clip another shape. */ @@ -20,14 +20,14 @@ export function canClipSelection(state: EditorState): boolean { */ export function clipSelection(state: EditorState): EditorState | null { if (!canClipSelection(state)) return null; - const selected = state.ui.selectionIds.map((id) => state.doc.shapes[id]).filter(Boolean) as ShapeRecord[]; + const selected = state.ui.selectionIds.map((id) => state.doc.shapes[id]).filter(Boolean) as EditorShapeRecord[]; const source = selected.find((shape) => shape.type === 'path'); const target = selected.find((shape) => shape.type !== 'path'); if (!source || source.type !== 'path' || !target) return null; const clipPath = transformGeometry(source.props, (point) => worldToLocal(localToWorld(source, point), target)); const shapes = { ...state.doc.shapes }; - shapes[target.id] = { ...target, props: { ...target.props, clipPath } } as ShapeRecord; + shapes[target.id] = { ...target, props: { ...target.props, clipPath } } as EditorShapeRecord; delete shapes[source.id]; const pages = Object.fromEntries( Object.entries(state.doc.pages).map(([id, page]) => [ @@ -57,7 +57,7 @@ export function clipSelection(state: EditorState): EditorState | null { export function removeClipFromSelection(state: EditorState): EditorState | null { const targets = state.ui.selectionIds .map((id) => state.doc.shapes[id]) - .filter((shape): shape is ShapeRecord => + .filter((shape): shape is EditorShapeRecord => Boolean(shape?.props && 'clipPath' in shape.props && shape.props.clipPath) ); if (targets.length === 0) return null; @@ -66,7 +66,7 @@ export function removeClipFromSelection(state: EditorState): EditorState | null for (const target of targets) { const props = { ...target.props } as Record; delete props.clipPath; - shapes[target.id] = { ...target, props } as ShapeRecord; + shapes[target.id] = { ...target, props } as EditorShapeRecord; } return { ...state, doc: { ...state.doc, shapes }, ui: { ...state.ui, selectionIds: [...targetIds] } }; } diff --git a/packages/core/tests/arrow-bindings.test.ts b/packages/core/tests/arrow-bindings.test.ts index 5d4f795..c6f62ed 100644 --- a/packages/core/tests/arrow-bindings.test.ts +++ b/packages/core/tests/arrow-bindings.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { Action } from "../src/actions"; import { resolveArrowEndpoints } from "../src/geom"; -import { BindingRecord, PageRecord, ShapeRecord } from "../src/model"; +import { EditorBindingRecord, EditorPageRecord, EditorShapeRecord } from "../src/editor-model"; import { EditorState } from "../src/reactivity"; import { SelectTool } from "../src/tools/select"; @@ -10,14 +10,14 @@ describe("Arrow binding behavior", () => { it("should preserve bindings when an arrow is moved (dragged)", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const rectStart = ShapeRecord.createRect(page.id, 50, 50, { + const rectStart = EditorShapeRecord.createRect(page.id, 50, 50, { w: 50, h: 50, fill: "#fff", @@ -25,7 +25,7 @@ describe("Arrow binding behavior", () => { radius: 0, }); - const rectEnd = ShapeRecord.createRect(page.id, 250, 50, { + const rectEnd = EditorShapeRecord.createRect(page.id, 250, 50, { w: 50, h: 50, fill: "#fff", @@ -33,14 +33,14 @@ describe("Arrow binding behavior", () => { radius: 0, }); - const arrow = ShapeRecord.createArrow(page.id, 100, 75, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 75, { points: [{ x: 0, y: 0 }, { x: 150, y: 0 }], start: { kind: "bound", bindingId: "binding-start" }, end: { kind: "bound", bindingId: "binding-end" }, style: { stroke: "#000", width: 2, headEnd: true }, }); - const bindingStart = BindingRecord.create( + const bindingStart = EditorBindingRecord.create( arrow.id, rectStart.id, "start", @@ -48,7 +48,7 @@ describe("Arrow binding behavior", () => { "binding-start", ); - const bindingEnd = BindingRecord.create( + const bindingEnd = EditorBindingRecord.create( arrow.id, rectEnd.id, "end", @@ -117,23 +117,23 @@ describe("Arrow binding behavior", () => { it("should NOT remove bindings when dragging an endpoint handle", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const rect = ShapeRecord.createRect(page.id, 250, 50, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }); + const rect = EditorShapeRecord.createRect(page.id, 250, 50, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }); - const arrow = ShapeRecord.createArrow(page.id, 100, 75, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 75, { points: [{ x: 0, y: 0 }, { x: 150, y: 0 }], start: { kind: "free" }, end: { kind: "bound", bindingId: "binding-end" }, style: { stroke: "#000", width: 2, headEnd: true }, }); - const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); + const binding = EditorBindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); state = { ...state, @@ -189,23 +189,23 @@ describe("Arrow binding behavior", () => { it("should be able to click and drag arrow endpoint handles when arrow is bound", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const rect = ShapeRecord.createRect(page.id, 250, 50, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }); + const rect = EditorShapeRecord.createRect(page.id, 250, 50, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }); - const arrow = ShapeRecord.createArrow(page.id, 100, 75, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 75, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "bound", bindingId: "binding-end" }, style: { stroke: "#000", width: 2, headEnd: true }, }); - const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); + const binding = EditorBindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); state = { ...state, @@ -264,14 +264,14 @@ describe("Arrow binding behavior", () => { it("should position arrow endpoints with offset to account for stroke widths", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const rect = ShapeRecord.createRect(page.id, 200, 100, { + const rect = EditorShapeRecord.createRect(page.id, 200, 100, { w: 100, h: 100, fill: "#fff", @@ -279,14 +279,14 @@ describe("Arrow binding behavior", () => { radius: 0, }); - const arrow = ShapeRecord.createArrow(page.id, 100, 150, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 150, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "bound", bindingId: "binding-end" }, style: { stroke: "#000", width: 2, headEnd: true }, }); - const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); + const binding = EditorBindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); state = { ...state, @@ -311,14 +311,14 @@ describe("Arrow binding behavior", () => { it("should apply offset for arrows with different stroke widths", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const rect = ShapeRecord.createRect(page.id, 200, 100, { + const rect = EditorShapeRecord.createRect(page.id, 200, 100, { w: 100, h: 100, fill: "#fff", @@ -326,14 +326,14 @@ describe("Arrow binding behavior", () => { radius: 0, }); - const arrow = ShapeRecord.createArrow(page.id, 100, 150, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 150, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "bound", bindingId: "binding-end" }, style: { stroke: "#000", width: 4, headEnd: true }, }); - const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); + const binding = EditorBindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); state = { ...state, @@ -358,14 +358,14 @@ describe("Arrow binding behavior", () => { it("should not apply offset for center anchors", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const rect = ShapeRecord.createRect(page.id, 200, 100, { + const rect = EditorShapeRecord.createRect(page.id, 200, 100, { w: 100, h: 100, fill: "#fff", @@ -373,14 +373,14 @@ describe("Arrow binding behavior", () => { radius: 0, }); - const arrow = ShapeRecord.createArrow(page.id, 100, 150, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 150, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "bound", bindingId: "binding-end" }, style: { stroke: "#000", width: 2, headEnd: true }, }); - const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); + const binding = EditorBindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); state = { ...state, @@ -404,14 +404,14 @@ describe("Arrow binding behavior", () => { it("should preserve intermediate points when dragging bound endpoints", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const rect = ShapeRecord.createRect(page.id, 300, 100, { + const rect = EditorShapeRecord.createRect(page.id, 300, 100, { w: 100, h: 100, fill: "#fff", @@ -419,14 +419,14 @@ describe("Arrow binding behavior", () => { radius: 0, }); - const arrow = ShapeRecord.createArrow(page.id, 100, 100, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 100, { points: [{ x: 0, y: 0 }, { x: 100, y: 50 }, { x: 200, y: 0 }], start: { kind: "free" }, end: { kind: "bound", bindingId: "binding-end" }, style: { stroke: "#000", width: 2, headEnd: true }, }); - const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); + const binding = EditorBindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); state = { ...state, diff --git a/packages/core/tests/arrow-curved.test.ts b/packages/core/tests/arrow-curved.test.ts index 4ca3e22..4a93dae 100644 --- a/packages/core/tests/arrow-curved.test.ts +++ b/packages/core/tests/arrow-curved.test.ts @@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest'; import { Action } from '../src/actions'; import { arrowGeometryForShape, - BindingRecord, + EditorBindingRecord, EditorState, SnapshotCommand, - PageRecord, + EditorPageRecord, SelectTool, - ShapeRecord, + EditorShapeRecord, Store } from '../src'; @@ -34,8 +34,8 @@ function pointerUp(world: { x: number; y: number }) { describe('curved arrow bend editing', () => { it('stores bend state through the direct manipulation handle', () => { - const page = PageRecord.create('Curved arrows', 'page:curved'); - const arrow = ShapeRecord.createArrow( + const page = EditorPageRecord.create('Curved arrows', 'page:curved'); + const arrow = EditorShapeRecord.createArrow( page.id, 100, 100, @@ -88,15 +88,15 @@ describe('curved arrow bend editing', () => { }); it('keeps bound endpoints resolved while the bend changes', () => { - const page = PageRecord.create('Bound curve', 'page:bound-curve'); - const target = ShapeRecord.createRect( + const page = EditorPageRecord.create('Bound curve', 'page:bound-curve'); + const target = EditorShapeRecord.createRect( page.id, 200, -25, { w: 50, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'rect:target' ); - const arrow = ShapeRecord.createArrow( + const arrow = EditorShapeRecord.createArrow( page.id, 0, 0, @@ -112,7 +112,7 @@ describe('curved arrow bend editing', () => { }, 'arrow:bound-curve' ); - const binding = BindingRecord.create( + const binding = EditorBindingRecord.create( arrow.id, target.id, 'end', @@ -158,8 +158,8 @@ describe('curved arrow bend editing', () => { }); it('round-trips curved routing through history and document serialization', () => { - const page = PageRecord.create('History curve', 'page:history-curve'); - const arrow = ShapeRecord.createArrow( + const page = EditorPageRecord.create('History curve', 'page:history-curve'); + const arrow = EditorShapeRecord.createArrow( page.id, 0, 0, diff --git a/packages/core/tests/arrow-label-routing.test.ts b/packages/core/tests/arrow-label-routing.test.ts index 93a155f..aa71911 100644 --- a/packages/core/tests/arrow-label-routing.test.ts +++ b/packages/core/tests/arrow-label-routing.test.ts @@ -4,7 +4,7 @@ import { computePolylineLength, getPointAtDistance, } from "../src/geom"; -import type { ArrowShape } from "../src/model"; +import type { ArrowShape } from "../src/editor-model"; describe("Arrow label placement under zoom/pan", () => { it("should maintain label position relative to arrow when zooming", () => { diff --git a/packages/core/tests/arrow-multipoint.test.ts b/packages/core/tests/arrow-multipoint.test.ts index 318a7ca..398e8b1 100644 --- a/packages/core/tests/arrow-multipoint.test.ts +++ b/packages/core/tests/arrow-multipoint.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { Action } from "../src/actions"; -import { BindingRecord, PageRecord, ShapeRecord } from "../src/model"; +import { EditorBindingRecord, EditorPageRecord, EditorShapeRecord } from "../src/editor-model"; import { EditorState } from "../src/reactivity"; import { SelectTool } from "../src/tools/select"; @@ -9,13 +9,13 @@ describe("Arrow multi-point editing", () => { it("should allow dragging an intermediate point", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const arrow = ShapeRecord.createArrow(page.id, 100, 100, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 100, { points: [{ x: 0, y: 0 }, { x: 50, y: 50 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -77,7 +77,7 @@ describe("Arrow multi-point editing", () => { it("should preserve bindings when dragging intermediate points", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, @@ -85,7 +85,7 @@ describe("Arrow multi-point editing", () => { }; // Create a target shape - const targetRect = ShapeRecord.createRect(page.id, 300, 100, { + const targetRect = EditorShapeRecord.createRect(page.id, 300, 100, { w: 100, h: 100, fill: "#fff", @@ -93,14 +93,14 @@ describe("Arrow multi-point editing", () => { radius: 0, }); - const arrow = ShapeRecord.createArrow(page.id, 100, 100, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 100, { points: [{ x: 0, y: 0 }, { x: 100, y: 50 }, { x: 200, y: 0 }], start: { kind: "free" }, end: { kind: "bound", bindingId: "binding-1" }, style: { stroke: "#000", width: 2, headEnd: true }, }); - const binding = BindingRecord.create(arrow.id, targetRect.id, "end", { kind: "center" }, "binding-1"); + const binding = EditorBindingRecord.create(arrow.id, targetRect.id, "end", { kind: "center" }, "binding-1"); state = { ...state, @@ -153,13 +153,13 @@ describe("Arrow multi-point editing", () => { it("should add a point when Alt+clicking on a segment", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const arrow = ShapeRecord.createArrow(page.id, 100, 100, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 100, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -204,13 +204,13 @@ describe("Arrow multi-point editing", () => { it("should not add a point when Alt+clicking far from any segment", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const arrow = ShapeRecord.createArrow(page.id, 100, 100, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 100, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -253,13 +253,13 @@ describe("Arrow multi-point editing", () => { it("should remove an intermediate point when Delete is pressed while dragging", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const arrow = ShapeRecord.createArrow(page.id, 100, 100, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 100, { points: [{ x: 0, y: 0 }, { x: 50, y: 50 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -305,13 +305,13 @@ describe("Arrow multi-point editing", () => { it("should not remove points if it would leave less than 2 points", () => { let state = EditorState.create(); - const page = PageRecord.create("Test Page"); + const page = EditorPageRecord.create("Test Page"); state = { ...state, doc: { ...state.doc, pages: { [page.id]: page } }, ui: { ...state.ui, currentPageId: page.id }, }; - const arrow = ShapeRecord.createArrow(page.id, 100, 100, { + const arrow = EditorShapeRecord.createArrow(page.id, 100, 100, { points: [{ x: 0, y: 0 }, { x: 50, y: 50 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, diff --git a/packages/core/tests/boolean-paths.test.ts b/packages/core/tests/boolean-paths.test.ts index cf878f8..97715ab 100644 --- a/packages/core/tests/boolean-paths.test.ts +++ b/packages/core/tests/boolean-paths.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { applyBooleanPathOperation, canBooleanPathSelection, PageRecord, ShapeRecord } from '../src'; +import { applyBooleanPathOperation, canBooleanPathSelection, EditorPageRecord, EditorShapeRecord } from '../src'; import type { EditorState } from '../src'; function rectangle(pageId: string, x: number, y: number, size: number, id: string) { - return ShapeRecord.createPath( + return EditorShapeRecord.createPath( pageId, x, y, @@ -28,7 +28,7 @@ function rectangle(pageId: string, x: number, y: number, size: number, id: strin } function stateFor(paths: ReturnType[]): EditorState { - const page = PageRecord.create('Boolean paths', 'page:boolean'); + const page = EditorPageRecord.create('Boolean paths', 'page:boolean'); page.shapeIds = paths.map((path) => path.id); return { doc: { diff --git a/packages/core/tests/canonical.test.ts b/packages/core/tests/canonical.test.ts index f92e325..803cc39 100644 --- a/packages/core/tests/canonical.test.ts +++ b/packages/core/tests/canonical.test.ts @@ -5,12 +5,12 @@ import { toCanonicalDocumentSnapshot } from '../src/persistence/canonical'; import { contentObjectToCard } from '../src/cards'; -import { BindingRecord, LayerRecord, PageRecord, ShapeRecord, type Document, type PathProps } from '../src/model'; +import { EditorBindingRecord, EditorLayerRecord, EditorPageRecord, EditorShapeRecord, type EditorDocument, type PathProps } from '../src/editor-model'; describe('toCanonicalDocumentSnapshot', () => { it('projects browser shapes into the canonical renderer input', () => { - const page = PageRecord.create('Page 1', 'page:one'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:one'); + const rect = EditorShapeRecord.createRect( page.id, 10, 20, @@ -41,8 +41,8 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('round-trips native clip paths and filters through the canonical projection', () => { - const page = PageRecord.create('Page 1', 'page:effects'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:effects'); + const rect = EditorShapeRecord.createRect( page.id, 0, 0, @@ -80,15 +80,15 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('round-trips typed relationships through the native projection', () => { - const page = PageRecord.create('Page 1', 'page:one'); - const source = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:one'); + const source = EditorShapeRecord.createRect( page.id, 0, 0, { w: 40, h: 20, fill: 'red', stroke: 'none', radius: 0 }, 'shape:source' ); - const target = ShapeRecord.createRect( + const target = EditorShapeRecord.createRect( page.id, 100, 0, @@ -96,8 +96,8 @@ describe('toCanonicalDocumentSnapshot', () => { 'shape:target' ); page.shapeIds = [source.id, target.id]; - const relation = BindingRecord.createRelation(source.id, target.id, 'depends_on', 'binding:depends-on'); - const document: Document = { + const relation = EditorBindingRecord.createRelation(source.id, target.id, 'depends_on', 'binding:depends-on'); + const document: EditorDocument = { pages: { [page.id]: page }, shapes: { [source.id]: source, [target.id]: target }, bindings: { [relation.id]: relation } @@ -116,7 +116,7 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('persists card metadata and child ordering in the native container', () => { - const page = PageRecord.create('Page 1', 'page:one'); + const page = EditorPageRecord.create('Page 1', 'page:one'); const cardShapes = contentObjectToCard( 'page:one', { x: 10, y: 20 }, @@ -153,8 +153,8 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('preserves object metadata in canonical projection and patches', () => { - const page = PageRecord.create('Page 1', 'page:one'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:one'); + const rect = EditorShapeRecord.createRect( page.id, 10, 20, @@ -176,7 +176,7 @@ describe('toCanonicalDocumentSnapshot', () => { provenance: { actorId: 'actor:test', origin: 'human', timestamp: 42, source: 'seed' } }; page.shapeIds.push(rect.id); - const before: Document = { pages: { [page.id]: page }, shapes: { [rect.id]: rect }, bindings: {} }; + const before: EditorDocument = { pages: { [page.id]: page }, shapes: { [rect.id]: rect }, bindings: {} }; const snapshot = toCanonicalDocumentSnapshot(before, { documentId: 'document:metadata' }); expect(snapshot.document.shapes[rect.id]?.metadata).toMatchObject({ name: 'Gateway', @@ -188,7 +188,7 @@ describe('toCanonicalDocumentSnapshot', () => { provenance: { actor_id: 'actor:test', source: 'seed' } }); - const after: Document = { + const after: EditorDocument = { ...before, shapes: { [rect.id]: { @@ -218,8 +218,8 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('turns editor moves into semantic Rust reconciliation patches', () => { - const page = PageRecord.create('Page 1', 'page:one'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:one'); + const rect = EditorShapeRecord.createRect( page.id, 10, 20, @@ -227,8 +227,8 @@ describe('toCanonicalDocumentSnapshot', () => { 'shape:rect' ); page.shapeIds.push(rect.id); - const before: Document = { pages: { [page.id]: page }, shapes: { [rect.id]: rect }, bindings: {} }; - const after: Document = { ...before, shapes: { [rect.id]: { ...rect, x: 30, y: 45 } } }; + const before: EditorDocument = { pages: { [page.id]: page }, shapes: { [rect.id]: rect }, bindings: {} }; + const after: EditorDocument = { ...before, shapes: { [rect.id]: { ...rect, x: 30, y: 45 } } }; const request = createEditorReconciliationRequest(before, after, { actor_id: 'browser', @@ -247,8 +247,8 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('turns a shape kind change into a native conversion patch', () => { - const page = PageRecord.create('Page 1', 'page:one'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:one'); + const rect = EditorShapeRecord.createRect( page.id, 10, 20, @@ -256,8 +256,8 @@ describe('toCanonicalDocumentSnapshot', () => { 'shape:rect' ); page.shapeIds.push(rect.id); - const before: Document = { pages: { [page.id]: page }, shapes: { [rect.id]: rect }, bindings: {} }; - const after: Document = { + const before: EditorDocument = { pages: { [page.id]: page }, shapes: { [rect.id]: rect }, bindings: {} }; + const after: EditorDocument = { ...before, shapes: { [rect.id]: { ...rect, type: 'ellipse', props: { w: 40, h: 20, fill: 'red', stroke: 'none' } } } }; @@ -281,7 +281,7 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('routes topology edits as canonical path patches', () => { - const page = PageRecord.create('Page 1', 'page:one'); + const page = EditorPageRecord.create('Page 1', 'page:one'); const props: PathProps = { subpaths: [ { @@ -296,10 +296,10 @@ describe('toCanonicalDocumentSnapshot', () => { fill_rule: 'nonzero', fill: '#fff' }; - const path = ShapeRecord.createPath(page.id, 0, 0, props, 'shape:path'); + const path = EditorShapeRecord.createPath(page.id, 0, 0, props, 'shape:path'); page.shapeIds.push(path.id); - const before: Document = { pages: { [page.id]: page }, shapes: { [path.id]: path }, bindings: {} }; - const after: Document = { + const before: EditorDocument = { pages: { [page.id]: page }, shapes: { [path.id]: path }, bindings: {} }; + const after: EditorDocument = { ...before, shapes: { [path.id]: { @@ -337,14 +337,14 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('reconciles page and layer structure as semantic patches', () => { - const page = PageRecord.create('Page 1', 'page:one'); - const back = LayerRecord.create(page.id, 'Back', 'layer:back'); + const page = EditorPageRecord.create('Page 1', 'page:one'); + const back = EditorLayerRecord.create(page.id, 'Back', 'layer:back'); page.layerIds = [back.id]; - const front = LayerRecord.create(page.id, 'Front', 'layer:front'); - const nextPage = PageRecord.create('Page 2', 'page:two'); - const nextLayer = LayerRecord.create(nextPage.id, 'Default', 'layer:two'); - const before: Document = { pages: { [page.id]: page }, layers: { [back.id]: back }, shapes: {}, bindings: {} }; - const after: Document = { + const front = EditorLayerRecord.create(page.id, 'Front', 'layer:front'); + const nextPage = EditorPageRecord.create('Page 2', 'page:two'); + const nextLayer = EditorLayerRecord.create(nextPage.id, 'Default', 'layer:two'); + const before: EditorDocument = { pages: { [page.id]: page }, layers: { [back.id]: back }, shapes: {}, bindings: {} }; + const after: EditorDocument = { pages: { [page.id]: { ...page, layerIds: [front.id, back.id] }, [nextPage.id]: { ...nextPage, layerIds: [nextLayer.id] } @@ -378,10 +378,10 @@ describe('toCanonicalDocumentSnapshot', () => { }); it('reconciles a layer deletion with a native move disposition', () => { - const page = PageRecord.create('Page 1', 'page:one'); - const source = LayerRecord.create(page.id, 'Source', 'layer:source'); - const destination = LayerRecord.create(page.id, 'Destination', 'layer:destination'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:one'); + const source = EditorLayerRecord.create(page.id, 'Source', 'layer:source'); + const destination = EditorLayerRecord.create(page.id, 'Destination', 'layer:destination'); + const shape = EditorShapeRecord.createRect( page.id, 10, 20, @@ -392,13 +392,13 @@ describe('toCanonicalDocumentSnapshot', () => { page.layerIds = [source.id, destination.id]; page.shapeIds = [shape.id]; source.shapeIds = [shape.id]; - const before: Document = { + const before: EditorDocument = { pages: { [page.id]: page }, layers: { [source.id]: source, [destination.id]: destination }, shapes: { [shape.id]: shape }, bindings: {} }; - const after: Document = { + const after: EditorDocument = { pages: { [page.id]: { ...page, layerIds: [destination.id], shapeIds: [shape.id] } }, layers: { [destination.id]: { ...destination, shapeIds: [shape.id] } }, shapes: { [shape.id]: { ...shape, layerId: destination.id } }, diff --git a/packages/core/tests/canvas-structure.test.ts b/packages/core/tests/canvas-structure.test.ts index 707e7d3..cd665d9 100644 --- a/packages/core/tests/canvas-structure.test.ts +++ b/packages/core/tests/canvas-structure.test.ts @@ -4,9 +4,9 @@ import { computeCurvedPath, exportToSVG, FrameTool, - PageRecord, + EditorPageRecord, SelectTool, - ShapeRecord, + EditorShapeRecord, Store, type EditorState } from '../src'; @@ -16,8 +16,8 @@ const down = { left: true, middle: false, right: false }; const up = { left: false, middle: false, right: false }; function frameState(): EditorState { - const page = PageRecord.create('Canvas', 'page:canvas'); - const card = ShapeRecord.createRect( + const page = EditorPageRecord.create('Canvas', 'page:canvas'); + const card = EditorShapeRecord.createRect( page.id, 20, 30, @@ -57,7 +57,7 @@ describe('canvas structure', () => { it('exports a selected frame together with its descendants and title', () => { let state = frameState(); - const frame = ShapeRecord.createContainer( + const frame = EditorShapeRecord.createContainer( 'page:canvas', 0, 0, diff --git a/packages/core/tests/committed-geometry.test.ts b/packages/core/tests/committed-geometry.test.ts index 7effafa..85b8557 100644 --- a/packages/core/tests/committed-geometry.test.ts +++ b/packages/core/tests/committed-geometry.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { boundsFromOutline, computeOutline, pathGeometryBounds } from '../src/geom'; -import type { BrushConfig, PathGeometry, StrokePoint } from '../src/model'; +import type { BrushConfig, PathGeometry, StrokePoint } from '../src/editor-model'; import fixture from '../../../fixtures/native/geometry/committed.json'; type BoundsFixture = { x: number; y: number; width: number; height: number }; diff --git a/packages/core/tests/direct-selection.test.ts b/packages/core/tests/direct-selection.test.ts index 26ab568..82d36bd 100644 --- a/packages/core/tests/direct-selection.test.ts +++ b/packages/core/tests/direct-selection.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { Action, DirectSelectTool, Modifiers, PageRecord, ShapeRecord, Store, hitTestPoint, shapeBounds } from '../src'; -import type { PathProps, PathSelection } from '../src/model'; +import { Action, DirectSelectTool, Modifiers, EditorPageRecord, EditorShapeRecord, Store, hitTestPoint, shapeBounds } from '../src'; +import type { PathProps, PathSelection } from '../src/editor-model'; const modifiers = Modifiers.create(); const buttons = { left: true, middle: false, right: false }; @@ -19,7 +19,7 @@ function pointerUp(x: number, y: number) { } function createPathState() { - const page = PageRecord.create('Page', 'page:direct-selection'); + const page = EditorPageRecord.create('Page', 'page:direct-selection'); const geometry: PathProps = { subpaths: [ { @@ -42,7 +42,7 @@ function createPathState() { fill_rule: 'nonzero', fill: '#fff' }; - const path = ShapeRecord.createPath(page.id, 0, 0, geometry, 'path:direct'); + const path = EditorShapeRecord.createPath(page.id, 0, 0, geometry, 'path:direct'); page.shapeIds = [path.id]; const state = new Store({ doc: { pages: { [page.id]: page }, shapes: { [path.id]: path }, bindings: {} }, @@ -136,8 +136,8 @@ describe('DirectSelectTool', () => { }); it('updates bounds and hit regions during a path-edit preview', () => { - const page = PageRecord.create('Page', 'page:direct-preview'); - const path = ShapeRecord.createPath( + const page = EditorPageRecord.create('Page', 'page:direct-preview'); + const path = EditorShapeRecord.createPath( page.id, 0, 0, @@ -185,8 +185,8 @@ describe('DirectSelectTool', () => { }); it('edits stroke width points independently from the stroke path points', () => { - const page = PageRecord.create('Page', 'page:direct-width'); - const stroke = ShapeRecord.createStroke( + const page = EditorPageRecord.create('Page', 'page:direct-width'); + const stroke = EditorShapeRecord.createStroke( page.id, 0, 0, diff --git a/packages/core/tests/export.test.ts b/packages/core/tests/export.test.ts index f4f53b8..059662c 100644 --- a/packages/core/tests/export.test.ts +++ b/packages/core/tests/export.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest'; import { exportToSVG } from '../src/export'; -import { PageRecord, ShapeRecord } from '../src/model'; +import { EditorPageRecord, EditorShapeRecord } from '../src/editor-model'; import { EditorState } from '../src/reactivity'; function createTestState() { const state = EditorState.create(); - const page = PageRecord.create('Test Page'); + const page = EditorPageRecord.create('Test Page'); state.doc.pages[page.id] = page; state.ui.currentPageId = page.id; return { state, pageId: page.id }; @@ -22,7 +22,7 @@ describe('exportToSVG', () => { it('should export variable-width strokes as outlined paths', () => { const { state, pageId } = createTestState(); - const stroke = ShapeRecord.createStroke(pageId, 0, 0, { + const stroke = EditorShapeRecord.createStroke(pageId, 0, 0, { points: [ [0, 0], [100, 0] @@ -47,7 +47,7 @@ describe('exportToSVG', () => { it('omits the synthetic background when transparent output is requested', () => { const { state, pageId } = createTestState(); - const rect = ShapeRecord.createRect(pageId, 10, 20, { w: 100, h: 50, fill: 'red', stroke: 'black', radius: 0 }); + const rect = EditorShapeRecord.createRect(pageId, 10, 20, { w: 100, h: 50, fill: 'red', stroke: 'black', radius: 0 }); state.doc.shapes[rect.id] = rect; state.doc.pages[pageId].shapeIds.push(rect.id); @@ -60,7 +60,7 @@ describe('exportToSVG', () => { it('should export SVG with a rectangle shape', () => { const { state, pageId } = createTestState(); - const rect = ShapeRecord.createRect(pageId, 10, 20, { w: 100, h: 50, fill: 'red', stroke: 'black', radius: 0 }); + const rect = EditorShapeRecord.createRect(pageId, 10, 20, { w: 100, h: 50, fill: 'red', stroke: 'black', radius: 0 }); state.doc.shapes[rect.id] = rect; state.doc.pages[pageId].shapeIds.push(rect.id); @@ -77,7 +77,7 @@ describe('exportToSVG', () => { it('should export text on path as a native SVG textPath reference', () => { const { state, pageId } = createTestState(); - const path = ShapeRecord.createPath(pageId, 10, 20, { + const path = EditorShapeRecord.createPath(pageId, 10, 20, { subpaths: [ { segments: [ @@ -91,7 +91,7 @@ describe('exportToSVG', () => { stroke: '#555555', stroke_width: 2 }); - const text = ShapeRecord.createText(pageId, 0, 0, { + const text = EditorShapeRecord.createText(pageId, 0, 0, { text: 'Along the line', fontSize: 16, fontFamily: 'sans-serif', @@ -114,7 +114,7 @@ describe('exportToSVG', () => { it('should export gradient definitions without flattening their stops', () => { const { state, pageId } = createTestState(); - const rect = ShapeRecord.createRect(pageId, 0, 0, { + const rect = EditorShapeRecord.createRect(pageId, 0, 0, { w: 100, h: 50, fill: { @@ -147,7 +147,7 @@ describe('exportToSVG', () => { it('should export native clipping, masks, and filters', () => { const { state, pageId } = createTestState(); - const rect = ShapeRecord.createRect(pageId, 10, 20, { + const rect = EditorShapeRecord.createRect(pageId, 10, 20, { w: 100, h: 50, fill: 'red', @@ -204,7 +204,7 @@ describe('exportToSVG', () => { it('should export semantic metadata for ordinary shapes', () => { const { state, pageId } = createTestState(); - const rect = ShapeRecord.createRect(pageId, 10, 20, { w: 100, h: 50, fill: 'red', stroke: 'black', radius: 0 }); + const rect = EditorShapeRecord.createRect(pageId, 10, 20, { w: 100, h: 50, fill: 'red', stroke: 'black', radius: 0 }); rect.metadata = { name: 'Gateway', title: null, @@ -233,7 +233,7 @@ describe('exportToSVG', () => { it('should export SVG with an ellipse shape', () => { const { state, pageId } = createTestState(); - const ellipse = ShapeRecord.createEllipse(pageId, 10, 20, { w: 100, h: 50, fill: 'blue', stroke: 'green' }); + const ellipse = EditorShapeRecord.createEllipse(pageId, 10, 20, { w: 100, h: 50, fill: 'blue', stroke: 'green' }); state.doc.shapes[ellipse.id] = ellipse; state.doc.pages[pageId].shapeIds.push(ellipse.id); @@ -249,7 +249,7 @@ describe('exportToSVG', () => { it('should export SVG with a line shape', () => { const { state, pageId } = createTestState(); - const line = ShapeRecord.createLine(pageId, 0, 0, { + const line = EditorShapeRecord.createLine(pageId, 0, 0, { a: { x: 0, y: 0 }, b: { x: 100, y: 100 }, stroke: 'red', @@ -272,7 +272,7 @@ describe('exportToSVG', () => { it('should export SVG with an arrow shape', () => { const { state, pageId } = createTestState(); - const arrow = ShapeRecord.createArrow(pageId, 0, 0, { + const arrow = EditorShapeRecord.createArrow(pageId, 0, 0, { points: [ { x: 0, y: 0 }, { x: 100, y: 0 } @@ -294,7 +294,7 @@ describe('exportToSVG', () => { it('should export curved arrows as native quadratic commands', () => { const { state, pageId } = createTestState(); - const arrow = ShapeRecord.createArrow(pageId, 0, 0, { + const arrow = EditorShapeRecord.createArrow(pageId, 0, 0, { points: [ { x: 0, y: 0 }, { x: 100, y: 0 } @@ -316,7 +316,7 @@ describe('exportToSVG', () => { it('should export SVG with a text shape', () => { const { state, pageId } = createTestState(); - const text = ShapeRecord.createText(pageId, 10, 20, { + const text = EditorShapeRecord.createText(pageId, 10, 20, { text: 'Hello World', fontSize: 16, fontFamily: 'Arial', @@ -336,7 +336,7 @@ describe('exportToSVG', () => { it('should export native path commands and fill rules', () => { const { state, pageId } = createTestState(); - const path = ShapeRecord.createPath( + const path = EditorShapeRecord.createPath( pageId, 10, 20, @@ -378,8 +378,8 @@ describe('exportToSVG', () => { it('should export only selected shapes when selectedOnly is true', () => { const { state, pageId } = createTestState(); - const rect1 = ShapeRecord.createRect(pageId, 0, 0, { w: 50, h: 50, fill: 'red', stroke: 'black', radius: 0 }); - const rect2 = ShapeRecord.createRect(pageId, 100, 100, { + const rect1 = EditorShapeRecord.createRect(pageId, 0, 0, { w: 50, h: 50, fill: 'red', stroke: 'black', radius: 0 }); + const rect2 = EditorShapeRecord.createRect(pageId, 100, 100, { w: 50, h: 50, fill: 'blue', @@ -401,7 +401,7 @@ describe('exportToSVG', () => { it('should escape XML special characters in shape properties', () => { const { state, pageId } = createTestState(); - const text = ShapeRecord.createText(pageId, 0, 0, { + const text = EditorShapeRecord.createText(pageId, 0, 0, { text: "", fontSize: 16, fontFamily: 'Arial', diff --git a/packages/core/tests/geom-stroke.test.ts b/packages/core/tests/geom-stroke.test.ts index 7187e2b..e122479 100644 --- a/packages/core/tests/geom-stroke.test.ts +++ b/packages/core/tests/geom-stroke.test.ts @@ -5,12 +5,12 @@ import { strokeWidthHandles, hitTestPoint, hitTestStroke, - PageRecord, + EditorPageRecord, shapeBounds, - ShapeRecord, + EditorShapeRecord, Store, } from "../src"; -import type { StrokePoint } from "../src/model"; +import type { StrokePoint } from "../src/editor-model"; describe("Stroke Geometry", () => { describe("computeOutline", () => { @@ -77,7 +77,7 @@ describe("Stroke Geometry", () => { const bounds = boundsFromOutline(outline); expect(bounds.max.y - bounds.min.y).toBeGreaterThan(10); - const stroke = ShapeRecord.createStroke("page:1", 0, 0, { + const stroke = EditorShapeRecord.createStroke("page:1", 0, 0, { points, brush, style: { color: "#000000", opacity: 1 }, @@ -132,7 +132,7 @@ describe("Stroke Geometry", () => { it("should return correct bounds for stroke shape", () => { const points: StrokePoint[] = [[0, 0], [100, 50], [200, 0]]; - const stroke = ShapeRecord.createStroke("page:1", 50, 100, { + const stroke = EditorShapeRecord.createStroke("page:1", 50, 100, { points, brush: { size: 16, thinning: 0.5, smoothing: 0.5, streamline: 0.5, simulatePressure: true }, style: { color: "#000000", opacity: 1.0 }, @@ -152,7 +152,7 @@ describe("Stroke Geometry", () => { it("should handle stroke with insufficient points", () => { const points: StrokePoint[] = [[0, 0]]; - const stroke = ShapeRecord.createStroke("page:1", 100, 100, { + const stroke = EditorShapeRecord.createStroke("page:1", 100, 100, { points, brush: { size: 16, thinning: 0.5, smoothing: 0.5, streamline: 0.5, simulatePressure: true }, style: { color: "#000000", opacity: 1.0 }, @@ -169,7 +169,7 @@ describe("Stroke Geometry", () => { it("should return true for point inside stroke outline", () => { const points: StrokePoint[] = [[0, 0], [100, 0]]; - const stroke = ShapeRecord.createStroke("page:1", 100, 100, { + const stroke = EditorShapeRecord.createStroke("page:1", 100, 100, { points, brush: { size: 20, thinning: 0.5, smoothing: 0.5, streamline: 0.5, simulatePressure: true }, style: { color: "#000000", opacity: 1.0 }, @@ -183,7 +183,7 @@ describe("Stroke Geometry", () => { it("should return false for point outside stroke bounds", () => { const points: StrokePoint[] = [[0, 0], [100, 0]]; - const stroke = ShapeRecord.createStroke("page:1", 100, 100, { + const stroke = EditorShapeRecord.createStroke("page:1", 100, 100, { points, brush: { size: 16, thinning: 0.5, smoothing: 0.5, streamline: 0.5, simulatePressure: true }, style: { color: "#000000", opacity: 1.0 }, @@ -198,7 +198,7 @@ describe("Stroke Geometry", () => { it("should return false for stroke with insufficient points", () => { const points: StrokePoint[] = [[0, 0]]; - const stroke = ShapeRecord.createStroke("page:1", 100, 100, { + const stroke = EditorShapeRecord.createStroke("page:1", 100, 100, { points, brush: { size: 16, thinning: 0.5, smoothing: 0.5, streamline: 0.5, simulatePressure: true }, style: { color: "#000000", opacity: 1.0 }, @@ -214,11 +214,11 @@ describe("Stroke Geometry", () => { describe("hitTestPoint with strokes", () => { it("should hit test stroke shapes", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); const points: StrokePoint[] = [[0, 0], [100, 0]]; - const stroke = ShapeRecord.createStroke("page:1", 100, 100, { + const stroke = EditorShapeRecord.createStroke("page:1", 100, 100, { points, brush: { size: 20, thinning: 0.5, smoothing: 0.5, streamline: 0.5, simulatePressure: true }, style: { color: "#000000", opacity: 1.0 }, @@ -241,11 +241,11 @@ describe("Stroke Geometry", () => { it("should return null for point outside stroke", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); const points: StrokePoint[] = [[0, 0], [100, 0]]; - const stroke = ShapeRecord.createStroke("page:1", 100, 100, { + const stroke = EditorShapeRecord.createStroke("page:1", 100, 100, { points, brush: { size: 16, thinning: 0.5, smoothing: 0.5, streamline: 0.5, simulatePressure: true }, style: { color: "#000000", opacity: 1.0 }, @@ -268,9 +268,9 @@ describe("Stroke Geometry", () => { it("should handle stroke with other shape types", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); - const rect = ShapeRecord.createRect("page:1", 0, 0, { + const rect = EditorShapeRecord.createRect("page:1", 0, 0, { w: 50, h: 50, fill: "#ff0000", @@ -280,7 +280,7 @@ describe("Stroke Geometry", () => { const points: StrokePoint[] = [[0, 0], [100, 0]]; - const stroke = ShapeRecord.createStroke("page:1", 100, 100, { + const stroke = EditorShapeRecord.createStroke("page:1", 100, 100, { points, brush: { size: 20, thinning: 0.5, smoothing: 0.5, streamline: 0.5, simulatePressure: true }, style: { color: "#000000", opacity: 1.0 }, diff --git a/packages/core/tests/geom.test.ts b/packages/core/tests/geom.test.ts index 2855d56..985f92c 100644 --- a/packages/core/tests/geom.test.ts +++ b/packages/core/tests/geom.test.ts @@ -1,25 +1,25 @@ import { describe, expect, it } from "vitest"; import { - BindingRecord, + EditorBindingRecord, computeEdgeAnchor, computeNormalizedAnchor, computeOrthogonalPath, hitTestPoint, - PageRecord, + EditorPageRecord, pointInEllipse, pointInRect, pointNearSegment, resolveArrowEndpoints, shapeBounds, shapeCenter, - ShapeRecord, + EditorShapeRecord, Store, } from "../src"; describe("Geometry", () => { describe("shapeBounds", () => { it("should return correct bounds for rect without rotation", () => { - const rect = ShapeRecord.createRect("page:1", 100, 200, { w: 50, h: 30, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 200, { w: 50, h: 30, fill: "", stroke: "", radius: 0 }); const bounds = shapeBounds(rect); @@ -28,7 +28,7 @@ describe("Geometry", () => { }); it("should return correct bounds for rect with rotation", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); rect.rot = Math.PI / 4; const bounds = shapeBounds(rect); @@ -40,7 +40,7 @@ describe("Geometry", () => { }); it("should return correct bounds for ellipse without rotation", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 50, 50, { w: 100, h: 80, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 50, 50, { w: 100, h: 80, fill: "", stroke: "" }); const bounds = shapeBounds(ellipse); @@ -49,7 +49,7 @@ describe("Geometry", () => { }); it("should return correct bounds for line", () => { - const line = ShapeRecord.createLine("page:1", 10, 10, { + const line = EditorShapeRecord.createLine("page:1", 10, 10, { a: { x: 0, y: 0 }, b: { x: 100, y: 50 }, stroke: "", @@ -63,7 +63,7 @@ describe("Geometry", () => { }); it("should return correct bounds for arrow", () => { - const arrow = ShapeRecord.createArrow("page:1", 20, 30, { + const arrow = EditorShapeRecord.createArrow("page:1", 20, 30, { points: [{ x: 10, y: 10 }, { x: 50, y: 60 }], start: { kind: "free" }, end: { kind: "free" }, @@ -77,7 +77,7 @@ describe("Geometry", () => { }); it("should return correct bounds for text", () => { - const text = ShapeRecord.createText("page:1", 100, 100, { + const text = EditorShapeRecord.createText("page:1", 100, 100, { text: "Hello", fontSize: 16, fontFamily: "Arial", @@ -93,7 +93,7 @@ describe("Geometry", () => { }); it("should return correct bounds for text without explicit width", () => { - const text = ShapeRecord.createText("page:1", 100, 100, { + const text = EditorShapeRecord.createText("page:1", 100, 100, { text: "Hello", fontSize: 20, fontFamily: "Arial", @@ -110,7 +110,7 @@ describe("Geometry", () => { describe("pointInRect", () => { it("should return true for point inside rect", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); expect(pointInRect({ x: 150, y: 125 }, rect)).toBe(true); expect(pointInRect({ x: 100, y: 100 }, rect)).toBe(true); @@ -118,7 +118,7 @@ describe("Geometry", () => { }); it("should return false for point outside rect", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); expect(pointInRect({ x: 99, y: 125 }, rect)).toBe(false); expect(pointInRect({ x: 201, y: 125 }, rect)).toBe(false); @@ -127,7 +127,7 @@ describe("Geometry", () => { }); it("should handle rotated rectangles", () => { - const rect = ShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); rect.rot = Math.PI / 4; const centerInLocal = { x: 50, y: 25 }; @@ -146,27 +146,27 @@ describe("Geometry", () => { describe("pointInEllipse", () => { it("should return true for point inside ellipse", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "", stroke: "" }); expect(pointInEllipse({ x: 150, y: 140 }, ellipse)).toBe(true); }); it("should return false for point outside ellipse", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "", stroke: "" }); expect(pointInEllipse({ x: 100, y: 100 }, ellipse)).toBe(false); expect(pointInEllipse({ x: 200, y: 180 }, ellipse)).toBe(false); }); it("should handle point at center of ellipse", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "", stroke: "" }); const center = { x: 150, y: 140 }; expect(pointInEllipse(center, ellipse)).toBe(true); }); it("should handle rotated ellipses", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "" }); ellipse.rot = Math.PI / 2; const centerInLocal = { x: 50, y: 25 }; @@ -288,7 +288,7 @@ describe("Geometry", () => { describe("pointInRect - edge cases", () => { it("should handle point exactly on rect boundary", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); expect(pointInRect({ x: 100, y: 125 }, rect)).toBe(true); expect(pointInRect({ x: 200, y: 125 }, rect)).toBe(true); @@ -297,28 +297,28 @@ describe("Geometry", () => { }); it("should handle zero-size rectangles", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 0, h: 0, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 0, h: 0, fill: "", stroke: "", radius: 0 }); expect(pointInRect({ x: 100, y: 100 }, rect)).toBe(true); expect(pointInRect({ x: 100.1, y: 100 }, rect)).toBe(false); }); it("should handle negative coordinates", () => { - const rect = ShapeRecord.createRect("page:1", -100, -100, { w: 50, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", -100, -100, { w: 50, h: 50, fill: "", stroke: "", radius: 0 }); expect(pointInRect({ x: -75, y: -75 }, rect)).toBe(true); expect(pointInRect({ x: -101, y: -75 }, rect)).toBe(false); }); it("should handle very small rectangles", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 0.1, h: 0.1, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 0.1, h: 0.1, fill: "", stroke: "", radius: 0 }); expect(pointInRect({ x: 100.05, y: 100.05 }, rect)).toBe(true); expect(pointInRect({ x: 100.2, y: 100.05 }, rect)).toBe(false); }); it("should handle 90 degree rotation", () => { - const rect = ShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); rect.rot = Math.PI / 2; const center = { x: -25, y: 50 }; @@ -326,7 +326,7 @@ describe("Geometry", () => { }); it("should handle 180 degree rotation", () => { - const rect = ShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); rect.rot = Math.PI; const center = { x: -50, y: -25 }; @@ -336,34 +336,34 @@ describe("Geometry", () => { describe("pointInEllipse - edge cases", () => { it("should handle point exactly on ellipse boundary", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "", stroke: "" }); const rightEdge = { x: 200, y: 140 }; expect(pointInEllipse(rightEdge, ellipse)).toBe(true); }); it("should handle very small ellipse", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { w: 0.2, h: 0.2, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 0.2, h: 0.2, fill: "", stroke: "" }); expect(pointInEllipse({ x: 100.1, y: 100.1 }, ellipse)).toBe(true); expect(pointInEllipse({ x: 100.2, y: 100.1 }, ellipse)).toBe(false); }); it("should handle circle (equal width and height)", () => { - const circle = ShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "" }); + const circle = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "" }); expect(pointInEllipse({ x: 150, y: 150 }, circle)).toBe(true); expect(pointInEllipse({ x: 200, y: 150 }, circle)).toBe(true); }); it("should handle negative coordinates", () => { - const ellipse = ShapeRecord.createEllipse("page:1", -100, -100, { w: 100, h: 80, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", -100, -100, { w: 100, h: 80, fill: "", stroke: "" }); expect(pointInEllipse({ x: -50, y: -60 }, ellipse)).toBe(true); }); it("should handle very flat ellipse", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { w: 200, h: 10, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 200, h: 10, fill: "", stroke: "" }); const center = { x: 200, y: 105 }; expect(pointInEllipse(center, ellipse)).toBe(true); @@ -373,7 +373,7 @@ describe("Geometry", () => { }); it("should handle very tall ellipse", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { w: 10, h: 200, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 10, h: 200, fill: "", stroke: "" }); const center = { x: 105, y: 200 }; expect(pointInEllipse(center, ellipse)).toBe(true); @@ -385,7 +385,7 @@ describe("Geometry", () => { describe("shapeBounds - edge cases", () => { it("should handle negative width/height gracefully", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: -50, h: -30, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: -50, h: -30, fill: "", stroke: "", radius: 0 }); const bounds = shapeBounds(rect); expect(bounds.min.x).toBeDefined(); @@ -395,7 +395,7 @@ describe("Geometry", () => { }); it("should handle zero-size shapes", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 0, h: 0, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 0, h: 0, fill: "", stroke: "", radius: 0 }); const bounds = shapeBounds(rect); expect(bounds.min).toEqual({ x: 100, y: 100 }); @@ -403,7 +403,7 @@ describe("Geometry", () => { }); it("should handle line with same start and end points", () => { - const line = ShapeRecord.createLine("page:1", 100, 100, { + const line = EditorShapeRecord.createLine("page:1", 100, 100, { a: { x: 0, y: 0 }, b: { x: 0, y: 0 }, stroke: "", @@ -416,7 +416,7 @@ describe("Geometry", () => { }); it("should handle multiple rotations", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); rect.rot = Math.PI * 2; const bounds = shapeBounds(rect); @@ -427,7 +427,7 @@ describe("Geometry", () => { }); it("should handle very large shapes", () => { - const rect = ShapeRecord.createRect("page:1", 0, 0, { w: 100_000, h: 100_000, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 0, 0, { w: 100_000, h: 100_000, fill: "", stroke: "", radius: 0 }); const bounds = shapeBounds(rect); expect(bounds.max.x).toBe(100_000); @@ -435,7 +435,7 @@ describe("Geometry", () => { }); it("should handle line with rotation", () => { - const line = ShapeRecord.createLine("page:1", 100, 100, { + const line = EditorShapeRecord.createLine("page:1", 100, 100, { a: { x: 0, y: 0 }, b: { x: 100, y: 0 }, stroke: "", @@ -452,8 +452,8 @@ describe("Geometry", () => { describe("hitTestPoint", () => { it("should return shape id for point inside rect", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const rect = ShapeRecord.createRect("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "#ff0000", @@ -475,8 +475,8 @@ describe("Geometry", () => { it("should return null for point outside all shapes", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const rect = ShapeRecord.createRect("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "#ff0000", @@ -498,15 +498,15 @@ describe("Geometry", () => { it("should return topmost shape for overlapping shapes", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const rect1 = ShapeRecord.createRect("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const rect1 = EditorShapeRecord.createRect("page:1", 100, 100, { w: 200, h: 200, fill: "#ff0000", stroke: "#000000", radius: 0, }, "shape:1"); - const rect2 = ShapeRecord.createRect("page:1", 150, 150, { + const rect2 = EditorShapeRecord.createRect("page:1", 150, 150, { w: 100, h: 100, fill: "#00ff00", @@ -532,8 +532,8 @@ describe("Geometry", () => { it("should hit test ellipse shapes", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 100, h: 80, fill: "#00ff00", @@ -558,8 +558,8 @@ describe("Geometry", () => { it("should hit test line shapes with tolerance", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const line = ShapeRecord.createLine("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const line = EditorShapeRecord.createLine("page:1", 100, 100, { a: { x: 0, y: 0 }, b: { x: 100, y: 100 }, stroke: "#000000", @@ -580,8 +580,8 @@ describe("Geometry", () => { it("should hit test arrow shapes with tolerance", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const arrow = ShapeRecord.createArrow("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const arrow = EditorShapeRecord.createArrow("page:1", 100, 100, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -602,8 +602,8 @@ describe("Geometry", () => { it("should hit test text shapes", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const text = ShapeRecord.createText("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const text = EditorShapeRecord.createText("page:1", 100, 100, { text: "Hello", fontSize: 16, fontFamily: "Arial", @@ -625,8 +625,8 @@ describe("Geometry", () => { it("should respect tolerance parameter for lines", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const line = ShapeRecord.createLine("page:1", 0, 0, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const line = EditorShapeRecord.createLine("page:1", 0, 0, { a: { x: 0, y: 0 }, b: { x: 100, y: 0 }, stroke: "#000000", @@ -661,21 +661,21 @@ describe("Geometry", () => { it("should handle multiple shape types on same page", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const rect = ShapeRecord.createRect("page:1", 0, 0, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const rect = EditorShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 100, fill: "#ff0000", stroke: "#000000", radius: 0, }, "shape:1"); - const ellipse = ShapeRecord.createEllipse("page:1", 200, 0, { + const ellipse = EditorShapeRecord.createEllipse("page:1", 200, 0, { w: 100, h: 100, fill: "#00ff00", stroke: "#000000", }, "shape:2"); - const line = ShapeRecord.createLine("page:1", 0, 200, { + const line = EditorShapeRecord.createLine("page:1", 0, 200, { a: { x: 0, y: 0 }, b: { x: 100, y: 100 }, stroke: "#000000", @@ -702,7 +702,7 @@ describe("Geometry", () => { describe("shapeCenter", () => { it("should return center of rect shape", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); const center = shapeCenter(rect); @@ -710,7 +710,7 @@ describe("Geometry", () => { }); it("should return center of ellipse shape", () => { - const ellipse = ShapeRecord.createEllipse("page:1", 100, 100, { w: 80, h: 60, fill: "", stroke: "" }); + const ellipse = EditorShapeRecord.createEllipse("page:1", 100, 100, { w: 80, h: 60, fill: "", stroke: "" }); const center = shapeCenter(ellipse); @@ -718,7 +718,7 @@ describe("Geometry", () => { }); it("should return center of line shape", () => { - const line = ShapeRecord.createLine("page:1", 100, 100, { + const line = EditorShapeRecord.createLine("page:1", 100, 100, { a: { x: 0, y: 0 }, b: { x: 100, y: 50 }, stroke: "", @@ -731,7 +731,7 @@ describe("Geometry", () => { }); it("should return center of arrow shape", () => { - const arrow = ShapeRecord.createArrow("page:1", 50, 50, { + const arrow = EditorShapeRecord.createArrow("page:1", 50, 50, { points: [{ x: -50, y: -50 }, { x: 50, y: 50 }], start: { kind: "free" }, end: { kind: "free" }, @@ -744,7 +744,7 @@ describe("Geometry", () => { }); it("should handle rotated shapes", () => { - const rect = ShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 0, 0, { w: 100, h: 50, fill: "", stroke: "", radius: 0 }); rect.rot = Math.PI / 4; const center = shapeCenter(rect); @@ -756,8 +756,8 @@ describe("Geometry", () => { describe("resolveArrowEndpoints", () => { it("should return arrow's own endpoints when no bindings exist", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const arrow = ShapeRecord.createArrow("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const arrow = EditorShapeRecord.createArrow("page:1", 100, 100, { points: [{ x: 0, y: 0 }, { x: 100, y: 50 }], start: { kind: "free" }, end: { kind: "free" }, @@ -778,22 +778,22 @@ describe("Geometry", () => { it("should resolve start endpoint when bound to a shape", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const targetRect = ShapeRecord.createRect( + const page = EditorPageRecord.create("Page 1", "page:1"); + const targetRect = EditorShapeRecord.createRect( "page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }, "rect:1", ); - const arrow = ShapeRecord.createArrow("page:1", 300, 300, { + const arrow = EditorShapeRecord.createArrow("page:1", 300, 300, { points: [{ x: -150, y: -150 }, { x: 100, y: 100 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "", width: 2 }, }, "arrow:1"); - const binding = BindingRecord.create(arrow.id, targetRect.id, "start", { kind: "center" }, "binding:1"); + const binding = EditorBindingRecord.create(arrow.id, targetRect.id, "start", { kind: "center" }, "binding:1"); store.setState((state) => ({ ...state, @@ -814,22 +814,22 @@ describe("Geometry", () => { it("should resolve end endpoint when bound to a shape", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const targetRect = ShapeRecord.createRect( + const page = EditorPageRecord.create("Page 1", "page:1"); + const targetRect = EditorShapeRecord.createRect( "page:1", 200, 200, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }, "rect:1", ); - const arrow = ShapeRecord.createArrow("page:1", 50, 50, { + const arrow = EditorShapeRecord.createArrow("page:1", 50, 50, { points: [{ x: 0, y: 0 }, { x: 200, y: 200 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "", width: 2 }, }, "arrow:1"); - const binding = BindingRecord.create(arrow.id, targetRect.id, "end", { kind: "center" }, "binding:1"); + const binding = EditorBindingRecord.create(arrow.id, targetRect.id, "end", { kind: "center" }, "binding:1"); store.setState((state) => ({ ...state, @@ -849,30 +849,30 @@ describe("Geometry", () => { it("should resolve both endpoints when both are bound", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const rect1 = ShapeRecord.createRect( + const page = EditorPageRecord.create("Page 1", "page:1"); + const rect1 = EditorShapeRecord.createRect( "page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }, "rect:1", ); - const rect2 = ShapeRecord.createRect( + const rect2 = EditorShapeRecord.createRect( "page:1", 300, 300, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }, "rect:2", ); - const arrow = ShapeRecord.createArrow("page:1", 0, 0, { + const arrow = EditorShapeRecord.createArrow("page:1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 100 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "", width: 2 }, }, "arrow:1"); - const binding1 = BindingRecord.create(arrow.id, rect1.id, "start", { kind: "center" }, "binding:1"); - const binding2 = BindingRecord.create(arrow.id, rect2.id, "end", { kind: "center" }, "binding:2"); + const binding1 = EditorBindingRecord.create(arrow.id, rect1.id, "start", { kind: "center" }, "binding:1"); + const binding2 = EditorBindingRecord.create(arrow.id, rect2.id, "end", { kind: "center" }, "binding:2"); store.setState((state) => ({ ...state, @@ -893,15 +893,15 @@ describe("Geometry", () => { it("should ignore bindings to missing shapes", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const arrow = ShapeRecord.createArrow("page:1", 100, 100, { + const page = EditorPageRecord.create("Page 1", "page:1"); + const arrow = EditorShapeRecord.createArrow("page:1", 100, 100, { points: [{ x: 0, y: 0 }, { x: 100, y: 50 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "", width: 2 }, }, "arrow:1"); - const binding = BindingRecord.create(arrow.id, "nonexistent:1", "start", { kind: "center" }, "binding:1"); + const binding = EditorBindingRecord.create(arrow.id, "nonexistent:1", "start", { kind: "center" }, "binding:1"); store.setState((state) => ({ ...state, @@ -930,8 +930,8 @@ describe("Geometry", () => { it("should return null for non-arrow shapes", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create("Page 1", "page:1"); + const rect = EditorShapeRecord.createRect( "page:1", 100, 100, @@ -953,22 +953,22 @@ describe("Geometry", () => { it("should handle bound arrows when target shape moves", () => { const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); - const targetRect = ShapeRecord.createRect( + const page = EditorPageRecord.create("Page 1", "page:1"); + const targetRect = EditorShapeRecord.createRect( "page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }, "rect:1", ); - const arrow = ShapeRecord.createArrow("page:1", 50, 50, { + const arrow = EditorShapeRecord.createArrow("page:1", 50, 50, { points: [{ x: 0, y: 0 }, { x: 100, y: 100 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "", width: 2 }, }, "arrow:1"); - const binding = BindingRecord.create(arrow.id, targetRect.id, "end", { kind: "center" }, "binding:1"); + const binding = EditorBindingRecord.create(arrow.id, targetRect.id, "end", { kind: "center" }, "binding:1"); store.setState((state) => ({ ...state, @@ -999,22 +999,22 @@ describe("Geometry", () => { it("should resolve edge anchors correctly", () => { const store = new Store(); - const page = PageRecord.create("Test Page", "page:1"); - const targetRect = ShapeRecord.createRect( + const page = EditorPageRecord.create("Test Page", "page:1"); + const targetRect = EditorShapeRecord.createRect( page.id, 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }, "rect:1", ); - const arrow = ShapeRecord.createArrow(page.id, 0, 0, { + const arrow = EditorShapeRecord.createArrow(page.id, 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 100 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "arrow:1"); - const binding = BindingRecord.create(arrow.id, targetRect.id, "end", { kind: "edge", nx: 1, ny: 0 }); + const binding = EditorBindingRecord.create(arrow.id, targetRect.id, "end", { kind: "edge", nx: 1, ny: 0 }); store.setState((state) => ({ ...state, @@ -1035,14 +1035,14 @@ describe("Geometry", () => { describe("computeEdgeAnchor", () => { it("should compute center anchor correctly", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); const anchor = computeEdgeAnchor(rect, 0, 0); expect(anchor).toEqual({ x: 150, y: 150 }); }); it("should compute edge anchors correctly", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); expect(computeEdgeAnchor(rect, -1, -1)).toEqual({ x: 100, y: 100 }); expect(computeEdgeAnchor(rect, 1, -1)).toEqual({ x: 200, y: 100 }); @@ -1055,7 +1055,7 @@ describe("Geometry", () => { describe("computeNormalizedAnchor", () => { it("should compute normalized anchor for center point", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); const anchor = computeNormalizedAnchor({ x: 150, y: 150 }, rect); expect(anchor.nx).toBeCloseTo(0, 5); @@ -1063,7 +1063,7 @@ describe("Geometry", () => { }); it("should compute normalized anchor for edge points", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); const topLeft = computeNormalizedAnchor({ x: 100, y: 100 }, rect); expect(topLeft.nx).toBeCloseTo(-1, 5); @@ -1079,7 +1079,7 @@ describe("Geometry", () => { }); it("should clamp normalized anchor values to [-1, 1]", () => { - const rect = ShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); + const rect = EditorShapeRecord.createRect("page:1", 100, 100, { w: 100, h: 100, fill: "", stroke: "", radius: 0 }); const far = computeNormalizedAnchor({ x: 300, y: 300 }, rect); expect(far.nx).toBe(1); diff --git a/packages/core/tests/hierarchical-selection.test.ts b/packages/core/tests/hierarchical-selection.test.ts index b5ad27a..7c2e074 100644 --- a/packages/core/tests/hierarchical-selection.test.ts +++ b/packages/core/tests/hierarchical-selection.test.ts @@ -1,15 +1,15 @@ import { describe, expect, it } from 'vitest'; -import { Action, EditorState, PageRecord, SelectTool, ShapeRecord, Store, hitTestPoint, shapeBounds } from '../src'; +import { Action, EditorState, EditorPageRecord, SelectTool, EditorShapeRecord, Store, hitTestPoint, shapeBounds } from '../src'; const modifiers = { ctrl: false, shift: false, alt: false, meta: false }; const buttons = { left: true, middle: false, right: false }; const buttonsUp = { left: false, middle: false, right: false }; function nestedState() { - const page = PageRecord.create('Page', 'page:one'); - const container = ShapeRecord.createContainer(page.id, 100, 50, { w: 100, h: 80 }, 'shape:container'); + const page = EditorPageRecord.create('Page', 'page:one'); + const container = EditorShapeRecord.createContainer(page.id, 100, 50, { w: 100, h: 80 }, 'shape:container'); container.editorTransform = { a: 2, b: 0, c: 0, d: 2, e: 100, f: 50 }; - const child = ShapeRecord.createRect( + const child = EditorShapeRecord.createRect( page.id, 120, 70, @@ -29,9 +29,9 @@ function multiParentState() { const state = nestedState(); const page = state.doc.pages['page:one']!; const layer = state.doc.layers?.[page.layerIds?.[0] ?? '']; - const sibling = ShapeRecord.createContainer(page.id, 300, 40, { w: 100, h: 80 }, 'shape:sibling'); + const sibling = EditorShapeRecord.createContainer(page.id, 300, 40, { w: 100, h: 80 }, 'shape:sibling'); sibling.editorTransform = { a: 1, b: 0, c: 0, d: 1, e: 300, f: 40 }; - const siblingChild = ShapeRecord.createRect( + const siblingChild = EditorShapeRecord.createRect( page.id, 320, 60, diff --git a/packages/core/tests/history.test.ts b/packages/core/tests/history.test.ts index 56b3e01..937eca1 100644 --- a/packages/core/tests/history.test.ts +++ b/packages/core/tests/history.test.ts @@ -9,7 +9,7 @@ import { SnapshotCommand, UpdateShapeCommand, } from "../src/history"; -import { PageRecord, ShapeRecord } from "../src/model"; +import { EditorPageRecord, EditorShapeRecord } from "../src/editor-model"; import { EditorState } from "../src/reactivity"; describe("History", () => { @@ -33,7 +33,7 @@ describe("History", () => { it("clones before/after states so mutations do not leak", () => { const before = EditorState.create(); const after = EditorState.clone(before); - const page = PageRecord.create("Snapshot Page"); + const page = EditorPageRecord.create("Snapshot Page"); after.doc.pages[page.id] = page; const command = new SnapshotCommand("Snapshot", "doc", before, after); @@ -41,7 +41,7 @@ describe("History", () => { expect(result).toEqual(after); expect(result).not.toBe(after); - (result.doc.pages[page.id] as PageRecord).name = "Mutated"; + (result.doc.pages[page.id] as EditorPageRecord).name = "Mutated"; expect(after.doc.pages[page.id]?.name).toBe("Snapshot Page"); const undoState = command.undo(after); expect(undoState).toEqual(before); @@ -52,7 +52,7 @@ describe("History", () => { it("works with history execute/undo/redo flow", () => { const before = EditorState.create(); const after = EditorState.clone(before); - const page = PageRecord.create("Snapshot Page"); + const page = EditorPageRecord.create("Snapshot Page"); after.doc.pages[page.id] = page; const command = new SnapshotCommand("Snapshot", "doc", before, after); const history = History.create(); @@ -74,8 +74,8 @@ describe("History", () => { describe("CreateShapeCommand", () => { it("should execute create shape command", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape = ShapeRecord.createRect("page:1", 10, 20, { + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape = EditorShapeRecord.createRect("page:1", 10, 20, { w: 100, h: 50, fill: "#fff", @@ -98,8 +98,8 @@ describe("History", () => { }); it("should round-trip: do -> undo returns to identical state", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape = ShapeRecord.createRect("page:1", 10, 20, { + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape = EditorShapeRecord.createRect("page:1", 10, 20, { w: 100, h: 50, fill: "#fff", @@ -124,8 +124,8 @@ describe("History", () => { }); it("should redo re-applies exactly", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape = ShapeRecord.createRect("page:1", 10, 20, { + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape = EditorShapeRecord.createRect("page:1", 10, 20, { w: 100, h: 50, fill: "#fff", @@ -155,8 +155,8 @@ describe("History", () => { describe("UpdateShapeCommand", () => { it("should execute update shape command", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape = EditorShapeRecord.createRect( "page:1", 10, 20, @@ -180,8 +180,8 @@ describe("History", () => { }); it("should round-trip: do -> undo returns to identical state", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape = EditorShapeRecord.createRect( "page:1", 10, 20, @@ -210,8 +210,8 @@ describe("History", () => { }); it("should redo re-applies exactly", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape = EditorShapeRecord.createRect( "page:1", 10, 20, @@ -245,15 +245,15 @@ describe("History", () => { describe("DeleteShapesCommand", () => { it("should execute delete shapes command", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape1 = ShapeRecord.createRect("page:1", 10, 20, { + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape1 = EditorShapeRecord.createRect("page:1", 10, 20, { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 0, }, "shape:1"); - const shape2 = ShapeRecord.createRect("page:1", 30, 40, { + const shape2 = EditorShapeRecord.createRect("page:1", 30, 40, { w: 200, h: 100, fill: "#fff", @@ -282,8 +282,8 @@ describe("History", () => { }); it("should round-trip: do -> undo returns to identical state", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape = EditorShapeRecord.createRect( "page:1", 10, 20, @@ -311,8 +311,8 @@ describe("History", () => { }); it("should redo re-applies exactly", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape = EditorShapeRecord.createRect( "page:1", 10, 20, @@ -478,15 +478,15 @@ describe("History", () => { }); it("should maintain command order through undo/redo", () => { - const page = PageRecord.create("Test Page", "page:1"); - const shape1 = ShapeRecord.createRect("page:1", 10, 20, { + const page = EditorPageRecord.create("Test Page", "page:1"); + const shape1 = EditorShapeRecord.createRect("page:1", 10, 20, { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 0, }, "shape:1"); - const shape2 = ShapeRecord.createRect("page:1", 30, 40, { + const shape2 = EditorShapeRecord.createRect("page:1", 30, 40, { w: 200, h: 100, fill: "#fff", diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index 00501a1..5a1cde9 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -13,9 +13,9 @@ describe("Core exports", () => { }); it("should export model types and functions", () => { - expect(core.ShapeRecord).toBeDefined(); - expect(core.PageRecord).toBeDefined(); - expect(core.Document).toBeDefined(); + expect(core.EditorShapeRecord).toBeDefined(); + expect(core.EditorPageRecord).toBeDefined(); + expect(core.EditorDocument).toBeDefined(); }); it("should export reactivity functions", () => { diff --git a/packages/core/tests/interchange-fixtures.test.ts b/packages/core/tests/interchange-fixtures.test.ts index e78a811..cf904c0 100644 --- a/packages/core/tests/interchange-fixtures.test.ts +++ b/packages/core/tests/interchange-fixtures.test.ts @@ -1,10 +1,10 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { - BindingRecord, + EditorBindingRecord, exportInterchange, importInterchange, - ShapeRecord, + EditorShapeRecord, validateDoc, type BoardExport, type ImportedAsset @@ -30,14 +30,14 @@ function mixedBoard(): BoardExport { locked: false, opacity: 1 }; - const frame = ShapeRecord.createContainer( + const frame = EditorShapeRecord.createContainer( page.id, 0, 0, { w: 500, h: 300, title: 'Mixed content', fill: '#f8fafc', stroke: '#94a3b8' }, 'frame' ); - const card = ShapeRecord.createMarkdown( + const card = EditorShapeRecord.createMarkdown( page.id, 40, 60, @@ -60,21 +60,21 @@ function mixedBoard(): BoardExport { digest: 'sha256:pixel', bytes: [137, 80, 78, 71] }; - const image = ShapeRecord.createImage( + const image = EditorShapeRecord.createImage( page.id, 260, 60, { w: 120, h: 90, assetId: asset.id, caption: 'Raster asset' }, 'image' ); - const link = ShapeRecord.createReference( + const link = EditorShapeRecord.createReference( page.id, 140, 210, { w: 220, h: 60, referenceType: 'url', value: 'https://example.com', label: 'External link' }, 'link' ); - const svgPath = ShapeRecord.createPath( + const svgPath = EditorShapeRecord.createPath( page.id, 420, 210, @@ -125,8 +125,8 @@ function mixedBoard(): BoardExport { handle: 'end' as const, anchor: { kind: 'edge' as const, nx: -1, ny: 0 } }; - const relation = BindingRecord.createRelation('card', 'link', 'references', 'binding:relation'); - const arrow = ShapeRecord.createArrow( + const relation = EditorBindingRecord.createRelation('card', 'link', 'references', 'binding:relation'); + const arrow = EditorShapeRecord.createArrow( page.id, 0, 0, diff --git a/packages/core/tests/interchange.test.ts b/packages/core/tests/interchange.test.ts index 5cfc0ed..94ed99a 100644 --- a/packages/core/tests/interchange.test.ts +++ b/packages/core/tests/interchange.test.ts @@ -1,19 +1,19 @@ import { describe, expect, it } from 'vitest'; import { - BindingRecord, + EditorBindingRecord, exportInterchange, importInterchange, - LayerRecord, - PageRecord, - ShapeRecord, + EditorLayerRecord, + EditorPageRecord, + EditorShapeRecord, type BoardExport } from '../src'; function board(): BoardExport { - const page = PageRecord.create('First', 'page:1'); - const layer = LayerRecord.create(page.id, 'Default', 'layer:1'); + const page = EditorPageRecord.create('First', 'page:1'); + const layer = EditorLayerRecord.create(page.id, 'Default', 'layer:1'); page.layerIds = [layer.id]; - const markdown = ShapeRecord.createMarkdown( + const markdown = EditorShapeRecord.createMarkdown( page.id, 10, 20, @@ -21,7 +21,7 @@ function board(): BoardExport { 'shape:card' ); markdown.layerId = layer.id; - const text = ShapeRecord.createText( + const text = EditorShapeRecord.createText( page.id, 400, 40, @@ -29,15 +29,15 @@ function board(): BoardExport { 'shape:target' ); text.layerId = layer.id; - const start = BindingRecord.create( + const start = EditorBindingRecord.create( 'shape:arrow', markdown.id, 'start', { kind: 'edge', nx: 1, ny: 0 }, 'binding:start' ); - const end = BindingRecord.create('shape:arrow', text.id, 'end', { kind: 'edge', nx: -1, ny: 0 }, 'binding:end'); - const arrow = ShapeRecord.createArrow( + const end = EditorBindingRecord.create('shape:arrow', text.id, 'end', { kind: 'edge', nx: -1, ny: 0 }, 'binding:end'); + const arrow = EditorShapeRecord.createArrow( page.id, 250, 80, diff --git a/packages/core/tests/layers.test.ts b/packages/core/tests/layers.test.ts index 1e6794c..ccc2f15 100644 --- a/packages/core/tests/layers.test.ts +++ b/packages/core/tests/layers.test.ts @@ -6,7 +6,7 @@ import { moveLayer, patchLayer, reorderShapes, - ShapeRecord, + EditorShapeRecord, Store, ensureDocumentLayers } from '../src'; @@ -15,14 +15,14 @@ import { describe, expect, it } from 'vitest'; function layeredState() { const state = EditorState.create(); state.doc.pages.page = { id: 'page', name: 'Page', shapeIds: ['back', 'front'] }; - state.doc.shapes.back = ShapeRecord.createRect( + state.doc.shapes.back = EditorShapeRecord.createRect( 'page', 0, 0, { w: 10, h: 10, fill: '#000', stroke: '#000', radius: 0 }, 'back' ); - state.doc.shapes.front = ShapeRecord.createRect( + state.doc.shapes.front = EditorShapeRecord.createRect( 'page', 20, 0, @@ -46,7 +46,7 @@ describe('layers', () => { const original = layeredState(); const withSecond = createLayer(original, 'Foreground'); const activeLayerId = withSecond.ui.activeLayerId!; - const shape = ShapeRecord.createRect( + const shape = EditorShapeRecord.createRect( 'page', 40, 0, @@ -92,7 +92,7 @@ describe('layers', () => { const original = layeredState(); const foregroundState = createLayer(original, 'Foreground'); const foreground = foregroundState.ui.activeLayerId!; - const foregroundShape = ShapeRecord.createRect( + const foregroundShape = EditorShapeRecord.createRect( 'page', 0, 0, diff --git a/packages/core/tests/layout.test.ts b/packages/core/tests/layout.test.ts index 854dc5d..61ef2bd 100644 --- a/packages/core/tests/layout.test.ts +++ b/packages/core/tests/layout.test.ts @@ -3,7 +3,7 @@ import { distributeShapes, EditorState, groupShapes, - ShapeRecord, + EditorShapeRecord, stackShapes, tidyShapes, ungroupShapes @@ -15,9 +15,9 @@ function stateWithShapes() { state.doc.pages.page = { id: 'page', name: 'Page', shapeIds: [] }; state.ui.currentPageId = 'page'; const shapes = [ - ShapeRecord.createRect('page', 30, 20, { w: 10, h: 10, fill: '#000', stroke: '#000', radius: 0 }, 'one'), - ShapeRecord.createRect('page', 0, 0, { w: 20, h: 10, fill: '#000', stroke: '#000', radius: 0 }, 'two'), - ShapeRecord.createRect('page', 80, 40, { w: 10, h: 20, fill: '#000', stroke: '#000', radius: 0 }, 'three') + EditorShapeRecord.createRect('page', 30, 20, { w: 10, h: 10, fill: '#000', stroke: '#000', radius: 0 }, 'one'), + EditorShapeRecord.createRect('page', 0, 0, { w: 20, h: 10, fill: '#000', stroke: '#000', radius: 0 }, 'two'), + EditorShapeRecord.createRect('page', 80, 40, { w: 10, h: 20, fill: '#000', stroke: '#000', radius: 0 }, 'three') ]; state.doc.shapes = Object.fromEntries(shapes.map((shape) => [shape.id, shape])); state.doc.pages.page.shapeIds = shapes.map((shape) => shape.id); diff --git a/packages/core/tests/markdown.test.ts b/packages/core/tests/markdown.test.ts index bf54571..7778fa4 100644 --- a/packages/core/tests/markdown.test.ts +++ b/packages/core/tests/markdown.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { pointInMarkdown, shapeBounds } from "../src/geom"; -import type { MarkdownProps } from "../src/model"; -import { Document, PageRecord, ShapeRecord, validateDoc } from "../src/model"; +import type { MarkdownProps } from "../src/editor-model"; +import { EditorDocument, EditorPageRecord, EditorShapeRecord, validateDoc } from "../src/editor-model"; import { EditorState as EditorStateOps } from "../src/reactivity"; import { Action, Modifiers, PointerButtons } from "../src/actions"; import { MarkdownTool } from "../src/tools/markdown"; @@ -22,7 +22,7 @@ describe("MarkdownShape", () => { describe("createMarkdown", () => { it("should create a markdown shape with generated ID", () => { const props = createProps(); - const shape = ShapeRecord.createMarkdown(pageId, 10, 20, props); + const shape = EditorShapeRecord.createMarkdown(pageId, 10, 20, props); expect(shape.id).toMatch(/^shape:/); expect(shape.type).toBe("markdown"); @@ -35,20 +35,20 @@ describe("MarkdownShape", () => { it("should create a markdown shape with custom ID", () => { const props = createProps({ md: "# Test", color: "#000" }); - const shape = ShapeRecord.createMarkdown(pageId, 10, 20, props, "shape:custom"); + const shape = EditorShapeRecord.createMarkdown(pageId, 10, 20, props, "shape:custom"); expect(shape.id).toBe("shape:custom"); }); it("should create a markdown shape with optional bg and border", () => { const props = createProps({ md: "# Styled", color: "#000", bg: "#ffffff", border: "#cccccc" }); - const shape = ShapeRecord.createMarkdown(pageId, 0, 0, props); + const shape = EditorShapeRecord.createMarkdown(pageId, 0, 0, props); expect(shape.props.bg).toBe("#ffffff"); expect(shape.props.border).toBe("#cccccc"); }); it("should create a markdown shape without height (auto-computed)", () => { const props = createProps({ md: "# Auto Height", w: 300, h: undefined, color: "#000" }); - const shape = ShapeRecord.createMarkdown(pageId, 0, 0, props); + const shape = EditorShapeRecord.createMarkdown(pageId, 0, 0, props); expect(shape.props.h).toBeUndefined(); }); @@ -61,7 +61,7 @@ describe("MarkdownShape", () => { "should create markdown with various content: %o", ({ md, w, h, fontSize }) => { const props: MarkdownProps = { md, w, h, fontSize, fontFamily: "sans-serif", color: "#000" }; - const shape = ShapeRecord.createMarkdown(pageId, 0, 0, props); + const shape = EditorShapeRecord.createMarkdown(pageId, 0, 0, props); expect(shape.props.md).toBe(md); expect(shape.props.w).toBe(w); @@ -73,8 +73,8 @@ describe("MarkdownShape", () => { describe("clone", () => { it("should clone a markdown shape", () => { const props = createProps({ md: "# Clone Test", color: "#000" }); - const shape = ShapeRecord.createMarkdown(pageId, 10, 20, props); - const cloned = ShapeRecord.clone(shape); + const shape = EditorShapeRecord.createMarkdown(pageId, 10, 20, props); + const cloned = EditorShapeRecord.clone(shape); expect(cloned).toEqual(shape); expect(cloned).not.toBe(shape); @@ -83,8 +83,8 @@ describe("MarkdownShape", () => { it("should deep clone props", () => { const props = createProps({ md: "# Original", fontFamily: "sans-serif", color: "#000" }); - const shape = ShapeRecord.createMarkdown(pageId, 10, 20, props); - const cloned = ShapeRecord.clone(shape); + const shape = EditorShapeRecord.createMarkdown(pageId, 10, 20, props); + const cloned = EditorShapeRecord.clone(shape); if (cloned.type === "markdown") { cloned.props.md = "# Modified"; @@ -99,7 +99,7 @@ describe("MarkdownShape", () => { describe("geometry", () => { describe("shapeBounds", () => { it("should compute bounds for markdown shape without rotation", () => { - const shape = ShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); + const shape = EditorShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); const bounds = shapeBounds(shape); expect(bounds.min.x).toBe(10); expect(bounds.min.y).toBe(20); @@ -108,7 +108,7 @@ describe("MarkdownShape", () => { }); it("should compute bounds for markdown shape with auto height", () => { - const shape = ShapeRecord.createMarkdown( + const shape = EditorShapeRecord.createMarkdown( pageId, 0, 0, @@ -122,7 +122,7 @@ describe("MarkdownShape", () => { }); it("should compute rotated bounds correctly", () => { - const shape = ShapeRecord.createMarkdown( + const shape = EditorShapeRecord.createMarkdown( pageId, 100, 100, @@ -141,18 +141,18 @@ describe("MarkdownShape", () => { describe("pointInMarkdown", () => { it("should return true for point inside markdown block", () => { - const shape = ShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); + const shape = EditorShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); expect(pointInMarkdown({ x: 100, y: 100 }, shape)).toBe(true); }); it("should return false for point outside markdown block", () => { - const shape = ShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); + const shape = EditorShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); expect(pointInMarkdown({ x: 400, y: 100 }, shape)).toBe(false); expect(pointInMarkdown({ x: 100, y: 300 }, shape)).toBe(false); }); it("should handle edge cases on bounds", () => { - const shape = ShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); + const shape = EditorShapeRecord.createMarkdown(pageId, 10, 20, createProps({ md: "# Test", color: "#000" })); expect(pointInMarkdown({ x: 10, y: 20 }, shape)).toBe(true); expect(pointInMarkdown({ x: 310, y: 220 }, shape)).toBe(true); expect(pointInMarkdown({ x: 9, y: 20 }, shape)).toBe(false); @@ -163,9 +163,9 @@ describe("MarkdownShape", () => { describe("validation", () => { it("should validate markdown shape with all required fields", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createMarkdown("page1", 0, 0, createProps({ md: "# Valid", color: "#000" }), "shape1"); + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createMarkdown("page1", 0, 0, createProps({ md: "# Valid", color: "#000" }), "shape1"); page.shapeIds = ["shape1"]; doc.pages = { page1: page }; @@ -176,9 +176,9 @@ describe("MarkdownShape", () => { }); it("should reject markdown with invalid fontSize", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createMarkdown( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createMarkdown( "page1", 0, 0, @@ -199,9 +199,9 @@ describe("MarkdownShape", () => { }); it("should reject markdown with invalid width", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createMarkdown( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createMarkdown( "page1", 0, 0, @@ -222,9 +222,9 @@ describe("MarkdownShape", () => { }); it("should reject markdown with negative height", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createMarkdown( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createMarkdown( "page1", 0, 0, @@ -245,9 +245,9 @@ describe("MarkdownShape", () => { }); it("should accept markdown with undefined height (auto-computed)", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createMarkdown( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createMarkdown( "page1", 0, 0, @@ -267,9 +267,9 @@ describe("MarkdownShape", () => { describe("JSON serialization", () => { it("should round-trip markdown shape", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createMarkdown( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createMarkdown( "page1", 10, 20, @@ -294,8 +294,8 @@ describe("MarkdownShape", () => { }); it("should round-trip markdown shape with complex markdown content", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); const markdown = `# Markdown Test ## Features @@ -314,7 +314,7 @@ const hello = "world"; 2. List 3. Items`; - const shape = ShapeRecord.createMarkdown("page1", 0, 0, { + const shape = EditorShapeRecord.createMarkdown("page1", 0, 0, { md: markdown, w: 400, h: 500, @@ -335,10 +335,10 @@ const hello = "world"; }); it("should round-trip document with markdown and other shapes", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page1", 0, 0, @@ -346,7 +346,7 @@ const hello = "world"; "shape1", ); - const markdown = ShapeRecord.createMarkdown("page1", 150, 100, { + const markdown = EditorShapeRecord.createMarkdown("page1", 150, 100, { md: "# Markdown\n\nNext to a rectangle", w: 300, h: 200, @@ -355,7 +355,7 @@ const hello = "world"; color: "#000", }, "shape2"); - const text = ShapeRecord.createText("page1", 500, 200, { + const text = EditorShapeRecord.createText("page1", 500, 200, { text: "Plain text", fontSize: 18, fontFamily: "Arial", @@ -377,8 +377,8 @@ const hello = "world"; describe("integration with EditorState", () => { it("should work in EditorState with markdown shapes", () => { const state = EditorStateOps.create(); - const page = PageRecord.create("Test Page", "page1"); - const markdown = ShapeRecord.createMarkdown( + const page = EditorPageRecord.create("Test Page", "page1"); + const markdown = EditorShapeRecord.createMarkdown( "page1", 0, 0, @@ -402,7 +402,7 @@ const hello = "world"; describe("MarkdownTool", () => { it("returns to select after placing a markdown block", () => { - const page = PageRecord.create("Page", "page"); + const page = EditorPageRecord.create("Page", "page"); const state = { ...EditorStateOps.create(), doc: { pages: { [page.id]: page }, shapes: {}, bindings: {} }, diff --git a/packages/core/tests/model.test.ts b/packages/core/tests/model.test.ts index 782c000..99cbadd 100644 --- a/packages/core/tests/model.test.ts +++ b/packages/core/tests/model.test.ts @@ -2,17 +2,17 @@ import { describe, expect, it } from "vitest"; import { type ArrowProps, type ArrowStyle, - BindingRecord, + EditorBindingRecord, createId, - Document, + EditorDocument, type EllipseProps, type LineProps, - PageRecord, + EditorPageRecord, type RectProps, - ShapeRecord, + EditorShapeRecord, type TextProps, validateDoc, -} from "../src/model"; +} from "../src/editor-model"; describe("createId", () => { it("should generate a valid UUID without prefix", () => { @@ -50,17 +50,17 @@ describe("createId", () => { }); }); -describe("PageRecord", () => { +describe("EditorPageRecord", () => { describe("create", () => { it("should create a page with generated ID", () => { - const page = PageRecord.create("My Page"); + const page = EditorPageRecord.create("My Page"); expect(page.id).toMatch(/^page:/); expect(page.name).toBe("My Page"); expect(page.shapeIds).toEqual([]); }); it("should create a page with custom ID", () => { - const page = PageRecord.create("Test Page", "page:123"); + const page = EditorPageRecord.create("Test Page", "page:123"); expect(page.id).toBe("page:123"); expect(page.name).toBe("Test Page"); }); @@ -68,7 +68,7 @@ describe("PageRecord", () => { it.each([{ name: "Untitled" }, { name: "Page 1" }, { name: "" }, { name: "A very long page name with special chars !@#$%", }])("should create page with name: \"$name\"", ({ name }) => { - const page = PageRecord.create(name); + const page = EditorPageRecord.create(name); expect(page.name).toBe(name); expect(page.shapeIds).toEqual([]); }); @@ -76,10 +76,10 @@ describe("PageRecord", () => { describe("clone", () => { it("should create a copy of the page", () => { - const page = PageRecord.create("Test"); + const page = EditorPageRecord.create("Test"); page.shapeIds = ["shape1", "shape2"]; - const cloned = PageRecord.clone(page); + const cloned = EditorPageRecord.clone(page); expect(cloned).toEqual(page); expect(cloned).not.toBe(page); @@ -87,10 +87,10 @@ describe("PageRecord", () => { }); it("should deep clone shapeIds array", () => { - const page = PageRecord.create("Test"); + const page = EditorPageRecord.create("Test"); page.shapeIds = ["shape1", "shape2"]; - const cloned = PageRecord.clone(page); + const cloned = EditorPageRecord.clone(page); cloned.shapeIds.push("shape3"); expect(page.shapeIds).toEqual(["shape1", "shape2"]); @@ -99,13 +99,13 @@ describe("PageRecord", () => { }); }); -describe("ShapeRecord", () => { +describe("EditorShapeRecord", () => { const pageId = "page:test"; describe("createRect", () => { it("should create a rectangle shape with generated ID", () => { const props: RectProps = { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 5 }; - const shape = ShapeRecord.createRect(pageId, 10, 20, props); + const shape = EditorShapeRecord.createRect(pageId, 10, 20, props); expect(shape.id).toMatch(/^shape:/); expect(shape.type).toBe("rect"); @@ -118,7 +118,7 @@ describe("ShapeRecord", () => { it("should create a rectangle with custom ID", () => { const props: RectProps = { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 5 }; - const shape = ShapeRecord.createRect(pageId, 10, 20, props, "shape:custom"); + const shape = EditorShapeRecord.createRect(pageId, 10, 20, props, "shape:custom"); expect(shape.id).toBe("shape:custom"); }); @@ -132,7 +132,7 @@ describe("ShapeRecord", () => { }, { w: 50.5, h: 25.3, fill: "rgba(0,0,0,0.5)", stroke: "#123456", radius: 2.5 }])( "should create rect with props: %o", (props) => { - const shape = ShapeRecord.createRect(pageId, 0, 0, props as RectProps); + const shape = EditorShapeRecord.createRect(pageId, 0, 0, props as RectProps); expect(shape.props).toEqual(props); }, ); @@ -141,7 +141,7 @@ describe("ShapeRecord", () => { describe("createEllipse", () => { it("should create an ellipse shape", () => { const props: EllipseProps = { w: 100, h: 50, fill: "#fff", stroke: "#000" }; - const shape = ShapeRecord.createEllipse(pageId, 10, 20, props); + const shape = EditorShapeRecord.createEllipse(pageId, 10, 20, props); expect(shape.id).toMatch(/^shape:/); expect(shape.type).toBe("ellipse"); @@ -156,7 +156,7 @@ describe("ShapeRecord", () => { describe("createLine", () => { it("should create a line shape", () => { const props: LineProps = { a: { x: 0, y: 0 }, b: { x: 100, y: 50 }, stroke: "#000", width: 2 }; - const shape = ShapeRecord.createLine(pageId, 10, 20, props); + const shape = EditorShapeRecord.createLine(pageId, 10, 20, props); expect(shape.id).toMatch(/^shape:/); expect(shape.type).toBe("line"); @@ -165,7 +165,7 @@ describe("ShapeRecord", () => { it("should handle negative coordinates in line endpoints", () => { const props: LineProps = { a: { x: -50, y: -30 }, b: { x: 100, y: 200 }, stroke: "#000", width: 1 }; - const shape = ShapeRecord.createLine(pageId, 0, 0, props); + const shape = EditorShapeRecord.createLine(pageId, 0, 0, props); expect(shape.props.a).toEqual({ x: -50, y: -30 }); expect(shape.props.b).toEqual({ x: 100, y: 200 }); @@ -180,7 +180,7 @@ describe("ShapeRecord", () => { end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }; - const shape = ShapeRecord.createArrow(pageId, 10, 20, props); + const shape = EditorShapeRecord.createArrow(pageId, 10, 20, props); expect(shape.id).toMatch(/^shape:/); expect(shape.type).toBe("arrow"); @@ -197,7 +197,7 @@ describe("ShapeRecord", () => { end: { kind: "free" }, style: { stroke: "#ff0000", width: 3 }, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); expect(shape.props.points?.length).toBe(3); expect(shape.props.points).toEqual(props.points); @@ -210,7 +210,7 @@ describe("ShapeRecord", () => { end: { kind: "bound", bindingId: "binding:2" }, style: { stroke: "#000", width: 2 }, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); expect(shape.props.start).toEqual({ kind: "bound", bindingId: "binding:1" }); expect(shape.props.end).toEqual({ kind: "bound", bindingId: "binding:2" }); @@ -224,7 +224,7 @@ describe("ShapeRecord", () => { end: { kind: "free" }, style, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); expect(shape.props.style?.headStart).toBe(true); expect(shape.props.style?.headEnd).toBe(true); @@ -238,7 +238,7 @@ describe("ShapeRecord", () => { end: { kind: "free" }, style, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); expect(shape.props.style?.dash).toEqual([5, 3]); }); @@ -251,7 +251,7 @@ describe("ShapeRecord", () => { style: { stroke: "#000", width: 2 }, routing: { kind: "orthogonal", cornerRadius: 5 }, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); expect(shape.props.routing).toEqual({ kind: "orthogonal", cornerRadius: 5 }); }); @@ -264,7 +264,7 @@ describe("ShapeRecord", () => { style: { stroke: "#000", width: 2 }, label: { text: "Connection", align: "center", offset: 0 }, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); expect(shape.props.label).toEqual({ text: "Connection", align: "center", offset: 0 }); }); @@ -280,7 +280,7 @@ describe("ShapeRecord", () => { style: { stroke: "#000", width: 2 }, label: { text: "Test", align, offset }, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); expect(shape.props.label?.align).toBe(align); expect(shape.props.label?.offset).toBe(offset); @@ -290,7 +290,7 @@ describe("ShapeRecord", () => { describe("createText", () => { it("should create a text shape without width", () => { const props: TextProps = { text: "Hello", fontSize: 16, fontFamily: "Arial", color: "#000" }; - const shape = ShapeRecord.createText(pageId, 10, 20, props); + const shape = EditorShapeRecord.createText(pageId, 10, 20, props); expect(shape.id).toMatch(/^shape:/); expect(shape.type).toBe("text"); @@ -300,7 +300,7 @@ describe("ShapeRecord", () => { it("should create a text shape with width", () => { const props: TextProps = { text: "Hello", fontSize: 16, fontFamily: "Arial", color: "#000", w: 200 }; - const shape = ShapeRecord.createText(pageId, 10, 20, props); + const shape = EditorShapeRecord.createText(pageId, 10, 20, props); expect(shape.props.w).toBe(200); }); @@ -313,7 +313,7 @@ describe("ShapeRecord", () => { }, { text: "Special chars: !@#$%^&*()", fontSize: 14, fontFamily: "Courier", color: "rgb(0,0,0)" }])( "should create text with props: %o", (props) => { - const shape = ShapeRecord.createText(pageId, 0, 0, props as TextProps); + const shape = EditorShapeRecord.createText(pageId, 0, 0, props as TextProps); expect(shape.props.text).toBe(props.text); expect(shape.props.fontSize).toBe(props.fontSize); }, @@ -323,9 +323,9 @@ describe("ShapeRecord", () => { describe("clone", () => { it("should clone a rect shape", () => { const props: RectProps = { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 5 }; - const shape = ShapeRecord.createRect(pageId, 10, 20, props); + const shape = EditorShapeRecord.createRect(pageId, 10, 20, props); - const cloned = ShapeRecord.clone(shape); + const cloned = EditorShapeRecord.clone(shape); expect(cloned).toEqual(shape); expect(cloned).not.toBe(shape); @@ -334,9 +334,9 @@ describe("ShapeRecord", () => { it("should deep clone props", () => { const props: RectProps = { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 5 }; - const shape = ShapeRecord.createRect(pageId, 10, 20, props); + const shape = EditorShapeRecord.createRect(pageId, 10, 20, props); - const cloned = ShapeRecord.clone(shape); + const cloned = EditorShapeRecord.clone(shape); if (cloned.type === "rect") { cloned.props.w = 200; } @@ -346,9 +346,9 @@ describe("ShapeRecord", () => { it("should clone line shape with Vec2 props", () => { const props: LineProps = { a: { x: 0, y: 0 }, b: { x: 100, y: 50 }, stroke: "#000", width: 2 }; - const shape = ShapeRecord.createLine(pageId, 0, 0, props); + const shape = EditorShapeRecord.createLine(pageId, 0, 0, props); - const cloned = ShapeRecord.clone(shape); + const cloned = EditorShapeRecord.clone(shape); expect(cloned).toEqual(shape); expect(cloned.props).not.toBe(shape.props); @@ -363,9 +363,9 @@ describe("ShapeRecord", () => { routing: { kind: "orthogonal", cornerRadius: 5 }, label: { text: "Test", align: "center", offset: 0 }, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); - const cloned = ShapeRecord.clone(shape); + const cloned = EditorShapeRecord.clone(shape); expect(cloned).toEqual(shape); expect(cloned.props).not.toBe(shape.props); @@ -386,9 +386,9 @@ describe("ShapeRecord", () => { end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); - const cloned = ShapeRecord.clone(shape); + const cloned = EditorShapeRecord.clone(shape); if (cloned.type === "arrow" && shape.type === "arrow" && cloned.props.points && shape.props.points) { cloned.props.points[0].x = 999; @@ -403,9 +403,9 @@ describe("ShapeRecord", () => { end: { kind: "free" }, style: { stroke: "#000", width: 2, dash: [5, 3] }, }; - const shape = ShapeRecord.createArrow(pageId, 0, 0, props); + const shape = EditorShapeRecord.createArrow(pageId, 0, 0, props); - const cloned = ShapeRecord.clone(shape); + const cloned = EditorShapeRecord.clone(shape); if (cloned.type === "arrow" && shape.type === "arrow" && cloned.props.style?.dash && shape.props.style?.dash) { cloned.props.style.dash[0] = 999; @@ -418,9 +418,9 @@ describe("ShapeRecord", () => { it("should create shapes at different positions", () => { const props: RectProps = { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 0 }; - const shape1 = ShapeRecord.createRect(pageId, 0, 0, props); - const shape2 = ShapeRecord.createRect(pageId, 100, 200, props); - const shape3 = ShapeRecord.createRect(pageId, -50, -30, props); + const shape1 = EditorShapeRecord.createRect(pageId, 0, 0, props); + const shape2 = EditorShapeRecord.createRect(pageId, 100, 200, props); + const shape3 = EditorShapeRecord.createRect(pageId, -50, -30, props); expect(shape1.x).toBe(0); expect(shape1.y).toBe(0); @@ -432,17 +432,17 @@ describe("ShapeRecord", () => { it("should initialize rotation to 0", () => { const props: RectProps = { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 0 }; - const shape = ShapeRecord.createRect(pageId, 0, 0, props); + const shape = EditorShapeRecord.createRect(pageId, 0, 0, props); expect(shape.rot).toBe(0); }); }); }); -describe("BindingRecord", () => { +describe("EditorBindingRecord", () => { describe("create", () => { it("should create a binding with default anchor", () => { - const binding = BindingRecord.create("arrow1", "shape1", "start"); + const binding = EditorBindingRecord.create("arrow1", "shape1", "start"); expect(binding.id).toMatch(/^binding:/); expect(binding.type).toBe("arrow-end"); @@ -453,13 +453,13 @@ describe("BindingRecord", () => { }); it("should create a binding with custom ID", () => { - const binding = BindingRecord.create("arrow1", "shape1", "end", { kind: "center" }, "binding:custom"); + const binding = EditorBindingRecord.create("arrow1", "shape1", "end", { kind: "center" }, "binding:custom"); expect(binding.id).toBe("binding:custom"); }); it("should create a typed semantic relationship", () => { - const relation = BindingRecord.createRelation("service", "database", "depends_on", "binding:depends-on"); + const relation = EditorBindingRecord.createRelation("service", "database", "depends_on", "binding:depends-on"); expect(relation).toMatchObject({ id: "binding:depends-on", @@ -473,14 +473,14 @@ describe("BindingRecord", () => { it.each([{ handle: "start" as const }, { handle: "end" as const }])( "should create binding with handle: $handle", ({ handle }) => { - const binding = BindingRecord.create("arrow1", "shape1", handle); + const binding = EditorBindingRecord.create("arrow1", "shape1", handle); expect(binding.handle).toBe(handle); }, ); it("should create binding with custom anchor", () => { const anchor = { kind: "center" as const }; - const binding = BindingRecord.create("arrow1", "shape1", "start", anchor); + const binding = EditorBindingRecord.create("arrow1", "shape1", "start", anchor); expect(binding.anchor).toEqual(anchor); }); @@ -488,9 +488,9 @@ describe("BindingRecord", () => { describe("clone", () => { it("should create a copy of the binding with center anchor", () => { - const binding = BindingRecord.create("arrow1", "shape1", "start"); + const binding = EditorBindingRecord.create("arrow1", "shape1", "start"); - const cloned = BindingRecord.clone(binding); + const cloned = EditorBindingRecord.clone(binding); expect(cloned).toEqual(binding); expect(cloned).not.toBe(binding); @@ -498,18 +498,18 @@ describe("BindingRecord", () => { }); it("should deep clone center anchor", () => { - const binding = BindingRecord.create("arrow1", "shape1", "start"); + const binding = EditorBindingRecord.create("arrow1", "shape1", "start"); - const cloned = BindingRecord.clone(binding); + const cloned = EditorBindingRecord.clone(binding); expect(cloned.anchor).toEqual(binding.anchor); expect(cloned.anchor).not.toBe(binding.anchor); }); it("should clone binding with edge anchor", () => { - const binding = BindingRecord.create("arrow1", "shape1", "end", { kind: "edge", nx: 0.5, ny: -0.5 }); + const binding = EditorBindingRecord.create("arrow1", "shape1", "end", { kind: "edge", nx: 0.5, ny: -0.5 }); - const cloned = BindingRecord.clone(binding); + const cloned = EditorBindingRecord.clone(binding); expect(cloned).toEqual(binding); expect(cloned).not.toBe(binding); @@ -517,9 +517,9 @@ describe("BindingRecord", () => { }); it("should deep clone edge anchor", () => { - const binding = BindingRecord.create("arrow1", "shape1", "start", { kind: "edge", nx: 1, ny: 0 }); + const binding = EditorBindingRecord.create("arrow1", "shape1", "start", { kind: "edge", nx: 1, ny: 0 }); - const cloned = BindingRecord.clone(binding); + const cloned = EditorBindingRecord.clone(binding); expect(cloned.anchor).toEqual({ kind: "edge", nx: 1, ny: 0 }); expect(cloned.anchor).not.toBe(binding.anchor); @@ -529,14 +529,14 @@ describe("BindingRecord", () => { describe("edge anchors", () => { it("should create binding with edge anchor at right edge", () => { const anchor = { kind: "edge" as const, nx: 1, ny: 0 }; - const binding = BindingRecord.create("arrow1", "shape1", "start", anchor); + const binding = EditorBindingRecord.create("arrow1", "shape1", "start", anchor); expect(binding.anchor).toEqual({ kind: "edge", nx: 1, ny: 0 }); }); it("should create binding with edge anchor at top-left corner", () => { const anchor = { kind: "edge" as const, nx: -1, ny: -1 }; - const binding = BindingRecord.create("arrow1", "shape1", "end", anchor); + const binding = EditorBindingRecord.create("arrow1", "shape1", "end", anchor); expect(binding.anchor).toEqual({ kind: "edge", nx: -1, ny: -1 }); }); @@ -551,17 +551,17 @@ describe("BindingRecord", () => { { nx: -0.5, ny: -0.5, desc: "top-left quadrant" }, ])("should create binding with edge anchor at $desc", ({ nx, ny }) => { const anchor = { kind: "edge" as const, nx, ny }; - const binding = BindingRecord.create("arrow1", "shape1", "start", anchor); + const binding = EditorBindingRecord.create("arrow1", "shape1", "start", anchor); expect(binding.anchor).toEqual({ kind: "edge", nx, ny }); }); }); }); -describe("Document", () => { +describe("EditorDocument", () => { describe("create", () => { it("should create an empty document", () => { - const doc = Document.create(); + const doc = EditorDocument.create(); expect(doc.pages).toEqual({}); expect(doc.shapes).toEqual({}); @@ -571,17 +571,17 @@ describe("Document", () => { describe("clone", () => { it("should clone an empty document", () => { - const doc = Document.create(); - const cloned = Document.clone(doc); + const doc = EditorDocument.create(); + const cloned = EditorDocument.clone(doc); expect(cloned).toEqual(doc); expect(cloned).not.toBe(doc); }); it("should deep clone document with pages and shapes", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -593,7 +593,7 @@ describe("Document", () => { doc.pages = { page1: page }; doc.shapes = { shape1: shape }; - const cloned = Document.clone(doc); + const cloned = EditorDocument.clone(doc); expect(cloned).toEqual(doc); expect(cloned.pages).not.toBe(doc.pages); @@ -603,11 +603,11 @@ describe("Document", () => { }); it("should deep clone bindings", () => { - const doc = Document.create(); - const binding = BindingRecord.create("arrow1", "shape1", "start", { kind: "center" }, "binding1"); + const doc = EditorDocument.create(); + const binding = EditorBindingRecord.create("arrow1", "shape1", "start", { kind: "center" }, "binding1"); doc.bindings = { binding1: binding }; - const cloned = Document.clone(doc); + const cloned = EditorDocument.clone(doc); expect(cloned.bindings).not.toBe(doc.bindings); expect(cloned.bindings.binding1).not.toBe(doc.bindings.binding1); @@ -619,16 +619,16 @@ describe("Document", () => { describe("validateDoc", () => { describe("valid documents", () => { it("should validate empty document", () => { - const doc = Document.create(); + const doc = EditorDocument.create(); const result = validateDoc(doc); expect(result.ok).toBe(true); }); it("should validate document with page and shape", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -646,16 +646,16 @@ describe("validateDoc", () => { }); it("should validate document with multiple shapes", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape1 = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape1 = EditorShapeRecord.createRect( "page1", 0, 0, { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "shape1", ); - const shape2 = ShapeRecord.createEllipse( + const shape2 = EditorShapeRecord.createEllipse( "page1", 50, 50, @@ -673,23 +673,23 @@ describe("validateDoc", () => { }); it("should validate document with binding", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "arrow1"); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page1", 100, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const binding = BindingRecord.create("arrow1", "rect1", "end", { kind: "center" }, "binding1"); - const relation = BindingRecord.createRelation("rect1", "arrow1", "depends_on", "relation1"); + const binding = EditorBindingRecord.create("arrow1", "rect1", "end", { kind: "center" }, "binding1"); + const relation = EditorBindingRecord.createRelation("rect1", "arrow1", "depends_on", "relation1"); page.shapeIds = ["arrow1", "rect1"]; doc.pages = { page1: page }; @@ -702,23 +702,23 @@ describe("validateDoc", () => { }); it("should reject an empty semantic relationship type", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const source = ShapeRecord.createRect("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const source = EditorShapeRecord.createRect("page1", 0, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0, }, "source"); - const target = ShapeRecord.createRect("page1", 100, 0, { + const target = EditorShapeRecord.createRect("page1", 100, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0, }, "target"); - const relation = BindingRecord.createRelation(source.id, target.id, "", "relation1"); + const relation = EditorBindingRecord.createRelation(source.id, target.id, "", "relation1"); page.shapeIds = [source.id, target.id]; doc.pages = { page1: page }; @@ -734,8 +734,8 @@ describe("validateDoc", () => { describe("invalid documents", () => { it("should reject document with shapes but no pages", () => { - const doc = Document.create(); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -753,9 +753,9 @@ describe("validateDoc", () => { }); it("should reject shape with mismatched ID", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -776,8 +776,8 @@ describe("validateDoc", () => { }); it("should reject shape referencing non-existent page", () => { - const doc = Document.create(); - const shape = ShapeRecord.createRect("nonexistent", 0, 0, { + const doc = EditorDocument.create(); + const shape = EditorShapeRecord.createRect("nonexistent", 0, 0, { w: 100, h: 50, fill: "#fff", @@ -796,9 +796,9 @@ describe("validateDoc", () => { }); it("should reject shape not listed in page shapeIds", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -818,8 +818,8 @@ describe("validateDoc", () => { }); it("should reject page referencing non-existent shape", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); page.shapeIds = ["nonexistent"]; doc.pages = { page1: page }; @@ -833,9 +833,9 @@ describe("validateDoc", () => { }); it("should reject page with duplicate shape IDs", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -856,16 +856,16 @@ describe("validateDoc", () => { }); it("should reject binding to non-existent fromShape", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const rect = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const rect = EditorShapeRecord.createRect( "page1", 0, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const binding = BindingRecord.create("nonexistent", "rect1", "end", { kind: "center" }, "binding1"); + const binding = EditorBindingRecord.create("nonexistent", "rect1", "end", { kind: "center" }, "binding1"); page.shapeIds = ["rect1"]; doc.pages = { page1: page }; @@ -881,15 +881,15 @@ describe("validateDoc", () => { }); it("should reject binding to non-existent toShape", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "arrow1"); - const binding = BindingRecord.create("arrow1", "nonexistent", "end", { kind: "center" }, "binding1"); + const binding = EditorBindingRecord.create("arrow1", "nonexistent", "end", { kind: "center" }, "binding1"); page.shapeIds = ["arrow1"]; doc.pages = { page1: page }; @@ -905,23 +905,23 @@ describe("validateDoc", () => { }); it("should reject binding from non-arrow shape", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const rect1 = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const rect1 = EditorShapeRecord.createRect( "page1", 0, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const rect2 = ShapeRecord.createRect( + const rect2 = EditorShapeRecord.createRect( "page1", 100, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect2", ); - const binding = BindingRecord.create("rect1", "rect2", "start", { kind: "center" }, "binding1"); + const binding = EditorBindingRecord.create("rect1", "rect2", "start", { kind: "center" }, "binding1"); page.shapeIds = ["rect1", "rect2"]; doc.pages = { page1: page }; @@ -937,9 +937,9 @@ describe("validateDoc", () => { }); it("should reject rect with negative width", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -960,9 +960,9 @@ describe("validateDoc", () => { }); it("should reject rect with negative height", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -983,9 +983,9 @@ describe("validateDoc", () => { }); it("should reject rect with negative radius", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -1006,9 +1006,9 @@ describe("validateDoc", () => { }); it("should reject ellipse with negative dimensions", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createEllipse( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createEllipse( "page1", 0, 0, @@ -1029,9 +1029,9 @@ describe("validateDoc", () => { }); it("should reject line with negative width", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createLine("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createLine("page1", 0, 0, { a: { x: 0, y: 0 }, b: { x: 100, y: 0 }, stroke: "#000", @@ -1051,9 +1051,9 @@ describe("validateDoc", () => { }); it("should reject text with invalid fontSize", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createText("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createText("page1", 0, 0, { text: "Test", fontSize: 0, fontFamily: "Arial", @@ -1073,9 +1073,9 @@ describe("validateDoc", () => { }); it("should reject text with negative width", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createText("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createText("page1", 0, 0, { text: "Test", fontSize: 12, fontFamily: "Arial", @@ -1096,16 +1096,16 @@ describe("validateDoc", () => { }); it("should collect multiple errors", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape1 = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape1 = EditorShapeRecord.createRect( "page1", 0, 0, { w: -100, h: -50, fill: "#fff", stroke: "#000", radius: 0 }, "shape1", ); - const shape2 = ShapeRecord.createRect("nonexistent", 0, 0, { + const shape2 = EditorShapeRecord.createRect("nonexistent", 0, 0, { w: 100, h: 50, fill: "#fff", @@ -1126,9 +1126,9 @@ describe("validateDoc", () => { }); it("should reject arrow with missing required fields", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createArrow("page1", 0, 0, {} as any, "arrow1"); + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createArrow("page1", 0, 0, {} as any, "arrow1"); page.shapeIds = ["arrow1"]; doc.pages = { page1: page }; @@ -1141,9 +1141,9 @@ describe("validateDoc", () => { }); it("should reject arrow with too few points in modern format", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -1163,9 +1163,9 @@ describe("validateDoc", () => { }); it("should reject arrow with negative width in modern format", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -1185,9 +1185,9 @@ describe("validateDoc", () => { }); it("should reject arrow with negative cornerRadius", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -1208,9 +1208,9 @@ describe("validateDoc", () => { }); it("should reject arrow with invalid label alignment", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, @@ -1231,22 +1231,22 @@ describe("validateDoc", () => { }); it("should reject binding with edge anchor nx out of range", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "arrow1"); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page1", 100, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const binding = BindingRecord.create("arrow1", "rect1", "end", { kind: "edge", nx: 1.5, ny: 0 }, "binding1"); + const binding = EditorBindingRecord.create("arrow1", "rect1", "end", { kind: "edge", nx: 1.5, ny: 0 }, "binding1"); page.shapeIds = ["arrow1", "rect1"]; doc.pages = { page1: page }; @@ -1262,22 +1262,22 @@ describe("validateDoc", () => { }); it("should reject binding with edge anchor ny out of range", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "arrow1"); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page1", 100, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const binding = BindingRecord.create("arrow1", "rect1", "start", { kind: "edge", nx: 0, ny: -2 }, "binding1"); + const binding = EditorBindingRecord.create("arrow1", "rect1", "start", { kind: "edge", nx: 0, ny: -2 }, "binding1"); page.shapeIds = ["arrow1", "rect1"]; doc.pages = { page1: page }; @@ -1293,9 +1293,9 @@ describe("validateDoc", () => { }); it("should accept valid modern arrow format", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 50, y: 25 }, { x: 100, y: 50 }], start: { kind: "free" }, end: { kind: "free" }, @@ -1314,22 +1314,22 @@ describe("validateDoc", () => { }); it("should accept binding with valid edge anchor", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "bound", bindingId: "binding1" }, style: { stroke: "#000", width: 2 }, }, "arrow1"); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page1", 100, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const binding = BindingRecord.create("arrow1", "rect1", "end", { kind: "edge", nx: 0.5, ny: -0.5 }, "binding1"); + const binding = EditorBindingRecord.create("arrow1", "rect1", "end", { kind: "edge", nx: 0.5, ny: -0.5 }, "binding1"); page.shapeIds = ["arrow1", "rect1"]; doc.pages = { page1: page }; @@ -1344,9 +1344,9 @@ describe("validateDoc", () => { describe("edge cases", () => { it("should accept zero-sized shapes", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 0, 0, @@ -1364,9 +1364,9 @@ describe("validateDoc", () => { }); it("should accept text with undefined width", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createText("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createText("page1", 0, 0, { text: "Test", fontSize: 12, fontFamily: "Arial", @@ -1383,8 +1383,8 @@ describe("validateDoc", () => { }); it("should accept empty page name", () => { - const doc = Document.create(); - const page = PageRecord.create("", "page1"); + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("", "page1"); doc.pages = { page1: page }; const result = validateDoc(doc); @@ -1396,7 +1396,7 @@ describe("validateDoc", () => { describe("JSON serialization", () => { it("should round-trip empty document", () => { - const doc = Document.create(); + const doc = EditorDocument.create(); const json = JSON.stringify(doc); const parsed = JSON.parse(json); @@ -1405,9 +1405,9 @@ describe("JSON serialization", () => { }); it("should round-trip document with page and shape", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const shape = EditorShapeRecord.createRect( "page1", 10, 20, @@ -1427,36 +1427,36 @@ describe("JSON serialization", () => { }); it("should round-trip document with all shape types", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page1", 0, 0, { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 5 }, "shape1", ); - const ellipse = ShapeRecord.createEllipse( + const ellipse = EditorShapeRecord.createEllipse( "page1", 100, 100, { w: 75, h: 75, fill: "#f00", stroke: "#000" }, "shape2", ); - const line = ShapeRecord.createLine("page1", 200, 200, { + const line = EditorShapeRecord.createLine("page1", 200, 200, { a: { x: 0, y: 0 }, b: { x: 100, y: 50 }, stroke: "#000", width: 2, }, "shape3"); - const arrow = ShapeRecord.createArrow("page1", 300, 300, { + const arrow = EditorShapeRecord.createArrow("page1", 300, 300, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "shape4"); - const text = ShapeRecord.createText("page1", 400, 400, { + const text = EditorShapeRecord.createText("page1", 400, 400, { text: "Hello World", fontSize: 16, fontFamily: "Arial", @@ -1476,22 +1476,22 @@ describe("JSON serialization", () => { }); it("should round-trip document with bindings", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "arrow1"); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page1", 100, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const binding = BindingRecord.create("arrow1", "rect1", "end", { kind: "center" }, "binding1"); + const binding = EditorBindingRecord.create("arrow1", "rect1", "end", { kind: "center" }, "binding1"); page.shapeIds = ["arrow1", "rect1"]; doc.pages = { page1: page }; @@ -1506,31 +1506,31 @@ describe("JSON serialization", () => { }); it("should round-trip complex document", () => { - const doc = Document.create(); - const page1 = PageRecord.create("Page 1", "page1"); - const page2 = PageRecord.create("Page 2", "page2"); + const doc = EditorDocument.create(); + const page1 = EditorPageRecord.create("Page 1", "page1"); + const page2 = EditorPageRecord.create("Page 2", "page2"); - const shape1 = ShapeRecord.createRect( + const shape1 = EditorShapeRecord.createRect( "page1", 0, 0, { w: 100, h: 50, fill: "#fff", stroke: "#000", radius: 5 }, "shape1", ); - const shape2 = ShapeRecord.createEllipse( + const shape2 = EditorShapeRecord.createEllipse( "page1", 100, 100, { w: 75, h: 75, fill: "#f00", stroke: "#000" }, "shape2", ); - const shape3 = ShapeRecord.createArrow("page2", 0, 0, { + const shape3 = EditorShapeRecord.createArrow("page2", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "shape3"); - const shape4 = ShapeRecord.createRect( + const shape4 = EditorShapeRecord.createRect( "page2", 100, 0, @@ -1538,7 +1538,7 @@ describe("JSON serialization", () => { "shape4", ); - const binding = BindingRecord.create("shape3", "shape4", "end", { kind: "center" }, "binding1"); + const binding = EditorBindingRecord.create("shape3", "shape4", "end", { kind: "center" }, "binding1"); page1.shapeIds = ["shape1", "shape2"]; page2.shapeIds = ["shape3", "shape4"]; @@ -1555,9 +1555,9 @@ describe("JSON serialization", () => { }); it("should round-trip arrow with modern format", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 50, y: 25 }, { x: 100, y: 50 }], start: { kind: "free" }, end: { kind: "free" }, @@ -1578,30 +1578,30 @@ describe("JSON serialization", () => { }); it("should round-trip arrow with bound endpoints", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "bound", bindingId: "binding1" }, end: { kind: "bound", bindingId: "binding2" }, style: { stroke: "#000", width: 2 }, }, "arrow1"); - const rect1 = ShapeRecord.createRect( + const rect1 = EditorShapeRecord.createRect( "page1", -50, -25, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const rect2 = ShapeRecord.createRect( + const rect2 = EditorShapeRecord.createRect( "page1", 100, -25, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect2", ); - const binding1 = BindingRecord.create("arrow1", "rect1", "start", { kind: "edge", nx: 1, ny: 0 }, "binding1"); - const binding2 = BindingRecord.create("arrow1", "rect2", "end", { kind: "edge", nx: -1, ny: 0 }, "binding2"); + const binding1 = EditorBindingRecord.create("arrow1", "rect1", "start", { kind: "edge", nx: 1, ny: 0 }, "binding1"); + const binding2 = EditorBindingRecord.create("arrow1", "rect2", "end", { kind: "edge", nx: -1, ny: 0 }, "binding2"); page.shapeIds = ["arrow1", "rect1", "rect2"]; doc.pages = { page1: page }; @@ -1616,22 +1616,22 @@ describe("JSON serialization", () => { }); it("should round-trip binding with edge anchor", () => { - const doc = Document.create(); - const page = PageRecord.create("Page 1", "page1"); - const arrow = ShapeRecord.createArrow("page1", 0, 0, { + const doc = EditorDocument.create(); + const page = EditorPageRecord.create("Page 1", "page1"); + const arrow = EditorShapeRecord.createArrow("page1", 0, 0, { points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], start: { kind: "free" }, end: { kind: "free" }, style: { stroke: "#000", width: 2 }, }, "arrow1"); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page1", 100, 0, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }, "rect1", ); - const binding = BindingRecord.create("arrow1", "rect1", "end", { kind: "edge", nx: -0.5, ny: 0.5 }, "binding1"); + const binding = EditorBindingRecord.create("arrow1", "rect1", "end", { kind: "edge", nx: -0.5, ny: 0.5 }, "binding1"); page.shapeIds = ["arrow1", "rect1"]; doc.pages = { page1: page }; diff --git a/packages/core/tests/opacity.test.ts b/packages/core/tests/opacity.test.ts index 001b6cc..d965a22 100644 --- a/packages/core/tests/opacity.test.ts +++ b/packages/core/tests/opacity.test.ts @@ -1,11 +1,11 @@ -import { Document, PageRecord, ShapeRecord, validateDoc } from '../src/model'; +import { EditorDocument, EditorPageRecord, EditorShapeRecord, validateDoc } from '../src/editor-model'; import { describe, expect, it } from 'vitest'; describe('shape opacity', () => { it('accepts finite values from zero to one and rejects values outside the range', () => { - const doc = Document.create(); - const page = PageRecord.create('Page', 'page'); - const shape = ShapeRecord.createRect( + const doc = EditorDocument.create(); + const page = EditorPageRecord.create('Page', 'page'); + const shape = EditorShapeRecord.createRect( page.id, 0, 0, diff --git a/packages/core/tests/path-metrics.test.ts b/packages/core/tests/path-metrics.test.ts index 6100110..68af7e5 100644 --- a/packages/core/tests/path-metrics.test.ts +++ b/packages/core/tests/path-metrics.test.ts @@ -10,7 +10,7 @@ import { transformPathGeometry, trimPathGeometry } from '../src/path-metrics'; -import type { PathGeometry } from '../src/model'; +import type { PathGeometry } from '../src/editor-model'; const line: PathGeometry = { subpaths: [ diff --git a/packages/core/tests/path-topology.test.ts b/packages/core/tests/path-topology.test.ts index ef29033..7bc3f29 100644 --- a/packages/core/tests/path-topology.test.ts +++ b/packages/core/tests/path-topology.test.ts @@ -4,18 +4,18 @@ import { Action, DirectSelectTool, Modifiers, - PageRecord, - ShapeRecord, + EditorPageRecord, + EditorShapeRecord, Store, applyPathTopologyOperations } from '../src'; -import type { PathProps, PathTopologyOperation } from '../src/model'; +import type { PathProps, PathTopologyOperation } from '../src/editor-model'; const buttons = { left: true, middle: false, right: false }; const modifiers = Modifiers.create(); function createPath() { - const page = PageRecord.create('Page', 'page:path-topology'); + const page = EditorPageRecord.create('Page', 'page:path-topology'); const props: PathProps = { subpaths: [ { @@ -36,7 +36,7 @@ function createPath() { fill_rule: 'nonzero', fill: '#fff' }; - const path = ShapeRecord.createPath(page.id, 0, 0, props, 'path:topology'); + const path = EditorShapeRecord.createPath(page.id, 0, 0, props, 'path:topology'); page.shapeIds = [path.id]; const state = new Store({ doc: { pages: { [page.id]: page }, shapes: { [path.id]: path }, bindings: {} }, @@ -54,8 +54,8 @@ function createPath() { describe('path topology previews', () => { it('matches the shared topology fixtures', () => { for (const testCase of fixture.cases) { - const page = PageRecord.create('Page', `page:${testCase.name}`); - const path = ShapeRecord.createPath( + const page = EditorPageRecord.create('Page', `page:${testCase.name}`); + const path = EditorShapeRecord.createPath( page.id, 0, 0, @@ -121,8 +121,8 @@ describe('path topology previews', () => { expect(closed?.type).toBe('path'); if (closed?.type === 'path') expect(closed.props.subpaths[0]?.closed).toBe(true); - const joinPage = PageRecord.create('Page', 'page:path-join'); - const joinPath = ShapeRecord.createPath( + const joinPage = EditorPageRecord.create('Page', 'page:path-join'); + const joinPath = EditorShapeRecord.createPath( joinPage.id, 0, 0, diff --git a/packages/core/tests/path.test.ts b/packages/core/tests/path.test.ts index 8eec1d0..7d06801 100644 --- a/packages/core/tests/path.test.ts +++ b/packages/core/tests/path.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { hitTestPath, pathGeometryBounds, pointInPath, pointNearPath, shapeBounds } from '../src/geom'; -import { ShapeRecord, type PathGeometry } from '../src/model'; +import { EditorShapeRecord, type PathGeometry } from '../src/editor-model'; describe('native path geometry', () => { const geometry: PathGeometry = { @@ -27,7 +27,7 @@ describe('native path geometry', () => { }); it('applies the path shape transform to bounds and hits', () => { - const shape = ShapeRecord.createPath('page', 10, 20, { ...geometry, fill: '#fff', stroke: '#000' }, 'path'); + const shape = EditorShapeRecord.createPath('page', 10, 20, { ...geometry, fill: '#fff', stroke: '#000' }, 'path'); const bounds = shapeBounds(shape); expect(bounds.min.x).toBe(10); expect(bounds.min.y).toBe(20); @@ -65,7 +65,7 @@ describe('native path geometry', () => { }); it('hits open path strokes with width and selection tolerance', () => { - const shape = ShapeRecord.createPath( + const shape = EditorShapeRecord.createPath( 'page', 0, 0, diff --git a/packages/core/tests/pen-tool.test.ts b/packages/core/tests/pen-tool.test.ts index e25b657..91df3ef 100644 --- a/packages/core/tests/pen-tool.test.ts +++ b/packages/core/tests/pen-tool.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from "vitest"; -import { PageRecord, Store } from "../src"; +import { EditorPageRecord, Store } from "../src"; import type { Action } from "../src/actions"; import { Modifiers, PointerButtons } from "../src/actions"; import { PenTool } from "../src/tools/pen"; @@ -78,7 +78,7 @@ describe("PenTool", () => { it("should initialize with clean state on enter", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -95,7 +95,7 @@ describe("PenTool", () => { it("should clean up draft stroke on exit", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -118,7 +118,7 @@ describe("PenTool", () => { it("should create stroke on pointer down", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -144,7 +144,7 @@ describe("PenTool", () => { it("should retain hardware pressure samples when available", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -168,7 +168,7 @@ describe("PenTool", () => { it("should add points on pointer move", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -195,7 +195,7 @@ describe("PenTool", () => { it("coalesces pointer updates within the same frame", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -224,7 +224,7 @@ describe("PenTool", () => { it("flushes pending points on pointer up even without a new frame", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -254,7 +254,7 @@ describe("PenTool", () => { it("applies injected stroke style when creating strokes", () => { const tool = new PenTool(undefined, () => ({ color: "#88c0d0", opacity: 0.75 })); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -279,7 +279,7 @@ describe("PenTool", () => { it("should not add point if moved less than minimum distance", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -305,7 +305,7 @@ describe("PenTool", () => { it("should finalize stroke on pointer up", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -332,7 +332,7 @@ describe("PenTool", () => { it("should delete stroke if too few points on pointer up", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -351,7 +351,7 @@ describe("PenTool", () => { it("should cancel stroke on Escape key", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -375,7 +375,7 @@ describe("PenTool", () => { it("should ignore other keys", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -407,7 +407,7 @@ describe("PenTool", () => { it("should handle pointer move without drawing", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, @@ -424,7 +424,7 @@ describe("PenTool", () => { it("should select created stroke", () => { const tool = new PenTool(); const store = new Store(); - const page = PageRecord.create("Page 1", "page:1"); + const page = EditorPageRecord.create("Page 1", "page:1"); store.setState((state) => ({ ...state, diff --git a/packages/core/tests/reactivity.test.ts b/packages/core/tests/reactivity.test.ts index 6400906..c18be65 100644 --- a/packages/core/tests/reactivity.test.ts +++ b/packages/core/tests/reactivity.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import { Camera } from '../src/camera'; import { CreateShapeCommand } from '../src/history'; -import { PageRecord, ShapeRecord } from '../src/model'; +import { EditorPageRecord, EditorShapeRecord } from '../src/editor-model'; import { EditorState as EditorStateOps, getAllPages, @@ -31,8 +31,8 @@ describe('EditorState', () => { describe('clone', () => { it('should deep clone editor state', () => { const state = EditorStateOps.create(); - const page = PageRecord.create('Page 1', 'page1'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page1'); + const shape = EditorShapeRecord.createRect( 'page1', 0, 0, @@ -128,7 +128,7 @@ describe('Store', () => { it('should update document', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); @@ -250,8 +250,8 @@ describe('Invariants', () => { it('should repair invalid currentPageId to first page when pages exist', () => { const store = new Store(); - const page1 = PageRecord.create('Page 1', 'page1'); - const page2 = PageRecord.create('Page 2', 'page2'); + const page1 = EditorPageRecord.create('Page 1', 'page1'); + const page2 = EditorPageRecord.create('Page 2', 'page2'); store.setState((state) => ({ ...state, @@ -265,7 +265,7 @@ describe('Invariants', () => { it('should keep valid currentPageId', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, @@ -293,8 +293,8 @@ describe('Invariants', () => { it('should remove non-existent shapes from selection', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page1'); + const shape = EditorShapeRecord.createRect( 'page1', 0, 0, @@ -316,17 +316,17 @@ describe('Invariants', () => { it('should remove shapes not on current page from selection', () => { const store = new Store(); - const page1 = PageRecord.create('Page 1', 'page1'); - const page2 = PageRecord.create('Page 2', 'page2'); + const page1 = EditorPageRecord.create('Page 1', 'page1'); + const page2 = EditorPageRecord.create('Page 2', 'page2'); - const shape1 = ShapeRecord.createRect( + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, { w: 100, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'shape1' ); - const shape2 = ShapeRecord.createRect( + const shape2 = EditorShapeRecord.createRect( 'page2', 0, 0, @@ -349,15 +349,15 @@ describe('Invariants', () => { it('should keep valid selection', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); - const shape1 = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page1'); + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, { w: 100, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'shape1' ); - const shape2 = ShapeRecord.createRect( + const shape2 = EditorShapeRecord.createRect( 'page1', 50, 50, @@ -379,8 +379,8 @@ describe('Invariants', () => { it('should maintain reference equality when no repair needed', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page1'); + const shape = EditorShapeRecord.createRect( 'page1', 0, 0, @@ -409,8 +409,8 @@ describe('Invariants', () => { describe('invariant repair on deletion', () => { it('should clear selection when selected shape is deleted', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); - const shape1 = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page1'); + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, @@ -437,8 +437,8 @@ describe('Invariants', () => { it('should update currentPageId when current page is deleted', () => { const store = new Store(); - const page1 = PageRecord.create('Page 1', 'page1'); - const page2 = PageRecord.create('Page 2', 'page2'); + const page1 = EditorPageRecord.create('Page 1', 'page1'); + const page2 = EditorPageRecord.create('Page 2', 'page2'); store.setState((state) => ({ ...state, @@ -465,7 +465,7 @@ describe('Selectors', () => { it('should return current page', () => { const state = EditorStateOps.create(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); state.doc.pages = { page1: page }; state.ui.currentPageId = 'page1'; @@ -496,7 +496,7 @@ describe('Selectors', () => { it('should return empty array for empty page', () => { const state = EditorStateOps.create(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); state.doc.pages = { page1: page }; state.ui.currentPageId = 'page1'; @@ -508,15 +508,15 @@ describe('Selectors', () => { it('should return all shapes on current page', () => { const state = EditorStateOps.create(); - const page = PageRecord.create('Page 1', 'page1'); - const shape1 = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page1'); + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, { w: 100, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'shape1' ); - const shape2 = ShapeRecord.createEllipse( + const shape2 = EditorShapeRecord.createEllipse( 'page1', 50, 50, @@ -538,8 +538,8 @@ describe('Selectors', () => { it('should filter out undefined shapes', () => { const state = EditorStateOps.create(); - const page = PageRecord.create('Page 1', 'page1'); - const shape1 = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page1'); + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, @@ -560,17 +560,17 @@ describe('Selectors', () => { it('should not include shapes from other pages', () => { const state = EditorStateOps.create(); - const page1 = PageRecord.create('Page 1', 'page1'); - const page2 = PageRecord.create('Page 2', 'page2'); + const page1 = EditorPageRecord.create('Page 1', 'page1'); + const page2 = EditorPageRecord.create('Page 2', 'page2'); - const shape1 = ShapeRecord.createRect( + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, { w: 100, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'shape1' ); - const shape2 = ShapeRecord.createRect( + const shape2 = EditorShapeRecord.createRect( 'page2', 0, 0, @@ -602,22 +602,22 @@ describe('Selectors', () => { it('should return selected shapes', () => { const state = EditorStateOps.create(); - const page = PageRecord.create('Page 1', 'page1'); - const shape1 = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page1'); + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, { w: 100, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'shape1' ); - const shape2 = ShapeRecord.createEllipse( + const shape2 = EditorShapeRecord.createEllipse( 'page1', 50, 50, { w: 75, h: 75, fill: '#000', stroke: '#fff' }, 'shape2' ); - const shape3 = ShapeRecord.createLine( + const shape3 = EditorShapeRecord.createLine( 'page1', 100, 100, @@ -640,7 +640,7 @@ describe('Selectors', () => { it('should filter out undefined shapes', () => { const state = EditorStateOps.create(); - const shape1 = ShapeRecord.createRect( + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, @@ -692,8 +692,8 @@ describe('Selectors', () => { it('should return all pages', () => { const state = EditorStateOps.create(); - const page1 = PageRecord.create('Page 1', 'page1'); - const page2 = PageRecord.create('Page 2', 'page2'); + const page1 = EditorPageRecord.create('Page 1', 'page1'); + const page2 = EditorPageRecord.create('Page 2', 'page2'); state.doc.pages = { page1, page2 }; @@ -715,7 +715,7 @@ describe('Selectors', () => { it('should return shape by ID', () => { const state = EditorStateOps.create(); - const shape = ShapeRecord.createRect( + const shape = EditorShapeRecord.createRect( 'page1', 0, 0, @@ -736,7 +736,7 @@ describe('Integration scenarios', () => { it('should handle complete workflow: create page, add shapes, select shapes', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } }, @@ -746,14 +746,14 @@ describe('Integration scenarios', () => { let state = store.getState(); expect(getCurrentPage(state)?.name).toBe('Page 1'); - const shape1 = ShapeRecord.createRect( + const shape1 = EditorShapeRecord.createRect( 'page1', 0, 0, { w: 100, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'shape1' ); - const shape2 = ShapeRecord.createEllipse( + const shape2 = EditorShapeRecord.createEllipse( 'page1', 50, 50, @@ -808,7 +808,7 @@ describe('History integration', () => { describe('executeCommand', () => { it('should execute command and add to history', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, @@ -816,7 +816,7 @@ describe('History integration', () => { ui: { ...state.ui, currentPageId: 'page1' } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -839,12 +839,12 @@ describe('History integration', () => { store.subscribe(listener); listener.mockClear(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); listener.mockClear(); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -862,11 +862,11 @@ describe('History integration', () => { describe('undo', () => { it('should undo last command', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -897,10 +897,10 @@ describe('History integration', () => { store.subscribe(listener); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -922,11 +922,11 @@ describe('History integration', () => { describe('redo', () => { it('should redo last undone command', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -958,10 +958,10 @@ describe('History integration', () => { store.subscribe(listener); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -991,11 +991,11 @@ describe('History integration', () => { it('should return true after executing command', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -1012,11 +1012,11 @@ describe('History integration', () => { it('should return true for redo after undo', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -1045,11 +1045,11 @@ describe('History integration', () => { it('should return updated history after commands', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -1070,11 +1070,11 @@ describe('History integration', () => { describe('clearHistory', () => { it('should clear all history', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape = ShapeRecord.createRect('page1', 10, 20, { + const shape = EditorShapeRecord.createRect('page1', 10, 20, { w: 100, h: 50, fill: '#fff', @@ -1097,11 +1097,11 @@ describe('History integration', () => { describe('history with multiple commands', () => { it('should handle multiple commands', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape1 = ShapeRecord.createRect( + const shape1 = EditorShapeRecord.createRect( 'page1', 10, 20, @@ -1109,7 +1109,7 @@ describe('History integration', () => { 'shape1' ); - const shape2 = ShapeRecord.createRect( + const shape2 = EditorShapeRecord.createRect( 'page1', 30, 40, @@ -1146,11 +1146,11 @@ describe('History integration', () => { it('should clear redo stack when new command is executed', () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page1'); + const page = EditorPageRecord.create('Page 1', 'page1'); store.setState((state) => ({ ...state, doc: { ...state.doc, pages: { page1: page } } })); - const shape1 = ShapeRecord.createRect( + const shape1 = EditorShapeRecord.createRect( 'page1', 10, 20, @@ -1158,7 +1158,7 @@ describe('History integration', () => { 'shape1' ); - const shape2 = ShapeRecord.createRect( + const shape2 = EditorShapeRecord.createRect( 'page1', 30, 40, diff --git a/packages/core/tests/selection-refinement.test.ts b/packages/core/tests/selection-refinement.test.ts index c0d36f7..9c23565 100644 --- a/packages/core/tests/selection-refinement.test.ts +++ b/packages/core/tests/selection-refinement.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from 'vitest'; import { Action, Modifiers, PointerButtons } from '../src/actions'; import { convertSelectedShapes, duplicateAndConnectSelection, duplicateSelection } from '../src/selection'; -import { PageRecord, ShapeRecord } from '../src/model'; +import { EditorPageRecord, EditorShapeRecord } from '../src/editor-model'; import { EditorState } from '../src/reactivity'; import { RectTool, SelectTool } from '../src/tools'; const down = PointerButtons.create(true, false, false); function selectionState() { - const page = PageRecord.create('Selection page', 'page:selection'); - const shape = ShapeRecord.createRect(page.id, 0, 0, { w: 40, h: 30, fill: '', stroke: '', radius: 0 }, 'shape:one'); + const page = EditorPageRecord.create('Selection page', 'page:selection'); + const shape = EditorShapeRecord.createRect(page.id, 0, 0, { w: 40, h: 30, fill: '', stroke: '', radius: 0 }, 'shape:one'); page.shapeIds = [shape.id]; return { ...EditorState.create(), @@ -68,15 +68,15 @@ describe('selection and movement refinements', () => { }); it('cycles through overlapping shapes on repeated clicks at one point', () => { - const page = PageRecord.create('Overlap page', 'page:overlap'); - const back = ShapeRecord.createRect( + const page = EditorPageRecord.create('Overlap page', 'page:overlap'); + const back = EditorShapeRecord.createRect( page.id, 0, 0, { w: 40, h: 30, fill: '', stroke: '', radius: 0 }, 'shape:back' ); - const front = ShapeRecord.createRect( + const front = EditorShapeRecord.createRect( page.id, 0, 0, @@ -126,22 +126,22 @@ describe('selection and movement refinements', () => { }); it('cycles independently inside a nested selection scope', () => { - const page = PageRecord.create('Nested page', 'page:nested'); - const frame = ShapeRecord.createContainer( + const page = EditorPageRecord.create('Nested page', 'page:nested'); + const frame = EditorShapeRecord.createContainer( page.id, 0, 0, { w: 50, h: 50, fill: '', stroke: '', radius: 0 }, 'shape:frame' ); - const back = ShapeRecord.createRect( + const back = EditorShapeRecord.createRect( page.id, 0, 0, { w: 40, h: 30, fill: '', stroke: '', radius: 0 }, 'shape:nested-back' ); - const front = ShapeRecord.createRect( + const front = EditorShapeRecord.createRect( page.id, 0, 0, @@ -245,7 +245,7 @@ describe('selection and movement refinements', () => { it('does not convert shapes that participate in a connector binding', () => { const state = selectionState() as EditorState; - const arrow = ShapeRecord.createArrow( + const arrow = EditorShapeRecord.createArrow( state.doc.pages['page:selection']!.id, 0, 0, @@ -275,7 +275,7 @@ describe('selection and movement refinements', () => { it('uses Shift and Alt for square centered rectangle creation', () => { const tool = new RectTool(); - const page = PageRecord.create('Draw page', 'page:draw'); + const page = EditorPageRecord.create('Draw page', 'page:draw'); const state = { ...EditorState.create(), doc: { pages: { [page.id]: page }, shapes: {}, bindings: {} }, diff --git a/packages/core/tests/snapping.test.ts b/packages/core/tests/snapping.test.ts index f47ecf5..c02d096 100644 --- a/packages/core/tests/snapping.test.ts +++ b/packages/core/tests/snapping.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from 'vitest'; import { snapAngle, snapPoint, snapTranslation } from '../src/snapping'; -import { PageRecord, ShapeRecord } from '../src/model'; +import { EditorPageRecord, EditorShapeRecord } from '../src/editor-model'; import { EditorState } from '../src/reactivity'; const options = { snapEnabled: true, gridEnabled: false, gridSize: 25, snapDistance: 8 }; -function stateWithShapes(shapes: ReturnType[]) { - const page = PageRecord.create('Snap page', 'page:snap'); +function stateWithShapes(shapes: ReturnType[]) { + const page = EditorPageRecord.create('Snap page', 'page:snap'); page.shapeIds = shapes.map((shape) => shape.id); return { ...EditorState.create(), @@ -21,7 +21,7 @@ function stateWithShapes(shapes: ReturnType[]) { describe('geometry snapping', () => { it('snaps a point to object edges and reports alignment guides', () => { - const target = ShapeRecord.createRect( + const target = EditorShapeRecord.createRect( 'page:snap', 100, 50, @@ -41,14 +41,14 @@ describe('geometry snapping', () => { }); it('snaps a moving selection to another shape while retaining its lead offset', () => { - const moving = ShapeRecord.createRect( + const moving = EditorShapeRecord.createRect( 'page:snap', 92, 20, { w: 20, h: 20, fill: '', stroke: '', radius: 0 }, 'shape:moving' ); - const target = ShapeRecord.createRect( + const target = EditorShapeRecord.createRect( 'page:snap', 100, 20, diff --git a/packages/core/tests/statusbar.test.ts b/packages/core/tests/statusbar.test.ts index e30e000..27540fe 100644 --- a/packages/core/tests/statusbar.test.ts +++ b/packages/core/tests/statusbar.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { CursorState as CursorStateOps } from "../src/cursor"; -import { ShapeRecord } from "../src/model"; -import type { ShapeRecord as ShapeRecordType } from "../src/model"; +import { EditorShapeRecord } from "../src/editor-model"; +import type { EditorShapeRecord as ShapeRecordType } from "../src/editor-model"; import { EditorState } from "../src/reactivity"; import { buildStatusBarVM, @@ -40,7 +40,7 @@ describe("Status bar selectors", () => { }); it("describes a single selected shape", () => { - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page-1", 10, 20, @@ -52,14 +52,14 @@ describe("Status bar selectors", () => { }); it("summarizes multiple selections with combined bounds and mixed kind", () => { - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page-1", 10, 20, { w: 40, h: 20, fill: "#000", stroke: "#fff", radius: 0 }, "shape-rect", ); - const ellipse = ShapeRecord.createEllipse( + const ellipse = EditorShapeRecord.createEllipse( "page-1", 100, 50, @@ -72,14 +72,14 @@ describe("Status bar selectors", () => { }); it("marks kind when all selected shapes match", () => { - const rectA = ShapeRecord.createRect( + const rectA = EditorShapeRecord.createRect( "page-1", 0, 0, { w: 10, h: 10, fill: "#000", stroke: "#fff", radius: 0 }, "shape-1", ); - const rectB = ShapeRecord.createRect( + const rectB = EditorShapeRecord.createRect( "page-1", 20, 20, @@ -101,7 +101,7 @@ describe("Status bar selectors", () => { describe("buildStatusBarVM", () => { it("composes slices into a status bar view model", () => { - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( "page-1", 0, 0, diff --git a/packages/core/tests/stencils.test.ts b/packages/core/tests/stencils.test.ts index 9811198..7d0e663 100644 --- a/packages/core/tests/stencils.test.ts +++ b/packages/core/tests/stencils.test.ts @@ -1,10 +1,10 @@ -import { EditorState, LayerRecord, PageRecord, cardToContentObject, contentObjectToCard, stencils } from '../src'; +import { EditorState, EditorLayerRecord, EditorPageRecord, cardToContentObject, contentObjectToCard, stencils } from '../src'; import { describe, expect, it } from 'vitest'; function stateForLayer({ visible = true, locked = false, opacity = 1 } = {}) { const state = EditorState.create(); - const page = PageRecord.create('Page', 'page'); - const layer = { ...LayerRecord.create(page.id, 'Active', 'layer'), visible, locked, opacity }; + const page = EditorPageRecord.create('Page', 'page'); + const layer = { ...EditorLayerRecord.create(page.id, 'Active', 'layer'), visible, locked, opacity }; return { ...state, doc: { diff --git a/packages/core/tests/text-path.test.ts b/packages/core/tests/text-path.test.ts index 4539afc..ec78f35 100644 --- a/packages/core/tests/text-path.test.ts +++ b/packages/core/tests/text-path.test.ts @@ -1,15 +1,15 @@ import { describe, expect, it } from 'vitest'; import { attachTextPathSelection, shapeBoundsForState } from '../src'; -import { PageRecord, ShapeRecord } from '../src/model'; +import { EditorPageRecord, EditorShapeRecord } from '../src/editor-model'; import { EditorState, Store } from '../src/reactivity'; import { SnapshotCommand } from '../src/history'; function createTextPathState() { const state = EditorState.create(); - const page = PageRecord.create('Text path'); + const page = EditorPageRecord.create('Text path'); state.doc.pages[page.id] = page; state.ui.currentPageId = page.id; - const path = ShapeRecord.createPath(page.id, 20, 40, { + const path = EditorShapeRecord.createPath(page.id, 20, 40, { subpaths: [ { segments: [ @@ -23,7 +23,7 @@ function createTextPathState() { stroke: '#000000', stroke_width: 2 }); - const text = ShapeRecord.createText(page.id, 0, 0, { + const text = EditorShapeRecord.createText(page.id, 0, 0, { text: 'Label', fontSize: 16, fontFamily: 'sans-serif', diff --git a/packages/core/tests/tools.test.ts b/packages/core/tests/tools.test.ts index ca1cd7a..e288770 100644 --- a/packages/core/tests/tools.test.ts +++ b/packages/core/tests/tools.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { Action, Modifiers, PointerButtons } from "../src/actions"; import { Vec2 } from "../src/math"; -import type { TextProps } from "../src/model"; +import type { TextProps } from "../src/editor-model"; import { EditorState } from "../src/reactivity"; import { createToolMap, routeAction, switchTool, TextTool } from "../src/tools"; import type { Tool } from "../src/tools"; diff --git a/packages/core/tests/vector-effects.test.ts b/packages/core/tests/vector-effects.test.ts index 58f23fa..8a5c534 100644 --- a/packages/core/tests/vector-effects.test.ts +++ b/packages/core/tests/vector-effects.test.ts @@ -2,19 +2,19 @@ import { describe, expect, it } from 'vitest'; import { Camera } from '../src/camera'; import { clipSelection, removeClipFromSelection } from '../src/vector-effects'; import { EditorState } from '../src/reactivity'; -import { PageRecord, ShapeRecord } from '../src/model'; +import { EditorPageRecord, EditorShapeRecord } from '../src/editor-model'; describe('vector effects', () => { it('turns a selected path into local clip geometry and removes the source', () => { - const page = PageRecord.create('Page 1', 'page:effects'); - const target = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:effects'); + const target = EditorShapeRecord.createRect( page.id, 100, 50, { w: 80, h: 60, fill: 'red', stroke: 'none', radius: 0 }, 'shape:target' ); - const source = ShapeRecord.createPath( + const source = EditorShapeRecord.createPath( page.id, 110, 60, diff --git a/packages/editor/src/renderer.ts b/packages/editor/src/renderer.ts index 3f47965..bbabc85 100644 --- a/packages/editor/src/renderer.ts +++ b/packages/editor/src/renderer.ts @@ -2,7 +2,7 @@ import type { ArrowLabel, ArrowShape, BindingIndex, - BindingRecord, + EditorBindingRecord, Camera, CursorState, EditorState, @@ -14,7 +14,7 @@ import type { PathShape, RectShape, TextPathLayout, - ShapeRecord, + EditorShapeRecord, Store, StrokeShape, TextShape, @@ -262,7 +262,7 @@ function drawScene( drawGrid(context, state.camera, viewport, snapSettings, theme); const shapes = getShapesOnCurrentPage(state); - const bindingsBySource = new Map(); + const bindingsBySource = new Map(); for (const binding of Object.values(state.doc.bindings)) { const bindings = bindingsBySource.get(binding.fromShapeId); if (bindings) bindings.push(binding); @@ -334,7 +334,7 @@ function getExpandedViewportBounds(camera: Camera, viewport: Viewport): VisibleB function isShapeVisible( state: EditorState, - shape: ShapeRecord, + shape: EditorShapeRecord, viewport: VisibleBounds, bindingsBySource?: BindingIndex ): boolean { @@ -543,7 +543,7 @@ function drawBindingPreview(context: CanvasRenderingContext2D, state: EditorStat function drawShape( context: CanvasRenderingContext2D, state: EditorState, - shape: ShapeRecord, + shape: EditorShapeRecord, bindingsBySource?: BindingIndex, theme: 'light' | 'dark' = 'light', textLayoutCache = new LruCache(512), @@ -618,8 +618,8 @@ function drawShape( } /** Apply the native clip, mask, and filter subset before drawing one shape. */ -function applyShapeEffects(context: CanvasRenderingContext2D, shape: ShapeRecord) { - const props = shape.props as ShapeRecord['props'] & { +function applyShapeEffects(context: CanvasRenderingContext2D, shape: EditorShapeRecord) { + const props = shape.props as EditorShapeRecord['props'] & { clipPath?: PathGeometry; maskEffect?: { geometry: PathGeometry; opacity?: number }; filter?: { @@ -682,7 +682,7 @@ function filterToCanvas(filter: FilterEffect): string { function drawImage( context: CanvasRenderingContext2D, state: EditorState, - shape: Extract, + shape: Extract, onImageLoaded?: () => void ) { const asset = state.doc.assets?.[shape.props.assetId]; @@ -739,7 +739,7 @@ function drawImage( } } -function drawReference(context: CanvasRenderingContext2D, shape: Extract) { +function drawReference(context: CanvasRenderingContext2D, shape: Extract) { const { w, h, referenceType, value, label } = shape.props; const accent = referenceType === 'url' ? '#2563eb' : referenceType === 'file' ? '#16a34a' : '#7c3aed'; context.fillStyle = '#f8fafc'; @@ -861,7 +861,7 @@ function drawLine(context: CanvasRenderingContext2D, shape: LineShape) { } } -function drawContainer(context: CanvasRenderingContext2D, shape: Extract) { +function drawContainer(context: CanvasRenderingContext2D, shape: Extract) { const { w = 0, h = 0, title, fill, stroke, radius = 0 } = shape.props; const shapeAlpha = context.globalAlpha; context.beginPath(); @@ -1490,7 +1490,7 @@ const SELECTION_COLOR = '#34d399'; function drawSelection( context: CanvasRenderingContext2D, state: EditorState, - shapes: ShapeRecord[], + shapes: EditorShapeRecord[], handleState?: HandleRenderState, bindingsBySource?: BindingIndex, theme: 'light' | 'dark' = 'light' @@ -1641,7 +1641,7 @@ const TEXT_HANDLE_OFFSET = 7; function drawHandles( context: CanvasRenderingContext2D, state: EditorState, - shape: ShapeRecord, + shape: EditorShapeRecord, handleState?: HandleRenderState, bindingsBySource?: BindingIndex, theme: 'light' | 'dark' = 'light' @@ -1769,7 +1769,11 @@ function drawStrokeEditingHandles( context.restore(); } -function getHandlesForShape(state: EditorState, shape: ShapeRecord, bindingsBySource?: BindingIndex): HandleVisual[] { +function getHandlesForShape( + state: EditorState, + shape: EditorShapeRecord, + bindingsBySource?: BindingIndex +): HandleVisual[] { const handles: HandleVisual[] = []; if (shape.type === 'text' && shape.props.textPath) { const position = textPathAnchorForShape(state, shape); @@ -1848,7 +1852,7 @@ function getHandlesForShape(state: EditorState, shape: ShapeRecord, bindingsBySo return handles; } -function applyShapeTransform(context: CanvasRenderingContext2D, shape: ShapeRecord): void { +function applyShapeTransform(context: CanvasRenderingContext2D, shape: EditorShapeRecord): void { const matrix = shapeTransform(shape); if (shape.editorTransform && typeof context.transform === 'function') { context.transform(matrix[0], matrix[1], matrix[3], matrix[4], matrix[6], matrix[7]); diff --git a/packages/editor/src/runtime.ts b/packages/editor/src/runtime.ts index 3d81209..9494205 100644 --- a/packages/editor/src/runtime.ts +++ b/packages/editor/src/runtime.ts @@ -1,6 +1,6 @@ import { type Action, - BindingRecord, + EditorBindingRecord, Camera, duplicateAndConnectSelection, type CommandKind, @@ -14,7 +14,7 @@ import { setShapesLocked, translateShapes, ungroupShapes, - ShapeRecord, + EditorShapeRecord, hitTestPoint, selectionTarget, type PathTopologyEdit, @@ -416,7 +416,7 @@ function duplicateSelection(state: EditorState): EditorState | null { const mapping = new Map(included.map((shape) => [shape.id, createId('shape')])); const shapes = { ...state.doc.shapes }; for (const source of included) { - const copy = ShapeRecord.clone(source); + const copy = EditorShapeRecord.clone(source); const id = mapping.get(source.id)!; const parentId = source.groupId ? mapping.get(source.groupId) : undefined; const copied = { @@ -455,7 +455,7 @@ function duplicateSelection(state: EditorState): EditorState | null { if (!fromShapeId) continue; const id = createId('binding'); const toShapeId = mapping.get(binding.toShapeId) ?? binding.toShapeId; - bindings[id] = { ...BindingRecord.clone(binding), id, fromShapeId, toShapeId }; + bindings[id] = { ...EditorBindingRecord.clone(binding), id, fromShapeId, toShapeId }; } for (const source of included) { const id = mapping.get(source.id); @@ -496,7 +496,11 @@ function hasSelectedAncestor(state: EditorState, id: string, selected: ReadonlyS return false; } -function hasAncestor(shape: import('@inkfinite/core').ShapeRecord, ancestorId: string, state: EditorState): boolean { +function hasAncestor( + shape: import('@inkfinite/core').EditorShapeRecord, + ancestorId: string, + state: EditorState +): boolean { let parentId = shape.groupId; while (parentId) { if (parentId === ancestorId) return true; diff --git a/packages/editor/tests/renderer.test.ts b/packages/editor/tests/renderer.test.ts index 206fab1..3db93ca 100644 --- a/packages/editor/tests/renderer.test.ts +++ b/packages/editor/tests/renderer.test.ts @@ -1,4 +1,4 @@ -import { PageRecord, ShapeRecord, Store } from '@inkfinite/core'; +import { EditorPageRecord, EditorShapeRecord, Store } from '@inkfinite/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { createRenderer } from '../src/renderer'; @@ -115,8 +115,8 @@ describe('Renderer', () => { return scheduledFrames.length; }); const store = new Store(); - const page = PageRecord.create('Page', 'page'); - const text = ShapeRecord.createText( + const page = EditorPageRecord.create('Page', 'page'); + const text = EditorShapeRecord.createText( page.id, 0, 0, @@ -158,15 +158,15 @@ describe('Renderer', () => { alphaWrites.push(value); } }); - const page = PageRecord.create('Page', 'page'); - const visible = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page', 'page'); + const visible = EditorShapeRecord.createRect( 'page', 0, 0, { w: 10, h: 10, fill: '#fff', stroke: '#000', radius: 0 }, 'visible' ); - const hidden = ShapeRecord.createRect( + const hidden = EditorShapeRecord.createRect( 'page', 20, 0, @@ -241,9 +241,9 @@ describe('Renderer', () => { vi.mocked(context.stroke).mockImplementation(() => { strokeAlphas.push(alpha); }); - const page = PageRecord.create('Page', 'page'); + const page = EditorPageRecord.create('Page', 'page'); const shape = { - ...ShapeRecord.createRect( + ...EditorShapeRecord.createRect( page.id, 0, 0, @@ -302,15 +302,15 @@ describe('Renderer', () => { scheduledFrames.push(callback); return scheduledFrames.length; }); - const page = PageRecord.create('Page 1', 'page:1'); - const visible = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const visible = EditorShapeRecord.createRect( 'page:1', 0, 0, { w: 50, h: 50, fill: '#fff', stroke: '#000', radius: 0 }, 'shape:visible' ); - const selectedOffscreen = ShapeRecord.createRect( + const selectedOffscreen = EditorShapeRecord.createRect( 'page:1', 10_000, 10_000, @@ -349,8 +349,8 @@ describe('Renderer', () => { it('should render scene with rect shape', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const rect = EditorShapeRecord.createRect( 'page:1', 100, 100, @@ -378,8 +378,8 @@ describe('Renderer', () => { it('should render scene with ellipse shape', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const ellipse = ShapeRecord.createEllipse( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const ellipse = EditorShapeRecord.createEllipse( 'page:1', 100, 100, @@ -410,8 +410,8 @@ describe('Renderer', () => { scheduledFrames.push(callback); return scheduledFrames.length; }); - const page = PageRecord.create('Page 1', 'page:1'); - const path = ShapeRecord.createPath( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const path = EditorShapeRecord.createPath( 'page:1', 0, 0, @@ -466,8 +466,8 @@ describe('Renderer', () => { scheduledFrames.push(callback); return scheduledFrames.length; }); - const page = PageRecord.create('Page 1', 'page:1'); - const path = ShapeRecord.createPath( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const path = EditorShapeRecord.createPath( page.id, 0, 0, @@ -510,8 +510,8 @@ describe('Renderer', () => { it('should render scene with line shape', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const line = ShapeRecord.createLine( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const line = EditorShapeRecord.createLine( 'page:1', 0, 0, @@ -539,8 +539,8 @@ describe('Renderer', () => { it('should render scene with arrow shape', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const arrow = ShapeRecord.createArrow( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const arrow = EditorShapeRecord.createArrow( 'page:1', 0, 0, @@ -568,8 +568,8 @@ describe('Renderer', () => { it('should render scene with text shape', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const text = ShapeRecord.createText( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const text = EditorShapeRecord.createText( 'page:1', 100, 100, @@ -597,8 +597,8 @@ describe('Renderer', () => { it('should render text shape with word wrapping', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const text = ShapeRecord.createText( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const text = EditorShapeRecord.createText( 'page:1', 100, 100, @@ -632,15 +632,15 @@ describe('Renderer', () => { it('should render multiple shapes', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const rect = EditorShapeRecord.createRect( 'page:1', 100, 100, { w: 200, h: 100, fill: '#ff0000', stroke: '#000000', radius: 0 }, 'shape:1' ); - const ellipse = ShapeRecord.createEllipse( + const ellipse = EditorShapeRecord.createEllipse( 'page:1', 400, 200, @@ -667,8 +667,8 @@ describe('Renderer', () => { it('traces selected arrows instead of drawing a rectangular outline', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const arrow = ShapeRecord.createArrow( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const arrow = EditorShapeRecord.createArrow( page.id, 0, 0, @@ -696,8 +696,8 @@ describe('Renderer', () => { it('renders a dashed outline for selected shapes', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const rect = EditorShapeRecord.createRect( 'page:1', 100, 100, @@ -733,8 +733,8 @@ describe('Renderer', () => { scheduledFrames.push(callback); return scheduledFrames.length; }); - const page = PageRecord.create('Page', 'page:direct-render'); - const path = ShapeRecord.createPath( + const page = EditorPageRecord.create('Page', 'page:direct-render'); + const path = EditorShapeRecord.createPath( page.id, 0, 0, @@ -790,7 +790,7 @@ describe('Renderer', () => { it('should update render when store changes', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); + const page = EditorPageRecord.create('Page 1', 'page:1'); store.setState((state) => ({ ...state, @@ -802,7 +802,7 @@ describe('Renderer', () => { await new Promise((resolve) => setTimeout(resolve, 50)); - const rect = ShapeRecord.createRect( + const rect = EditorShapeRecord.createRect( 'page:1', 100, 100, @@ -827,8 +827,8 @@ describe('Renderer', () => { it('should apply camera transform correctly', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const rect = EditorShapeRecord.createRect( 'page:1', 0, 0, @@ -857,8 +857,8 @@ describe('Renderer', () => { it('should handle rounded rectangle', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const rect = EditorShapeRecord.createRect( 'page:1', 100, 100, @@ -886,8 +886,8 @@ describe('Renderer', () => { it('should render shapes with rotation', async () => { const store = new Store(); - const page = PageRecord.create('Page 1', 'page:1'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page 1', 'page:1'); + const rect = EditorShapeRecord.createRect( 'page:1', 100, 100, diff --git a/packages/editor/tests/runtime.test.ts b/packages/editor/tests/runtime.test.ts index 38d52f1..91f3b7a 100644 --- a/packages/editor/tests/runtime.test.ts +++ b/packages/editor/tests/runtime.test.ts @@ -1,4 +1,4 @@ -import { Action, EditorState, ShapeRecord, Store, type Tool } from '@inkfinite/core'; +import { Action, EditorState, EditorShapeRecord, Store, type Tool } from '@inkfinite/core'; import { describe, expect, it, vi } from 'vitest'; import { EditorRuntime, type SelectionTool } from '../src/runtime'; @@ -33,14 +33,14 @@ function createRuntime(store: Store, options: Partial { it('keeps hierarchy, assets, bindings, and root selection on paste', () => { const before = state(); const pageId = before.ui.currentPageId!; - const group = ShapeRecord.createContainer( + const group = EditorShapeRecord.createContainer( pageId, 10, 20, { w: 100, h: 80 }, 'shape:group' ); - const image = ShapeRecord.createImage( + const image = EditorShapeRecord.createImage( pageId, 20, 30, @@ -151,7 +151,7 @@ describe('clipboard selections', () => { before.doc.shapes[image.id] = image; before.doc.pages[pageId].shapeIds = [group.id, image.id]; before.doc.layers!['layer:test'].shapeIds = [group.id]; - const target = ShapeRecord.createRect( + const target = EditorShapeRecord.createRect( pageId, 200, 0, @@ -162,7 +162,7 @@ describe('clipboard selections', () => { before.doc.shapes[target.id] = target; before.doc.pages[pageId].shapeIds.push(target.id); before.doc.layers!['layer:test'].shapeIds.push(target.id); - const binding = BindingRecord.create( + const binding = EditorBindingRecord.create( 'shape:target', 'shape:group', 'end', diff --git a/packages/ui/src/lib/editor/canvas/canvas-store.svelte.ts b/packages/ui/src/lib/editor/canvas/canvas-store.svelte.ts index 7f211ab..3e34b92 100644 --- a/packages/ui/src/lib/editor/canvas/canvas-store.svelte.ts +++ b/packages/ui/src/lib/editor/canvas/canvas-store.svelte.ts @@ -22,12 +22,12 @@ import { hitTestPoint, selectionTarget, LineTool, - LayerRecord, + EditorLayerRecord, exportInterchange, exportToSVG, importInterchange, MarkdownTool, - PageRecord, + EditorPageRecord, PenTool, RectTool, SelectTool, @@ -132,8 +132,8 @@ export function createCanvasController( let overlayViewport = $state({ width: 1, height: 1 }); let renderer: Renderer | null = null; let inputAdapter: InputAdapter | null = null; - const initialPage = PageRecord.create('Page 1'); - const initialLayer = LayerRecord.create(initialPage.id, 'Default'); + const initialPage = EditorPageRecord.create('Page 1'); + const initialLayer = EditorLayerRecord.create(initialPage.id, 'Default'); const handleResize = () => { overlayViewport = measureViewport(canvas); camera.refit(); diff --git a/packages/ui/src/lib/editor/canvas/controllers/__tests__/camera-controller.test.ts b/packages/ui/src/lib/editor/canvas/controllers/__tests__/camera-controller.test.ts index eda2543..0d0fc9e 100644 --- a/packages/ui/src/lib/editor/canvas/controllers/__tests__/camera-controller.test.ts +++ b/packages/ui/src/lib/editor/canvas/controllers/__tests__/camera-controller.test.ts @@ -1,4 +1,11 @@ -import { Action, Camera, Modifiers, PageRecord, ShapeRecord, Store } from '@inkfinite/core'; +import { + Action, + Camera, + Modifiers, + EditorPageRecord, + EditorShapeRecord, + Store +} from '@inkfinite/core'; import { describe, expect, it } from 'vitest'; import { CameraController } from '../camera-controller'; @@ -54,8 +61,8 @@ describe('CameraController', () => { }); it('fits the current drawing inside the viewport', () => { - const page = PageRecord.create('Page', 'page'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page', 'page'); + const shape = EditorShapeRecord.createRect( page.id, 100, 200, @@ -80,8 +87,8 @@ describe('CameraController', () => { }); it('keeps a fitted drawing framed when the viewport changes size', () => { - const page = PageRecord.create('Page', 'page'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page', 'page'); + const shape = EditorShapeRecord.createRect( page.id, 100, 200, diff --git a/packages/ui/src/lib/editor/clipboard.ts b/packages/ui/src/lib/editor/clipboard.ts index 07a90e5..032f9f8 100644 --- a/packages/ui/src/lib/editor/clipboard.ts +++ b/packages/ui/src/lib/editor/clipboard.ts @@ -1,11 +1,11 @@ import { - BindingRecord, + EditorBindingRecord, createId, - ShapeRecord, + EditorShapeRecord, shapeBounds, type EditorState, type ImportedAsset, - type ShapeRecord as Shape, + type EditorShapeRecord as Shape, type Vec2 } from '@inkfinite/core'; @@ -18,7 +18,7 @@ export type ClipboardPayload = { kind: typeof CLIPBOARD_KIND; version: 1 | 2; shapes: Shape[]; - bindings: BindingRecord[]; + bindings: EditorBindingRecord[]; rootIds: string[]; assets: ImportedAsset[]; }; @@ -52,12 +52,12 @@ export function createClipboardPayload(state: EditorState): ClipboardPayload | n .filter((id) => includedIds.has(id)) .map((id) => state.doc.shapes[id]) .filter((shape): shape is Shape => Boolean(shape)) - .map((shape) => ShapeRecord.clone(shape)); + .map((shape) => EditorShapeRecord.clone(shape)); const bindings = Object.values(state.doc.bindings) .filter( (binding) => includedIds.has(binding.fromShapeId) && includedIds.has(binding.toShapeId) ) - .map((binding) => BindingRecord.clone(binding)); + .map((binding) => EditorBindingRecord.clone(binding)); const assetIds = new Set( shapes.flatMap((shape) => (shape.type === 'image' ? [shape.props.assetId] : [])) ); @@ -320,7 +320,7 @@ export function pasteClipboard( for (const source of payload.shapes) { const id = mapping.get(source.id); if (!id) continue; - const copy = ShapeRecord.clone(source); + const copy = EditorShapeRecord.clone(source); const translated = copy.editorTransform ? { ...copy, @@ -350,7 +350,7 @@ export function pasteClipboard( ...translated.props, textPath: { ...translated.props.textPath, pathId } } - } as ShapeRecord; + } as EditorShapeRecord; } } if (payload.rootIds.includes(source.id)) pastedIds.push(id); @@ -363,7 +363,7 @@ export function pasteClipboard( if (!fromShapeId || !toShapeId) continue; const id = createId('binding'); bindingMapping.set(binding.id, id); - bindings[id] = { ...BindingRecord.clone(binding), id, fromShapeId, toShapeId }; + bindings[id] = { ...EditorBindingRecord.clone(binding), id, fromShapeId, toShapeId }; } for (const source of payload.shapes) { const id = mapping.get(source.id); @@ -425,14 +425,14 @@ export function pasteText( if (!pageId || !page || !text) return state; const point = position ?? { x: 0, y: 0 }; const shape = markdown - ? ShapeRecord.createMarkdown(pageId, point.x, point.y, { + ? EditorShapeRecord.createMarkdown(pageId, point.x, point.y, { md: text, w: 320, fontSize: 16, fontFamily: 'Instrument Sans Variable', color: '#1e1e1e' }) - : ShapeRecord.createText(pageId, point.x, point.y, { + : EditorShapeRecord.createText(pageId, point.x, point.y, { text, fontSize: 20, fontFamily: 'Instrument Sans Variable', @@ -470,7 +470,7 @@ export async function pasteImage( const width = Math.max(1, size.width * scale); const height = Math.max(1, size.height * scale); const point = position ?? { x: 0, y: 0 }; - const image = ShapeRecord.createImage(pageId, point.x, point.y, { + const image = EditorShapeRecord.createImage(pageId, point.x, point.y, { w: width, h: height, assetId: asset.id @@ -565,7 +565,9 @@ function parsePayload(text: string): ClipboardPayload | null { kind: CLIPBOARD_KIND, version: value.version, shapes: value.shapes as Shape[], - bindings: Array.isArray(value.bindings) ? (value.bindings as BindingRecord[]) : [], + bindings: Array.isArray(value.bindings) + ? (value.bindings as EditorBindingRecord[]) + : [], rootIds: Array.isArray(value.rootIds) ? value.rootIds : [], assets: Array.isArray(value.assets) ? (value.assets as ImportedAsset[]) : [] }; diff --git a/packages/ui/src/lib/editor/components/LayerPanel.svelte b/packages/ui/src/lib/editor/components/LayerPanel.svelte index 010e2f6..5c03a19 100644 --- a/packages/ui/src/lib/editor/components/LayerPanel.svelte +++ b/packages/ui/src/lib/editor/components/LayerPanel.svelte @@ -8,7 +8,7 @@ moveLayer, patchLayer, type EditorState, - type LayerRecord, + type EditorLayerRecord, type Store } from '@inkfinite/core'; import { tick, untrack } from 'svelte'; @@ -169,7 +169,7 @@ setPanelPosition(left, top); } - function selectLayer(layer: LayerRecord) { + function selectLayer(layer: EditorLayerRecord) { commit('Select Layer', activateLayer(editorState, layer.id)); } @@ -177,12 +177,12 @@ renamingLayerId = layerId; } - function finishRename(layer: LayerRecord, value: string) { + function finishRename(layer: EditorLayerRecord, value: string) { commit('Rename Layer', patchLayer(editorState, layer.id, { name: value })); renamingLayerId = null; } - function openMenu(layer: LayerRecord, event: MouseEvent | PointerEvent) { + function openMenu(layer: EditorLayerRecord, event: MouseEvent | PointerEvent) { event.preventDefault(); event.stopPropagation(); menuLayerId = layer.id; @@ -196,12 +196,12 @@ menuOpen = true; } - function beginDelete(layer: LayerRecord) { + function beginDelete(layer: EditorLayerRecord) { deletingLayerId = layer.id; deleteDestinationId = nearestWritableDestination(layer.id)?.id ?? null; } - function nearestWritableDestination(sourceId: string): LayerRecord | null { + function nearestWritableDestination(sourceId: string): EditorLayerRecord | null { const sourceIndex = layers.findIndex((layer) => layer.id === sourceId); return ( [...layers] diff --git a/packages/ui/src/lib/editor/components/SelectionControls.svelte b/packages/ui/src/lib/editor/components/SelectionControls.svelte index 50015e3..29551f9 100644 --- a/packages/ui/src/lib/editor/components/SelectionControls.svelte +++ b/packages/ui/src/lib/editor/components/SelectionControls.svelte @@ -6,7 +6,7 @@ MarkdownShape, PaintValue, ShapeMetadata, - ShapeRecord, + EditorShapeRecord, Store, TextShape, ToolId @@ -162,7 +162,7 @@ ); let cardTargets = $derived( selectedShapes.filter( - (shape): shape is Extract => + (shape): shape is Extract => shape.type === 'container' && shape.metadata?.title !== null && shape.metadata?.title !== undefined @@ -181,7 +181,8 @@ }); let imageTargets = $derived( selectedShapes.filter( - (shape): shape is Extract => shape.type === 'image' + (shape): shape is Extract => + shape.type === 'image' ) ); let imageTarget = $derived(imageTargets.length === 1 ? imageTargets[0] : undefined); @@ -297,7 +298,7 @@ return { value: shared ?? true, mixed: values.length > 1 && shared === null }; } - function filterPreset(shape: ShapeRecord | undefined): string { + function filterPreset(shape: EditorShapeRecord | undefined): string { const primitive = shape?.props.filter?.primitives[0]; return primitive?.type ?? 'none'; } @@ -309,7 +310,7 @@ ? ({ ...shape, props: { ...shape.props, maskEffect: { ...shape.props.maskEffect, mode } } - } as ShapeRecord) + } as EditorShapeRecord) : shape ); } @@ -337,11 +338,11 @@ : undefined; updateSelectedShapes( 'Set filter', - (shape) => ({ ...shape, props: { ...shape.props, filter } }) as ShapeRecord + (shape) => ({ ...shape, props: { ...shape.props, filter } }) as EditorShapeRecord ); } - function metadataForShape(shape: ShapeRecord): ShapeMetadata { + function metadataForShape(shape: EditorShapeRecord): ShapeMetadata { return ( shape.metadata ?? { name: null, @@ -359,7 +360,7 @@ ); } - function shapeSupportsFill(shape: ShapeRecord): boolean { + function shapeSupportsFill(shape: EditorShapeRecord): boolean { return ( shape.type === 'rect' || shape.type === 'ellipse' || @@ -370,7 +371,7 @@ ); } - function shapeSupportsStroke(shape: ShapeRecord): boolean { + function shapeSupportsStroke(shape: EditorShapeRecord): boolean { return ( shape.type === 'rect' || shape.type === 'ellipse' || @@ -383,7 +384,7 @@ ); } - function shapeSupportsFillOpacity(shape: ShapeRecord): boolean { + function shapeSupportsFillOpacity(shape: EditorShapeRecord): boolean { return ( shape.type === 'rect' || shape.type === 'ellipse' || @@ -395,7 +396,7 @@ ); } - function shapeSupportsStrokeOpacity(shape: ShapeRecord): boolean { + function shapeSupportsStrokeOpacity(shape: EditorShapeRecord): boolean { return ( shape.type === 'rect' || shape.type === 'ellipse' || @@ -408,7 +409,7 @@ ); } - function getFillPaint(shape: ShapeRecord): PaintValue | null { + function getFillPaint(shape: EditorShapeRecord): PaintValue | null { switch (shape.type) { case 'text': return shape.props.color; @@ -424,7 +425,7 @@ } } - function getStrokePaint(shape: ShapeRecord): PaintValue | null { + function getStrokePaint(shape: EditorShapeRecord): PaintValue | null { switch (shape.type) { case 'arrow': return shape.props.style.stroke; @@ -443,7 +444,10 @@ } } - function updateSelectedShapes(label: string, update: (shape: ShapeRecord) => ShapeRecord) { + function updateSelectedShapes( + label: string, + update: (shape: EditorShapeRecord) => EditorShapeRecord + ) { const state = store.getState(); if (state.ui.selectionIds.length === 0) return; const before = EditorState.clone(state); @@ -468,14 +472,20 @@ updateSelectedShapes('Set fill paint', (shape) => { switch (shape.type) { case 'text': - return { ...shape, props: { ...shape.props, color: paint } } as ShapeRecord; + return { + ...shape, + props: { ...shape.props, color: paint } + } as EditorShapeRecord; case 'rect': case 'ellipse': case 'path': case 'container': - return { ...shape, props: { ...shape.props, fill: paint } } as ShapeRecord; + return { + ...shape, + props: { ...shape.props, fill: paint } + } as EditorShapeRecord; case 'markdown': - return { ...shape, props: { ...shape.props, bg: paint } } as ShapeRecord; + return { ...shape, props: { ...shape.props, bg: paint } } as EditorShapeRecord; default: return shape; } @@ -489,20 +499,26 @@ return { ...shape, props: { ...shape.props, style: { ...shape.props.style, stroke: paint } } - } as ShapeRecord; + } as EditorShapeRecord; case 'stroke': return { ...shape, props: { ...shape.props, style: { ...shape.props.style, color: paint } } - } as ShapeRecord; + } as EditorShapeRecord; case 'rect': case 'ellipse': case 'line': case 'path': case 'container': - return { ...shape, props: { ...shape.props, stroke: paint } } as ShapeRecord; + return { + ...shape, + props: { ...shape.props, stroke: paint } + } as EditorShapeRecord; case 'markdown': - return { ...shape, props: { ...shape.props, border: paint } } as ShapeRecord; + return { + ...shape, + props: { ...shape.props, border: paint } + } as EditorShapeRecord; default: return shape; } @@ -539,7 +555,7 @@ shapes[shapeId] = { ...shape, props: { ...shape.props, [field]: value } - } as ShapeRecord; + } as EditorShapeRecord; } store.executeCommand( new SnapshotCommand( @@ -570,7 +586,7 @@ ...shape.props, textPath: { ...shape.props.textPath, [field]: value } } - } as ShapeRecord) + } as EditorShapeRecord) : shape ); } @@ -597,7 +613,7 @@ ? { customMetadata: { ...fields.customMetadata } } : {}) } - } as ShapeRecord; + } as EditorShapeRecord; }); } @@ -649,7 +665,7 @@ }; const shapes = { ...state.doc.shapes, - [cardTarget.id]: { ...cardTarget, metadata: nextMetadata } as ShapeRecord + [cardTarget.id]: { ...cardTarget, metadata: nextMetadata } as EditorShapeRecord }; for (const shape of Object.values(state.doc.shapes)) { if (shape.groupId !== cardTarget.id) continue; @@ -670,7 +686,7 @@ function updateImageFields( label: string, - fields: Partial['props']> + fields: Partial['props']> ) { updateSelectedShapes(label, (shape) => shape.type === 'image' ? { ...shape, props: { ...shape.props, ...fields } } : shape @@ -700,7 +716,7 @@ } function updateReferenceFields( - fields: Partial['props']> + fields: Partial['props']> ) { updateSelectedShapes('Update reference', (shape) => shape.type === 'reference' ? { ...shape, props: { ...shape.props, ...fields } } : shape diff --git a/packages/ui/src/lib/editor/components/__tests__/LayerPanel.svelte.test.ts b/packages/ui/src/lib/editor/components/__tests__/LayerPanel.svelte.test.ts index 9e56573..65379c3 100644 --- a/packages/ui/src/lib/editor/components/__tests__/LayerPanel.svelte.test.ts +++ b/packages/ui/src/lib/editor/components/__tests__/LayerPanel.svelte.test.ts @@ -1,4 +1,4 @@ -import { EditorState, ShapeRecord, Store } from '@inkfinite/core'; +import { EditorState, EditorShapeRecord, Store } from '@inkfinite/core'; import { describe, expect, it, vi } from 'vitest'; import { render } from 'vitest-browser-svelte'; @@ -7,7 +7,7 @@ import LayerPanel from '../LayerPanel.svelte'; function editorStore() { const state = EditorState.create(); state.doc.pages.page = { id: 'page', name: 'Page', shapeIds: ['shape'] }; - state.doc.shapes.shape = ShapeRecord.createRect( + state.doc.shapes.shape = EditorShapeRecord.createRect( 'page', 0, 0, diff --git a/packages/ui/src/lib/editor/components/__tests__/SelectionControls.svelte.test.ts b/packages/ui/src/lib/editor/components/__tests__/SelectionControls.svelte.test.ts index 18bd4f6..f183773 100644 --- a/packages/ui/src/lib/editor/components/__tests__/SelectionControls.svelte.test.ts +++ b/packages/ui/src/lib/editor/components/__tests__/SelectionControls.svelte.test.ts @@ -1,10 +1,10 @@ import { EditorState, - PageRecord, - ShapeRecord, + EditorPageRecord, + EditorShapeRecord, Store, contentObjectToCard, - type ShapeRecord as Shape + type EditorShapeRecord as Shape } from '@inkfinite/core'; import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; @@ -13,7 +13,7 @@ import SelectionControls from '../SelectionControls.svelte'; function createSelectionStore(shapes: Shape[], selectionIds = shapes.map((shape) => shape.id)) { const state = EditorState.create(); - const page = PageRecord.create('Test page', 'page:test'); + const page = EditorPageRecord.create('Test page', 'page:test'); state.doc.pages[page.id] = { ...page, shapeIds: shapes.map((shape) => shape.id) }; for (const shape of shapes) state.doc.shapes[shape.id] = shape; state.ui.currentPageId = page.id; @@ -23,7 +23,7 @@ function createSelectionStore(shapes: Shape[], selectionIds = shapes.map((shape) describe('SelectionControls', () => { it('exposes boolean path controls for a closed path selection', async () => { - const page = PageRecord.create('Test page', 'page:test'); + const page = EditorPageRecord.create('Test page', 'page:test'); const pathProps = { subpaths: [ { @@ -39,8 +39,8 @@ describe('SelectionControls', () => { fill_rule: 'evenodd' as const, fill: '#ffffff' }; - const first = ShapeRecord.createPath(page.id, 0, 0, pathProps, 'first'); - const second = ShapeRecord.createPath(page.id, 20, 0, pathProps, 'second'); + const first = EditorShapeRecord.createPath(page.id, 0, 0, pathProps, 'first'); + const second = EditorShapeRecord.createPath(page.id, 20, 0, pathProps, 'second'); const store = createSelectionStore([first, second]); const screen = render(SelectionControls, { currentTool: 'select', @@ -57,8 +57,8 @@ describe('SelectionControls', () => { }); it('shows appearance and object metadata controls for a selected rectangle', async () => { - const page = PageRecord.create('Test page', 'page:test'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Test page', 'page:test'); + const rect = EditorShapeRecord.createRect( page.id, 0, 0, @@ -86,8 +86,8 @@ describe('SelectionControls', () => { }); it('keeps contextual sections on one horizontal viewport with scroll controls', async () => { - const page = PageRecord.create('Test page', 'page:test'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Test page', 'page:test'); + const rect = EditorShapeRecord.createRect( page.id, 0, 0, @@ -115,8 +115,8 @@ describe('SelectionControls', () => { }); it('collapses contextual actions and restores them', async () => { - const page = PageRecord.create('Test page', 'page:test'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Test page', 'page:test'); + const rect = EditorShapeRecord.createRect( page.id, 0, 0, @@ -144,8 +144,8 @@ describe('SelectionControls', () => { }); it('projects and edits semantic metadata for ordinary objects', async () => { - const page = PageRecord.create('Test page', 'page:test'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Test page', 'page:test'); + const rect = EditorShapeRecord.createRect( page.id, 0, 0, @@ -211,15 +211,15 @@ describe('SelectionControls', () => { }); it('shows focused typography controls for text and Markdown selections', async () => { - const page = PageRecord.create('Test page', 'page:test'); - const text = ShapeRecord.createText( + const page = EditorPageRecord.create('Test page', 'page:test'); + const text = EditorShapeRecord.createText( page.id, 0, 0, { text: 'Title', fontSize: 24, fontFamily: 'Inter', color: '#111111' }, 'text' ); - const markdown = ShapeRecord.createMarkdown( + const markdown = EditorShapeRecord.createMarkdown( page.id, 0, 50, @@ -258,15 +258,15 @@ describe('SelectionControls', () => { }); it('shows mixed values and multi-selection layout actions', async () => { - const page = PageRecord.create('Test page', 'page:test'); - const first = ShapeRecord.createRect( + const page = EditorPageRecord.create('Test page', 'page:test'); + const first = EditorShapeRecord.createRect( page.id, 0, 0, { w: 40, h: 40, fill: '#ffffff', stroke: '#111111', radius: 0 }, 'first' ); - const second = ShapeRecord.createRect( + const second = EditorShapeRecord.createRect( page.id, 60, 0, @@ -302,7 +302,7 @@ describe('SelectionControls', () => { }); it('edits card fields and exposes frame navigation', async () => { - const page = PageRecord.create('Test page', 'page:test'); + const page = EditorPageRecord.create('Test page', 'page:test'); const cardShapes = contentObjectToCard( 'page:test', { x: 0, y: 0 }, @@ -361,15 +361,15 @@ describe('SelectionControls', () => { }); it('edits image content, reuses assets, and exposes references', async () => { - const page = PageRecord.create('Test page', 'page:test'); - const image = ShapeRecord.createImage( + const page = EditorPageRecord.create('Test page', 'page:test'); + const image = EditorShapeRecord.createImage( page.id, 0, 0, { w: 160, h: 100, assetId: 'asset:image', caption: 'Original' }, 'image' ); - const reference = ShapeRecord.createReference( + const reference = EditorShapeRecord.createReference( page.id, 200, 0, @@ -431,8 +431,8 @@ describe('SelectionControls', () => { }); it('keeps agent controls opt-in for desktop callers', async () => { - const page = PageRecord.create('Test page', 'page:test'); - const rect = ShapeRecord.createRect( + const page = EditorPageRecord.create('Test page', 'page:test'); + const rect = EditorShapeRecord.createRect( page.id, 0, 0, diff --git a/packages/ui/src/lib/editor/components/__tests__/Toolbar.svelte.test.ts b/packages/ui/src/lib/editor/components/__tests__/Toolbar.svelte.test.ts index e189a15..8533ba4 100644 --- a/packages/ui/src/lib/editor/components/__tests__/Toolbar.svelte.test.ts +++ b/packages/ui/src/lib/editor/components/__tests__/Toolbar.svelte.test.ts @@ -1,4 +1,4 @@ -import { PageRecord, ShapeRecord, Store } from '@inkfinite/core'; +import { EditorPageRecord, EditorShapeRecord, Store } from '@inkfinite/core'; import { describe, expect, it, vi } from 'vitest'; import { render } from 'vitest-browser-svelte'; @@ -6,8 +6,8 @@ import { createBrushStore } from '../../status'; import Toolbar from '../Toolbar.svelte'; function createSelectedRectStore() { - const page = PageRecord.create('Page', 'page'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page', 'page'); + const shape = EditorShapeRecord.createRect( page.id, 0, 0, @@ -262,8 +262,8 @@ describe('Editor Toolbar', () => { }); it('changes selected colors and opacity through labeled undoable controls', async () => { - const page = PageRecord.create('Page', 'page'); - const shape = ShapeRecord.createRect( + const page = EditorPageRecord.create('Page', 'page'); + const shape = EditorShapeRecord.createRect( page.id, 0, 0, diff --git a/packages/ui/src/lib/editor/stories/editor.stories.fixtures.ts b/packages/ui/src/lib/editor/stories/editor.stories.fixtures.ts index c0d35c1..666f7b2 100644 --- a/packages/ui/src/lib/editor/stories/editor.stories.fixtures.ts +++ b/packages/ui/src/lib/editor/stories/editor.stories.fixtures.ts @@ -2,8 +2,8 @@ import { CursorStore, EditorState, FileBrowserVM, - PageRecord, - ShapeRecord, + EditorPageRecord, + EditorShapeRecord, Store, type BoardMeta, type DocRepo, @@ -45,8 +45,8 @@ export function createStoryFileBrowser(boards: BoardMeta[] = storyBoards) { /** Creates an editor store with a selected arrow so arrow controls are visible. */ export function createStoryStore(): Store { const state = EditorState.create(); - const page = PageRecord.create('Sketch', 'page:story'); - const arrow = ShapeRecord.createArrow( + const page = EditorPageRecord.create('Sketch', 'page:story'); + const arrow = EditorShapeRecord.createArrow( page.id, 120, 90, @@ -125,7 +125,7 @@ export function createStoryPlatform(): EditorPlatformAdapter { function createEditorDocument(): EditorStateType { const state = EditorState.create(); - const page = PageRecord.create('Untitled', 'page:story-editor'); + const page = EditorPageRecord.create('Untitled', 'page:story-editor'); state.doc.pages[page.id] = page; state.ui.currentPageId = page.id; return state; diff --git a/packages/ui/src/test/editor-fixtures.ts b/packages/ui/src/test/editor-fixtures.ts index 77db48f..2fad768 100644 --- a/packages/ui/src/test/editor-fixtures.ts +++ b/packages/ui/src/test/editor-fixtures.ts @@ -1,8 +1,8 @@ import { EditorState, FileBrowserVM, - PageRecord, - ShapeRecord, + EditorPageRecord, + EditorShapeRecord, Store, type BoardMeta, type DocRepo @@ -31,8 +31,8 @@ export function createFileBrowserFixture(boards: BoardMeta[] = testBoards) { /** Creates a store with one selected arrow for editor control tests. */ export function createSelectedArrowStore(): Store { const state = EditorState.create(); - const page = PageRecord.create('Test page', 'page:test'); - const arrow = ShapeRecord.createArrow( + const page = EditorPageRecord.create('Test page', 'page:test'); + const arrow = EditorShapeRecord.createArrow( page.id, 10, 20,