@classytic/stage 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/kit/glyphs.d.mts +5 -5
  3. package/dist/assets/kit/glyphs.mjs +4 -4
  4. package/dist/builder/SceneBuilder.mjs +4 -4
  5. package/dist/builder/editor.d.mts +2 -1
  6. package/dist/builder/editor.mjs +14 -4
  7. package/dist/builder/tools.d.mts +1 -1
  8. package/dist/builder/tools.mjs +1 -1
  9. package/dist/chem/index.d.mts +5 -5
  10. package/dist/chem/index.mjs +4 -4
  11. package/dist/circuit/index.d.mts +92 -0
  12. package/dist/circuit/index.mjs +333 -0
  13. package/dist/core/clock.d.mts +1 -1
  14. package/dist/core/clock.mjs +3 -3
  15. package/dist/core/control.d.mts +1 -1
  16. package/dist/core/control.mjs +2 -2
  17. package/dist/core/coords.d.mts +4 -4
  18. package/dist/core/coords.mjs +1 -1
  19. package/dist/core/learner.d.mts +1 -1
  20. package/dist/core/learner.mjs +1 -1
  21. package/dist/core/motion.d.mts +6 -6
  22. package/dist/core/motion.mjs +6 -6
  23. package/dist/core/richText.d.mts +2 -2
  24. package/dist/core/vec.d.mts +1 -1
  25. package/dist/field/index.d.mts +11 -3
  26. package/dist/field/index.mjs +20 -3
  27. package/dist/index.d.mts +3 -1
  28. package/dist/index.mjs +3 -1
  29. package/dist/interaction/MovableDot.d.mts +17 -1
  30. package/dist/interaction/MovableDot.mjs +141 -43
  31. package/dist/interaction/index.d.mts +2 -1
  32. package/dist/interaction/index.mjs +2 -1
  33. package/dist/interaction/useDraggable.d.mts +1 -1
  34. package/dist/interaction/useDraggable.mjs +31 -1
  35. package/dist/interaction/usePressSpring.d.mts +24 -0
  36. package/dist/interaction/usePressSpring.mjs +67 -0
  37. package/dist/logic/ast.d.mts +1 -1
  38. package/dist/logic/index.mjs +8 -3
  39. package/dist/logic/minimize.d.mts +2 -2
  40. package/dist/logic/minimize.mjs +56 -18
  41. package/dist/logic/table.mjs +1 -1
  42. package/dist/math/ast.d.mts +1 -1
  43. package/dist/math/ast.mjs +1 -1
  44. package/dist/math/calculus.mjs +68 -9
  45. package/dist/math/compile.d.mts +8 -0
  46. package/dist/math/compile.mjs +40 -0
  47. package/dist/math/defs.mjs +1 -1
  48. package/dist/math/index.d.mts +4 -3
  49. package/dist/math/index.mjs +4 -3
  50. package/dist/math/parse.mjs +2 -2
  51. package/dist/math/tokenize.mjs +1 -1
  52. package/dist/primitives/CanvasLayer.mjs +3 -3
  53. package/dist/primitives/Grid.d.mts +12 -1
  54. package/dist/primitives/Grid.mjs +15 -9
  55. package/dist/primitives/Label.d.mts +1 -1
  56. package/dist/primitives/Label.mjs +1 -1
  57. package/dist/primitives/Tex.mjs +2 -2
  58. package/dist/scene/Scene.mjs +1 -1
  59. package/dist/scene/assets.d.mts +2 -2
  60. package/dist/scene/assets.mjs +2 -2
  61. package/dist/scene/commands.d.mts +10 -1
  62. package/dist/scene/commands.mjs +42 -2
  63. package/dist/scene/evaluators.mjs +2 -2
  64. package/dist/scene/index.d.mts +2 -2
  65. package/dist/scene/index.mjs +2 -2
  66. package/dist/scene/migrate.mjs +1 -1
  67. package/dist/scene/sims.mjs +1 -1
  68. package/dist/scene/types.d.mts +4 -4
  69. package/dist/sim/particles.d.mts +2 -2
  70. package/dist/sim/rate.d.mts +4 -4
  71. package/dist/sim/rate.mjs +1 -1
  72. package/dist/sim/registry.d.mts +1 -1
  73. package/dist/sim/sampler.d.mts +5 -5
  74. package/dist/sim/thermal.d.mts +3 -3
  75. package/dist/sim/thermal.mjs +2 -2
  76. package/dist/sim/types.d.mts +2 -2
  77. package/dist/sim/wave.d.mts +2 -2
  78. package/dist/sim/wave.mjs +2 -2
  79. package/dist/steps/index.d.mts +1 -1
  80. package/dist/steps/index.mjs +3 -3
  81. package/dist/thermo/index.d.mts +1 -1
  82. package/dist/thermo/index.mjs +1 -1
  83. package/dist/view/Stage.mjs +2 -2
  84. package/dist/view/useInView.mjs +1 -1
  85. package/package.json +8 -4
  86. package/styles.css +23 -0
package/dist/index.d.mts CHANGED
@@ -32,6 +32,7 @@ import { CanvasLayer, CanvasLayerProps } from "./primitives/CanvasLayer.mjs";
32
32
  import { useScreenToMath } from "./interaction/useScreenToMath.mjs";
33
33
  import { AriaSliderProps, Constrain, DraggableHandlers, UseDraggable, UseDraggableArgs, useDraggable } from "./interaction/useDraggable.mjs";
34
34
  import { MovableDot, MovableDotProps } from "./interaction/MovableDot.mjs";
35
+ import { PressSpringOpts, useImpulse, usePressSpring } from "./interaction/usePressSpring.mjs";
35
36
  import { HitCandidate, hitTest } from "./interaction/hitTest.mjs";
36
37
  import { OP_REFS, isRef, parentIds } from "./scene/schema.mjs";
37
38
  import { EVALUATORS, EvalCtx } from "./scene/evaluators.mjs";
@@ -42,6 +43,7 @@ import { RenderOpts, renderElements } from "./scene/render.mjs";
42
43
  import { BinOp, Node, compileNode, evaluate, freeVars } from "./math/ast.mjs";
