feat: initial node groups #44
@@ -51,7 +51,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
id: number;
|
id: number;
|
||||||
nodes: SerializedNode[];
|
nodes: SerializedNode[];
|
||||||
edges: SerializedEdge[];
|
edges: SerializedEdge[];
|
||||||
cameraPosition: [number, number, number];
|
nodeId: number; // group instance node id that was entered to reach the next level
|
||||||
}[] = $state([]);
|
}[] = $state([]);
|
||||||
|
|
||||||
// ID of the currently active group, or null when at the root graph.
|
// ID of the currently active group, or null when at the root graph.
|
||||||
@@ -642,11 +642,11 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
isInsideGroup = $derived(this.currentGroupId !== null);
|
isInsideGroup = $derived(this.currentGroupId !== null);
|
||||||
|
|
||||||
enterGroup(nodeId: number, cameraPosition: [number, number, number]): boolean {
|
enterGroup(nodeId: number): boolean {
|
||||||
const groupNode = this.getNode(nodeId);
|
const groupNode = this.getNode(nodeId);
|
||||||
if (!groupNode || groupNode.type !== '__internal/group/instance') return false;
|
if (!groupNode || groupNode.type !== '__internal/group/instance') return false;
|
||||||
|
|
||||||
log.log('entering group', { nodeId, cameraPosition });
|
log.log('entering group', { nodeId });
|
||||||
|
|
||||||
const groupId = groupNode.props?.groupId as number;
|
const groupId = groupNode.props?.groupId as number;
|
||||||
const group = this.getGroup(groupId);
|
const group = this.getGroup(groupId);
|
||||||
@@ -657,7 +657,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
id: this.currentGroupId ?? this.id,
|
id: this.currentGroupId ?? this.id,
|
||||||
nodes: [...this.nodes.values()].map(n => serializeNode(n)),
|
nodes: [...this.nodes.values()].map(n => serializeNode(n)),
|
||||||
edges: [...this.edges.values()].map(e => serializeEdge(e)),
|
edges: [...this.edges.values()].map(e => serializeEdge(e)),
|
||||||
cameraPosition
|
nodeId
|
||||||
});
|
});
|
||||||
this.currentGroupId = groupId;
|
this.currentGroupId = groupId;
|
||||||
|
|
||||||
@@ -685,6 +685,8 @@ export class GraphManager extends EventEmitter<{
|
|||||||
this.history.reset();
|
this.history.reset();
|
||||||
this.execute();
|
this.execute();
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
|
return { nodeId: parent.nodeId };
|
||||||
}
|
}
|
||||||
|
|
||||||
createNodeId() {
|
createNodeId() {
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ export class GraphState {
|
|||||||
if (this.activeNodeId === -1) return;
|
if (this.activeNodeId === -1) return;
|
||||||
const node = this.graph.getNode(this.activeNodeId);
|
const node = this.graph.getNode(this.activeNodeId);
|
||||||
if (!node || node.type !== '__internal/group/instance') return;
|
if (!node || node.type !== '__internal/group/instance') return;
|
||||||
const ok = this.graph.enterGroup(this.activeNodeId, [...this.cameraPosition]);
|
const ok = this.graph.enterGroup(this.activeNodeId);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
this.activeNodeId = -1;
|
this.activeNodeId = -1;
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
@@ -375,7 +375,6 @@ export class GraphState {
|
|||||||
exitGroupNode() {
|
exitGroupNode() {
|
||||||
const result = this.graph.exitGroup();
|
const result = this.graph.exitGroup();
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
this.cameraPosition = result.camera;
|
|
||||||
this.activeNodeId = result.nodeId;
|
this.activeNodeId = result.nodeId;
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ type Color = { hue: number; saturation: number; lightness: number };
|
|||||||
|
|
||||||
export class ColorGenerator {
|
export class ColorGenerator {
|
||||||
private colors: Map<string, Color> = new Map();
|
private colors: Map<string, Color> = new Map();
|
||||||
private lightnessLevels = [10, 60];
|
// private lightnessLevels = [10, 60];
|
||||||
|
|
||||||
constructor(predefined: Record<string, Color>) {
|
constructor(predefined: Record<string, Color>) {
|
||||||
for (const [id, colorStr] of Object.entries(predefined)) {
|
for (const [id, colorStr] of Object.entries(predefined)) {
|
||||||
@@ -10,6 +10,14 @@ export class ColorGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getColors() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
this.colors.entries().map(([key, col]) => {
|
||||||
|
return [key, this.colorToHsl(col)];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public getColor(id: string): string {
|
public getColor(id: string): string {
|
||||||
if (this.colors.has(id)) {
|
if (this.colors.has(id)) {
|
||||||
return this.colorToHsl(this.colors.get(id)!);
|
return this.colorToHsl(this.colors.get(id)!);
|
||||||
|
|||||||
@@ -26,6 +26,13 @@
|
|||||||
const rightBump = $derived(
|
const rightBump = $derived(
|
||||||
!!nodeType?.outputs?.length && node.type !== '__internal/group/input'
|
!!nodeType?.outputs?.length && node.type !== '__internal/group/input'
|
||||||
);
|
);
|
||||||
|
const cornerBottom = $derived(
|
||||||
|
node.type === '__internal/group/input'
|
||||||
|
? (Object.keys(nodeType?.inputs ?? {}).length ? 0 : 10)
|
||||||
|
: node.type === '__internal/group/output'
|
||||||
|
? (nodeType?.outputs?.length ? 0 : 10)
|
||||||
|
: 0
|
||||||
|
);
|
||||||
|
|
||||||
const aspectRatio = 0.25;
|
const aspectRatio = 0.25;
|
||||||
|
|
||||||
@@ -35,6 +42,7 @@
|
|||||||
height: 34,
|
height: 34,
|
||||||
y: 49,
|
y: 49,
|
||||||
cornerTop,
|
cornerTop,
|
||||||
|
cornerBottom,
|
||||||
rightBump,
|
rightBump,
|
||||||
aspectRatio
|
aspectRatio
|
||||||
})
|
})
|
||||||
@@ -45,6 +53,7 @@
|
|||||||
height: 40,
|
height: 40,
|
||||||
y: 49,
|
y: 49,
|
||||||
cornerTop,
|
cornerTop,
|
||||||
|
cornerBottom,
|
||||||
rightBump,
|
rightBump,
|
||||||
aspectRatio
|
aspectRatio
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { GraphManager } from '$lib/graph-interface/graph-manager.svelte';
|
import type { GraphManager } from '$lib/graph-interface/graph-manager.svelte';
|
||||||
|
import { GraphState } from '$lib/graph-interface/graph-state.svelte';
|
||||||
import type { NodeInstance } from '@nodarium/types';
|
import type { NodeInstance } from '@nodarium/types';
|
||||||
import InputSelect from '../../../../../packages/ui/src/lib/inputs/InputSelect.svelte';
|
import { SocketTable } from '@nodarium/ui';
|
||||||
import UnusedGroupsPanel from './UnusedGroupsPanel.svelte';
|
import UnusedGroupsPanel from './UnusedGroupsPanel.svelte';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
manager: GraphManager;
|
manager: GraphManager;
|
||||||
|
graphState: GraphState;
|
||||||
node?: NodeInstance;
|
node?: NodeInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { manager, node = $bindable() }: Props = $props();
|
const { manager, graphState, node = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const activeGroup = $derived.by(() => {
|
const activeGroup = $derived.by(() => {
|
||||||
if (node?.type === '__internal/group/instance') {
|
if (node?.type === '__internal/group/instance') {
|
||||||
return manager.getGroup(node.props?.groupId as number);
|
let group = manager.getGroup(node.props?.groupId as number);
|
||||||
|
if (group) return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manager?.isInsideGroup) {
|
if (manager?.isInsideGroup && manager.currentGroupId !== null) {
|
||||||
const activeGroupId = manager.parentStack?.at(-1)?.id;
|
return manager.getGroup(manager.currentGroupId);
|
||||||
if (activeGroupId !== undefined) {
|
|
||||||
return manager.getGroup(activeGroupId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,6 +29,47 @@
|
|||||||
const name = (e.target as HTMLInputElement).value;
|
const name = (e.target as HTMLInputElement).value;
|
||||||
if (activeGroup) manager.renameGroup(activeGroup.id, name);
|
if (activeGroup) manager.renameGroup(activeGroup.id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRemoveInput(key: string) {
|
||||||
|
if (!activeGroup) return;
|
||||||
|
const group = manager.getGroup(activeGroup?.id);
|
||||||
|
const inputs = $state.snapshot(group?.inputs ?? {});
|
||||||
|
delete inputs[key];
|
||||||
|
activeGroup.inputs = inputs;
|
||||||
|
manager.nodes = manager.nodes;
|
||||||
|
manager.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = $derived(
|
||||||
|
Array.from(
|
||||||
|
new Set(
|
||||||
|
manager?.registry
|
||||||
|
? manager.registry.getAllNodes()
|
||||||
|
.flatMap(n =>
|
||||||
|
Object.values(n.inputs ?? {})
|
||||||
|
.map(v => v.type)
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let outputType = $derived(activeGroup?.outputs?.[0]?.type ?? 'unknown');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!activeGroup) return;
|
||||||
|
const group = manager.getGroup(activeGroup?.id);
|
||||||
|
const outputs = $state.snapshot(group?.outputs ?? []);
|
||||||
|
if (outputs?.[0]?.type === outputType) return;
|
||||||
|
activeGroup.outputs = [
|
||||||
|
{
|
||||||
|
label: outputs[0]?.label ?? 'Output',
|
||||||
|
type: outputType
|
||||||
|
}
|
||||||
|
];
|
||||||
|
manager.nodes = manager.nodes;
|
||||||
|
manager.save();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if activeGroup}
|
{#if activeGroup}
|
||||||
@@ -39,7 +80,7 @@
|
|||||||
|
|
||||||
{#if activeGroup}
|
{#if activeGroup}
|
||||||
{#key activeGroup.id}
|
{#key activeGroup.id}
|
||||||
<div class="group-settings">
|
<div class="p-4 group-settings">
|
||||||
<label for="group-name">Group name</label>
|
<label for="group-name">Group name</label>
|
||||||
<input
|
<input
|
||||||
id="group-name"
|
id="group-name"
|
||||||
@@ -51,18 +92,33 @@
|
|||||||
|
|
||||||
<label for="group-name">Group Inputs</label>
|
<label for="group-name">Group Inputs</label>
|
||||||
<div>
|
<div>
|
||||||
{#each Object.keys(activeGroup?.inputs ?? {}) as key (key)}
|
<SocketTable
|
||||||
<div class="flex">
|
{types}
|
||||||
<InputSelect
|
onremove={handleRemoveInput}
|
||||||
value={activeGroup.inputs?.[key].type}
|
bind:inputs={activeGroup.inputs}
|
||||||
options={['seed', 'float', 'boolean']}
|
colors={graphState?.colors?.getColors()}
|
||||||
/>
|
/>
|
||||||
<input type="text" placeholder="Input {key}" />
|
|
||||||
<button>
|
|
||||||
🥊
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label for="group-name mb-2">Group output</label>
|
||||||
|
<div class="flex bg-layer-2 rounded-sm outline outline-outline w-min">
|
||||||
|
<span
|
||||||
|
style:background={graphState?.colors?.getColor(outputType)}
|
||||||
|
class="block opacity-50 min-w-2 ml-2 w-2 h-2 my-auto rounded-sm"
|
||||||
|
></span>
|
||||||
|
<select
|
||||||
|
class="text-[0.9em] shrink-0 px-2 py-1 border-outline"
|
||||||
|
bind:value={outputType}
|
||||||
|
>
|
||||||
|
{#each types as type (type)}
|
||||||
|
<option>
|
||||||
|
<span
|
||||||
|
style="background: {graphState?.colors?.getColor(type)}; width: 5px; height: 5px; border-radius: 5px;"
|
||||||
|
></span>
|
||||||
|
{type}
|
||||||
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
@@ -77,10 +133,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.4em;
|
gap: 0.4em;
|
||||||
padding: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-settings label {
|
label {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if unusedTree.length}
|
{#if unusedTree.length}
|
||||||
<div class="panel">
|
<div class="panel p-4">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span>Unused groups</span>
|
<span>Unused groups</span>
|
||||||
<button class="remove-all" onclick={() => manager.removeUnusedGroups()}>
|
<button class="remove-all" onclick={() => manager.removeUnusedGroups()}>
|
||||||
@@ -78,8 +78,9 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.panel {
|
.panel {
|
||||||
|
border-top: 1px solid var(--color-outline);
|
||||||
|
margin-top: -1px;
|
||||||
border-bottom: 1px solid var(--color-outline);
|
border-bottom: 1px solid var(--color-outline);
|
||||||
padding: 0.75em 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|||||||
@@ -349,7 +349,7 @@
|
|||||||
/>
|
/>
|
||||||
{#key activeNode}
|
{#key activeNode}
|
||||||
<ActiveNodeSettings {manager} bind:node={activeNode} />
|
<ActiveNodeSettings {manager} bind:node={activeNode} />
|
||||||
<GroupSettings {manager} bind:node={activeNode} />
|
<GroupSettings graphState={graphInterface?.state} {manager} bind:node={activeNode} />
|
||||||
{/key}
|
{/key}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel
|
<Panel
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
@source inline("{hover:,}{bg-,outline-,text-,}layer-3{/30,/50,}");
|
@source inline("{hover:,}{bg-,outline-,text-,}layer-3{/30,/50,}");
|
||||||
@source inline("{hover:,}{bg-,outline-,text-,}active");
|
@source inline("{hover:,}{bg-,outline-,text-,}active");
|
||||||
@source inline("{hover:,}{bg-,outline-,text-,}selected");
|
@source inline("{hover:,}{bg-,outline-,text-,}selected");
|
||||||
@source inline("{hover:,}{bg-,outline-,text-,}outline{!,}");
|
@source inline("{hover:,}{bg-,outline-,text-,border-,divide-}outline{!,}");
|
||||||
@source inline("{hover:,}{bg-,outline-,text-,}connection");
|
@source inline("{hover:,}{bg-,outline-,text-,}connection");
|
||||||
@source inline("{hover:,}{bg-,outline-,text-,}text");
|
@source inline("{hover:,}{bg-,outline-,text-,}text");
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export { default as InputNumber } from './inputs/InputNumber.svelte';
|
|||||||
export { default as InputSelect } from './inputs/InputSelect.svelte';
|
export { default as InputSelect } from './inputs/InputSelect.svelte';
|
||||||
export { default as InputShape } from './inputs/InputShape.svelte';
|
export { default as InputShape } from './inputs/InputShape.svelte';
|
||||||
export { default as InputVec3 } from './inputs/InputVec3.svelte';
|
export { default as InputVec3 } from './inputs/InputVec3.svelte';
|
||||||
|
export { default as SocketTable } from './inputs/SocketTable.svelte';
|
||||||
|
|
||||||
export { default as Details } from './Details.svelte';
|
export { default as Details } from './Details.svelte';
|
||||||
export { default as JsonViewer } from './JsonViewer.svelte';
|
export { default as JsonViewer } from './JsonViewer.svelte';
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { NodeInput } from '@nodarium/types';
|
||||||
|
type Props = {
|
||||||
|
inputs?: Record<string, NodeInput>;
|
||||||
|
colors: Record<string, string>;
|
||||||
|
onremove?: (key: string) => void;
|
||||||
|
types: string[];
|
||||||
|
};
|
||||||
|
let { inputs = $bindable(), onremove, colors = {}, types = ['seed', 'float', 'path'] }: Props =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
let potentialRow = $state<
|
||||||
|
{
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
} | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
function showPotentialRow() {
|
||||||
|
potentialRow = {
|
||||||
|
type: types[0],
|
||||||
|
label: 'Input ' + Object.keys(inputs ?? {}).length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function realizePotentialRow() {
|
||||||
|
if (inputs) inputs[`input_${Object.keys(inputs).length}`] = potentialRow as NodeInput;
|
||||||
|
potentialRow = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(key?: string) {
|
||||||
|
if (!key) {
|
||||||
|
potentialRow = undefined;
|
||||||
|
} else if (inputs) {
|
||||||
|
onremove?.(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColor(type: string) {
|
||||||
|
if (type in colors) {
|
||||||
|
return colors[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '#f00';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet row(input: { type: string; label?: string }, remove: () => void, add?: () => void)}
|
||||||
|
<div class="flex min-w-0">
|
||||||
|
<span
|
||||||
|
style:background={getColor(input.type)}
|
||||||
|
data-type={input.type}
|
||||||
|
class="block opacity-50 min-w-2 ml-2 w-2 h-2 my-auto rounded-sm"
|
||||||
|
></span>
|
||||||
|
<select
|
||||||
|
class="text-[0.9em] border-r w-19 shrink-0 px-2 py-1 border-outline"
|
||||||
|
bind:value={input.type}
|
||||||
|
>
|
||||||
|
{#each types as type (type)}
|
||||||
|
<option>
|
||||||
|
<span
|
||||||
|
style="background: {getColor(type)}; width: 5px; height: 5px; border-radius: 5px;"
|
||||||
|
></span>
|
||||||
|
{type}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
class="px-2 grow min-w-30 border-r border-outline text-[0.9em]"
|
||||||
|
type="text"
|
||||||
|
bind:value={input.label}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="px-2 cursor-pointer opacity-50 hover:opacity-100 hover:bg-red-400"
|
||||||
|
onclick={remove}
|
||||||
|
aria-label="remove"
|
||||||
|
>
|
||||||
|
{#if add}
|
||||||
|
<span class="py-1 block i-[tabler--cancel]"></span>
|
||||||
|
{:else}
|
||||||
|
<span class="py-1 block i-[tabler--trash]"></span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if add}
|
||||||
|
<button
|
||||||
|
class="px-2 border-l hover:bg-green-300 opacity-50 hover:opacity-100 hover:text-layer-1 border-outline cursor-pointer"
|
||||||
|
onclick={add}
|
||||||
|
aria-label="add"
|
||||||
|
>
|
||||||
|
<span class="py-1 block i-[tabler--circle-plus]"></span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div class="rounded-sm overflow-hidden bg-layer-2 divide-y divide-outline outline-1 outline-outline">
|
||||||
|
{#each Object.entries(inputs ?? {}) as [key, input] (key)}
|
||||||
|
{@render row(input, () => removeRow(key))}
|
||||||
|
{/each}
|
||||||
|
{#if potentialRow}
|
||||||
|
<div class="opacity-80">
|
||||||
|
{@render row(potentialRow, () => removeRow(), () => realizePotentialRow())}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="opacity-40">
|
||||||
|
<div class="flex h-[27px]">
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
<button
|
||||||
|
class="border-l hover:bg-green-300 hover:text-layer-1 border-outline py-1 px-2 cursor-pointer"
|
||||||
|
onclick={() => showPotentialRow()}
|
||||||
|
aria-label="remove"
|
||||||
|
>
|
||||||
|
<span class="block i-[tabler--circle-plus]"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
JsonViewer,
|
JsonViewer,
|
||||||
ShortCut
|
ShortCut
|
||||||
} from '$lib';
|
} from '$lib';
|
||||||
|
import SocketTable from '$lib/inputs/SocketTable.svelte';
|
||||||
import Section from './Section.svelte';
|
import Section from './Section.svelte';
|
||||||
import Theme from './Theme.svelte';
|
import Theme from './Theme.svelte';
|
||||||
import ThemeSelector from './ThemeSelector.svelte';
|
import ThemeSelector from './ThemeSelector.svelte';
|
||||||
@@ -38,6 +39,17 @@
|
|||||||
settings: { seed: 42, enabled: true }
|
settings: { seed: 42, enabled: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let socketTypes = $state({
|
||||||
|
input_0: {
|
||||||
|
'label': 'Input 0',
|
||||||
|
'type': 'path'
|
||||||
|
},
|
||||||
|
input_1: {
|
||||||
|
'label': 'Input 1',
|
||||||
|
'type': 'float'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function randomlyUpdateJson() {
|
function randomlyUpdateJson() {
|
||||||
const rand = Math.floor(Math.random() * 5);
|
const rand = Math.floor(Math.random() * 5);
|
||||||
if (rand === 0) {
|
if (rand === 0) {
|
||||||
@@ -150,6 +162,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Socket Table">
|
||||||
|
<SocketTable
|
||||||
|
colors={{
|
||||||
|
seed: '#f00',
|
||||||
|
float: '#0f0',
|
||||||
|
path: '#00f'
|
||||||
|
}}
|
||||||
|
types={['seed', 'float', 'path']}
|
||||||
|
bind:inputs={socketTypes}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section title="Shortcut">
|
<Section title="Shortcut">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<ShortCut ctrl key="S" />
|
<ShortCut ctrl key="S" />
|
||||||
|
|||||||
@@ -1,3 +1,60 @@
|
|||||||
|
interface LogEntry {
|
||||||
|
time: string;
|
||||||
|
scope: string;
|
||||||
|
level: string;
|
||||||
|
args: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const logBuffer: LogEntry[] = [];
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
function formatTime(): string {
|
||||||
|
const ms = Date.now() - startTime;
|
||||||
|
const h = Math.floor(ms / 3600000).toString().padStart(2, '0');
|
||||||
|
const m = Math.floor((ms % 3600000) / 60000).toString().padStart(2, '0');
|
||||||
|
const s = Math.floor((ms % 60000) / 1000).toString().padStart(2, '0');
|
||||||
|
const mss = (ms % 1000).toString().padStart(3, '0');
|
||||||
|
return `${h}:${m}:${s}.${mss}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serialize(arg: unknown): string {
|
||||||
|
if (typeof arg === 'string') return arg;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(arg);
|
||||||
|
} catch {
|
||||||
|
return String(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEntry(entry: LogEntry, scopeWidth: number): string {
|
||||||
|
const scope = `[${entry.scope}]`.padEnd(scopeWidth + 2);
|
||||||
|
const level = entry.level.toUpperCase().padEnd(5);
|
||||||
|
const msg = entry.args.map(serialize).join(' ');
|
||||||
|
return `${entry.time} ${scope} ${level} ${msg}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
(globalThis as Record<string, unknown>).copyLogs = () => {
|
||||||
|
if (logBuffer.length === 0) {
|
||||||
|
console.log('%c[logger] No log entries to copy', 'color: #888');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scopeWidth = logBuffer.reduce((max, e) => Math.max(max, e.scope.length), 0);
|
||||||
|
const lines = [
|
||||||
|
`=== Log Export (${logBuffer.length} entries) ===`,
|
||||||
|
'',
|
||||||
|
...logBuffer.map(e => formatEntry(e, scopeWidth))
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(lines).then(() => {
|
||||||
|
console.log(`%c[logger] Copied ${logBuffer.length} entries to clipboard`, 'color: #4f4');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
(globalThis as Record<string, unknown>).clearLogs = () => {
|
||||||
|
logBuffer.length = 0;
|
||||||
|
console.log('%c[logger] Log buffer cleared', 'color: #888');
|
||||||
|
};
|
||||||
|
|
||||||
export const createLogger = (() => {
|
export const createLogger = (() => {
|
||||||
let maxLength = 5;
|
let maxLength = 5;
|
||||||
return (scope: string) => {
|
return (scope: string) => {
|
||||||
@@ -6,18 +63,35 @@ export const createLogger = (() => {
|
|||||||
|
|
||||||
let isGrouped = false;
|
let isGrouped = false;
|
||||||
|
|
||||||
function s(color: string, ...args: any) {
|
function s(color: string, ...args: unknown[]) {
|
||||||
return isGrouped
|
return isGrouped
|
||||||
? [...args]
|
? [...args]
|
||||||
: [`[%c${scope.padEnd(maxLength, ' ')}]:`, `color: ${color}`, ...args];
|
: [`[%c${scope.padEnd(maxLength, ' ')}]:`, `color: ${color}`, ...args];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function record(level: string, args: unknown[]) {
|
||||||
|
logBuffer.push({ time: formatTime(), scope, level, args });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log: (...args: any[]) => !muted && console.log(...s('#888', ...args)),
|
log: (...args: unknown[]) => {
|
||||||
info: (...args: any[]) => !muted && console.info(...s('#888', ...args)),
|
record('log', args);
|
||||||
warn: (...args: any[]) => !muted && console.warn(...s('#888', ...args)),
|
!muted && console.log(...s('#888', ...args));
|
||||||
error: (...args: any[]) => console.error(...s('#f88', ...args)),
|
},
|
||||||
group: (...args: any[]) => {
|
info: (...args: unknown[]) => {
|
||||||
|
record('info', args);
|
||||||
|
!muted && console.info(...s('#888', ...args));
|
||||||
|
},
|
||||||
|
warn: (...args: unknown[]) => {
|
||||||
|
record('warn', args);
|
||||||
|
!muted && console.warn(...s('#888', ...args));
|
||||||
|
},
|
||||||
|
error: (...args: unknown[]) => {
|
||||||
|
record('error', args);
|
||||||
|
console.error(...s('#f88', ...args));
|
||||||
|
},
|
||||||
|
group: (...args: unknown[]) => {
|
||||||
|
record('group', args);
|
||||||
if (!muted) {
|
if (!muted) {
|
||||||
console.groupCollapsed(...s('#888', ...args));
|
console.groupCollapsed(...s('#888', ...args));
|
||||||
isGrouped = true;
|
isGrouped = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user