Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
560 lines
18 KiB
Go
560 lines
18 KiB
Go
package workbenchservice
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// NormalizeSnapshot validates and encodes a workbench snapshot in canonical form.
|
|
func NormalizeSnapshot(snapshot WorkbenchSnapshot) (json.RawMessage, int, error) {
|
|
return normalizeWorkbenchSnapshot(snapshot)
|
|
}
|
|
|
|
func normalizeWorkbenchSnapshot(snapshot WorkbenchSnapshot) (json.RawMessage, int, error) {
|
|
if snapshot.SchemaVersion != workbenchSnapshotContractSchemaVersion {
|
|
return nil, 0, fmt.Errorf("unsupported workbench snapshot schema version: %d", snapshot.SchemaVersion)
|
|
}
|
|
if snapshot.Nodes == nil {
|
|
return nil, 0, errors.New("workbench snapshot nodes are required")
|
|
}
|
|
if len(snapshot.Nodes) > workbenchSnapshotContractMaxNodes {
|
|
return nil, 0, fmt.Errorf("workbench snapshot nodes must contain at most %d items", workbenchSnapshotContractMaxNodes)
|
|
}
|
|
|
|
ids := make(map[string]struct{}, len(snapshot.Nodes))
|
|
for index, node := range snapshot.Nodes {
|
|
if strings.TrimSpace(node.ID) == "" {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d id is required", index)
|
|
}
|
|
nodeID := strings.TrimSpace(node.ID)
|
|
if _, exists := ids[nodeID]; exists {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d id is duplicated", index)
|
|
}
|
|
ids[nodeID] = struct{}{}
|
|
if len(node.ID) > workbenchSnapshotContractMaxNodeIDLength {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d id is too long", index)
|
|
}
|
|
|
|
if strings.TrimSpace(node.Kind) == "" {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d kind is required", index)
|
|
}
|
|
if len(node.Kind) > workbenchSnapshotContractMaxKindLength {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d kind is too long", index)
|
|
}
|
|
if len(node.Title) > workbenchSnapshotContractMaxTitleLength {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d title is too long", index)
|
|
}
|
|
if node.DisplayMode != nil {
|
|
if _, ok := workbenchSnapshotContractDisplayModes[*node.DisplayMode]; !ok {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d display mode is invalid", index)
|
|
}
|
|
}
|
|
if err := validateWorkbenchFrame(node.Frame); err != nil {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d frame: %w", index, err)
|
|
}
|
|
if node.RestoreFrame != nil {
|
|
if err := validateWorkbenchFrame(*node.RestoreFrame); err != nil {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d restore frame: %w", index, err)
|
|
}
|
|
}
|
|
if node.MinimizedAtUnixMs != nil && *node.MinimizedAtUnixMs < 0 {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node %d minimized timestamp is invalid", index)
|
|
}
|
|
}
|
|
if snapshot.NodeStack != nil {
|
|
for index, nodeID := range *snapshot.NodeStack {
|
|
if strings.TrimSpace(nodeID) == "" {
|
|
return nil, 0, fmt.Errorf("workbench snapshot node stack item %d is required", index)
|
|
}
|
|
}
|
|
}
|
|
if snapshot.Spaces != nil {
|
|
for index, space := range *snapshot.Spaces {
|
|
if strings.TrimSpace(space.ID) == "" {
|
|
return nil, 0, fmt.Errorf("workbench snapshot space %d id is required", index)
|
|
}
|
|
if space.NodeIDs == nil {
|
|
return nil, 0, fmt.Errorf("workbench snapshot space %d node ids are required", index)
|
|
}
|
|
for nodeIDIndex, nodeID := range space.NodeIDs {
|
|
if strings.TrimSpace(nodeID) == "" {
|
|
return nil, 0, fmt.Errorf("workbench snapshot space %d node id %d is required", index, nodeIDIndex)
|
|
}
|
|
}
|
|
if space.Frame != nil {
|
|
if err := validateWorkbenchFrame(*space.Frame); err != nil {
|
|
return nil, 0, fmt.Errorf("workbench snapshot space %d frame: %w", index, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if snapshot.LayoutBasis != nil {
|
|
if err := validateWorkbenchLayoutBasis(*snapshot.LayoutBasis); err != nil {
|
|
return nil, 0, fmt.Errorf("workbench snapshot layout basis: %w", err)
|
|
}
|
|
}
|
|
if snapshot.LockedLayout != nil {
|
|
if err := validateWorkbenchLockedLayout(*snapshot.LockedLayout, ids); err != nil {
|
|
return nil, 0, fmt.Errorf("workbench snapshot locked layout: %w", err)
|
|
}
|
|
}
|
|
|
|
canonicalSnapshot := canonicalizeWorkbenchSnapshot(snapshot)
|
|
normalizedJSON, err := json.Marshal(canonicalSnapshot)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("encode workbench snapshot: %w", err)
|
|
}
|
|
if len(normalizedJSON) > workbenchSnapshotContractMaxSerializedBytes {
|
|
return nil, 0, fmt.Errorf("workbench snapshot exceeds %d bytes", workbenchSnapshotContractMaxSerializedBytes)
|
|
}
|
|
|
|
return normalizedJSON, snapshot.SchemaVersion, nil
|
|
}
|
|
|
|
type canonicalWorkbenchSnapshot struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
Nodes []canonicalWorkbenchSnapshotNode `json:"nodes"`
|
|
NodeStack []string `json:"nodeStack"`
|
|
ActiveNodeID *string `json:"activeNodeId"`
|
|
Spaces *[]canonicalWorkbenchSnapshotSpace `json:"spaces,omitempty"`
|
|
ActiveSpaceID *string `json:"activeSpaceId"`
|
|
LayoutBasis *WorkbenchSnapshotLayoutBasis `json:"layoutBasis,omitempty"`
|
|
LockedLayout *canonicalWorkbenchSnapshotLockedLayout `json:"lockedLayout,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
}
|
|
|
|
type canonicalWorkbenchSnapshotLockedLayout struct {
|
|
Preset WorkbenchSnapshotLayoutPreset `json:"preset"`
|
|
NodeIDs []string `json:"nodeIDs"`
|
|
NormalizedFrames map[string]WorkbenchSnapshotNormalizedFrame `json:"normalizedFrames,omitempty"`
|
|
}
|
|
|
|
type canonicalWorkbenchSnapshotNode struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
Title string `json:"title"`
|
|
Frame WorkbenchSnapshotFrame `json:"frame"`
|
|
DisplayMode WorkbenchSnapshotDisplayMode `json:"displayMode"`
|
|
RestoreFrame *WorkbenchSnapshotFrame `json:"restoreFrame"`
|
|
IsMinimized bool `json:"isMinimized"`
|
|
MinimizedAtUnixMs *int64 `json:"minimizedAtUnixMs,omitempty"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
AdapterState map[string]interface{} `json:"adapterState,omitempty"`
|
|
}
|
|
|
|
type canonicalWorkbenchSnapshotSpace struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
NodeIDs []string `json:"nodeIds"`
|
|
Frame *WorkbenchSnapshotFrame `json:"frame"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
func canonicalizeWorkbenchSnapshot(snapshot WorkbenchSnapshot) canonicalWorkbenchSnapshot {
|
|
nodes := make([]canonicalWorkbenchSnapshotNode, len(snapshot.Nodes))
|
|
for index, node := range snapshot.Nodes {
|
|
nodes[index] = canonicalizeWorkbenchSnapshotNode(node)
|
|
}
|
|
sort.Slice(nodes, func(left, right int) bool {
|
|
return nodes[left].ID < nodes[right].ID
|
|
})
|
|
|
|
nodeIDs := make(map[string]struct{}, len(nodes))
|
|
for _, node := range nodes {
|
|
nodeIDs[node.ID] = struct{}{}
|
|
}
|
|
|
|
nodeStack := uniqueTrimmedStrings(derefStringSlice(snapshot.NodeStack))
|
|
filteredNodeStack := make([]string, 0, len(nodeStack))
|
|
for _, nodeID := range nodeStack {
|
|
if _, ok := nodeIDs[nodeID]; ok {
|
|
filteredNodeStack = append(filteredNodeStack, nodeID)
|
|
}
|
|
}
|
|
for _, node := range nodes {
|
|
if !containsString(filteredNodeStack, node.ID) {
|
|
filteredNodeStack = append(filteredNodeStack, node.ID)
|
|
}
|
|
}
|
|
|
|
var spaces *[]canonicalWorkbenchSnapshotSpace
|
|
spaceIDs := map[string]struct{}{}
|
|
if snapshot.Spaces != nil {
|
|
normalizedSpaces := make([]canonicalWorkbenchSnapshotSpace, len(*snapshot.Spaces))
|
|
for index, space := range *snapshot.Spaces {
|
|
normalizedSpaces[index] = canonicalizeWorkbenchSnapshotSpace(space)
|
|
}
|
|
sort.Slice(normalizedSpaces, func(left, right int) bool {
|
|
return normalizedSpaces[left].ID < normalizedSpaces[right].ID
|
|
})
|
|
for _, space := range normalizedSpaces {
|
|
spaceIDs[space.ID] = struct{}{}
|
|
}
|
|
spaces = &normalizedSpaces
|
|
}
|
|
|
|
return canonicalWorkbenchSnapshot{
|
|
SchemaVersion: workbenchSnapshotContractSchemaVersion,
|
|
Nodes: nodes,
|
|
NodeStack: filteredNodeStack,
|
|
ActiveNodeID: canonicalActiveNodeID(snapshot.ActiveNodeID, filteredNodeStack, nodeIDs),
|
|
Spaces: spaces,
|
|
ActiveSpaceID: canonicalActiveSpaceID(snapshot.ActiveSpaceID, spaces, spaceIDs),
|
|
LayoutBasis: canonicalizeWorkbenchLayoutBasis(snapshot.LayoutBasis),
|
|
LockedLayout: canonicalizeWorkbenchLockedLayout(snapshot.LockedLayout, nodeIDs),
|
|
Metadata: snapshot.Metadata,
|
|
}
|
|
}
|
|
|
|
func canonicalizeWorkbenchSnapshotNode(node WorkbenchSnapshotNode) canonicalWorkbenchSnapshotNode {
|
|
return canonicalWorkbenchSnapshotNode{
|
|
ID: strings.TrimSpace(node.ID),
|
|
Kind: strings.TrimSpace(node.Kind),
|
|
Title: node.Title,
|
|
Frame: canonicalizeWorkbenchFrame(node.Frame),
|
|
DisplayMode: canonicalDisplayMode(node.DisplayMode),
|
|
RestoreFrame: canonicalRestoreFrame(node.RestoreFrame),
|
|
IsMinimized: canonicalIsMinimized(node.IsMinimized),
|
|
MinimizedAtUnixMs: canonicalMinimizedAtUnixMs(node.IsMinimized, node.MinimizedAtUnixMs),
|
|
Data: node.Data,
|
|
AdapterState: node.AdapterState,
|
|
}
|
|
}
|
|
|
|
func canonicalizeWorkbenchSnapshotSpace(space WorkbenchSnapshotSpace) canonicalWorkbenchSnapshotSpace {
|
|
return canonicalWorkbenchSnapshotSpace{
|
|
ID: strings.TrimSpace(space.ID),
|
|
Name: space.Name,
|
|
NodeIDs: uniqueTrimmedStrings(space.NodeIDs),
|
|
Frame: canonicalRestoreFrame(space.Frame),
|
|
Data: space.Data,
|
|
}
|
|
}
|
|
|
|
func canonicalizeWorkbenchFrame(frame WorkbenchSnapshotFrame) WorkbenchSnapshotFrame {
|
|
return WorkbenchSnapshotFrame{
|
|
X: canonicalizeWorkbenchNumber(frame.X),
|
|
Y: canonicalizeWorkbenchNumber(frame.Y),
|
|
Width: math.Max(workbenchSnapshotContractMinFrameWidth, canonicalizeWorkbenchNumber(frame.Width)),
|
|
Height: math.Max(workbenchSnapshotContractMinFrameHeight, canonicalizeWorkbenchNumber(frame.Height)),
|
|
}
|
|
}
|
|
|
|
func canonicalizeWorkbenchLayoutBasis(
|
|
layoutBasis *WorkbenchSnapshotLayoutBasis,
|
|
) *WorkbenchSnapshotLayoutBasis {
|
|
if layoutBasis == nil {
|
|
return nil
|
|
}
|
|
|
|
return &WorkbenchSnapshotLayoutBasis{
|
|
SurfaceSize: WorkbenchSnapshotSize{
|
|
Width: canonicalizeWorkbenchNumber(layoutBasis.SurfaceSize.Width),
|
|
Height: canonicalizeWorkbenchNumber(layoutBasis.SurfaceSize.Height),
|
|
},
|
|
LayoutConstraints: WorkbenchSnapshotLayoutConstraints{
|
|
MinWidth: canonicalizeWorkbenchNumber(layoutBasis.LayoutConstraints.MinWidth),
|
|
MinHeight: canonicalizeWorkbenchNumber(layoutBasis.LayoutConstraints.MinHeight),
|
|
SurfacePadding: canonicalizeWorkbenchNumber(layoutBasis.LayoutConstraints.SurfacePadding),
|
|
SafeArea: WorkbenchSnapshotSafeArea{
|
|
Top: canonicalizeWorkbenchNumber(layoutBasis.LayoutConstraints.SafeArea.Top),
|
|
Right: canonicalizeWorkbenchNumber(layoutBasis.LayoutConstraints.SafeArea.Right),
|
|
Bottom: canonicalizeWorkbenchNumber(layoutBasis.LayoutConstraints.SafeArea.Bottom),
|
|
Left: canonicalizeWorkbenchNumber(layoutBasis.LayoutConstraints.SafeArea.Left),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func canonicalizeWorkbenchLockedLayout(
|
|
lockedLayout *WorkbenchSnapshotLockedLayout,
|
|
knownNodeIDs map[string]struct{},
|
|
) *canonicalWorkbenchSnapshotLockedLayout {
|
|
if lockedLayout == nil {
|
|
return nil
|
|
}
|
|
|
|
nodeIDs := make([]string, 0, len(lockedLayout.NodeIDs))
|
|
for _, nodeID := range uniqueTrimmedStrings(lockedLayout.NodeIDs) {
|
|
if _, ok := knownNodeIDs[nodeID]; ok {
|
|
nodeIDs = append(nodeIDs, nodeID)
|
|
}
|
|
}
|
|
if len(nodeIDs) < 2 {
|
|
return nil
|
|
}
|
|
|
|
canonical := &canonicalWorkbenchSnapshotLockedLayout{
|
|
Preset: lockedLayout.Preset,
|
|
NodeIDs: nodeIDs,
|
|
}
|
|
if lockedLayout.NormalizedFrames == nil {
|
|
return canonical
|
|
}
|
|
|
|
canonical.NormalizedFrames = make(
|
|
map[string]WorkbenchSnapshotNormalizedFrame,
|
|
len(nodeIDs),
|
|
)
|
|
for _, nodeID := range nodeIDs {
|
|
frame := lockedLayout.NormalizedFrames[nodeID]
|
|
canonical.NormalizedFrames[nodeID] = canonicalizeWorkbenchNormalizedFrame(frame)
|
|
}
|
|
return canonical
|
|
}
|
|
|
|
func canonicalizeWorkbenchNormalizedFrame(
|
|
frame WorkbenchSnapshotNormalizedFrame,
|
|
) WorkbenchSnapshotNormalizedFrame {
|
|
x := canonicalizeWorkbenchNumber(frame.X)
|
|
y := canonicalizeWorkbenchNumber(frame.Y)
|
|
|
|
return WorkbenchSnapshotNormalizedFrame{
|
|
X: x,
|
|
Y: y,
|
|
Width: math.Min(canonicalizeWorkbenchNumber(frame.Width), canonicalizeWorkbenchNumber(1-x)),
|
|
Height: math.Min(canonicalizeWorkbenchNumber(frame.Height), canonicalizeWorkbenchNumber(1-y)),
|
|
}
|
|
}
|
|
|
|
func canonicalizeWorkbenchNumber(value float64) float64 {
|
|
normalized := math.Round(value*1000) / 1000
|
|
if normalized == 0 {
|
|
return 0
|
|
}
|
|
|
|
return normalized
|
|
}
|
|
|
|
func canonicalDisplayMode(displayMode *WorkbenchSnapshotDisplayMode) WorkbenchSnapshotDisplayMode {
|
|
if displayMode == nil {
|
|
return WorkbenchSnapshotDisplayModeFloating
|
|
}
|
|
|
|
return *displayMode
|
|
}
|
|
|
|
func canonicalRestoreFrame(frame *WorkbenchSnapshotFrame) *WorkbenchSnapshotFrame {
|
|
if frame == nil {
|
|
return nil
|
|
}
|
|
|
|
normalized := canonicalizeWorkbenchFrame(*frame)
|
|
return &normalized
|
|
}
|
|
|
|
func canonicalIsMinimized(value *bool) bool {
|
|
if value == nil {
|
|
return false
|
|
}
|
|
|
|
return *value
|
|
}
|
|
|
|
func canonicalMinimizedAtUnixMs(isMinimized *bool, value *int64) *int64 {
|
|
if !canonicalIsMinimized(isMinimized) || value == nil {
|
|
return nil
|
|
}
|
|
|
|
normalized := *value
|
|
return &normalized
|
|
}
|
|
|
|
func canonicalActiveNodeID(activeNodeID *string, nodeStack []string, nodeIDs map[string]struct{}) *string {
|
|
if activeNodeID != nil {
|
|
if _, ok := nodeIDs[*activeNodeID]; ok {
|
|
return cloneStringPointer(*activeNodeID)
|
|
}
|
|
}
|
|
if len(nodeStack) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return cloneStringPointer(nodeStack[len(nodeStack)-1])
|
|
}
|
|
|
|
func canonicalActiveSpaceID(
|
|
activeSpaceID *string,
|
|
spaces *[]canonicalWorkbenchSnapshotSpace,
|
|
spaceIDs map[string]struct{},
|
|
) *string {
|
|
if activeSpaceID != nil {
|
|
if _, ok := spaceIDs[*activeSpaceID]; ok {
|
|
return cloneStringPointer(*activeSpaceID)
|
|
}
|
|
}
|
|
if spaces == nil || len(*spaces) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return cloneStringPointer((*spaces)[0].ID)
|
|
}
|
|
|
|
func derefStringSlice(value *[]string) []string {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
return append([]string(nil), (*value)...)
|
|
}
|
|
|
|
func uniqueTrimmedStrings(values []string) []string {
|
|
if len(values) == 0 {
|
|
return []string{}
|
|
}
|
|
|
|
unique := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, value := range values {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[trimmed]; ok {
|
|
continue
|
|
}
|
|
seen[trimmed] = struct{}{}
|
|
unique = append(unique, trimmed)
|
|
}
|
|
|
|
return unique
|
|
}
|
|
|
|
func containsString(values []string, target string) bool {
|
|
for _, value := range values {
|
|
if value == target {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func cloneStringPointer(value string) *string {
|
|
cloned := value
|
|
return &cloned
|
|
}
|
|
|
|
func validateWorkbenchFrame(frame WorkbenchSnapshotFrame) error {
|
|
if !isFinite(frame.X) || !isFinite(frame.Y) || !isFinite(frame.Width) || !isFinite(frame.Height) {
|
|
return errors.New("numbers must be finite")
|
|
}
|
|
if frame.Width < workbenchSnapshotContractMinFrameWidth {
|
|
return fmt.Errorf("width must be at least %d", workbenchSnapshotContractMinFrameWidth)
|
|
}
|
|
if frame.Height < workbenchSnapshotContractMinFrameHeight {
|
|
return fmt.Errorf("height must be at least %d", workbenchSnapshotContractMinFrameHeight)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateWorkbenchLayoutBasis(layoutBasis WorkbenchSnapshotLayoutBasis) error {
|
|
if !isPositiveFinite(layoutBasis.SurfaceSize.Width) || !isPositiveFinite(layoutBasis.SurfaceSize.Height) {
|
|
return errors.New("surface size must contain positive finite numbers")
|
|
}
|
|
constraints := layoutBasis.LayoutConstraints
|
|
if !isNonNegativeFinite(constraints.MinWidth) ||
|
|
!isNonNegativeFinite(constraints.MinHeight) ||
|
|
!isNonNegativeFinite(constraints.SurfacePadding) {
|
|
return errors.New("layout constraints must contain non-negative finite numbers")
|
|
}
|
|
safeArea := constraints.SafeArea
|
|
if !isNonNegativeFinite(safeArea.Top) ||
|
|
!isNonNegativeFinite(safeArea.Right) ||
|
|
!isNonNegativeFinite(safeArea.Bottom) ||
|
|
!isNonNegativeFinite(safeArea.Left) {
|
|
return errors.New("safe area must contain non-negative finite numbers")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateWorkbenchLockedLayout(
|
|
lockedLayout WorkbenchSnapshotLockedLayout,
|
|
knownNodeIDs map[string]struct{},
|
|
) error {
|
|
if _, ok := workbenchSnapshotContractLayoutPresetKinds[lockedLayout.Preset.Kind]; !ok {
|
|
return errors.New("preset kind is invalid")
|
|
}
|
|
if len(lockedLayout.NodeIDs) < 2 {
|
|
return errors.New("must contain at least two node IDs")
|
|
}
|
|
|
|
lockedNodeIDs := make(map[string]struct{}, len(lockedLayout.NodeIDs))
|
|
for index, nodeID := range lockedLayout.NodeIDs {
|
|
trimmedNodeID := strings.TrimSpace(nodeID)
|
|
if trimmedNodeID == "" {
|
|
return fmt.Errorf("node ID %d is required", index)
|
|
}
|
|
if len(nodeID) > workbenchSnapshotContractMaxNodeIDLength {
|
|
return fmt.Errorf("node ID %d is too long", index)
|
|
}
|
|
if _, duplicate := lockedNodeIDs[trimmedNodeID]; duplicate {
|
|
return fmt.Errorf("node ID %d is duplicated", index)
|
|
}
|
|
if _, exists := knownNodeIDs[trimmedNodeID]; !exists {
|
|
return fmt.Errorf("node ID %d does not exist", index)
|
|
}
|
|
lockedNodeIDs[trimmedNodeID] = struct{}{}
|
|
}
|
|
|
|
if lockedLayout.NormalizedFrames == nil {
|
|
return nil
|
|
}
|
|
for nodeID := range lockedNodeIDs {
|
|
frame, exists := lockedLayout.NormalizedFrames[nodeID]
|
|
if !exists {
|
|
return fmt.Errorf("normalized frame for node %q is required", nodeID)
|
|
}
|
|
if err := validateWorkbenchNormalizedFrame(frame); err != nil {
|
|
return fmt.Errorf("normalized frame for node %q: %w", nodeID, err)
|
|
}
|
|
}
|
|
for nodeID := range lockedLayout.NormalizedFrames {
|
|
if _, ok := lockedNodeIDs[nodeID]; !ok {
|
|
return fmt.Errorf("normalized frame node %q is not locked", nodeID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateWorkbenchNormalizedFrame(frame WorkbenchSnapshotNormalizedFrame) error {
|
|
if !isUnitInterval(frame.X, false) || !isUnitInterval(frame.Y, false) ||
|
|
!isUnitInterval(frame.Width, true) || !isUnitInterval(frame.Height, true) {
|
|
return errors.New("values must stay within the unit layout rect")
|
|
}
|
|
if frame.X+frame.Width > 1+workbenchNormalizedFrameUnitRectTransportTolerance ||
|
|
frame.Y+frame.Height > 1+workbenchNormalizedFrameUnitRectTransportTolerance {
|
|
return errors.New("must stay within the unit layout rect")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const workbenchNormalizedFrameUnitRectTransportTolerance = 0.000001
|
|
|
|
func isUnitInterval(value float64, exclusiveMinimum bool) bool {
|
|
if !isFinite(value) || value > 1 {
|
|
return false
|
|
}
|
|
if exclusiveMinimum {
|
|
return value > 0
|
|
}
|
|
return value >= 0
|
|
}
|
|
|
|
func isPositiveFinite(value float64) bool {
|
|
return isFinite(value) && value > 0
|
|
}
|
|
|
|
func isNonNegativeFinite(value float64) bool {
|
|
return isFinite(value) && value >= 0
|
|
}
|
|
|
|
func isFinite(value float64) bool {
|
|
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
|
}
|