Before: ApplyTemplate dropped devices in a horizontal row at fixed canvas coords with frame_id NULL — devices appeared anywhere and m had no way to express "these belong together". Now: each apply creates a frame named after the template (suffixed "… 2/3/…" on name collision) and lays the devices out in a uniform grid inside it. Grid is roughly square (cols = ceil(sqrt(N)), capped at 4) with 30/50 px gaps and 32/48 px padding. Each device gets the new frame's id and grid-cell coords. Schema unchanged. ApplyTemplateResult.frames_added carries the new frame so the frontend can refresh the canvas without a full snapshot reload. Tests: - TestApplyTemplate_CreatesFrameAndPlacesDevicesInside — frame is created with the template's name, every device has frame_id set, every device sits inside the frame rect, no two devices share a grid cell. - TestApplyTemplate_FrameNameSuffixOnCollision — pre-existing "Living Room" frame in the project ⇒ template's frame named "Living Room 2". - Existing tests unchanged.
224 lines
9.0 KiB
Go
224 lines
9.0 KiB
Go
package db
|
|
|
|
// Project is the top-level entity. One project ↔ one .excalidraw drawing.
|
|
type Project struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
DrawingName string `json:"drawing_name"`
|
|
Description string `json:"description"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// CableType is global. Renaming/recolouring affects every project.
|
|
type CableType struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// Frame is a sub-zone inside a project (`desk`, `rack`, …).
|
|
type Frame struct {
|
|
ID int64 `json:"id"`
|
|
ProjectID int64 `json:"project_id"`
|
|
Name string `json:"name"`
|
|
X float64 `json:"x"`
|
|
Y float64 `json:"y"`
|
|
Width float64 `json:"width"`
|
|
Height float64 `json:"height"`
|
|
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// Device is a hardware item inside a project, optionally inside a frame.
|
|
// v4: type_id (nullable) lets a device inherit its port profile from a
|
|
// device_types catalog row.
|
|
type Device struct {
|
|
ID int64 `json:"id"`
|
|
ProjectID int64 `json:"project_id"`
|
|
FrameID *int64 `json:"frame_id"` // nullable: device "outside" any frame
|
|
TypeID *int64 `json:"type_id"` // nullable: freeform device when null
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
X float64 `json:"x"`
|
|
Y float64 `json:"y"`
|
|
Width float64 `json:"width"`
|
|
Height float64 `json:"height"`
|
|
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// DeviceType is a catalog row. Built-in rows have ProjectID nil and
|
|
// BuiltIn true. Project-custom rows have ProjectID set.
|
|
type DeviceType struct {
|
|
ID int64 `json:"id"`
|
|
ProjectID *int64 `json:"project_id"`
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Icon *string `json:"icon,omitempty"`
|
|
Description string `json:"description"`
|
|
BuiltIn bool `json:"built_in"`
|
|
Ports []DeviceTypePort `json:"ports"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// DeviceTypePort is a row of a type's port profile. The seeder uses
|
|
// (cable_type_id, count, label_prefix, edge, sort_order) to lay out
|
|
// concrete ports on a freshly-created device.
|
|
type DeviceTypePort struct {
|
|
ID int64 `json:"id"`
|
|
DeviceTypeID int64 `json:"device_type_id"`
|
|
CableTypeID int64 `json:"cable_type_id"`
|
|
LabelPrefix string `json:"label_prefix"`
|
|
Count int `json:"count"`
|
|
Edge string `json:"edge"`
|
|
SortOrder int `json:"sort_order"`
|
|
}
|
|
|
|
// Port is a connector on a device. cable_type colour drives the visual
|
|
// rendering; ports are instance-owned even when seeded from a type.
|
|
type Port struct {
|
|
ID int64 `json:"id"`
|
|
ProjectID int64 `json:"project_id"`
|
|
DeviceID int64 `json:"device_id"`
|
|
TypeID int64 `json:"type_id"` // cable type
|
|
Label *string `json:"label"`
|
|
XOffset float64 `json:"x_offset"`
|
|
YOffset float64 `json:"y_offset"`
|
|
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// ConnectionRequirement is the solver's per-project input.
|
|
// pair_lo/pair_hi are the ordered (MIN,MAX) of (from, to) so the
|
|
// UNIQUE on (project_id, pair_lo, pair_hi, preferred_cable_type_id)
|
|
// prevents (A,B,T) AND (B,A,T) from coexisting.
|
|
type ConnectionRequirement struct {
|
|
ID int64 `json:"id"`
|
|
ProjectID int64 `json:"project_id"`
|
|
FromDeviceID int64 `json:"from_device_id"`
|
|
ToDeviceID int64 `json:"to_device_id"`
|
|
PreferredCableTypeID *int64 `json:"preferred_cable_type_id"`
|
|
MustConnect bool `json:"must_connect"`
|
|
Notes string `json:"notes"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// Cable is a typed connection. Each endpoint is exactly one of
|
|
// (port, device, io-marker). Auto=true means the solver placed it.
|
|
type Cable struct {
|
|
ID int64 `json:"id"`
|
|
ProjectID int64 `json:"project_id"`
|
|
TypeID int64 `json:"type_id"`
|
|
Label *string `json:"label"`
|
|
FromPortID *int64 `json:"from_port_id"`
|
|
FromDeviceID *int64 `json:"from_device_id"`
|
|
FromIOID *int64 `json:"from_io_id"`
|
|
ToPortID *int64 `json:"to_port_id"`
|
|
ToDeviceID *int64 `json:"to_device_id"`
|
|
ToIOID *int64 `json:"to_io_id"`
|
|
Auto bool `json:"auto"`
|
|
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// Bundle is a named group of cables that physically run together.
|
|
type Bundle struct {
|
|
ID int64 `json:"id"`
|
|
ProjectID int64 `json:"project_id"`
|
|
Name string `json:"name"`
|
|
Auto bool `json:"auto"`
|
|
CableIDs []int64 `json:"cable_ids"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
// SetupTemplate is a named recipe of device-types + requirements.
|
|
type SetupTemplate struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
BuiltIn bool `json:"built_in"`
|
|
Devices []SetupTemplateDevice `json:"devices"`
|
|
Requirements []SetupTemplateRequirement `json:"requirements"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
type SetupTemplateDevice struct {
|
|
ID int64 `json:"id"`
|
|
TemplateID int64 `json:"template_id"`
|
|
DeviceTypeID int64 `json:"device_type_id"`
|
|
DeviceType *DeviceType `json:"device_type,omitempty"`
|
|
SuggestedName *string `json:"suggested_name"`
|
|
SortOrder int `json:"sort_order"`
|
|
}
|
|
|
|
type SetupTemplateRequirement struct {
|
|
ID int64 `json:"id"`
|
|
TemplateID int64 `json:"template_id"`
|
|
FromTemplateDeviceID int64 `json:"from_template_device_id"`
|
|
ToTemplateDeviceID int64 `json:"to_template_device_id"`
|
|
PreferredCableTypeID *int64 `json:"preferred_cable_type_id"`
|
|
MustConnect bool `json:"must_connect"`
|
|
}
|
|
|
|
// SolveResult is the response shape from POST /api/projects/:pid/solve.
|
|
type SolveResult struct {
|
|
CablesAdded []Cable `json:"cables_added"`
|
|
CablesKept []int64 `json:"cables_kept"`
|
|
CablesRemoved []int64 `json:"cables_removed"`
|
|
BundlesAdded []Bundle `json:"bundles_added"`
|
|
BundlesRemoved []int64 `json:"bundles_removed"`
|
|
Unsatisfied []UnsatisfiedReq `json:"unsatisfied"`
|
|
Warnings []string `json:"warnings"`
|
|
}
|
|
|
|
type UnsatisfiedReq struct {
|
|
RequirementID int64 `json:"requirement_id"`
|
|
Reason string `json:"reason"`
|
|
WhichSide string `json:"which_side,omitempty"` // "from" | "to" | "" when both/neither
|
|
CableType string `json:"cable_type,omitempty"` // when known
|
|
}
|
|
|
|
// ApplyTemplateResult is the response from POST /apply-template.
|
|
type ApplyTemplateResult struct {
|
|
FramesAdded []Frame `json:"frames_added"`
|
|
DevicesAdded []Device `json:"devices_added"`
|
|
RequirementsAdded []ConnectionRequirement `json:"requirements_added"`
|
|
SkippedDevices []SkippedTemplateDevice `json:"skipped_devices"`
|
|
RequirementsSkipped []SkippedTemplateReq `json:"requirements_skipped"`
|
|
}
|
|
|
|
type SkippedTemplateDevice struct {
|
|
TemplateDeviceID int64 `json:"template_device_id"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
type SkippedTemplateReq struct {
|
|
TemplateRequirementID int64 `json:"template_requirement_id"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// Snapshot is the editor's one-shot loader payload for a single project.
|
|
// Arrays for collections still gated by future slices stay non-nil [] so
|
|
// JSON encodes as [] not null.
|
|
type Snapshot struct {
|
|
Project Project `json:"project"`
|
|
Frames []Frame `json:"frames"`
|
|
Devices []Device `json:"devices"`
|
|
Ports []Port `json:"ports"`
|
|
Cables []Cable `json:"cables"`
|
|
IOMarkers []IOMarker `json:"io_markers"`
|
|
Bundles []Bundle `json:"bundles"`
|
|
CableTypes []CableType `json:"cable_types"`
|
|
ConnectionRequirements []ConnectionRequirement `json:"connection_requirements"`
|
|
}
|