43
44
  import { parse as parse$1 } from "./math/parse.mjs";
44
45
  import { differentiate, simplify } from "./math/calculus.mjs";
46
+ import { CompiledFn, compile } from "./math/compile.mjs";
45
47
  import { toLatex as toLatex$1 } from "./math/latex.mjs";
46
48
  import { CompiledExpr, ExprError, ExprResult, compileExpr } from "./math/index.mjs";
47
49
  import { BinOp as BinOp$1, LNode, evalBool, freeVars as freeVars$1 } from "./logic/ast.mjs";
@@ -51,4 +53,4 @@ import { toLatex } from "./logic/latex.mjs";
51
53
  import { Cube, Minimization, cubeCovers, cubeOfSelection, cubeTerm, minimalCover, minimize, primeImplicants } from "./logic/minimize.mjs";
52
54
  import { CompiledLogic, LogicResult, compileLogic } from "./logic/index.mjs";
53
55
  import { AssetResolveArgs, AssetSpec, assetMeta, getAsset, listAssets, registerAsset } from "./scene/assets.mjs";
54
- export { type A11yProps, AUTHORING_TOOLS, type AriaSliderProps, type AssetGeometry, type AssetResolveArgs, type AssetSpec, Axes, type AxesProps, type BinOp, type Binding, BucketGlyph, CIRCLE, CURRENT_SCHEMA_VERSION, CanvasLayer, type CanvasLayerProps, Circle, type CircleProps, type CircleVal, type Classification, ClockProvider, type ClockProviderProps, type Command, type CommandResult, type CompiledExpr, type CompiledLogic, type Constrain, type ControlMap, type ControlPick, type ControlSpec, type ControlSurface, type CoordinateSystem, CoordsContext, type CoordsOptions, type Cube, DELETE, type DerivedDef, type DerivedElement, type DerivedKind, type DispatchOpts, Dot, type DotProps, DragOverlayContext, type DraggableHandlers, EVALUATORS, Editor, type ElementBase, type ElementStyle, Ellipse, type EllipseProps, type EvalCtx, type ExprError, type ExprResult, type FrameDriver, type FrameInfo, type FreeElement, type FreeNote, type FreePoint, type FreeScalar, GEOMETRY_TOOLS, type GlyphBox, Grid, type GridProps, HangerHook, type HangerHookProps, type HitCandidate, INTERSECT, type IconName, type Id, type InstanceState, LINE, type LNode, type LabMeta, Label, type LabelProps, type Learner, LearnerProvider, type LearnerResult, Line, type LineProps, type BinOp$1 as LogicBinOp, type LogicResult, MEASURE, MIDPOINT, type Matrix, type Minimization, MovableDot, type MovableDotProps, type MutatePatch, NOTE, type Node, type NumOrRef, OP_REFS, type OfXProps, type OfYProps, type Op, POINT, Palette, type PaletteProps, type ParametricProps, Path, type PathProps, Plot, Point, Polygon, type PolygonProps, Polyline, type PolylineProps, type PriorAttempts, Ray, type Ref, type RenderOpts, type Resolved, type RichSpan, SCALAR, SEGMENT, SELECT, Scene, SceneBuilder, type SceneBuilderProps, type SceneDoc, type SceneElement, type SceneMeta, type SceneProps, SceneStore, type Vec2 as SceneVec2, type Script, Segment, type SegmentProps, type ShapeLineVal, type Size, Stage, StageAssetDefs, type StageProps, StageRefContext, type StepProgress, StepProgressProvider, StepScope, type StepSpec, type StepState, type StyleProps, Tex, type TexProps, type Tool, type ToolCtx, ToolIcon, type TruthRow, type TruthTable, type UseDraggable, type UseDraggableArgs, type Val, type Vec2, Vector, type VectorProps, type ViewBox, WeightGlyph, XBlockGlyph, applyCommand, assetMeta, classify, collectRemoved, compileExpr, compileLogic, compileNode, controlsFromScene, createCoords, cubeCovers, cubeOfSelection, cubeTerm, differentiate, emptyDoc, equivalent, evalBool, evaluate, fmt, freeVars, getAsset, getControlSurface, hitTest, inverse, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isRef, isVec2, listAssets, listControlSurfaces, freeVars$1 as logicFreeVars, toLatex as logicToLatex, mat, migrate, minimalCover, minimize, niceStep, numOf, onControlChange, parentIds, parse, parse$1 as parseExpr, parseLogic, parseRichText, primeImplicants, registerAsset, renderElements, resolve, serialize, simplify, toCNF, toDNF, toLatex$1 as toLatex, truthTable, useControlSurface, useCoords, useDragOverlay, useDraggable, useEditor, useElementSize, useFrameLoop, useInView, useLearner, useScreenToMath, useStageRef, useStepProgress, vec };
56
+ export { type A11yProps, AUTHORING_TOOLS, type AriaSliderProps, type AssetGeometry, type AssetResolveArgs, type AssetSpec, Axes, type AxesProps, type BinOp, type Binding, BucketGlyph, CIRCLE, CURRENT_SCHEMA_VERSION, CanvasLayer, type CanvasLayerProps, Circle, type CircleProps, type CircleVal, type Classification, ClockProvider, type ClockProviderProps, type Command, type CommandResult, type CompiledExpr, type CompiledFn, type CompiledLogic, type Constrain, type ControlMap, type ControlPick, type ControlSpec, type ControlSurface, type CoordinateSystem, CoordsContext, type CoordsOptions, type Cube, DELETE, type DerivedDef, type DerivedElement, type DerivedKind, type DispatchOpts, Dot, type DotProps, DragOverlayContext, type DraggableHandlers, EVALUATORS, Editor, type ElementBase, type ElementStyle, Ellipse, type EllipseProps, type EvalCtx, type ExprError, type ExprResult, type FrameDriver, type FrameInfo, type FreeElement, type FreeNote, type FreePoint, type FreeScalar, GEOMETRY_TOOLS, type GlyphBox, Grid, type GridProps, HangerHook, type HangerHookProps, type HitCandidate, INTERSECT, type IconName, type Id, type InstanceState, LINE, type LNode, type LabMeta, Label, type LabelProps, type Learner, LearnerProvider, type LearnerResult, Line, type LineProps, type BinOp$1 as LogicBinOp, type LogicResult, MEASURE, MIDPOINT, type Matrix, type Minimization, MovableDot, type MovableDotProps, type MutatePatch, NOTE, type Node, type NumOrRef, OP_REFS, type OfXProps, type OfYProps, type Op, POINT, Palette, type PaletteProps, type ParametricProps, Path, type PathProps, Plot, Point, Polygon, type PolygonProps, Polyline, type PolylineProps, type PressSpringOpts, type PriorAttempts, Ray, type Ref, type RenderOpts, type Resolved, type RichSpan, SCALAR, SEGMENT, SELECT, Scene, SceneBuilder, type SceneBuilderProps, type SceneDoc, type SceneElement, type SceneMeta, type SceneProps, SceneStore, type Vec2 as SceneVec2, type Script, Segment, type SegmentProps, type ShapeLineVal, type Size, Stage, StageAssetDefs, type StageProps, StageRefContext, type StepProgress, StepProgressProvider, StepScope, type StepSpec, type StepState, type StyleProps, Tex, type TexProps, type Tool, type ToolCtx, ToolIcon, type TruthRow, type TruthTable, type UseDraggable, type UseDraggableArgs, type Val, type Vec2, Vector, type VectorProps, type ViewBox, WeightGlyph, XBlockGlyph, applyCommand, assetMeta, classify, collectRemoved, compile, compileExpr, compileLogic, compileNode, controlsFromScene, createCoords, cubeCovers, cubeOfSelection, cubeTerm, differentiate, emptyDoc, equivalent, evalBool, evaluate, fmt, freeVars, getAsset, getControlSurface, hitTest, inverse, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isRef, isVec2, listAssets, listControlSurfaces, freeVars$1 as logicFreeVars, toLatex as logicToLatex, mat, migrate, minimalCover, minimize, niceStep, numOf, onControlChange, parentIds, parse, parse$1 as parseExpr, parseLogic, parseRichText, primeImplicants, registerAsset, renderElements, resolve, serialize, simplify, toCNF, toDNF, toLatex$1 as toLatex, truthTable, useControlSurface, useCoords, useDragOverlay, useDraggable, useEditor, useElementSize, useFrameLoop, useImpulse, useInView, useLearner, usePressSpring, useScreenToMath, useStageRef, useStepProgress, vec };
package/dist/index.mjs CHANGED
@@ -19,6 +19,7 @@ import { Plot } from "./primitives/Plot.mjs";
19
19
  import { CanvasLayer } from "./primitives/CanvasLayer.mjs";
