@tsplc/sim 0.0.0-stage → 0.1.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.
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/paths.ts", "../src/hardware.ts", "../src/line.ts", "../src/simulation.ts", "../src/layout.ts", "../src/io.ts"],
4
+ "sourcesContent": [
5
+ "declare const valueType: unique symbol;\nconst pathKey = Symbol(\"tsplc.path\");\n\n/** One PLC variable of type `Value`, such as `LineIO.Conveyor_Inbound.Cards[3].Output.MotionAsserted`. */\nexport interface Path<Value> {\n readonly [valueType]?: Value;\n}\n\n/**\n * Typed paths into a PLC's globals: records nest, `.at(index)` selects a record array's element, and\n * scalars and scalar arrays (read and written whole) are `Path`s.\n */\nexport type Paths<T> =\n T extends ReadonlyArray<infer Item>\n ? Item extends object\n ? { readonly at: (index: number) => Paths<Item> }\n : Path<T>\n : T extends object\n ? { readonly [Key in keyof T]: Paths<T[Key]> }\n : Path<T>;\n\nconst join = (segments: readonly string[]) =>\n segments.reduce(\n (path, segment) =>\n /^\\d+$/.test(segment) ? `${path}[${segment}]` : path ? `${path}.${segment}` : segment,\n \"\",\n );\n\n// A function target so that `.at` is callable, and still a path for a record field named `at`.\nconst proxy = (segments: readonly string[]): unknown =>\n new Proxy(() => undefined, {\n get: (_, key) => {\n if (key === pathKey) return join(segments);\n if (typeof key === \"symbol\") return undefined;\n return proxy([...segments, key]);\n },\n apply: (_, __, [index]: [number]) => proxy([...segments.slice(0, -1), String(index)]),\n });\n\n/**\n * Builds typed paths for a generated `PlcGlobals`:\n * `plcPaths<PlcGlobals>().LineIO.Conveyor_Inbound.Cards.at(3).Output.MotionAsserted`.\n */\nexport const plcPaths = <Globals>(): Paths<Globals> => proxy([]) as Paths<Globals>;\n\n/** The dotted variable path, e.g. `LineIO.Conveyor_Inbound.Cards[3].Output.MotionAsserted`. */\nexport const pathName = (path: Path<unknown>): string => {\n const name: unknown = Reflect.get(path, pathKey);\n if (typeof name !== \"string\" || !name) throw new Error(\"Expected a path from plcPaths().\");\n return name;\n};\n",
6
+ "import { type Path, pathName } from \"./paths\";\n\ntype PinType = \"bool\" | \"number\" | \"bytes\";\ntype PinValue<Type extends PinType> = Type extends \"bool\"\n ? boolean\n : Type extends \"number\"\n ? number\n : number[];\n\n/** An electrical terminal, seen from the device: `in` is driven by the PLC, `out` drives a PLC input. */\nexport interface Pin {\n readonly direction: \"in\" | \"out\";\n readonly type: PinType;\n}\nexport const pinIn = <Type extends PinType>(type: Type) => ({ direction: \"in\", type }) as const;\nexport const pinOut = <Type extends PinType>(type: Type) => ({ direction: \"out\", type }) as const;\nexport type Pins = Record<string, Pin>;\n\n/** Converts between a PLC value and a pin value, such as raw analog counts and volts. */\nexport interface Scaled {\n readonly path: Path<number>;\n readonly toPin: (plc: number) => number;\n readonly toPlc: (pin: number) => number;\n}\nexport type Wire<Value> = Path<Value> | (Value extends number ? Scaled : never);\n\n/**\n * A linear analog channel: `analog(path, { plc: [0, 32767], pin: [0, 10] })` wires a raw\n * 0..32767 output to a 0..10 V input.\n */\nexport const analog = (\n path: Path<number>,\n range: { plc: readonly [number, number]; pin: readonly [number, number] },\n): Scaled => {\n const [plcLow, plcHigh] = range.plc,\n [pinLow, pinHigh] = range.pin;\n return {\n path,\n toPin: (plc) => pinLow + ((plc - plcLow) * (pinHigh - pinLow)) / (plcHigh - plcLow),\n toPlc: (pin) => Math.round(plcLow + ((pin - pinLow) * (plcHigh - plcLow)) / (pinHigh - pinLow)),\n };\n};\n\n/** Each pin may be wired to one PLC variable of its type; unwired inputs read as their default. */\nexport type Wiring<Spec extends Pins> = {\n readonly [Name in keyof Spec]?: Wire<PinValue<Spec[Name][\"type\"]>>;\n};\ntype PinValues<Spec extends Pins> = { [Name in keyof Spec]: PinValue<Spec[Name][\"type\"]> };\n\nexport interface DeviceView {\n status: \"running\" | \"stopped\" | \"idle\" | \"busy\" | \"warning\" | \"fault\";\n label?: string;\n details?: Record<string, string | number | boolean>;\n}\n\nexport interface HardwareContext<Spec extends Pins, World extends WorldPorts, Config, State> {\n /** Input pins hold this tick's PLC values; set output pins to drive PLC inputs. */\n readonly pins: PinValues<Spec>;\n /** Physical ports the topology connects: `in` is sensed from the line, `out` acts on it. */\n readonly world: World;\n readonly config: Config;\n readonly state: State;\n readonly dtMs: number;\n}\n\ninterface WorldPorts {\n in: Record<string, unknown>;\n out: Record<string, unknown>;\n}\n\nexport interface HardwareModel<Spec extends Pins, World extends WorldPorts, Config, State> {\n /** Product name, such as \"Intelligrated CS1001\". */\n readonly name: string;\n readonly pins: Spec;\n /** Initial physical port values; they also fix each port's type. */\n readonly world: () => World;\n /** Validates the configuration (switch settings) and creates the device's private state. */\n readonly setup: (config: Config) => State;\n /** Runs once per tick: read input pins and `world.in`, then set output pins and `world.out`. */\n readonly update: (context: HardwareContext<Spec, World, Config, State>) => void;\n readonly view?: (context: HardwareContext<Spec, World, Config, State>) => DeviceView;\n readonly actions?: Record<\n string,\n (context: HardwareContext<Spec, World, Config, State>, argument: string) => void\n >;\n}\n\n/** One physical device on the line: a model, its configuration and its wiring. */\nexport interface Device<World extends WorldPorts = WorldPorts> {\n readonly model: string;\n readonly world: World;\n /** PLC variables this device reads each tick. */\n readonly reads: readonly string[];\n /** Sets input pins from PLC values, runs the model and returns the PLC values it drives. */\n readonly update: (plc: ReadonlyMap<string, unknown>, dtMs: number) => Map<string, unknown>;\n readonly view: () => DeviceView;\n readonly pins: () => Record<string, unknown>;\n readonly actions: readonly string[];\n readonly act: (action: string, argument: string) => void;\n}\n\nconst defaultValue = (type: PinType): PinValue<PinType> =>\n type === \"bool\" ? false : type === \"number\" ? 0 : [];\n\nconst isScaled = (wire: Wire<never> | Scaled | Path<unknown>): wire is Scaled => \"toPin\" in wire;\n/** The PLC variable a wire connects to. */\nexport const wirePath = (wire: Path<unknown> | Scaled) =>\n isScaled(wire) ? pathName(wire.path) : pathName(wire);\n\n/**\n * Defines a hardware model. The result is a factory that instantiates one device from its\n * configuration and wiring: `cs1001({ config: { switches }, wiring: { \"CN2-1\": path } })`.\n */\nexport const defineHardware =\n <Spec extends Pins, World extends WorldPorts, Config, State>(\n model: HardwareModel<Spec, World, Config, State>,\n ) =>\n (options: { config: Config; wiring: Wiring<Spec> }): Device<World> => {\n const state = model.setup(options.config);\n const world = model.world();\n const pins = Object.fromEntries(\n Object.entries(model.pins).map(([name, pin]) => [name, defaultValue(pin.type)]),\n ) as PinValues<Spec>; // Each entry is the default of its declared pin type.\n let dtMs = 0;\n const context = (): HardwareContext<Spec, World, Config, State> => ({\n pins,\n world,\n config: options.config,\n state,\n dtMs,\n });\n const wired = Object.entries(options.wiring).flatMap(([name, wire]) => {\n const pin = model.pins[name];\n if (!pin) throw new Error(`${model.name} has no pin ${String(name)}.`);\n const target: Path<unknown> | Scaled | undefined = wire;\n return target ? [{ name, pin, wire: target, path: wirePath(target) }] : [];\n });\n return {\n model: model.name,\n world,\n reads: wired.filter(({ pin }) => pin.direction === \"in\").map(({ path }) => path),\n update: (plc, elapsed) => {\n dtMs = elapsed;\n for (const { name, pin, wire, path } of wired) {\n if (pin.direction !== \"in\" || !plc.has(path)) continue;\n const value = plc.get(path);\n Reflect.set(pins, name, isScaled(wire) ? wire.toPin(Number(value)) : value);\n }\n model.update(context());\n const writes = new Map<string, unknown>();\n for (const { name, pin, wire, path } of wired) {\n if (pin.direction !== \"out\") continue;\n const value: unknown = Reflect.get(pins, name);\n writes.set(path, isScaled(wire) ? wire.toPlc(Number(value)) : value);\n }\n return writes;\n },\n view: () => model.view?.(context()) ?? { status: \"idle\" },\n pins: () => ({ ...pins }),\n actions: Object.keys(model.actions ?? {}),\n act: (action, argument) => {\n const handler = model.actions?.[action];\n if (!handler) throw new Error(`${model.name} has no action ${action}.`);\n handler(context(), argument);\n },\n };\n };\n",
7
+ "import type { Device, Wire } from \"./hardware\";\n\ntype AnyDevice = Device<{ in: Record<string, unknown>; out: Record<string, unknown> }>;\ntype Devices = Record<string, AnyDevice>;\n/** A zone controller senses the zone's photoeye and drives its rollers. */\ntype ZoneController = Device<{ in: { photoeye: boolean }; out: { motor: number } }>;\ntype MotorDevice = Device<{ in: Record<string, unknown>; out: { motor: number } }>;\ntype ReaderDevice = Device<{ in: { read: string | null }; out: Record<string, unknown> }>;\n\n/**\n * How a part moves the loads on it this tick, in mm/s: `along` the conveyor, or `across` it\n * toward the right (positive) or left (negative) exit. With `across` set, loads whose middle is on\n * the part do not travel along; they move sideways until they leave by the exit.\n */\nexport type Motion = { along: number } | { across: number } | { held: true };\n\n/** What a part's sensors can see, in millimetres from the part's start. */\nexport interface Sensing {\n /** A beam across the conveyor at `at` is blocked (by a load not yet moved off to the side). */\n blocked: (at: number) => boolean;\n /** Any load is on the part, including mid-transfer. */\n occupied: boolean;\n /** The loads on the part: edges in millimetres from its start, and how far each has moved across. */\n loads: ReadonlyArray<{ head: number; tail: number; lateral: number }>;\n}\n\n/** Where a load goes next: a station or the start of another conveyor. */\nexport type Target = Conveyor | Station;\n\n/**\n * A length of conveyor and the devices on it, such as a zone, a gravity section or a project's\n * own diverter. `motion` turns the devices' outputs into load movement; `sense` feeds what the\n * part's sensors see to its devices' inputs and to PLC inputs wired straight to sensors.\n */\nexport interface CustomPart<Parts extends Devices = Devices> {\n readonly kind: \"part\";\n readonly id?: string;\n /** What it is, for the view, such as \"zone\" or \"diverter\". */\n readonly label?: string;\n /** Millimetres along the conveyor. */\n readonly length: number;\n /** Lateral travel before a load leaves by a side exit. */\n readonly width?: number;\n readonly exits?: { readonly left?: Target; readonly right?: Target };\n readonly devices: Parts;\n motion(devices: Parts): Motion;\n sense?(sensing: Sensing, devices: Parts): ReadonlyArray<readonly [Wire<boolean>, boolean]> | void;\n}\nexport const part = <Parts extends Devices>(\n options: Omit<CustomPart<Parts>, \"kind\">,\n): CustomPart<Parts> => ({ kind: \"part\", ...options });\n\n/** A driven length whose controller watches a photoeye, by default 50 mm before its end. */\nexport const zone = (options: {\n id?: string;\n length: number;\n controller: ZoneController;\n eye?: number;\n}) =>\n part({\n ...(options.id !== undefined && { id: options.id }),\n label: \"zone\",\n length: options.length,\n devices: { controller: options.controller },\n motion: ({ controller }) => ({ along: controller.world.out.motor }),\n sense: ({ blocked }, { controller }) => {\n controller.world.in.photoeye = blocked(options.eye ?? options.length - 50);\n },\n });\n\n/** A length with a motor, or a fixed speed such as a gravity section (0 = passive). */\nexport const section = (options: {\n id?: string;\n length: number;\n motor?: MotorDevice;\n speed?: number;\n}) =>\n part({\n ...(options.id !== undefined && { id: options.id }),\n label: \"section\",\n length: options.length,\n devices: options.motor ? { motor: options.motor } : {},\n motion: () => ({ along: options.motor?.world.out.motor ?? options.speed ?? 0 }),\n });\n\n/** A photoeye beam across the conveyor, wired straight to a PLC input. */\nexport interface Photoeye {\n readonly kind: \"photoeye\";\n readonly id?: string;\n /** Millimetres from where it appears in the parts list; negative places it inside the part before. */\n readonly offset?: number;\n readonly output: Wire<boolean>;\n}\n/** A barcode reader that reads each load's code as its leading edge passes. */\nexport interface Reader {\n readonly kind: \"reader\";\n readonly id?: string;\n readonly offset?: number;\n readonly device: ReaderDevice;\n}\n/** Where an operator takes loads off the line into a station, as they reach it. */\nexport interface Pickup {\n readonly kind: \"pickup\";\n readonly id?: string;\n readonly offset?: number;\n readonly station: Station;\n /** Which side of the conveyor the station is drawn on, by default the right. */\n readonly side?: \"left\" | \"right\";\n}\nexport const pickup = (options: Omit<Pickup, \"kind\">): Pickup => ({ kind: \"pickup\", ...options });\n\nexport type Part = CustomPart | Photoeye | Reader | Pickup;\n\nexport const photoeye = (options: Omit<Photoeye, \"kind\">): Photoeye => ({\n kind: \"photoeye\",\n ...options,\n});\nexport const reader = (options: Omit<Reader, \"kind\">): Reader => ({ kind: \"reader\", ...options });\n\nexport type Heading = \"right\" | \"left\" | \"up\" | \"down\";\n\nexport interface Conveyor {\n /**\n * Where it starts on the floor, in millimetres with y growing downward. Omit it to start where\n * another conveyor discharges into this one or feeds it from a side exit.\n */\n readonly at?: { readonly x: number; readonly y: number };\n /** Its direction from `at`, by default right. One fed from another conveyor takes its direction from there. */\n readonly heading?: Heading;\n /** Corners, each `after` millimetres past the last; it runs on to the length of its parts. */\n readonly turns?: ReadonlyArray<{ readonly after: number; readonly to: Heading }>;\n readonly parts: readonly Part[];\n /**\n * Where the last part discharges: a conveyor's start or a station, or `{ to, at }` to merge\n * onto another conveyor at `at` mm from its start.\n */\n readonly next?: Target | { readonly to: Conveyor; readonly at: number };\n}\nexport const conveyor = (options: Conveyor): Conveyor => options;\n\nexport interface StationSensor {\n readonly output: Wire<boolean>;\n readonly active: (station: { readonly loads: number }) => boolean;\n}\n/** Where loads leave the line: an operator station, a collection point or an exit. */\nexport interface Station {\n /** Loads it holds before refusing more; unlimited when omitted. */\n readonly capacity?: number;\n readonly sensors?: readonly StationSensor[];\n readonly label?: string;\n}\nexport const station = (options: Station = {}): Station => options;\n\nexport interface LoadKind {\n /** Millimetres along the direction of travel. */\n readonly length: number;\n readonly width?: number;\n}\n\nexport interface Line {\n readonly conveyors: Readonly<Record<string, Conveyor>>;\n /** Devices not on a conveyor, such as pushbuttons and stack lights. */\n readonly devices?: Readonly<Record<string, AnyDevice>>;\n readonly stations?: Readonly<Record<string, Station>>;\n readonly loads: Readonly<Record<string, LoadKind>>;\n}\n\n/** The name each conveyor and station has in a line. */\nexport const targetNames = (line: Line): ReadonlyMap<Target, string> =>\n new Map<Target, string>([\n ...Object.entries(line.conveyors).map(([name, target]) => [target, name] as const),\n ...Object.entries(line.stations ?? {}).map(([name, target]) => [target, name] as const),\n ]);\n\n/** Checks every conveyor and station a part leads to is in the line, and returns the line unchanged. */\nexport const defineLine = <const Definition extends Line>(line: Definition): Definition => {\n const names = targetNames(line);\n for (const [id, item] of Object.entries(line.conveyors)) {\n const next = item.next && \"to\" in item.next ? item.next.to : item.next;\n const targets: Array<Target | undefined> = [next];\n for (const part of item.parts) {\n if (part.kind === \"part\") targets.push(part.exits?.left, part.exits?.right);\n if (part.kind === \"pickup\") targets.push(part.station);\n }\n for (const target of targets)\n if (target !== undefined && !names.has(target))\n throw new Error(`Conveyor ${id} leads to a conveyor or station missing from the line.`);\n }\n return line;\n};\n\n/** A record with one value per id, keeping the ids in its type. */\nexport const keyed = <Id extends string, Value>(\n ids: readonly Id[],\n make: (id: Id) => Value,\n): Record<Id, Value> =>\n // Object.fromEntries erases the keys; every id has just been given a value.\n Object.fromEntries(ids.map((id) => [id, make(id)])) as Record<Id, Value>;\n",
8
+ "import { setTimeout as delay } from \"node:timers/promises\";\nimport { type Device, type DeviceView, wirePath } from \"./hardware\";\nimport { drawLine } from \"./layout\";\nimport {\n type CustomPart,\n type Line,\n type Motion,\n type Part,\n type Station,\n type Target,\n targetNames,\n} from \"./line\";\n\n/** Reads and writes PLC variables by dotted path. */\nexport interface SimulationIo {\n read: (paths: readonly string[]) => Promise<ReadonlyMap<string, unknown>>;\n write: (values: ReadonlyMap<string, unknown>) => Promise<void>;\n}\n\nexport interface Load<Data> {\n readonly id: string;\n readonly kind: string;\n readonly length: number;\n readonly width: number;\n /** What a reader reports for this load, such as its barcode; relabelling changes it. */\n code: string;\n data: Data;\n}\n\n/** Where a load is: its leading edge along a conveyor, or held at a station. */\ntype Place =\n | {\n kind: \"conveyor\";\n conveyor: string;\n head: number;\n lateral: number;\n /** While its tail still hangs off the conveyor it came from, entered at `at`. */\n from?: { conveyor: string; at: number };\n }\n | { kind: \"station\"; station: string }\n | { kind: \"waiting\"; conveyor: string };\n\ninterface Placed<Data> {\n load: Load<Data>;\n place: Place;\n}\n\ninterface Segment {\n id: string;\n part: CustomPart;\n start: number;\n end: number;\n}\ninterface Point {\n id: string;\n part: Exclude<Part, CustomPart>;\n at: number;\n}\ninterface Track {\n id: string;\n draw: string;\n length: number;\n next?: { to: string; at: number };\n segments: Segment[];\n points: Point[];\n}\n\nexport interface SimulationState {\n timeMs: number;\n error: string | null;\n conveyors: Array<{\n id: string;\n draw: string;\n length: number;\n next?: string;\n parts: Array<{\n id: string;\n kind: Part[\"kind\"];\n label?: string;\n start: number;\n end: number;\n exits?: { left?: string; right?: string };\n station?: string;\n side?: \"left\" | \"right\";\n view: DeviceView;\n }>;\n }>;\n stations: Array<{ id: string; label: string; loads: number }>;\n loads: Array<{\n id: string;\n kind: string;\n code: string;\n length: number;\n width: number;\n place: Place;\n }>;\n devices: Array<{\n id: string;\n /** The part it belongs to; null for devices off the conveyors, such as pushbuttons. */\n part: string | null;\n model: string;\n view: DeviceView;\n pins: Record<string, unknown>;\n actions: readonly string[];\n }>;\n}\n\nconst same = (a: unknown, b: unknown) =>\n Array.isArray(a) && Array.isArray(b)\n ? a.length === b.length && a.every((item, index) => item === b[index])\n : a === b;\n\nconst resolve = (\n id: string,\n conveyor: Line[\"conveyors\"][string],\n draw: string,\n name: (target: Target) => string,\n): Track => {\n const segments: Segment[] = [];\n const points: Point[] = [];\n let position = 0;\n conveyor.parts.forEach((part, index) => {\n const partId = part.id ?? `${id}[${index}]`;\n if (part.kind !== \"part\") {\n points.push({ id: partId, part, at: position + (part.offset ?? 0) });\n return;\n }\n segments.push({ id: partId, part, start: position, end: position + part.length });\n position += part.length;\n });\n return {\n id,\n draw,\n length: position,\n segments,\n points,\n ...(conveyor.next !== undefined && {\n next:\n \"to\" in conveyor.next\n ? { to: name(conveyor.next.to), at: conveyor.next.at }\n : { to: name(conveyor.next), at: 0 },\n }),\n };\n};\n\nconst devicesOf = (segment: Segment): Array<[string, Device]> =>\n Object.entries(segment.part.devices).map(([name, device]) => [`${segment.id}.${name}`, device]);\n\nconst along = (motion: Motion) => (\"along\" in motion ? Math.max(0, motion.along) : 0);\n\n/**\n * Runs a line against a PLC. Each tick reads the PLC outputs the devices are wired to,\n * senses the line, updates every device, writes the changed PLC inputs, then moves loads.\n */\nexport const createSimulation = <Data = unknown, Definition extends Line = Line>(\n line: Definition,\n options: { io: SimulationIo },\n) => {\n type ConveyorName = keyof Definition[\"conveyors\"] & string;\n type StationName = keyof NonNullable<Definition[\"stations\"]> & string;\n const names = targetNames(line);\n const name = (target: Target) => {\n const found = names.get(target);\n if (found === undefined) throw new Error(\"A part leads outside the line.\");\n return found;\n };\n const drawings = drawLine(line);\n const tracks = new Map(\n Object.entries(line.conveyors).map(([id, item]) => [\n id,\n resolve(id, item, drawings.get(item)!, name),\n ]),\n );\n const stations = new Map<string, Station>(Object.entries(line.stations ?? {}));\n const devices = new Map<string, Device>();\n const lineDevices = new Set(Object.keys(line.devices ?? {}));\n for (const [id, device] of Object.entries(line.devices ?? {})) devices.set(id, device);\n for (const track of tracks.values()) {\n for (const segment of track.segments)\n for (const [id, device] of devicesOf(segment)) devices.set(id, device);\n for (const point of track.points)\n if (point.part.kind === \"reader\") devices.set(point.id, point.part.device);\n }\n const reads = [...new Set([...devices.values()].flatMap((device) => device.reads))];\n const placed = new Map<string, Placed<Data>>();\n const written = new Map<string, unknown>();\n const pendingReads = new Map<string, string[]>();\n let timeMs = 0,\n sequence = 0,\n error: string | null = null;\n\n const onConveyor = (conveyor: string) =>\n [...placed.values()]\n .flatMap(({ load, place }) =>\n place.kind === \"conveyor\" && place.conveyor === conveyor ? [{ load, place }] : [],\n )\n .sort((a, b) => b.place.head - a.place.head);\n /** Loads whose footprint covers part of this conveyor, with their head in its coordinates. */\n const footprints = (track: Track) => [\n ...onConveyor(track.id).map(({ load, place }) => ({ load, head: place.head, place })),\n ...(track.next && tracks.has(track.next.to)\n ? onConveyor(track.next.to).flatMap(({ load, place }) =>\n place.from?.conveyor === track.id && place.head - place.from.at < load.length\n ? [{ load, head: track.length + place.head - place.from.at, place }]\n : [],\n )\n : []),\n ];\n const covers = (head: number, length: number, at: number) => head - length <= at && at <= head;\n const stationLoads = (id: string) =>\n [...placed.values()].filter(({ place }) => place.kind === \"station\" && place.station === id)\n .length;\n\n const sense = () => {\n const writes = new Map<string, unknown>();\n for (const track of tracks.values()) {\n const loads = footprints(track);\n const blocked = (at: number) =>\n loads.some(\n ({ load, head, place }) =>\n covers(head, load.length, at) && Math.abs(place.lateral) < load.width / 2,\n );\n for (const segment of track.segments) {\n const onPart = loads\n .filter(({ load, head }) => head > segment.start && head - load.length < segment.end)\n .map(({ load, head, place }) => ({\n head: head - segment.start,\n tail: head - load.length - segment.start,\n lateral: place.lateral,\n }));\n const sensed = segment.part.sense?.(\n {\n blocked: (at) => blocked(segment.start + at),\n occupied: onPart.length > 0,\n loads: onPart,\n },\n segment.part.devices,\n );\n for (const [wire, value] of sensed ?? []) writes.set(wirePath(wire), value);\n }\n for (const point of track.points) {\n if (point.part.kind === \"photoeye\")\n writes.set(wirePath(point.part.output), blocked(point.at));\n else if (point.part.kind === \"reader\") {\n const queue = pendingReads.get(point.id) ?? [];\n point.part.device.world.in.read = queue.shift() ?? null;\n pendingReads.set(point.id, queue);\n }\n }\n }\n for (const [id, item] of stations)\n for (const sensor of item.sensors ?? [])\n writes.set(wirePath(sensor.output), sensor.active({ loads: stationLoads(id) }));\n return writes;\n };\n\n /** Puts a load onto a conveyor with its head at `head`, or into a station, if there is room. */\n const enter = (\n target: string,\n load: Load<Data>,\n head: number,\n from?: { conveyor: string; at: number },\n ) => {\n const track = tracks.get(target);\n if (track) {\n const blocked = onConveyor(target).some(\n (other) =>\n other.load !== load &&\n other.place.head > head - load.length &&\n other.place.head - other.load.length < head,\n );\n if (blocked) return false;\n placed.set(load.id, {\n load,\n place: {\n kind: \"conveyor\",\n conveyor: target,\n head,\n lateral: 0,\n ...(from !== undefined && { from }),\n },\n });\n return true;\n }\n const capacity = stations.get(target)?.capacity;\n if (capacity !== undefined && stationLoads(target) >= capacity) return false;\n placed.set(load.id, { load, place: { kind: \"station\", station: target } });\n return true;\n };\n\n const move = (dtMs: number) => {\n for (const track of tracks.values()) {\n const motions = new Map(\n track.segments.map((segment) => [segment, segment.part.motion(segment.part.devices)]),\n );\n let limit = Number.POSITIVE_INFINITY;\n for (const { load, place } of onConveyor(track.id)) {\n if (place.kind !== \"conveyor\") continue;\n const tail = place.head - load.length;\n // A part moving loads across, such as lifting belts, carries a load whose middle is on it.\n const middle = place.head - load.length / 2;\n const across = track.segments.find(\n (segment) => segment.start <= middle && middle <= segment.end,\n );\n const motion = across && motions.get(across);\n if (across && motion && \"across\" in motion) {\n place.lateral += (motion.across * dtMs) / 1000;\n const width = across.part.width ?? 0;\n const exit = across.part.exits?.[place.lateral > 0 ? \"right\" : \"left\"];\n // A missing or full exit holds the load at the part's edge.\n if (\n Math.abs(place.lateral) >= width &&\n (exit === undefined || !enter(name(exit), load, load.length))\n )\n place.lateral = Math.sign(place.lateral) * width;\n limit = tail;\n continue;\n }\n if (place.lateral !== 0) {\n limit = tail;\n continue;\n }\n const covered = track.segments.filter(\n ({ start, end }) => start < place.head && end > Math.max(tail, 0),\n );\n const speed = covered.length\n ? Math.min(...covered.map((segment) => along(motions.get(segment) ?? { held: true })))\n : 0;\n let target = Math.min(place.head + (speed * dtMs) / 1000, limit);\n let picked = false;\n for (const point of track.points) {\n if (target < point.at) continue;\n // An operator takes it off here; it waits on the point while the station is full.\n if (point.part.kind === \"pickup\") {\n if (picked || place.head > point.at) continue;\n picked = enter(name(point.part.station), load, 0);\n if (!picked) target = point.at;\n } else if (point.part.kind === \"reader\" && place.head < point.at)\n pendingReads.set(point.id, [...(pendingReads.get(point.id) ?? []), load.code]);\n }\n if (picked) continue;\n const next = track.next;\n if (target > track.length && next !== undefined) {\n const entered = enter(next.to, load, next.at + target - track.length, {\n conveyor: track.id,\n at: next.at,\n });\n if (!entered) place.head = track.length;\n } else place.head = Math.min(target, track.length);\n limit = place.head - load.length;\n }\n }\n for (const { load, place } of placed.values())\n if (place.kind === \"waiting\") enter(place.conveyor, load, load.length);\n };\n\n const pointView = (point: Point): DeviceView => {\n switch (point.part.kind) {\n case \"reader\":\n return point.part.device.view();\n case \"photoeye\":\n return { status: written.get(wirePath(point.part.output)) ? \"busy\" : \"idle\" };\n case \"pickup\":\n return {\n status: stationLoads(name(point.part.station)) ? \"busy\" : \"idle\",\n label: name(point.part.station),\n };\n }\n };\n\n const segmentView = (segment: Segment): DeviceView => {\n const views = devicesOf(segment).map(([, device]) => device.view());\n if (!views.length)\n return {\n status: along(segment.part.motion(segment.part.devices)) > 0 ? \"running\" : \"stopped\",\n };\n const order = [\"fault\", \"warning\", \"busy\", \"running\", \"stopped\", \"idle\"] as const;\n return views.reduce((worst, view) =>\n order.indexOf(view.status) < order.indexOf(worst.status) ? view : worst,\n );\n };\n\n const simulation = {\n /** Advances the line by `dtMs`, exchanging I/O with the PLC once. */\n tick: async (dtMs: number) => {\n const plc = await options.io.read(reads);\n const writes = sense();\n for (const device of devices.values())\n for (const [path, value] of device.update(plc, dtMs)) writes.set(path, value);\n const changed = new Map(\n [...writes].filter(([path, value]) => !same(written.get(path), value)),\n );\n if (changed.size) {\n await options.io.write(changed);\n for (const [path, value] of changed) written.set(path, value);\n }\n move(dtMs);\n timeMs += dtMs;\n },\n /** Ticks in real time until aborted, skipping ticks while `paused()`; failed ticks are reported in `state().error`. */\n run: async (\n signal: AbortSignal,\n options: { periodMs?: number; paused?: () => boolean } = {},\n ) => {\n const periodMs = options.periodMs ?? 50;\n let last = performance.now();\n while (!signal.aborted) {\n const now = performance.now();\n if (!options.paused?.()) await simulation.step(Math.min(now - last, 1000));\n last = now;\n await delay(Math.max(0, periodMs - (performance.now() - now)), undefined, {\n signal,\n }).catch(() => undefined);\n }\n },\n /** One tick that records a failure in `state().error` instead of throwing, then rewrites every input. */\n step: async (dtMs: number) => {\n try {\n await simulation.tick(dtMs);\n error = null;\n } catch (cause) {\n error = cause instanceof Error ? cause.message : String(cause);\n // The PLC may have restarted or missed writes, so send every input again.\n written.clear();\n }\n },\n /** Queues a load at a conveyor's start; it enters once there is room. */\n addLoad: (\n conveyor: ConveyorName,\n load: { kind: keyof Definition[\"loads\"] & string; code: string; data: Data },\n ) => {\n const kind = line.loads[load.kind];\n if (!kind) throw new Error(`Unknown load kind ${load.kind}.`);\n if (!tracks.has(conveyor)) throw new Error(`Unknown conveyor ${conveyor}.`);\n const created: Load<Data> = {\n id: `L${++sequence}`,\n length: kind.length,\n width: kind.width ?? kind.length,\n ...load,\n };\n placed.set(created.id, { load: created, place: { kind: \"waiting\", conveyor } });\n return created;\n },\n /**\n * Moves a load as an operator would: into a station, onto a conveyor's start once there is\n * room, or onto a conveyor with its tail at `at` mm (failing if that spot is taken).\n */\n place: (id: string, target: ConveyorName | StationName, at?: number) => {\n const entry = placed.get(id);\n if (!entry) throw new Error(`Unknown load ${id}.`);\n if (tracks.has(target) && at === undefined)\n entry.place = { kind: \"waiting\", conveyor: target };\n else if (!enter(target, entry.load, (at ?? 0) + entry.load.length))\n throw new Error(`No room for ${entry.load.code} at ${target}.`);\n },\n remove: (id: string) => {\n placed.delete(id);\n },\n loads: () => [...placed.values()].map(({ load, place }) => ({ load, place })),\n loadsAt: (station: StationName) =>\n [...placed.values()]\n .filter(({ place }) => place.kind === \"station\" && place.station === station)\n .map(({ load }) => load),\n act: (device: string, action: string, argument = \"\") => {\n const target = devices.get(device);\n if (!target) throw new Error(`Unknown device ${device}.`);\n target.act(action, argument);\n },\n state: (): SimulationState => ({\n timeMs,\n error,\n conveyors: [...tracks.values()].map((track) => ({\n id: track.id,\n draw: track.draw,\n length: track.length,\n ...(track.next !== undefined && { next: track.next.to }),\n parts: [\n ...track.segments.map((segment) => ({\n id: segment.id,\n kind: segment.part.kind,\n ...(segment.part.label !== undefined && { label: segment.part.label }),\n start: segment.start,\n end: segment.end,\n ...(segment.part.exits && {\n exits: {\n ...(segment.part.exits.left && { left: name(segment.part.exits.left) }),\n ...(segment.part.exits.right && { right: name(segment.part.exits.right) }),\n },\n }),\n view: segmentView(segment),\n })),\n ...track.points.map((point) => ({\n id: point.id,\n kind: point.part.kind,\n start: point.at,\n end: point.at,\n ...(point.part.kind === \"pickup\" && {\n station: name(point.part.station),\n side: point.part.side ?? \"right\",\n }),\n view: pointView(point),\n })),\n ],\n })),\n stations: [...stations].map(([id, item]) => ({\n id,\n label: item.label ?? id,\n loads: stationLoads(id),\n })),\n loads: [...placed.values()].map(({ load, place }) => ({\n id: load.id,\n kind: load.kind,\n code: load.code,\n length: load.length,\n width: load.width,\n place: { ...place },\n })),\n devices: [...devices].map(([id, device]) => ({\n id,\n part: lineDevices.has(id) ? null : id.split(\".\")[0]!,\n model: device.model,\n view: device.view(),\n pins: device.pins(),\n actions: device.actions,\n })),\n }),\n };\n return simulation;\n};\n\nexport type Simulation<Data = unknown, Definition extends Line = Line> = ReturnType<\n typeof createSimulation<Data, Definition>\n>;\n",
9
+ "import type { Conveyor, Heading, Line, Target } from \"./line\";\n\ninterface Point {\n x: number;\n y: number;\n}\ninterface Route {\n /** Corners in millimetres, from start to end. */\n points: Point[];\n headings: Heading[];\n}\n\n/** The view's fixed drawing scale, px per mm, shared by every conveyor so loads keep their proportions. */\nconst scale = 0.05;\n/** How far past a side exit a conveyor it feeds starts, leaving room to draw the transfer. */\nconst exitClearance = 1000;\n\nconst direction: Record<Heading, Point> = {\n right: { x: 1, y: 0 },\n left: { x: -1, y: 0 },\n up: { x: 0, y: -1 },\n down: { x: 0, y: 1 },\n};\nconst leftOf: Record<Heading, Heading> = { right: \"up\", up: \"left\", left: \"down\", down: \"right\" };\nconst rightOf: Record<Heading, Heading> = { right: \"down\", down: \"left\", left: \"up\", up: \"right\" };\n\nconst lengthOf = (conveyor: Conveyor) =>\n conveyor.parts.reduce((sum, part) => sum + (part.kind === \"part\" ? part.length : 0), 0);\n\nconst travel = (from: Point, heading: Heading, distance: number): Point => ({\n x: from.x + direction[heading].x * distance,\n y: from.y + direction[heading].y * distance,\n});\n\nconst pointAt = (route: Route, mm: number) => {\n let remaining = mm;\n for (const [index, heading] of route.headings.entries()) {\n const from = route.points[index]!,\n to = route.points[index + 1]!,\n length = Math.abs(to.x - from.x) + Math.abs(to.y - from.y);\n if (remaining <= length || index === route.headings.length - 1)\n return { point: travel(from, heading, remaining), heading };\n remaining -= length;\n }\n throw new Error(\"A conveyor needs a route.\");\n};\n\n/**\n * Lays a line out in millimetres and returns each conveyor's drawing. A conveyor's length comes\n * from its parts. One with no `at` starts where another discharges into it or feeds it sideways.\n */\nexport const drawLine = (line: Line): ReadonlyMap<Conveyor, string> => {\n const conveyors = Object.values(line.conveyors);\n const isConveyor = new Set<Target>(conveyors);\n const feeders = new Map<Target, { from: Conveyor; at: number; side?: \"left\" | \"right\" }>();\n for (const conveyor of conveyors) {\n let position = 0;\n for (const part of conveyor.parts) {\n if (part.kind !== \"part\") continue;\n for (const side of [\"left\", \"right\"] as const) {\n const exit = part.exits?.[side];\n if (exit && isConveyor.has(exit))\n feeders.set(exit, { from: conveyor, at: position + part.length / 2, side });\n }\n position += part.length;\n }\n const next = conveyor.next;\n if (next && !(\"to\" in next) && isConveyor.has(next))\n feeders.set(next, { from: conveyor, at: position });\n }\n\n const routes = new Map<Conveyor, Route>();\n const active = new Set<Conveyor>();\n const route = (conveyor: Conveyor): Route => {\n const known = routes.get(conveyor);\n if (known) return known;\n if (active.has(conveyor)) throw new Error(\"Conveyors with no `at` cannot feed each other.\");\n active.add(conveyor);\n let start: Point = conveyor.at ?? { x: 0, y: 0 };\n let heading: Heading = conveyor.heading ?? \"right\";\n const feeder = conveyor.at ? undefined : feeders.get(conveyor);\n if (feeder) {\n const exit = pointAt(route(feeder.from), feeder.at);\n heading = !feeder.side\n ? exit.heading\n : feeder.side === \"left\"\n ? leftOf[exit.heading]\n : rightOf[exit.heading];\n start = feeder.side ? travel(exit.point, heading, exitClearance) : exit.point;\n }\n const result: Route = { points: [start], headings: [] };\n let remaining = lengthOf(conveyor);\n for (const turn of conveyor.turns ?? []) {\n if (turn.after <= 0 || turn.after >= remaining)\n throw new Error(`A turn ${turn.after} mm along must fall inside its conveyor.`);\n result.headings.push(heading);\n result.points.push(travel(result.points.at(-1)!, heading, turn.after));\n remaining -= turn.after;\n heading = turn.to;\n }\n result.headings.push(heading);\n result.points.push(travel(result.points.at(-1)!, heading, remaining));\n active.delete(conveyor);\n routes.set(conveyor, result);\n return result;\n };\n\n const pixels = (mm: number) => Math.round(mm * scale * 100) / 100;\n return new Map(\n conveyors.map((conveyor) => [\n conveyor,\n route(conveyor)\n .points.map(({ x, y }, index) => `${index ? \"L\" : \"M\"} ${pixels(x)} ${pixels(y)}`)\n .join(\" \"),\n ]),\n );\n};\n",
10
+ "import type { SimulationIo } from \"./simulation\";\n\n/** The PLC client calls the simulation needs, as provided by `createPlc` from `@tsplc/opcua`. */\ninterface PlcClient {\n read: (\n paths: Iterable<string>,\n ) => Promise<Record<string, { quality: string; status: string; value: unknown }>>;\n write: (values: ReadonlyMap<string, unknown>) => Promise<unknown>;\n}\n\n/** Exchanges the simulation's I/O over a `createPlc` session; unreadable inputs fail the tick. */\nexport const plcIo = (plc: PlcClient): SimulationIo => ({\n read: async (paths) => {\n const readings = await plc.read(paths);\n return new Map(\n paths.map((path) => {\n const reading = readings[path];\n if (reading?.quality !== \"good\")\n throw new Error(`Cannot read ${path}: ${reading?.status ?? \"missing\"}`);\n return [path, reading.value];\n }),\n );\n },\n write: async (values) => {\n await plc.write(values);\n },\n});\n"
11
+ ],
12
+ "mappings": "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",
13
+ "debugId": "68AEBE1381BBE02964756E2164756E21",
14
+ "names": []
15
+ }
package/dist/serve.js ADDED
@@ -0,0 +1,292 @@
1
+ // packages/sim/src/serve.ts
2
+ import { z } from "zod";
3
+
4
+ // packages/sim/src/page.ts
5
+ var escape = (value) => value.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
6
+ var page = (options) => `<!doctype html>
7
+ <html><head><meta charset="utf-8"><title>${escape(options.title)}</title>
8
+ <style>
9
+ :root{--bg:#f6f7f9;--panel:#fff;--fg:#1c2026;--muted:#6b7280;--border:#d8dce3;--track:#c9ced8;
10
+ --running:#16a34a;--busy:#2563eb;--stopped:#9aa3b2;--idle:#c3c9d4;--warning:#d97706;--fault:#dc2626;--load:#7c3aed}
11
+ @media (prefers-color-scheme:dark){:root{--bg:#111318;--panel:#191c22;--fg:#e6e8ec;--muted:#9aa1ad;
12
+ --border:#2c313a;--track:#3a404b;--stopped:#6b7280;--idle:#4b5260;--load:#a78bfa}}
13
+ *{box-sizing:border-box}body{margin:0;font:13px system-ui,sans-serif;background:var(--bg);color:var(--fg);
14
+ display:grid;grid-template-rows:auto 1fr;height:100vh}
15
+ header{display:flex;gap:10px;align-items:center;padding:8px 14px;border-bottom:1px solid var(--border);background:var(--panel)}
16
+ header h1{font-size:14px;margin:0 8px 0 0}#clock{font-family:ui-monospace,monospace;color:var(--muted)}
17
+ #error{color:var(--fault)}button{font:inherit;border:1px solid var(--border);background:var(--bg);color:var(--fg);
18
+ border-radius:6px;padding:3px 9px;cursor:pointer}button:hover{border-color:var(--muted)}
19
+ main{display:grid;grid-template-columns:1fr 340px;min-height:0}svg{width:100%;height:100%}
20
+ aside{border-left:1px solid var(--border);background:var(--panel);overflow:auto;padding:12px}
21
+ h2{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:14px 0 6px}
22
+ .device{border:1px solid var(--border);border-radius:8px;padding:8px;margin-bottom:8px}
23
+ .device b{display:block;margin-bottom:4px}table{width:100%;border-collapse:collapse;font-size:12px}
24
+ td{padding:2px 0;border-bottom:1px dashed var(--border)}td:last-child{text-align:right;font-family:ui-monospace,monospace}
25
+ .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
26
+ .actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
27
+ .control{display:flex;align-items:center;gap:6px;padding:3px 0;border-bottom:1px dashed var(--border)}
28
+ .control b{flex:1;font-weight:500}.control span{color:var(--muted)}.control button{padding:1px 7px}.part{cursor:pointer}
29
+ #toast{position:fixed;bottom:14px;left:14px;background:var(--fault);color:#fff;padding:6px 10px;border-radius:6px;display:none}
30
+ ${options.css}
31
+ </style></head><body>
32
+ <header><h1>${escape(options.title)}</h1><span id="clock"></span>
33
+ <button id="pause">Pause</button><button id="step" disabled>Step</button><span id="error"></span></header>
34
+ <main><svg id="line"><g id="tracks"></g><g id="parts"></g><g id="stations"></g><g id="loads"></g></svg>
35
+ <aside><div id="inspector"><p style="color:var(--muted)">Select a part of the line to inspect it.</p></div>
36
+ <h2>Actions</h2><div id="actions"></div><h2>Controls</h2><div id="controls"></div></aside></main><div id="toast"></div>
37
+ <script>
38
+ const NS = "http://www.w3.org/2000/svg", TRACK = 16;
39
+ const $ = (id) => document.getElementById(id);
40
+ const el = (name, attrs = {}, parent) => { const node = document.createElementNS(NS, name);
41
+ for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v); parent?.appendChild(node); return node; };
42
+ const html = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
43
+ let state, selected = null, selectedLoad = null, built = "", geometry = new Map(), stationAt = new Map();
44
+
45
+ const point = (id, mm) => {
46
+ const g = geometry.get(id), d = Math.max(0, Math.min(mm, g.length)) * g.scale;
47
+ const p = g.path.getPointAtLength(d), a = g.path.getPointAtLength(Math.max(0, d - 1)),
48
+ b = g.path.getPointAtLength(Math.min(g.total, d + 1)), angle = Math.atan2(b.y - a.y, b.x - a.x);
49
+ return { x: p.x, y: p.y, angle, nx: -Math.sin(angle), ny: Math.cos(angle) };
50
+ };
51
+ const color = (status) => "var(--" + status + ")";
52
+
53
+ const build = () => {
54
+ for (const id of ["tracks", "parts", "stations"]) $(id).replaceChildren();
55
+ geometry = new Map(); stationAt = new Map();
56
+ for (const c of state.conveyors) {
57
+ const path = el("path", { d: c.draw, fill: "none", stroke: "var(--track)", "stroke-width": TRACK, "stroke-linecap": "round" }, $("tracks"));
58
+ const total = path.getTotalLength();
59
+ geometry.set(c.id, { path, total, length: c.length, scale: total / c.length });
60
+ }
61
+ for (const c of state.conveyors) {
62
+ const g = geometry.get(c.id);
63
+ for (const part of c.parts) {
64
+ if (part.start === part.end) {
65
+ const p = point(c.id, part.start), size = part.kind === "reader" ? 7 : 5, sign = part.side === "left" ? -1 : 1;
66
+ if (part.station && !stationAt.has(part.station)) stationAt.set(part.station, { x: p.x + sign * p.nx * 45, y: p.y + sign * p.ny * 45 });
67
+ const node = part.kind === "reader"
68
+ ? el("rect", { x: p.x + p.nx * (TRACK / 2 + 6) - size / 2, y: p.y + p.ny * (TRACK / 2 + 6) - size / 2, width: size, height: size }, $("parts"))
69
+ : el("circle", { cx: p.x + sign * p.nx * (TRACK / 2 + 5), cy: p.y + sign * p.ny * (TRACK / 2 + 5), r: size / 2 + 1 }, $("parts"));
70
+ node.dataset.part = part.id; node.classList.add("part");
71
+ continue;
72
+ }
73
+ const seg = el("path", { d: c.draw, fill: "none", "stroke-width": TRACK - 6,
74
+ "stroke-dasharray": "0 " + part.start * g.scale + " " + (part.end - part.start) * g.scale + " " + g.total }, $("parts"));
75
+ seg.dataset.part = part.id; seg.classList.add("part");
76
+ const b = point(c.id, part.start);
77
+ el("line", { x1: b.x + b.nx * TRACK / 2, y1: b.y + b.ny * TRACK / 2, x2: b.x - b.nx * TRACK / 2, y2: b.y - b.ny * TRACK / 2, stroke: "var(--bg)", "stroke-width": 1.5 }, $("tracks"));
78
+ for (const [side, sign] of [["left", -1], ["right", 1]]) {
79
+ const exit = part.exits?.[side]; if (!exit) continue;
80
+ const p = point(c.id, (part.start + part.end) / 2);
81
+ el("line", { x1: p.x, y1: p.y, x2: p.x + sign * p.nx * 46, y2: p.y + sign * p.ny * 46, stroke: "var(--track)", "stroke-width": 8 }, $("tracks"));
82
+ if (!stationAt.has(exit)) stationAt.set(exit, { x: p.x + sign * p.nx * 70, y: p.y + sign * p.ny * 70 });
83
+ }
84
+ }
85
+ }
86
+ for (const c of state.conveyors) {
87
+ const next = c.next ?? null;
88
+ if (next && !geometry.has(next) && !stationAt.has(next)) {
89
+ const p = point(c.id, c.length); stationAt.set(next, { x: p.x + Math.cos(p.angle) * 45, y: p.y + Math.sin(p.angle) * 45 });
90
+ }
91
+ }
92
+ for (const s of state.stations) {
93
+ const at = stationAt.get(s.id) ?? { x: 0, y: 0 };
94
+ const group = el("g", { transform: "translate(" + at.x + " " + at.y + ")" }, $("stations"));
95
+ group.dataset.part = s.id; group.classList.add("part");
96
+ el("rect", { x: -38, y: -14, width: 76, height: 28, rx: 5, fill: "var(--panel)", stroke: "var(--border)" }, group);
97
+ el("text", { "text-anchor": "middle", y: -1, "font-size": 9, fill: "var(--fg)" }, group).textContent = s.label;
98
+ el("text", { "text-anchor": "middle", y: 10, "font-size": 9, fill: "var(--muted)", "data-count": s.id }, group);
99
+ }
100
+ const box = $("line").getBBox(), pad = 60;
101
+ $("line").setAttribute("viewBox", [box.x - pad, box.y - pad, box.width + 2 * pad, box.height + 2 * pad].join(" "));
102
+ };
103
+
104
+ const render = () => {
105
+ const key = JSON.stringify(state.conveyors.map((c) => [c.id, c.draw, c.parts.map((p) => p.id)]));
106
+ if (key !== built) { built = key; build(); }
107
+ $("clock").textContent = (state.timeMs / 1000).toFixed(1) + " s";
108
+ $("error").textContent = state.error ?? "";
109
+ $("pause").textContent = state.paused ? "Resume" : "Pause"; $("step").disabled = !state.paused;
110
+ const views = new Map(state.conveyors.flatMap((c) => c.parts.map((p) => [p.id, p])));
111
+ for (const node of document.querySelectorAll("[data-part]")) {
112
+ const part = views.get(node.dataset.part); if (!part) continue;
113
+ const status = color(part.view.status);
114
+ node.setAttribute(node.tagName === "path" ? "stroke" : "fill", status);
115
+ node.setAttribute("stroke-opacity", selected === part.id ? 1 : 0.85);
116
+ }
117
+ for (const s of state.stations) {
118
+ const count = document.querySelector('[data-count="' + s.id + '"]'); if (count) count.textContent = s.loads + " held";
119
+ }
120
+ const loads = $("loads"); loads.replaceChildren();
121
+ const waiting = new Map();
122
+ for (const load of state.loads) {
123
+ if (load.place.kind === "waiting") { waiting.set(load.place.conveyor, (waiting.get(load.place.conveyor) ?? 0) + 1); continue; }
124
+ if (load.place.kind !== "conveyor") continue;
125
+ const g = geometry.get(load.place.conveyor), p = point(load.place.conveyor, load.place.head - load.length / 2);
126
+ const w = Math.max(load.length * g.scale, 4), h = Math.max(Math.min(load.width * g.scale, TRACK + 6), 4);
127
+ const x = p.x + p.nx * load.place.lateral * g.scale, y = p.y + p.ny * load.place.lateral * g.scale;
128
+ const r = el("rect", { x: -w / 2, y: -h / 2, width: w, height: h, rx: 2, fill: "var(--load)", opacity: .9,
129
+ transform: "translate(" + x + " " + y + ") rotate(" + p.angle * 180 / Math.PI + ")" }, loads);
130
+ r.dataset.load = load.id; r.classList.add("part");
131
+ if (selectedLoad === load.id) r.setAttribute("stroke", "var(--fg)");
132
+ el("title", {}, r).textContent = load.code + " (" + load.kind + ")";
133
+ }
134
+ for (const [conveyor, count] of waiting) {
135
+ const p = point(conveyor, 0);
136
+ el("text", { x: p.x - Math.cos(p.angle) * 30, y: p.y - Math.sin(p.angle) * 30, "font-size": 10, fill: "var(--muted)", "text-anchor": "middle" }, loads).textContent = "+" + count + " waiting";
137
+ }
138
+ inspect(); renderControls(); renderActions();
139
+ };
140
+
141
+ const removeButton = (id) => "<button data-remove='" + html(id) + "'>Remove</button>";
142
+ const inspectLoad = () => {
143
+ const load = state.loads.find((l) => l.id === selectedLoad);
144
+ if (!load) { $("inspector").innerHTML = "<p style=color:var(--muted)>The load has left the line.</p>"; return; }
145
+ const where = load.place.kind === "conveyor" ? load.place.conveyor + " at " + Math.round(load.place.head) + " mm"
146
+ : load.place.kind === "station" ? load.place.station : "waiting for " + load.place.conveyor;
147
+ $("inspector").innerHTML = "<h2>" + html(load.code) + " · " + html(load.kind) + "</h2><div>" + html(where) + "</div>" +
148
+ "<div class=actions>" + state.actions.filter((a) => a.at === load.id).map((a) => "<button data-run='" + html(a.id) + "'>" + html(a.label) + "</button>").join("") +
149
+ removeButton(load.id) + "</div>";
150
+ };
151
+ const inspect = () => {
152
+ if (selectedLoad) return inspectLoad();
153
+ if (!selected) return;
154
+ const part = state.conveyors.flatMap((c) => c.parts).find((p) => p.id === selected);
155
+ const devices = state.devices.filter((d) => d.part === selected);
156
+ const loads = state.loads.filter((l) => l.place.kind === "station" && l.place.station === selected);
157
+ const rows = (obj) => Object.entries(obj ?? {}).map(([k, v]) => "<tr><td>" + html(k) + "</td><td>" + html(Array.isArray(v) ? "[" + v.length + "]" : v) + "</td></tr>").join("");
158
+ $("inspector").innerHTML = "<h2>" + html(selected) + (part ? " · " + part.kind : "") + "</h2>" +
159
+ (part ? "<div><span class=dot style=background:" + color(part.view.status) + "></span>" + html(part.view.label ?? part.view.status) + "</div>" : "") +
160
+ (loads.length ? "<table>" + loads.map((l) => "<tr><td>" + html(l.code) + " · " + html(l.kind) + "</td><td>" + removeButton(l.id) + "</td></tr>").join("") + "</table>" : "") +
161
+ "<div class=actions>" + state.actions.filter((a) => a.at === selected).map((a) => "<button data-run='" + html(a.id) + "'>" + html(a.label) + "</button>").join("") + "</div>" +
162
+ devices.map((d) => "<div class=device><b><span class=dot style=background:" + color(d.view.status) + "></span>" +
163
+ html(d.id.slice(selected.length + 1) || d.model) + " · " + html(d.model) + "</b>" +
164
+ (d.view.label ? "<div style=color:var(--muted)>" + html(d.view.label) + "</div>" : "") +
165
+ "<table>" + rows(d.view.details) + rows(d.pins) + "</table><div class=actions>" +
166
+ d.actions.map((a) => "<button data-device='" + html(d.id) + "' data-action='" + html(a) + "'>" + html(a) + "</button>").join("") +
167
+ "</div></div>").join("");
168
+ };
169
+
170
+ const renderControls = () => {
171
+ const controls = state.devices.filter((d) => d.part === null);
172
+ $("controls").innerHTML = controls.length
173
+ ? controls.map((d) => "<div class=control><i class=dot style=background:" + color(d.view.status) + "></i><b>" + html(d.id) + "</b><span>" +
174
+ html(d.view.label ?? d.view.status) + "</span>" + d.actions.map((a) => "<button data-device='" + html(d.id) + "' data-action='" + html(a) + "'>" + html(a) + "</button>").join("") + "</div>").join("")
175
+ : "<p style=color:var(--muted)>None.</p>";
176
+ };
177
+ const renderActions = () => {
178
+ const groups = new Map();
179
+ for (const a of state.actions) groups.set(a.at ?? "", [...(groups.get(a.at ?? "") ?? []), a]);
180
+ $("actions").innerHTML = groups.size
181
+ ? [...groups].map(([at, actions]) => (at ? "<div style='margin-top:8px;color:var(--muted)'>" + html(at) + "</div>" : "") +
182
+ "<div class=actions>" + actions.map((a) => "<button data-run='" + html(a.id) + "'>" + html(a.label) + "</button>").join("") + "</div>").join("")
183
+ : "<p style=color:var(--muted)>None available.</p>";
184
+ };
185
+
186
+ const post = async (url, body) => {
187
+ const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body ?? {}) });
188
+ if (!response.ok) { const toast = $("toast"); toast.textContent = (await response.json()).message ?? "Failed"; toast.style.display = "block"; setTimeout(() => toast.style.display = "none", 3000); }
189
+ await refresh();
190
+ };
191
+ document.addEventListener("click", (event) => {
192
+ const target = event.target.closest("[data-part],[data-device],[data-run],[data-load],[data-remove]");
193
+ if (!target) return;
194
+ if (target.dataset.run) return post("/actions/" + encodeURIComponent(target.dataset.run));
195
+ if (target.dataset.remove) return post("/loads/remove", { id: target.dataset.remove });
196
+ if (target.dataset.load) { selectedLoad = target.dataset.load; selected = null; return render(); }
197
+ if (target.dataset.device) {
198
+ const argument = target.dataset.action === "read" ? prompt("Code to read (empty for a no-read)") : "";
199
+ if (argument !== null) post("/devices/act", { device: target.dataset.device, action: target.dataset.action, argument });
200
+ return;
201
+ }
202
+ selected = target.dataset.part; selectedLoad = null; render();
203
+ });
204
+ $("pause").onclick = () => post("/control", { pause: !state.paused });
205
+ $("step").onclick = () => post("/control", { step: true });
206
+ const refresh = async () => { state = await (await fetch("/state")).json(); render(); };
207
+ const loop = async () => { try { await refresh(); } catch {} setTimeout(loop, 200); };
208
+ loop();
209
+ </script></body></html>`;
210
+
211
+ // packages/sim/src/serve.ts
212
+ var controlSchema = z.object({ pause: z.boolean().optional(), step: z.boolean().optional() });
213
+ var deviceActionSchema = z.object({
214
+ device: z.string(),
215
+ action: z.string(),
216
+ argument: z.string().default("")
217
+ });
218
+ var loadSchema = z.object({ id: z.string() });
219
+ var json = (body, status = 200) => Response.json(body, { status, headers: { "Cache-Control": "no-store" } });
220
+ var serveSimulation = (simulation, options) => {
221
+ const controller = new AbortController;
222
+ const periodMs = options.periodMs ?? 50;
223
+ let paused = false;
224
+ const actions = () => options.actions?.() ?? [];
225
+ const attempt = async (run) => {
226
+ try {
227
+ await run();
228
+ return json({ ok: true });
229
+ } catch (error) {
230
+ options.log?.warn("Simulation action failed", error);
231
+ return json({ message: error instanceof Error ? error.message : String(error) }, 409);
232
+ }
233
+ };
234
+ const server = Bun.serve({
235
+ hostname: options.hostname,
236
+ port: options.port,
237
+ routes: {
238
+ "/": () => new Response(page({ title: options.title ?? "Simulation", css: options.css ?? "" }), {
239
+ headers: { "Content-Type": "text/html; charset=utf-8" }
240
+ }),
241
+ "/state": () => json({
242
+ ...simulation.state(),
243
+ paused,
244
+ actions: actions().map(({ id, label, at }) => ({ id, label, at }))
245
+ }),
246
+ "/control": {
247
+ POST: async (request) => {
248
+ const command = controlSchema.parse(await request.json());
249
+ if (command.pause !== undefined)
250
+ paused = command.pause;
251
+ if (command.step)
252
+ await simulation.step(periodMs);
253
+ return json({ paused });
254
+ }
255
+ },
256
+ "/devices/act": {
257
+ POST: async (request) => {
258
+ const { device, action, argument } = deviceActionSchema.parse(await request.json());
259
+ return attempt(() => simulation.act(device, action, argument));
260
+ }
261
+ },
262
+ "/loads/remove": {
263
+ POST: async (request) => {
264
+ const { id } = loadSchema.parse(await request.json());
265
+ return attempt(() => simulation.remove(id));
266
+ }
267
+ },
268
+ "/actions/:id": {
269
+ POST: (request) => {
270
+ const action = actions().find(({ id }) => id === request.params.id);
271
+ return action ? attempt(action.run) : json({ message: "Action no longer available" }, 404);
272
+ }
273
+ }
274
+ },
275
+ fetch: () => json({ message: "Not found" }, 404)
276
+ });
277
+ const running = simulation.run(controller.signal, { periodMs, paused: () => paused });
278
+ return {
279
+ url: server.url,
280
+ stop: async () => {
281
+ controller.abort();
282
+ await running;
283
+ await server.stop(true);
284
+ }
285
+ };
286
+ };
287
+ export {
288
+ serveSimulation
289
+ };
290
+
291
+ //# debugId=D2ECDD3F29C011B364756E2164756E21
292
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/serve.ts", "../src/page.ts"],
4
+ "sourcesContent": [
5
+ "import { z } from \"zod\";\nimport { page } from \"./page\";\nimport type { Simulation } from \"./simulation\";\n\n/** An operator or test action the view offers, such as \"Receive bin at Framing01\". */\nexport interface SimulationAction {\n id: string;\n label: string;\n /** The station, part or load it concerns; the view lists it there. */\n at?: string;\n run: () => Promise<void> | void;\n}\n\nexport interface ServeSimulationOptions {\n hostname: string;\n port: number;\n title?: string;\n periodMs?: number;\n /** Actions currently available, recomputed for every view refresh. */\n actions?: () => SimulationAction[];\n /** Extra CSS for the page, such as colour overrides. */\n css?: string;\n log?: { warn: (message: string, error: unknown) => void };\n}\n\nconst controlSchema = z.object({ pause: z.boolean().optional(), step: z.boolean().optional() });\nconst deviceActionSchema = z.object({\n device: z.string(),\n action: z.string(),\n argument: z.string().default(\"\"),\n});\n\nconst loadSchema = z.object({ id: z.string() });\n\nconst json = (body: unknown, status = 200) =>\n Response.json(body, { status, headers: { \"Cache-Control\": \"no-store\" } });\n\n/**\n * Runs a simulation in real time and serves its live view: the line drawn from its topology,\n * device inspector and actions, the application's actions, removing loads by hand, and pause/step.\n */\nexport const serveSimulation = (\n simulation: Pick<Simulation, \"state\" | \"step\" | \"act\" | \"remove\" | \"run\">,\n options: ServeSimulationOptions,\n) => {\n const controller = new AbortController();\n const periodMs = options.periodMs ?? 50;\n let paused = false;\n const actions = () => options.actions?.() ?? [];\n const attempt = async (run: () => Promise<void> | void) => {\n try {\n await run();\n return json({ ok: true });\n } catch (error) {\n options.log?.warn(\"Simulation action failed\", error);\n return json({ message: error instanceof Error ? error.message : String(error) }, 409);\n }\n };\n const server = Bun.serve({\n hostname: options.hostname,\n port: options.port,\n routes: {\n \"/\": () =>\n new Response(page({ title: options.title ?? \"Simulation\", css: options.css ?? \"\" }), {\n headers: { \"Content-Type\": \"text/html; charset=utf-8\" },\n }),\n \"/state\": () =>\n json({\n ...simulation.state(),\n paused,\n actions: actions().map(({ id, label, at }) => ({ id, label, at })),\n }),\n \"/control\": {\n POST: async (request) => {\n const command = controlSchema.parse(await request.json());\n if (command.pause !== undefined) paused = command.pause;\n if (command.step) await simulation.step(periodMs);\n return json({ paused });\n },\n },\n \"/devices/act\": {\n POST: async (request) => {\n const { device, action, argument } = deviceActionSchema.parse(await request.json());\n return attempt(() => simulation.act(device, action, argument));\n },\n },\n \"/loads/remove\": {\n POST: async (request) => {\n const { id } = loadSchema.parse(await request.json());\n return attempt(() => simulation.remove(id));\n },\n },\n \"/actions/:id\": {\n POST: (request) => {\n const action = actions().find(({ id }) => id === request.params.id);\n return action\n ? attempt(action.run)\n : json({ message: \"Action no longer available\" }, 404);\n },\n },\n },\n fetch: () => json({ message: \"Not found\" }, 404),\n });\n const running = simulation.run(controller.signal, { periodMs, paused: () => paused });\n return {\n url: server.url,\n stop: async () => {\n controller.abort();\n await running;\n await server.stop(true);\n },\n };\n};\n",
6
+ "const escape = (value: string) =>\n value.replace(/[&<>\"]/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\" })[c]!);\n\n/** The live view: a self-contained page that polls `/state`. */\nexport const page = (options: { title: string; css: string }) => `<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>${escape(options.title)}</title>\n<style>\n:root{--bg:#f6f7f9;--panel:#fff;--fg:#1c2026;--muted:#6b7280;--border:#d8dce3;--track:#c9ced8;\n--running:#16a34a;--busy:#2563eb;--stopped:#9aa3b2;--idle:#c3c9d4;--warning:#d97706;--fault:#dc2626;--load:#7c3aed}\n@media (prefers-color-scheme:dark){:root{--bg:#111318;--panel:#191c22;--fg:#e6e8ec;--muted:#9aa1ad;\n--border:#2c313a;--track:#3a404b;--stopped:#6b7280;--idle:#4b5260;--load:#a78bfa}}\n*{box-sizing:border-box}body{margin:0;font:13px system-ui,sans-serif;background:var(--bg);color:var(--fg);\ndisplay:grid;grid-template-rows:auto 1fr;height:100vh}\nheader{display:flex;gap:10px;align-items:center;padding:8px 14px;border-bottom:1px solid var(--border);background:var(--panel)}\nheader h1{font-size:14px;margin:0 8px 0 0}#clock{font-family:ui-monospace,monospace;color:var(--muted)}\n#error{color:var(--fault)}button{font:inherit;border:1px solid var(--border);background:var(--bg);color:var(--fg);\nborder-radius:6px;padding:3px 9px;cursor:pointer}button:hover{border-color:var(--muted)}\nmain{display:grid;grid-template-columns:1fr 340px;min-height:0}svg{width:100%;height:100%}\naside{border-left:1px solid var(--border);background:var(--panel);overflow:auto;padding:12px}\nh2{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:14px 0 6px}\n.device{border:1px solid var(--border);border-radius:8px;padding:8px;margin-bottom:8px}\n.device b{display:block;margin-bottom:4px}table{width:100%;border-collapse:collapse;font-size:12px}\ntd{padding:2px 0;border-bottom:1px dashed var(--border)}td:last-child{text-align:right;font-family:ui-monospace,monospace}\n.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}\n.actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}\n.control{display:flex;align-items:center;gap:6px;padding:3px 0;border-bottom:1px dashed var(--border)}\n.control b{flex:1;font-weight:500}.control span{color:var(--muted)}.control button{padding:1px 7px}.part{cursor:pointer}\n#toast{position:fixed;bottom:14px;left:14px;background:var(--fault);color:#fff;padding:6px 10px;border-radius:6px;display:none}\n${options.css}\n</style></head><body>\n<header><h1>${escape(options.title)}</h1><span id=\"clock\"></span>\n<button id=\"pause\">Pause</button><button id=\"step\" disabled>Step</button><span id=\"error\"></span></header>\n<main><svg id=\"line\"><g id=\"tracks\"></g><g id=\"parts\"></g><g id=\"stations\"></g><g id=\"loads\"></g></svg>\n<aside><div id=\"inspector\"><p style=\"color:var(--muted)\">Select a part of the line to inspect it.</p></div>\n<h2>Actions</h2><div id=\"actions\"></div><h2>Controls</h2><div id=\"controls\"></div></aside></main><div id=\"toast\"></div>\n<script>\nconst NS = \"http://www.w3.org/2000/svg\", TRACK = 16;\nconst $ = (id) => document.getElementById(id);\nconst el = (name, attrs = {}, parent) => { const node = document.createElementNS(NS, name);\n for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v); parent?.appendChild(node); return node; };\nconst html = (s) => String(s).replace(/[&<>\"]/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\" })[c]);\nlet state, selected = null, selectedLoad = null, built = \"\", geometry = new Map(), stationAt = new Map();\n\nconst point = (id, mm) => {\n const g = geometry.get(id), d = Math.max(0, Math.min(mm, g.length)) * g.scale;\n const p = g.path.getPointAtLength(d), a = g.path.getPointAtLength(Math.max(0, d - 1)),\n b = g.path.getPointAtLength(Math.min(g.total, d + 1)), angle = Math.atan2(b.y - a.y, b.x - a.x);\n return { x: p.x, y: p.y, angle, nx: -Math.sin(angle), ny: Math.cos(angle) };\n};\nconst color = (status) => \"var(--\" + status + \")\";\n\nconst build = () => {\n for (const id of [\"tracks\", \"parts\", \"stations\"]) $(id).replaceChildren();\n geometry = new Map(); stationAt = new Map();\n for (const c of state.conveyors) {\n const path = el(\"path\", { d: c.draw, fill: \"none\", stroke: \"var(--track)\", \"stroke-width\": TRACK, \"stroke-linecap\": \"round\" }, $(\"tracks\"));\n const total = path.getTotalLength();\n geometry.set(c.id, { path, total, length: c.length, scale: total / c.length });\n }\n for (const c of state.conveyors) {\n const g = geometry.get(c.id);\n for (const part of c.parts) {\n if (part.start === part.end) {\n const p = point(c.id, part.start), size = part.kind === \"reader\" ? 7 : 5, sign = part.side === \"left\" ? -1 : 1;\n if (part.station && !stationAt.has(part.station)) stationAt.set(part.station, { x: p.x + sign * p.nx * 45, y: p.y + sign * p.ny * 45 });\n const node = part.kind === \"reader\"\n ? el(\"rect\", { x: p.x + p.nx * (TRACK / 2 + 6) - size / 2, y: p.y + p.ny * (TRACK / 2 + 6) - size / 2, width: size, height: size }, $(\"parts\"))\n : el(\"circle\", { cx: p.x + sign * p.nx * (TRACK / 2 + 5), cy: p.y + sign * p.ny * (TRACK / 2 + 5), r: size / 2 + 1 }, $(\"parts\"));\n node.dataset.part = part.id; node.classList.add(\"part\");\n continue;\n }\n const seg = el(\"path\", { d: c.draw, fill: \"none\", \"stroke-width\": TRACK - 6,\n \"stroke-dasharray\": \"0 \" + part.start * g.scale + \" \" + (part.end - part.start) * g.scale + \" \" + g.total }, $(\"parts\"));\n seg.dataset.part = part.id; seg.classList.add(\"part\");\n const b = point(c.id, part.start);\n el(\"line\", { x1: b.x + b.nx * TRACK / 2, y1: b.y + b.ny * TRACK / 2, x2: b.x - b.nx * TRACK / 2, y2: b.y - b.ny * TRACK / 2, stroke: \"var(--bg)\", \"stroke-width\": 1.5 }, $(\"tracks\"));\n for (const [side, sign] of [[\"left\", -1], [\"right\", 1]]) {\n const exit = part.exits?.[side]; if (!exit) continue;\n const p = point(c.id, (part.start + part.end) / 2);\n el(\"line\", { x1: p.x, y1: p.y, x2: p.x + sign * p.nx * 46, y2: p.y + sign * p.ny * 46, stroke: \"var(--track)\", \"stroke-width\": 8 }, $(\"tracks\"));\n if (!stationAt.has(exit)) stationAt.set(exit, { x: p.x + sign * p.nx * 70, y: p.y + sign * p.ny * 70 });\n }\n }\n }\n for (const c of state.conveyors) {\n const next = c.next ?? null;\n if (next && !geometry.has(next) && !stationAt.has(next)) {\n const p = point(c.id, c.length); stationAt.set(next, { x: p.x + Math.cos(p.angle) * 45, y: p.y + Math.sin(p.angle) * 45 });\n }\n }\n for (const s of state.stations) {\n const at = stationAt.get(s.id) ?? { x: 0, y: 0 };\n const group = el(\"g\", { transform: \"translate(\" + at.x + \" \" + at.y + \")\" }, $(\"stations\"));\n group.dataset.part = s.id; group.classList.add(\"part\");\n el(\"rect\", { x: -38, y: -14, width: 76, height: 28, rx: 5, fill: \"var(--panel)\", stroke: \"var(--border)\" }, group);\n el(\"text\", { \"text-anchor\": \"middle\", y: -1, \"font-size\": 9, fill: \"var(--fg)\" }, group).textContent = s.label;\n el(\"text\", { \"text-anchor\": \"middle\", y: 10, \"font-size\": 9, fill: \"var(--muted)\", \"data-count\": s.id }, group);\n }\n const box = $(\"line\").getBBox(), pad = 60;\n $(\"line\").setAttribute(\"viewBox\", [box.x - pad, box.y - pad, box.width + 2 * pad, box.height + 2 * pad].join(\" \"));\n};\n\nconst render = () => {\n const key = JSON.stringify(state.conveyors.map((c) => [c.id, c.draw, c.parts.map((p) => p.id)]));\n if (key !== built) { built = key; build(); }\n $(\"clock\").textContent = (state.timeMs / 1000).toFixed(1) + \" s\";\n $(\"error\").textContent = state.error ?? \"\";\n $(\"pause\").textContent = state.paused ? \"Resume\" : \"Pause\"; $(\"step\").disabled = !state.paused;\n const views = new Map(state.conveyors.flatMap((c) => c.parts.map((p) => [p.id, p])));\n for (const node of document.querySelectorAll(\"[data-part]\")) {\n const part = views.get(node.dataset.part); if (!part) continue;\n const status = color(part.view.status);\n node.setAttribute(node.tagName === \"path\" ? \"stroke\" : \"fill\", status);\n node.setAttribute(\"stroke-opacity\", selected === part.id ? 1 : 0.85);\n }\n for (const s of state.stations) {\n const count = document.querySelector('[data-count=\"' + s.id + '\"]'); if (count) count.textContent = s.loads + \" held\";\n }\n const loads = $(\"loads\"); loads.replaceChildren();\n const waiting = new Map();\n for (const load of state.loads) {\n if (load.place.kind === \"waiting\") { waiting.set(load.place.conveyor, (waiting.get(load.place.conveyor) ?? 0) + 1); continue; }\n if (load.place.kind !== \"conveyor\") continue;\n const g = geometry.get(load.place.conveyor), p = point(load.place.conveyor, load.place.head - load.length / 2);\n const w = Math.max(load.length * g.scale, 4), h = Math.max(Math.min(load.width * g.scale, TRACK + 6), 4);\n const x = p.x + p.nx * load.place.lateral * g.scale, y = p.y + p.ny * load.place.lateral * g.scale;\n const r = el(\"rect\", { x: -w / 2, y: -h / 2, width: w, height: h, rx: 2, fill: \"var(--load)\", opacity: .9,\n transform: \"translate(\" + x + \" \" + y + \") rotate(\" + p.angle * 180 / Math.PI + \")\" }, loads);\n r.dataset.load = load.id; r.classList.add(\"part\");\n if (selectedLoad === load.id) r.setAttribute(\"stroke\", \"var(--fg)\");\n el(\"title\", {}, r).textContent = load.code + \" (\" + load.kind + \")\";\n }\n for (const [conveyor, count] of waiting) {\n const p = point(conveyor, 0);\n el(\"text\", { x: p.x - Math.cos(p.angle) * 30, y: p.y - Math.sin(p.angle) * 30, \"font-size\": 10, fill: \"var(--muted)\", \"text-anchor\": \"middle\" }, loads).textContent = \"+\" + count + \" waiting\";\n }\n inspect(); renderControls(); renderActions();\n};\n\nconst removeButton = (id) => \"<button data-remove='\" + html(id) + \"'>Remove</button>\";\nconst inspectLoad = () => {\n const load = state.loads.find((l) => l.id === selectedLoad);\n if (!load) { $(\"inspector\").innerHTML = \"<p style=color:var(--muted)>The load has left the line.</p>\"; return; }\n const where = load.place.kind === \"conveyor\" ? load.place.conveyor + \" at \" + Math.round(load.place.head) + \" mm\"\n : load.place.kind === \"station\" ? load.place.station : \"waiting for \" + load.place.conveyor;\n $(\"inspector\").innerHTML = \"<h2>\" + html(load.code) + \" · \" + html(load.kind) + \"</h2><div>\" + html(where) + \"</div>\" +\n \"<div class=actions>\" + state.actions.filter((a) => a.at === load.id).map((a) => \"<button data-run='\" + html(a.id) + \"'>\" + html(a.label) + \"</button>\").join(\"\") +\n removeButton(load.id) + \"</div>\";\n};\nconst inspect = () => {\n if (selectedLoad) return inspectLoad();\n if (!selected) return;\n const part = state.conveyors.flatMap((c) => c.parts).find((p) => p.id === selected);\n const devices = state.devices.filter((d) => d.part === selected);\n const loads = state.loads.filter((l) => l.place.kind === \"station\" && l.place.station === selected);\n const rows = (obj) => Object.entries(obj ?? {}).map(([k, v]) => \"<tr><td>\" + html(k) + \"</td><td>\" + html(Array.isArray(v) ? \"[\" + v.length + \"]\" : v) + \"</td></tr>\").join(\"\");\n $(\"inspector\").innerHTML = \"<h2>\" + html(selected) + (part ? \" · \" + part.kind : \"\") + \"</h2>\" +\n (part ? \"<div><span class=dot style=background:\" + color(part.view.status) + \"></span>\" + html(part.view.label ?? part.view.status) + \"</div>\" : \"\") +\n (loads.length ? \"<table>\" + loads.map((l) => \"<tr><td>\" + html(l.code) + \" · \" + html(l.kind) + \"</td><td>\" + removeButton(l.id) + \"</td></tr>\").join(\"\") + \"</table>\" : \"\") +\n \"<div class=actions>\" + state.actions.filter((a) => a.at === selected).map((a) => \"<button data-run='\" + html(a.id) + \"'>\" + html(a.label) + \"</button>\").join(\"\") + \"</div>\" +\n devices.map((d) => \"<div class=device><b><span class=dot style=background:\" + color(d.view.status) + \"></span>\" +\n html(d.id.slice(selected.length + 1) || d.model) + \" · \" + html(d.model) + \"</b>\" +\n (d.view.label ? \"<div style=color:var(--muted)>\" + html(d.view.label) + \"</div>\" : \"\") +\n \"<table>\" + rows(d.view.details) + rows(d.pins) + \"</table><div class=actions>\" +\n d.actions.map((a) => \"<button data-device='\" + html(d.id) + \"' data-action='\" + html(a) + \"'>\" + html(a) + \"</button>\").join(\"\") +\n \"</div></div>\").join(\"\");\n};\n\nconst renderControls = () => {\n const controls = state.devices.filter((d) => d.part === null);\n $(\"controls\").innerHTML = controls.length\n ? controls.map((d) => \"<div class=control><i class=dot style=background:\" + color(d.view.status) + \"></i><b>\" + html(d.id) + \"</b><span>\" +\n html(d.view.label ?? d.view.status) + \"</span>\" + d.actions.map((a) => \"<button data-device='\" + html(d.id) + \"' data-action='\" + html(a) + \"'>\" + html(a) + \"</button>\").join(\"\") + \"</div>\").join(\"\")\n : \"<p style=color:var(--muted)>None.</p>\";\n};\nconst renderActions = () => {\n const groups = new Map();\n for (const a of state.actions) groups.set(a.at ?? \"\", [...(groups.get(a.at ?? \"\") ?? []), a]);\n $(\"actions\").innerHTML = groups.size\n ? [...groups].map(([at, actions]) => (at ? \"<div style='margin-top:8px;color:var(--muted)'>\" + html(at) + \"</div>\" : \"\") +\n \"<div class=actions>\" + actions.map((a) => \"<button data-run='\" + html(a.id) + \"'>\" + html(a.label) + \"</button>\").join(\"\") + \"</div>\").join(\"\")\n : \"<p style=color:var(--muted)>None available.</p>\";\n};\n\nconst post = async (url, body) => {\n const response = await fetch(url, { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(body ?? {}) });\n if (!response.ok) { const toast = $(\"toast\"); toast.textContent = (await response.json()).message ?? \"Failed\"; toast.style.display = \"block\"; setTimeout(() => toast.style.display = \"none\", 3000); }\n await refresh();\n};\ndocument.addEventListener(\"click\", (event) => {\n const target = event.target.closest(\"[data-part],[data-device],[data-run],[data-load],[data-remove]\");\n if (!target) return;\n if (target.dataset.run) return post(\"/actions/\" + encodeURIComponent(target.dataset.run));\n if (target.dataset.remove) return post(\"/loads/remove\", { id: target.dataset.remove });\n if (target.dataset.load) { selectedLoad = target.dataset.load; selected = null; return render(); }\n if (target.dataset.device) {\n const argument = target.dataset.action === \"read\" ? prompt(\"Code to read (empty for a no-read)\") : \"\";\n if (argument !== null) post(\"/devices/act\", { device: target.dataset.device, action: target.dataset.action, argument });\n return;\n }\n selected = target.dataset.part; selectedLoad = null; render();\n});\n$(\"pause\").onclick = () => post(\"/control\", { pause: !state.paused });\n$(\"step\").onclick = () => post(\"/control\", { step: true });\nconst refresh = async () => { state = await (await fetch(\"/state\")).json(); render(); };\nconst loop = async () => { try { await refresh(); } catch {} setTimeout(loop, 200); };\nloop();\n</script></body></html>`;\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "D2ECDD3F29C011B364756E2164756E21",
10
+ "names": []
11
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@tsplc/sim",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Plant simulation for tsPLC programs: hardware models, line topology, moving loads and a live view.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/livingforjesus/tsplc.git",
10
+ "directory": "packages/sim"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "bun": "./src/index.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./hardware/intelligrated": {
19
+ "types": "./src/hardware/intelligrated.ts",
20
+ "bun": "./src/hardware/intelligrated.ts",
21
+ "default": "./dist/hardware/intelligrated.js"
22
+ },
23
+ "./hardware/cognex": {
24
+ "types": "./src/hardware/cognex.ts",
25
+ "bun": "./src/hardware/cognex.ts",
26
+ "default": "./dist/hardware/cognex.js"
27
+ },
28
+ "./serve": {
29
+ "types": "./src/serve.ts",
30
+ "bun": "./src/serve.ts",
31
+ "default": "./dist/serve.js"
32
+ },
33
+ "./hardware/generic": {
34
+ "types": "./src/hardware/generic.ts",
35
+ "bun": "./src/hardware/generic.ts",
36
+ "default": "./dist/hardware/generic.js"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "src",
42
+ "!**/*.test.ts"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "zod": "4.3.6"
49
+ }
50
+ }