Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .codex/skills/shift-docs/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
name: shift-docs
description: Route Shift documentation, architecture notes, and tickets correctly. Use when Codex is about to create or move docs, write an architecture plan, capture a design decision, create a ticket, or update repo documentation for Shift.
---

# Shift Docs

Use the right destination for the artifact.

## Routing

- Stable repo documentation belongs in `docs/` or the module's canonical `DOCS.md`, following `docs/architecture/index.md`.
- Tickets, design scratchpads, future-work plans, and exploratory architecture proposals do not belong in repo `docs/`.
- Put tickets and exploratory plans in Obsidian under `~/Documents/KostyaVault/projects/shift/`, or open a GitHub issue when the user asks for a tracked issue.
- Do not create `CONTEXT.md` files.

## Before Writing

1. Read `docs/architecture/index.md` when changing stable repo documentation.
2. If the artifact is a ticket, plan, or unresolved proposal, route it to Obsidian or GitHub instead of `docs/`.
3. If unsure whether something is stable documentation or a ticket, ask briefly before writing.
4 changes: 4 additions & 0 deletions .codex/skills/shift-docs/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Shift Docs"
short_description: "Route Shift documentation and tickets correctly."
default_prompt: "Use the Shift docs skill to decide where architecture notes, tickets, and repo docs should live."
72 changes: 25 additions & 47 deletions apps/desktop/src/renderer/src/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,17 @@ import { ShiftStore } from "@/lib/store/ShiftStore";
import { EditorGesture, EditorInput, EditorViewState } from "./EditorState";
import type { PointerTarget } from "@/types/target";
import type { SelectableId, ShiftEditorRecord, ShiftId, ShiftObject } from "@/types";
import type { GlyphNode } from "@/types/node";
import type { GlyphNode, NodeKind } from "@/types/node";
import { AnchorObject, ContourObject, NodeObject, PointObject, SegmentObject } from "@/lib/objects";
import type { NodeDefinition, NodeDefinitionConstructor } from "@/lib/nodes/NodeDefinition";
import { GlyphNodeDefinition } from "../nodes/GlyphNodeDefinition";

const DEFAULT_NODE_DEFINITIONS: NodeDefinitionConstructor[] = [GlyphNodeDefinition];

interface EditorOptions {
font: Font;
clipboard: SystemClipboard;
nodeDefinitions?: readonly NodeDefinitionConstructor[];
}

/**
Expand Down Expand Up @@ -118,6 +123,7 @@ export class Editor {
readonly hover: Hover;
readonly font: Font;
readonly scene: Scene;
readonly #nodeDefinitions: Map<NodeKind, NodeDefinition> = new Map();
readonly #store: ShiftStore<ShiftEditorRecord>;

/**
Expand Down Expand Up @@ -186,6 +192,14 @@ export class Editor {
this.#store = new ShiftStore();
this.scene = new Scene(this.#store);

const nodeDefs = new Map<NodeKind, NodeDefinition>();
for (const Def of options.nodeDefinitions ?? DEFAULT_NODE_DEFINITIONS) {
const def = new Def(this);
nodeDefs.set(def.kind, def);
}

this.#nodeDefinitions = nodeDefs;

const initialDesignLocation = emptyAxisLocation();

this.#designLocation = signal(initialDesignLocation, {
Expand Down Expand Up @@ -449,7 +463,7 @@ export class Editor {
const node = this.scene.node(id);
if (!node) return null;

return new NodeObject(node);
return new NodeObject(node, this.nodeDefinition(node.kind));
}

if (isPointId(id)) {
Expand Down Expand Up @@ -549,6 +563,10 @@ export class Editor {
return null;
}

nodeDefinition(kind: NodeKind): NodeDefinition | null {
return this.#nodeDefinitions.get(kind) ?? null;
}

/**
* Resolves editor-addressable ids to objects.
*
Expand Down Expand Up @@ -700,52 +718,12 @@ export class Editor {
const node = nodes[i];
if (!node) continue;

switch (node.kind) {
case "glyph": {
const nodePoint = this.getPointInNodeSpace(point, node.position);
const instance = this.font.instance(node.glyphId, this.designLocationCell);
if (!instance) break;

const hit = instance.geometry.hitAt(nodePoint, this.hitRadius);
if (!hit) break;

if (hit.kind === "segment") {
const segment = instance.geometry.segment(hit.id);
if (!segment) break;

return {
...hit,
nodeId: node.id,
glyphId: node.glyphId,
point: nodePoint,
segmentId: hit.id,
pointIds: segment.pointIds,
};
}

if (hit.kind === "point") {
return {
...hit,
nodeId: node.id,
glyphId: node.glyphId,
point: nodePoint,
pointId: hit.id,
};
}

if (hit.kind === "anchor") {
return {
...hit,
nodeId: node.id,
glyphId: node.glyphId,
point: nodePoint,
anchorId: hit.id,
};
}
const definition = this.nodeDefinition(node.kind);
if (!definition) continue;

break;
}
}
const nodePoint = this.getPointInNodeSpace(point, node.position);
const target = definition.hit(node, nodePoint);
if (target) return target;
}

return { kind: "canvas", point };
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/renderer/src/lib/editor/rendering/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ export class Canvas {
}

/**
* Run a drawing callback in glyph-local UPM coordinates.
* Runs a drawing callback in root scene coordinates.
*
* @param drawOffset - Glyph-local offset applied after the camera transform.
* @param draw - Drawing operation to run while the context is in glyph space.
* @param drawOffset - Scene-space offset applied after the camera transform.
* @param draw - Drawing operation to run while the context is in scene space.
*/
withGlyphSpace(drawOffset: Point2D, draw: (canvas: Canvas) => void): void {
withSceneSpace(drawOffset: Point2D, draw: (canvas: Canvas) => void): void {
const camera = this.camera;

this.ctx.save();
Expand Down
Loading
Loading