20
20
  import { useScreenToMath } from "./interaction/useScreenToMath.mjs";
21
21
  import { useDraggable } from "./interaction/useDraggable.mjs";
22
+ import { useImpulse, usePressSpring } from "./interaction/usePressSpring.mjs";
22
23
  import { MovableDot } from "./interaction/MovableDot.mjs";
23
24
  import { hitTest } from "./interaction/hitTest.mjs";
24
25
  import { isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isVec2, numOf } from "./scene/types.mjs";
@@ -27,6 +28,7 @@ import { assetMeta, getAsset, listAssets, registerAsset } from "./scene/assets.m
27
28
  import { compileNode, evaluate, freeVars } from "./math/ast.mjs";
28
29
  import { parse as parse$1 } from "./math/parse.mjs";
29
30
  import { differentiate, simplify } from "./math/calculus.mjs";
31
+ import { compile } from "./math/compile.mjs";
30
32
  import { toLatex as toLatex$1 } from "./math/latex.mjs";
31
33
  import { compileExpr } from "./math/index.mjs";
32
34
  import { EVALUATORS } from "./scene/evaluators.mjs";
@@ -52,4 +54,4 @@ import { toLatex } from "./logic/latex.mjs";
52
54
  import { cubeCovers, cubeOfSelection, cubeTerm, minimalCover, minimize, primeImplicants } from "./logic/minimize.mjs";
53
55
  import { compileLogic } from "./logic/index.mjs";
54
56
 
55
- export { AUTHORING_TOOLS, Axes, BucketGlyph, CIRCLE, CURRENT_SCHEMA_VERSION, CanvasLayer, Circle, ClockProvider, CoordsContext, DELETE, Dot, DragOverlayContext, EVALUATORS, Editor, Ellipse, GEOMETRY_TOOLS, Grid, HangerHook, INTERSECT, LINE, Label, LearnerProvider, Line, MEASURE, MIDPOINT, MovableDot, NOTE, OP_REFS, POINT, Palette, Path, Plot, Point, Polygon, Polyline, Ray, SCALAR, SEGMENT, SELECT, Scene, SceneBuilder, SceneStore, Segment, Stage, StageAssetDefs, StageRefContext, StepProgressProvider, StepScope, Tex, ToolIcon, Vector, WeightGlyph, XBlockGlyph, applyCommand, assetMeta, classify, collectRemoved, compileExpr, compileLogic, compileNode, controlsFromScene, createCoords, cubeCovers, cubeOfSelection, cubeTerm, differentiate, emptyDoc, equivalent, evalBool, evaluate, fmt, freeVars, getAsset, getControlSurface, hitTest, inverse, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isRef, isVec2, listAssets, listControlSurfaces, freeVars$1 as logicFreeVars, toLatex as logicToLatex, mat, migrate, minimalCover, minimize, niceStep, numOf, onControlChange, parentIds, parse, parse$1 as parseExpr, parseLogic, parseRichText, primeImplicants, registerAsset, renderElements, resolve, serialize, simplify, toCNF, toDNF, toLatex$1 as toLatex, truthTable, useControlSurface, useCoords, useDragOverlay, useDraggable, useEditor, useElementSize, useFrameLoop, useInView, useLearner, useScreenToMath, useStageRef, useStepProgress, vec };
57
+ export { AUTHORING_TOOLS, Axes, BucketGlyph, CIRCLE, CURRENT_SCHEMA_VERSION, CanvasLayer, Circle, ClockProvider, CoordsContext, DELETE, Dot, DragOverlayContext, EVALUATORS, Editor, Ellipse, GEOMETRY_TOOLS, Grid, HangerHook, INTERSECT, LINE, Label, LearnerProvider, Line, MEASURE, MIDPOINT, MovableDot, NOTE, OP_REFS, POINT, Palette, Path, Plot, Point, Polygon, Polyline, Ray, SCALAR, SEGMENT, SELECT, Scene, SceneBuilder, SceneStore, Segment, Stage, StageAssetDefs, StageRefContext, StepProgressProvider, StepScope, Tex, ToolIcon, Vector, WeightGlyph, XBlockGlyph, applyCommand, assetMeta, classify, collectRemoved, compile, compileExpr, compileLogic, compileNode, controlsFromScene, createCoords, cubeCovers, cubeOfSelection, cubeTerm, differentiate, emptyDoc, equivalent, evalBool, evaluate, fmt, freeVars, getAsset, getControlSurface, hitTest, inverse, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isRef, isVec2, listAssets, listControlSurfaces, freeVars$1 as logicFreeVars, toLatex as logicToLatex, mat, migrate, minimalCover, minimize, niceStep, numOf, onControlChange, parentIds, parse, parse$1 as parseExpr, parseLogic, parseRichText, primeImplicants, registerAsset, renderElements, resolve, serialize, simplify, toCNF, toDNF, toLatex$1 as toLatex, truthTable, useControlSurface, useCoords, useDragOverlay, useDraggable, useEditor, useElementSize, useFrameLoop, useImpulse, useInView, useLearner, usePressSpring, useScreenToMath, useStageRef, useStepProgress, vec };
@@ -18,6 +18,18 @@ interface MovableDotProps {
18
18
  /** Keyboard arrow-key nudge in math units. Match the lab's snap grid so a
19
19
  * single arrow press actually moves (e.g. 1 for an integer number line). */
20
20
  step?: number;
21
+ /** Math-unit snap grid. Tells the handle the lattice so it can preview the
22
+ * candidate landing spots (ghosts) and a snap-tick — purely visual; the lab
23
+ * still owns the actual snapping in its onMove. 0/undefined = no preview. */
24
+ snap?: number;
25
+ /** Faint candidate snap-stops shown while dragging (default: on when `snap`
26
+ * is set). Pass explicit points to drive non-grid lattices (e.g. stops along
27
+ * a ray); otherwise they're derived from `snap` + the view + `constrain`. */
28
+ ghosts?: boolean;
29
+ ghostPoints?: Vec2[];
30
+ /** Live value pill near the handle while dragging. true = default formatter,
31
+ * a fn for custom text, false to hide. Default true. */
32
+ readout?: boolean | ((v: Vec2) => string);
21
33
  }
22
34
  declare function MovableDot({
23
35
  value,
@@ -27,7 +39,11 @@ declare function MovableDot({
27
39
  r,
28
40
  ariaLabel,
29
41
  range,
30
- step
42
+ step,
43
+ snap,
44
+ ghosts,
45
+ ghostPoints,
46
+ readout
31
47
  }: MovableDotProps): ReactNode;
32
48
  //#endregion
33
49
  export { MovableDot, MovableDotProps };
@@ -2,12 +2,52 @@
2
2
 
3
3
  import { useCoords } from "../core/context.mjs";
4
4
  import { useDraggable } from "./useDraggable.mjs";
5
+ import { useImpulse, usePressSpring } from "./usePressSpring.mjs";
5
6
  import { useState } from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
8
 
8
9
  //#region src/interaction/MovableDot.tsx
9
- function MovableDot({ value, onMove, constrain, color = "var(--stage-accent)", r = 7, ariaLabel = "draggable point", range, step }) {
10
- const [px, py] = useCoords().toPx(value.x, value.y);
10
+ const ACCENT = "var(--stage-accent)";
11
+ function fnum(n) {
12
+ const r = Math.round(n * 100) / 100;
13
+ return Object.is(r, -0) ? "0" : String(r);
14
+ }
15
+ /** Candidate snap-stops within a small window around `value`, derived from the
16
+ * grid + which axes are free. Kept LOCAL (±3 steps) so the guide reads as
17
+ * "here are your nearby choices", not a wall of dots. */
18
+ function autoGhosts(value, snap, constrain, view, range) {
19
+ if (!(snap > 0)) return [];
20
+ const W = 3;
21
+ const stops = [];
22
+ const along = (axis) => {
23
+ const base = axis === "x" ? value.x : value.y;
24
+ const lo = axis === "x" ? range?.min ?? view.xMin : range?.min ?? view.yMin;
25
+ const hi = axis === "x" ? range?.max ?? view.xMax : range?.max ?? view.yMax;
26
+ for (let k = -3; k <= W; k++) {
27
+ if (k === 0) continue;
28
+ const v = Math.round(base / snap) * snap + k * snap;
29
+ if (v < lo - 1e-9 || v > hi + 1e-9) continue;
30
+ stops.push(axis === "x" ? {
31
+ x: v,
32
+ y: value.y
33
+ } : {
34
+ x: value.x,
35
+ y: v
36
+ });
37
+ }
38
+ };
39
+ const oneD = constrain === "horizontal" || constrain === "vertical";
40
+ if (constrain === "horizontal") along("x");
41
+ else if (constrain === "vertical") along("y");
42
+ else if (!oneD && typeof constrain !== "function") {
43
+ along("x");
44
+ along("y");
45
+ }
46
+ return stops;
47
+ }
48
+ function MovableDot({ value, onMove, constrain, color = ACCENT, r = 7, ariaLabel = "draggable point", range, step, snap, ghosts, ghostPoints, readout = true }) {
49
+ const c = useCoords();
50
+ const [px, py] = c.toPx(value.x, value.y);
11
51
  const { dragging, handlers, ariaProps } = useDraggable({
12
52
  value,
13
53
  onMove,
@@ -16,50 +56,108 @@ function MovableDot({ value, onMove, constrain, color = "var(--stage-accent)", r
16
56
  ...step !== void 0 ? { step } : {}
17
57
  });
18
58
  const [hovered, setHovered] = useState(false);
19
- const lift = hovered && !dragging;
20
- const dotR = lift ? r + 1.5 : r;
21
- return /* @__PURE__ */ jsxs("g", {
22
- ...handlers,
23
- ...ariaProps,
24
- "aria-label": ariaLabel,
25
- onPointerEnter: () => setHovered(true),
26
- onPointerLeave: () => setHovered(false),
27
- style: {
28
- cursor: dragging ? "grabbing" : "grab",
29
- outline: "none"
30
- },
31
- children: [
32
- /* @__PURE__ */ jsx("circle", {
33
- cx: px,
34
- cy: py,
35
- r: Math.max(r + 14, 12),
36
- fill: "transparent"
37
- }),
38
- dragging ? /* @__PURE__ */ jsx("circle", {
39
- cx: px,
40
- cy: py,
41
- r: r + 6,
42
- fill: color,
43
- opacity: .2
44
- }) : /* @__PURE__ */ jsx("circle", {
59
+ const press = usePressSpring(dragging, {
60
+ to: 1,
61
+ stiffness: 360,
62
+ damping: 24
63
+ });
64
+ const dotR = r + (hovered && !dragging ? 1.5 : 0) + press * 3;
65
+ const tickRaw = useImpulse(`${Math.round(value.x * 1e4)}:${Math.round(value.y * 1e4)}`, 280);
66
+ const tick = dragging ? tickRaw : 0;
67
+ const stops = !(dragging && (ghosts ?? (snap != null && snap > 0))) ? [] : ghostPoints ?? autoGhosts(value, snap ?? 0, constrain, c.view, range);
68
+ const label = readout === false ? null : typeof readout === "function" ? readout(value) : constrain === "horizontal" || constrain === "vertical" ? fnum(constrain === "vertical" ? value.y : value.x) : `(${fnum(value.x)}, ${fnum(value.y)})`;
69
+ const pillW = label ? Math.max(30, label.length * 7.5 + 14) : 0;
70
+ const pillY = py - dotR - 30;
71
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ stops.length > 0 && /* @__PURE__ */ jsx("g", {
73
+ style: { pointerEvents: "none" },
74
+ children: stops.map((s, i) => {
75
+ const [gx, gy] = c.toPx(s.x, s.y);
76
+ const d = Math.hypot(gx - px, gy - py);
77
+ return /* @__PURE__ */ jsx("circle", {
78
+ cx: gx,
79
+ cy: gy,
80
+ r: 3,
81
+ fill: color,
82
+ opacity: Math.max(.12, .5 - d / 360)
83
+ }, i);
84
+ })
85
+ }),
86
+ /* @__PURE__ */ jsxs("g", {
87
+ ...handlers,
88
+ ...ariaProps,
89
+ "aria-label": ariaLabel,
90
+ onPointerEnter: () => setHovered(true),
91
+ onPointerLeave: () => setHovered(false),
92
+ style: {
93
+ cursor: dragging ? "grabbing" : "grab",
94
+ outline: "none"
95
+ },
96
+ children: [
97
+ /* @__PURE__ */ jsx("circle", {
98
+ cx: px,
99
+ cy: py,
100
+ r: Math.max(r + 14, 12),
101
+ fill: "transparent"
102
+ }),
103
+ dragging ? /* @__PURE__ */ jsx("circle", {
104
+ cx: px,
105
+ cy: py,
106
+ r: r + 6 + press * 4,
107
+ fill: color,
108
+ opacity: .18
109
+ }) : /* @__PURE__ */ jsx("circle", {
110
+ cx: px,
111
+ cy: py,
112
+ r: r + 5,
113
+ fill: "none",
114
+ stroke: color,
115
+ strokeWidth: 1.5,
116
+ opacity: hovered ? .55 : .28
117
+ }),
118
+ /* @__PURE__ */ jsx("circle", {
119
+ cx: px,
120
+ cy: py,
121
+ r: dotR,
122
+ fill: color,
123
+ stroke: "var(--stage-bg)",
124
+ strokeWidth: 2
125
+ })
126
+ ]
127
+ }),
128
+ /* @__PURE__ */ jsxs("g", {
129
+ style: { pointerEvents: "none" },
130
+ children: [tick > 0 && /* @__PURE__ */ jsx("circle", {
45
131
  cx: px,
46
132
  cy: py,
47
- r: r + 5,
133
+ r: dotR + (1 - tick) * 12,
48
134
  fill: "none",
49
135
  stroke: color,
50
- strokeWidth: 1.5,
51
- opacity: lift ? .55 : .28
52
- }),
53
- /* @__PURE__ */ jsx("circle", {
54
- cx: px,
55
- cy: py,
56
- r: dotR,
57
- fill: color,
58
- stroke: "var(--stage-bg)",
59
- strokeWidth: 2
60
- })
61
- ]
62
- });
136
+ strokeWidth: 2,
137
+ opacity: tick * .6
138
+ }), dragging && label && /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("rect", {
139
+ x: px - pillW / 2,
140
+ y: pillY,
141
+ width: pillW,
142
+ height: 22,
143
+ rx: 7,
144
+ fill: "var(--stage-bg)",
145
+ stroke: "var(--stage-grid)",
146
+ strokeWidth: 1,
147
+ opacity: .96
148
+ }), /* @__PURE__ */ jsx("text", {
149
+ x: px,
150
+ y: pillY + 11,
151
+ textAnchor: "middle",
152
+ dominantBaseline: "central",
153
+ fontSize: 12,
154
+ fontWeight: 700,
155
+ fill: "var(--stage-fg)",
156
+ style: { fontVariantNumeric: "tabular-nums" },
157
+ children: label
158
+ })] })]
159
+ })
160
+ ] });
63
161
  }
64
162
 
65
163
  //#endregion
@@ -1,5 +1,6 @@
1
1
  import { useScreenToMath } from "./useScreenToMath.mjs";
2
2
  import { AriaSliderProps, Constrain, DraggableHandlers, UseDraggable, UseDraggableArgs, useDraggable } from "./useDraggable.mjs";
3
3
  import { MovableDot, MovableDotProps } from "./MovableDot.mjs";
4
+ import { PressSpringOpts, useImpulse, usePressSpring } from "./usePressSpring.mjs";
4
5
  import { HitCandidate, hitTest } from "./hitTest.mjs";
5
- export { type AriaSliderProps, type Constrain, type DraggableHandlers, type HitCandidate, MovableDot, type MovableDotProps, type UseDraggable, type UseDraggableArgs, hitTest, useDraggable, useScreenToMath };
6
+ export { type AriaSliderProps, type Constrain, type DraggableHandlers, type HitCandidate, MovableDot, type MovableDotProps, type PressSpringOpts, type UseDraggable, type UseDraggableArgs, hitTest, useDraggable, useImpulse, usePressSpring, useScreenToMath };
@@ -1,6 +1,7 @@
1
1
  import { useScreenToMath } from "./useScreenToMath.mjs";
2
2
  import { useDraggable } from "./useDraggable.mjs";
3
+ import { useImpulse, usePressSpring } from "./usePressSpring.mjs";
3
4
  import { MovableDot } from "./MovableDot.mjs";
4
5
  import { hitTest } from "./hitTest.mjs";
5
6
 
6
- export { MovableDot, hitTest, useDraggable, useScreenToMath };
7
+ export { MovableDot, hitTest, useDraggable, useImpulse, usePressSpring, useScreenToMath };
@@ -10,7 +10,7 @@ interface UseDraggableArgs {
10
10
  constrain?: Constrain;
11
11
  /** Keyboard nudge step in math units. */
12
12
  step?: number;
13
- /** Bounds on the active axis drives aria-valuemin/max for a proper slider
13
+ /** Bounds on the active axis, drives aria-valuemin/max for a proper slider
14
14
  * (WAI-ARIA APG). Omit for an unbounded 2D point (valuetext still describes it). */
15
15
  range?: {
16
16
  min: number;
@@ -57,6 +57,7 @@ function useDraggable({ value, onMove, constrain, step = .5, range }) {
57
57
  ]);
58
58
  const onKeyDown = useCallback((e) => {
59
59
  const d = step * (e.shiftKey ? 5 : 1) * (e.altKey ? .25 : 1);
60
+ const vertical = constrain === "vertical";
60
61
  let np = null;
61
62
  if (e.key === "ArrowLeft") np = {
62
63
  x: value.x - d,
@@ -74,6 +75,34 @@ function useDraggable({ value, onMove, constrain, step = .5, range }) {
74
75
  x: value.x,
75
76
  y: value.y - d
76
77
  };
78
+ else if (e.key === "PageUp") np = vertical ? {
79
+ x: value.x,
80
+ y: value.y + 5 * step
81
+ } : {
82
+ x: value.x + 5 * step,
83
+ y: value.y
84
+ };
85
+ else if (e.key === "PageDown") np = vertical ? {
86
+ x: value.x,
87
+ y: value.y - 5 * step
88
+ } : {
89
+ x: value.x - 5 * step,
90
+ y: value.y
91
+ };
92
+ else if (e.key === "Home" && range) np = vertical ? {
93
+ x: value.x,
94
+ y: range.min
95
+ } : {
96
+ x: range.min,
97
+ y: value.y
98
+ };
99
+ else if (e.key === "End" && range) np = vertical ? {
100
+ x: value.x,
101
+ y: range.max
102
+ } : {
103
+ x: range.max,
104
+ y: value.y
105
+ };
77
106
  if (np) {
78
107
  e.preventDefault();
79
108
  const p = constrained(np, value, constrain);
@@ -84,7 +113,8 @@ function useDraggable({ value, onMove, constrain, step = .5, range }) {
84
113
  value,
85
114
  step,
86
115
  constrain,
87
- onMove
116
+ onMove,
117
+ range
88
118
  ]);
89
119
  const oneD = constrain === "horizontal" || constrain === "vertical";
90
120
  const vertical = constrain === "vertical";
@@ -0,0 +1,24 @@
1
+ //#region src/interaction/usePressSpring.d.ts
2
+ interface PressSpringOpts {
3
+ /** Resting value when inactive. Default 0. */
4
+ from?: number;
5
+ /** Target value when active. Default 1. */
6
+ to?: number;
7
+ stiffness?: number;
8
+ damping?: number;
9
+ }
10
+ /**
11
+ * A scalar that springs toward `to` while `active`, back to `from` on release,
12
+ * e.g. a 0→1 "press" amount you map to a radius bump or halo grow. The RAF loop
13
+ * is gated on "still moving" so a page full of idle handles costs nothing.
14
+ * Under reduced-motion it returns the target immediately (no animation).
15
+ */
16
+ declare function usePressSpring(active: boolean, opts?: PressSpringOpts): number;
17
+ /**
18
+ * A one-shot impulse: jumps to 1 whenever `key` changes, then decays to 0 over
19
+ * `ms`. Map it to an expanding ring / flash to make a discrete event (a snap, a
20
+ * correct landing) FEEL like it clicked. Returns 0 under reduced-motion.
21
+ */
22
+ declare function useImpulse(key: number | string, ms?: number): number;
23
+ //#endregion
24
+ export { PressSpringOpts, useImpulse, usePressSpring };
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { useFrameLoop } from "../core/clock.mjs";
4
+ import { prefersReducedMotion, spring } from "../core/motion.mjs";
5
+ import { useRef, useState } from "react";
6
+
7
+ //#region src/interaction/usePressSpring.ts
8
+ /**
9
+ * Interaction "feel" hooks, the juice layer for draggable handles. Both are thin
10
+ * wrappers over the pure `spring` / decay maths in core/motion.ts; they own a
11
+ * frame loop that runs ONLY while the value is unsettled (no idle RAF), and they
12
+ * honor prefers-reduced-motion by snapping to the end state. SSR-safe.
13
+ */
14
+ const EPS = 8e-4;
15
+ /**
16
+ * A scalar that springs toward `to` while `active`, back to `from` on release,
17
+ * e.g. a 0→1 "press" amount you map to a radius bump or halo grow. The RAF loop
18
+ * is gated on "still moving" so a page full of idle handles costs nothing.
19
+ * Under reduced-motion it returns the target immediately (no animation).
20
+ */
21
+ function usePressSpring(active, opts = {}) {
22
+ const { from = 0, to = 1, stiffness = 320, damping = 26 } = opts;
23
+ const target = active ? to : from;
24
+ const reduce = prefersReducedMotion();
25
+ const [val, setVal] = useState(from);
26
+ const valRef = useRef(from);
27
+ const vel = useRef(0);
28
+ const settled = Math.abs(valRef.current - target) < EPS && Math.abs(vel.current) < EPS;
29
+ useFrameLoop((f) => {
30
+ const next = spring(valRef.current, target, vel, f.dtMs / 1e3, {
31
+ stiffness,
32
+ damping
33
+ });
34
+ if (Math.abs(next - target) < EPS && Math.abs(vel.current) < EPS) {
35
+ valRef.current = target;
36
+ vel.current = 0;
37
+ setVal(target);
38
+ } else {
39
+ valRef.current = next;
40
+ setVal(next);
41
+ }
42
+ }, { running: !reduce && !settled });
43
+ return reduce ? target : val;
44
+ }
45
+ /**
46
+ * A one-shot impulse: jumps to 1 whenever `key` changes, then decays to 0 over
47
+ * `ms`. Map it to an expanding ring / flash to make a discrete event (a snap, a
48
+ * correct landing) FEEL like it clicked. Returns 0 under reduced-motion.
49
+ */
50
+ function useImpulse(key, ms = 260) {
51
+ const [v, setV] = useState(0);
52
+ const amp = useRef(0);
53
+ const prevKey = useRef(key);
54
+ const reduce = prefersReducedMotion();
55
+ if (prevKey.current !== key) {
56
+ prevKey.current = key;
57
+ if (!reduce) amp.current = 1;
58
+ }
59
+ useFrameLoop((f) => {
60
+ amp.current = Math.max(0, amp.current - f.dtMs / ms);
61
+ setV(amp.current);
62
+ }, { running: !reduce && amp.current > 0 });
63
+ return reduce ? 0 : v;
64
+ }
65
+
66
+ //#endregion
67
+ export { useImpulse, usePressSpring };
@@ -1,6 +1,6 @@
1
1
  //#region src/logic/ast.d.ts
2
2
  /**
3
- * Propositional-logic AST the boolean-domain sibling of the `expr` engine's
3
+ * Propositional-logic AST, the boolean-domain sibling of the `expr` engine's
4
4
  * numeric AST. A formula is variables joined by ¬ ∧ ∨ ⊕ → ↔. Pure + zero-dep;
5
5
  * `evalBool` is the single evaluator every consumer (truth tables, equivalence,
6
6
  * boolean circuits) runs.
@@ -6,7 +6,7 @@ import { cubeCovers, cubeOfSelection, cubeTerm, minimalCover, minimize, primeImp
6
6
 
7
7
  //#region src/logic/index.ts
8
8
  /**
9
- * @classytic/stage logic kernel a pure, dependency-free propositional-logic
9
+ * @classytic/stage logic kernel, a pure, dependency-free propositional-logic
10
10
  * engine, the boolean-domain sibling of the `expr` (numeric) engine. Parses
11
11
  * ¬ ∧ ∨ ⊕ → ↔ (unicode or ASCII), evaluates, builds truth tables, classifies
12
12
  * tautology/contradiction/contingency, checks equivalence, and emits DNF/CNF +
@@ -18,11 +18,16 @@ import { cubeCovers, cubeOfSelection, cubeTerm, minimalCover, minimize, primeImp
18
18
  function compileLogic(src) {
19
19
  try {
20
20
  const ast = parseLogic(src);
21
+ const vars = freeVars(ast);
21
22
  return {
22
23
  ok: true,
23
24
  ast,
24
- vars: freeVars(ast),
25
- eval: (env) => evalBool(ast, env)
25
+ vars,
26
+ eval: (env) => {
27
+ const missing = vars.filter((v) => !(v in env));
28
+ if (missing.length) throw new Error(`logic: assignment is missing variable${missing.length > 1 ? "s" : ""} ${missing.join(", ")}`);
29
+ return evalBool(ast, env);
30
+ }
26
31
  };
27
32
  } catch (e) {
28
33
  return {
@@ -17,11 +17,11 @@ interface Minimization {
17
17
  }
18
18
  /** All minterms a cube covers, within an n-variable space. */
19
19
  declare function cubeCovers(c: Cube, n: number): number[];
20
- /** The product term for a cube a literal per FIXED bit (MSB = first var). */
20
+ /** The product term for a cube, a literal per FIXED bit (MSB = first var). */
21
21
  declare function cubeTerm(c: Cube, vars: string[], neg?: string, join?: string): string;
22
22
  /** Quine–McCluskey prime implicants of a set of minterms (+ optional don't-cares). */
23
23
  declare function primeImplicants(minterms: number[], n: number, dontCares?: number[]): Cube[];
24
- /** A minimal SOP cover: essential prime implicants + a greedy fill of the rest. */
24
+ /** A minimum SOP cover: essential prime implicants + Petrick's exact cover of the rest. */
25
25
  declare function minimalCover(minterms: number[], n: number, dontCares?: number[]): Cube[];
26
26
  /** Full minimisation of a formula (or explicit minterms) → cover + SOP string. */
27
27
  declare function minimize(input: LNode | {
@@ -3,9 +3,9 @@ import { truthTable } from "./table.mjs";
3
3
 
4
4
  //#region src/logic/minimize.ts
5
5
  /**
6
- * Boolean minimisation Quine–McCluskey: the algorithm a Karnaugh map performs
6
+ * Boolean minimisation, Quine–McCluskey: the algorithm a Karnaugh map performs
7
7
  * by eye. Given a function's minterms (the rows where it is 1) over n variables,
8
- * it finds the PRIME IMPLICANTS (maximal "don't-care cubes" groups on the
8
+ * it finds the PRIME IMPLICANTS (maximal "don't-care cubes", groups on the
9
9
  * K-map) and a minimal SUM-OF-PRODUCTS cover (essential PIs + a greedy fill).
10
10
  * Pure + dependency-free, the boolean sibling of `simplify` in the expr engine;
11
11
  * the K-map lab renders these cubes as the coloured groups, and the same routine
@@ -31,7 +31,7 @@ function cubeCovers(c, n) {
31
31
  for (let m = 0; m < total; m++) if ((m & c.mask) === c.value) out.push(m);
32
32
  return out;
33
33
  }
34
- /** The product term for a cube a literal per FIXED bit (MSB = first var). */
34
+ /** The product term for a cube, a literal per FIXED bit (MSB = first var). */
35
35
  function cubeTerm(c, vars, neg = "¬", join = " ∧ ") {
36
36
  const n = vars.length;
37
37
  const lits = [];
@@ -86,7 +86,56 @@ function coverageOf(pis, required) {
86
86
  });
87
87
  return map;
88
88
  }
89
- /** A minimal SOP cover: essential prime implicants + a greedy fill of the rest. */
89
+ /** Keep only the minimal PI-sets (drop any that is a strict superset of another). */
90
+ function absorb(products) {
91
+ const uniq = /* @__PURE__ */ new Map();
92
+ for (const p of products) {
93
+ const s = new Set(p);
94
+ uniq.set([...s].sort((a, b) => a - b).join(","), s);
95
+ }
96
+ const sets = [...uniq.values()];
97
+ return sets.filter((s) => !sets.some((o) => o !== s && o.size < s.size && [...o].every((x) => s.has(x)))).map((s) => [...s]);
98
+ }
99
+ /** Greedy fallback: take the PI covering the most still-uncovered minterms, repeat. */
100
+ function greedyCover(uncovered, cov, candidates) {
101
+ const left = new Set(uncovered);
102
+ const picked = [];
103
+ while (left.size) {
104
+ let best = -1, bestCount = 0;
105
+ for (const i of candidates) {
106
+ if (picked.includes(i)) continue;
107
+ const c = cov.get(i).filter((m) => left.has(m)).length;
108
+ if (c > bestCount) {
109
+ bestCount = c;
110
+ best = i;
111
+ }
112
+ }
113
+ if (best < 0) break;
114
+ picked.push(best);
115
+ for (const m of cov.get(best)) left.delete(m);
116
+ }
117
+ return picked;
118
+ }
119
+ /**
120
+ * Petrick's method: the EXACT minimum-size cover of the remaining minterms. Each minterm
121
+ * contributes a clause (the PIs covering it); multiplying the clauses out (with absorption)
122
+ * enumerates every irredundant cover, and we keep the one with the fewest PIs. Falls back to
123
+ * greedy if the residual is large enough that the expansion could blow up (K-map labs are small).
124
+ */
125
+ function petrickCover(uncovered, cov, candidates) {
126
+ if (candidates.length > 18) return greedyCover(uncovered, cov, candidates);
127
+ let products = [[]];
128
+ for (const m of uncovered) {
129
+ const clause = candidates.filter((i) => cov.get(i).includes(m));
130
+ if (clause.length === 0) return greedyCover(uncovered, cov, candidates);
131
+ const next = [];
132
+ for (const p of products) for (const pi of clause) next.push(p.includes(pi) ? p : [...p, pi]);
133
+ products = absorb(next);
134
+ if (products.length > 4e3) return greedyCover(uncovered, cov, candidates);
135
+ }
136
+ return products.reduce((best, p) => p.length < best.length ? p : best, products[0] ?? []);
137
+ }
138
+ /** A minimum SOP cover: essential prime implicants + Petrick's exact cover of the rest. */
90
139
  function minimalCover(minterms, n, dontCares = []) {
91
140
  const required = new Set(minterms);
92
141
  if (required.size === 0) return [];
@@ -100,20 +149,9 @@ function minimalCover(minterms, n, dontCares = []) {
100
149
  if (owners.length === 1) chosen.add(owners[0]);
101
150
  }
102
151
  for (const i of chosen) for (const m of cov.get(i)) uncovered.delete(m);
103
- while (uncovered.size) {
104
- let best = -1, bestCount = -1;
105
- pis.forEach((_, i) => {
106
- if (chosen.has(i)) return;
107
- const c = cov.get(i).filter((m) => uncovered.has(m)).length;
108
- if (c > bestCount) {
109
- bestCount = c;
110
- best = i;
111
- }
112
- });
113
- if (best < 0 || bestCount <= 0) break;
114
- chosen.add(best);
115
- for (const m of cov.get(best)) uncovered.delete(m);
116
- }
152
+ if (uncovered.size === 0) return [...chosen].map((i) => pis[i]);
153
+ const candidates = pis.map((_, i) => i).filter((i) => !chosen.has(i));
154
+ for (const i of petrickCover([...uncovered], cov, candidates)) chosen.add(i);
117
155
  return [...chosen].map((i) => pis[i]);
118
156
  }
119
157
  /** Full minimisation of a formula (or explicit minterms) → cover + SOP string. */
@@ -4,7 +4,7 @@ import { evalBool, freeVars } from "./ast.mjs";
4
4
  /**
5
5
  * Truth tables + the things derived from them: classification (tautology /
6
6
  * contradiction / contingency), logical equivalence, and DNF/CNF. Everything is
7
- * a fold over the 2ⁿ assignments the truth table is the source of truth, so
7
+ * a fold over the 2ⁿ assignments, the truth table is the source of truth, so
8
8
  * equivalence is just "same output column" (no algebraic law engine needed).
9
9
  */
10
10
  /** Enumerate all 2^vars assignments (MSB = first var), evaluating `n` on each. */
@@ -1,6 +1,6 @@
1
1
  //#region src/math/ast.d.ts
2
2
  /**
3
- * expr/ast the Abstract Syntax Tree: node types, evaluation, free-variable
3
+ * expr/ast, the Abstract Syntax Tree: node types, evaluation, free-variable
4
4
  * collection, and small builder helpers. The AST is the shared representation
5
5
  * that evaluation, differentiation, simplification, and LaTeX printing all
6
6
  * operate on.