diff --git a/.changeset/petrinaut-sdcpn-capabilities.md b/.changeset/petrinaut-sdcpn-capabilities.md new file mode 100644 index 00000000000..6ae54f68a77 --- /dev/null +++ b/.changeset/petrinaut-sdcpn-capabilities.md @@ -0,0 +1,6 @@ +--- +"@hashintel/petrinaut": patch +"@hashintel/petrinaut-core": patch +--- + +Add handle capabilities for disabling SDCPN extensions and global parameters. diff --git a/libs/@hashintel/petrinaut-core/src/actions.test.ts b/libs/@hashintel/petrinaut-core/src/actions.test.ts index 21f71f2651d..3f71eedb7c4 100644 --- a/libs/@hashintel/petrinaut-core/src/actions.test.ts +++ b/libs/@hashintel/petrinaut-core/src/actions.test.ts @@ -265,6 +265,79 @@ describe("Petrinaut core actions", () => { expect(instance.definition.get().places).toEqual([]); }); + test("honors disabled extension capabilities", () => { + const instance = createPetrinaut({ + document: createJsonDocHandle({ + initial: cloneSDCPN(emptySDCPN), + capabilities: { + disabledExtensions: [ + "colors", + "stochasticity", + "dynamics", + "parameters", + ], + }, + }), + }); + + instance.mutations.addType({ + id: "type-1", + name: "Particle", + iconSlug: "circle", + displayColor: "#34a0fa", + elements: [], + }); + instance.mutations.addDifferentialEquation({ + id: "equation-1", + name: "Motion", + colorId: "type-1", + code: "export default Dynamics(() => []);", + }); + instance.mutations.addPlace({ + id: "place-1", + name: "Dynamic", + colorId: "type-1", + dynamicsEnabled: true, + differentialEquationId: "equation-1", + visualizerCode: "export default Visualization(() => null);", + x: 0, + y: 0, + }); + instance.mutations.addTransition({ + id: "transition-1", + name: "Move", + inputArcs: [], + outputArcs: [], + lambdaType: "stochastic", + lambdaCode: "export default Lambda(() => 1);", + transitionKernelCode: "export default TransitionKernel(() => ({}));", + x: 0, + y: 0, + }); + instance.mutations.addParameter({ + id: "parameter-1", + name: "Rate", + variableName: "rate", + type: "real", + defaultValue: "1", + }); + + const definition = instance.definition.get(); + expect(definition.types).toEqual([]); + expect(definition.differentialEquations).toEqual([]); + expect(definition.parameters).toEqual([]); + expect(definition.places[0]).toMatchObject({ + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + }); + expect(definition.places[0]).not.toHaveProperty("visualizerCode"); + expect(definition.transitions[0]).toMatchObject({ + lambdaType: "predicate", + transitionKernelCode: "", + }); + }); + test("validates add action inputs before mutating", () => { const instance = createInstance(); diff --git a/libs/@hashintel/petrinaut-core/src/actions.ts b/libs/@hashintel/petrinaut-core/src/actions.ts index 783fc4a448c..5a9123eb59a 100644 --- a/libs/@hashintel/petrinaut-core/src/actions.ts +++ b/libs/@hashintel/petrinaut-core/src/actions.ts @@ -10,6 +10,13 @@ import { type MutationActionInput, } from "./action-schemas"; import { generateArcId } from "./arc-id"; +import { + DEFAULT_PETRINAUT_EXTENSIONS, + sanitizePlaceForExtensions, + sanitizeTransitionForExtensions, + stripDisabledExtensionData, + type PetrinautExtensionSettings, +} from "./extensions"; import type { SDCPN } from "./types/sdcpn"; @@ -58,21 +65,40 @@ function assertPlaceDynamicsReferences( export function createPetrinautActions( mutate: (fn: (sdcpn: SDCPN) => void) => void, + extensions: PetrinautExtensionSettings = DEFAULT_PETRINAUT_EXTENSIONS, ): MutationHelperFunctions { + const canUseColors = extensions.colors; + const canUseDynamics = extensions.colors && extensions.dynamics; + const canUseParameters = extensions.parameters; + + const mutateWithExtensionGuards = (fn: (sdcpn: SDCPN) => void): void => { + mutate((sdcpn) => { + fn(sdcpn); + stripDisabledExtensionData(sdcpn, extensions); + }); + }; + return { addPlace(place) { - const parsedPlace = placeSchema.parse(place); - mutate((sdcpn) => { + const parsedPlace = sanitizePlaceForExtensions( + placeSchema.parse(place), + extensions, + ); + mutateWithExtensionGuards((sdcpn) => { assertPlaceDynamicsReferences(parsedPlace, sdcpn.differentialEquations); sdcpn.places.push(parsedPlace); }); }, updatePlace(input) { const parsed = mutationActionInputSchemas.updatePlace.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const place of sdcpn.places) { if (place.id === parsed.placeId) { Object.assign(place, parsed.update); + Object.assign( + place, + sanitizePlaceForExtensions(place, extensions), + ); placeSchema.parse(place); assertPlaceDynamicsReferences(place, sdcpn.differentialEquations); break; @@ -83,7 +109,7 @@ export function createPetrinautActions( updatePlacePosition(input) { const parsed = mutationActionInputSchemas.updatePlacePosition.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const place of sdcpn.places) { if (place.id === parsed.placeId) { place.x = parsed.position.x; @@ -96,7 +122,7 @@ export function createPetrinautActions( removePlace(input) { const { placeId: parsedPlaceId } = mutationActionInputSchemas.removePlace.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const [placeIndex, place] of sdcpn.places.entries()) { if (place.id === parsedPlaceId) { sdcpn.places.splice(placeIndex, 1); @@ -119,17 +145,24 @@ export function createPetrinautActions( }); }, addTransition(transition) { - const parsedTransition = transitionSchema.parse(transition); - mutate((sdcpn) => { + const parsedTransition = sanitizeTransitionForExtensions( + transitionSchema.parse(transition), + extensions, + ); + mutateWithExtensionGuards((sdcpn) => { sdcpn.transitions.push(parsedTransition); }); }, updateTransition(input) { const parsed = mutationActionInputSchemas.updateTransition.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === parsed.transitionId) { Object.assign(transition, parsed.update); + Object.assign( + transition, + sanitizeTransitionForExtensions(transition, extensions), + ); transitionSchema.parse(transition); break; } @@ -139,7 +172,7 @@ export function createPetrinautActions( updateTransitionPosition(input) { const parsed = mutationActionInputSchemas.updateTransitionPosition.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === parsed.transitionId) { transition.x = parsed.position.x; @@ -152,7 +185,7 @@ export function createPetrinautActions( removeTransition(input) { const { transitionId: parsedTransitionId } = mutationActionInputSchemas.removeTransition.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const [index, transition] of sdcpn.transitions.entries()) { if (transition.id === parsedTransitionId) { sdcpn.transitions.splice(index, 1); @@ -163,7 +196,7 @@ export function createPetrinautActions( }, addArc(input) { const parsed = mutationActionInputSchemas.addArc.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === parsed.transitionId) { if (parsed.arcDirection === "input") { @@ -185,7 +218,7 @@ export function createPetrinautActions( }, removeArc(input) { const parsed = mutationActionInputSchemas.removeArc.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === parsed.transitionId) { for (const [index, arc] of transition[ @@ -205,7 +238,7 @@ export function createPetrinautActions( }, updateArcWeight(input) { const parsed = mutationActionInputSchemas.updateArcWeight.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === parsed.transitionId) { for (const arc of transition[ @@ -223,7 +256,7 @@ export function createPetrinautActions( }, updateArcType(input) { const parsed = mutationActionInputSchemas.updateArcType.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === parsed.transitionId) { for (const arc of transition.inputArcs) { @@ -239,7 +272,7 @@ export function createPetrinautActions( }, updateArcPlace(input) { const parsed = mutationActionInputSchemas.updateArcPlace.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const transition of sdcpn.transitions) { if (transition.id === parsed.transitionId) { for (const arc of transition[ @@ -257,13 +290,19 @@ export function createPetrinautActions( }, addType(type) { const parsedType = colorSchema.parse(type); - mutate((sdcpn) => { + if (!canUseColors) { + return; + } + mutateWithExtensionGuards((sdcpn) => { sdcpn.types.push(parsedType); }); }, updateType(input) { const parsed = mutationActionInputSchemas.updateType.parse(input); - mutate((sdcpn) => { + if (!canUseColors) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const type of sdcpn.types) { if (type.id === parsed.typeId) { Object.assign(type, parsed.update); @@ -275,7 +314,10 @@ export function createPetrinautActions( }, addTypeElement(input) { const parsed = mutationActionInputSchemas.addTypeElement.parse(input); - mutate((sdcpn) => { + if (!canUseColors) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const type of sdcpn.types) { if (type.id === parsed.typeId) { type.elements.push(parsed.element); @@ -287,7 +329,10 @@ export function createPetrinautActions( }, updateTypeElement(input) { const parsed = mutationActionInputSchemas.updateTypeElement.parse(input); - mutate((sdcpn) => { + if (!canUseColors) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const type of sdcpn.types) { if (type.id === parsed.typeId) { for (const element of type.elements) { @@ -304,7 +349,10 @@ export function createPetrinautActions( }, removeTypeElement(input) { const parsed = mutationActionInputSchemas.removeTypeElement.parse(input); - mutate((sdcpn) => { + if (!canUseColors) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const type of sdcpn.types) { if (type.id === parsed.typeId) { for (const [index, element] of type.elements.entries()) { @@ -321,7 +369,10 @@ export function createPetrinautActions( }, moveTypeElement(input) { const parsed = mutationActionInputSchemas.moveTypeElement.parse(input); - mutate((sdcpn) => { + if (!canUseColors) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const type of sdcpn.types) { if (type.id === parsed.typeId) { const fromIndex = type.elements.findIndex( @@ -343,7 +394,10 @@ export function createPetrinautActions( removeType(input) { const { typeId: parsedTypeId } = mutationActionInputSchemas.removeType.parse(input); - mutate((sdcpn) => { + if (!canUseColors) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const [index, type] of sdcpn.types.entries()) { if (type.id === parsedTypeId) { sdcpn.types.splice(index, 1); @@ -364,7 +418,10 @@ export function createPetrinautActions( }, addDifferentialEquation(equation) { const parsedEquation = differentialEquationSchema.parse(equation); - mutate((sdcpn) => { + if (!canUseDynamics) { + return; + } + mutateWithExtensionGuards((sdcpn) => { sdcpn.differentialEquations.push(parsedEquation); for (const place of sdcpn.places) { if (place.differentialEquationId === parsedEquation.id) { @@ -376,7 +433,10 @@ export function createPetrinautActions( updateDifferentialEquation(input) { const parsed = mutationActionInputSchemas.updateDifferentialEquation.parse(input); - mutate((sdcpn) => { + if (!canUseDynamics) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const equation of sdcpn.differentialEquations) { if (equation.id === parsed.equationId) { Object.assign(equation, parsed.update); @@ -399,7 +459,10 @@ export function createPetrinautActions( mutationActionInputSchemas.removeDifferentialEquation.parse({ ...input, }); - mutate((sdcpn) => { + if (!canUseDynamics) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const [index, equation] of sdcpn.differentialEquations.entries()) { if (equation.id === parsedEquationId) { sdcpn.differentialEquations.splice(index, 1); @@ -415,13 +478,19 @@ export function createPetrinautActions( }, addParameter(parameter) { const parsedParameter = parameterSchema.parse(parameter); - mutate((sdcpn) => { + if (!canUseParameters) { + return; + } + mutateWithExtensionGuards((sdcpn) => { sdcpn.parameters.push(parsedParameter); }); }, updateParameter(input) { const parsed = mutationActionInputSchemas.updateParameter.parse(input); - mutate((sdcpn) => { + if (!canUseParameters) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const parameter of sdcpn.parameters) { if (parameter.id === parsed.parameterId) { Object.assign(parameter, parsed.update); @@ -434,7 +503,10 @@ export function createPetrinautActions( removeParameter(input) { const { parameterId: parsedParameterId } = mutationActionInputSchemas.removeParameter.parse(input); - mutate((sdcpn) => { + if (!canUseParameters) { + return; + } + mutateWithExtensionGuards((sdcpn) => { for (const [index, parameter] of sdcpn.parameters.entries()) { if (parameter.id === parsedParameterId) { sdcpn.parameters.splice(index, 1); @@ -445,7 +517,7 @@ export function createPetrinautActions( }, addScenario(scenario) { const parsedScenario = scenarioSchema.parse(scenario); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { const scenarios = sdcpn.scenarios ?? []; scenarios.push(parsedScenario); // eslint-disable-next-line no-param-reassign -- mutating draft inside immer/structuredClone @@ -454,7 +526,7 @@ export function createPetrinautActions( }, updateScenario(input) { const parsed = mutationActionInputSchemas.updateScenario.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const scenario of sdcpn.scenarios ?? []) { if (scenario.id === parsed.scenarioId) { Object.assign(scenario, parsed.update); @@ -467,7 +539,7 @@ export function createPetrinautActions( removeScenario(input) { const { scenarioId: parsedScenarioId } = mutationActionInputSchemas.removeScenario.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { const scenarios = sdcpn.scenarios; if (!scenarios) { return; @@ -482,7 +554,7 @@ export function createPetrinautActions( }, addMetric(metric) { const parsedMetric = metricSchema.parse(metric); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { const metrics = sdcpn.metrics ?? []; metrics.push(parsedMetric); // eslint-disable-next-line no-param-reassign -- mutating draft inside immer/structuredClone @@ -491,7 +563,7 @@ export function createPetrinautActions( }, updateMetric(input) { const parsed = mutationActionInputSchemas.updateMetric.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const metric of sdcpn.metrics ?? []) { if (metric.id === parsed.metricId) { Object.assign(metric, parsed.update); @@ -504,7 +576,7 @@ export function createPetrinautActions( removeMetric(input) { const { metricId: parsedMetricId } = mutationActionInputSchemas.removeMetric.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { const metrics = sdcpn.metrics; if (!metrics) { return; @@ -520,7 +592,7 @@ export function createPetrinautActions( deleteItemsByIds(input) { const parsedItems = mutationActionInputSchemas.deleteItemsByIds.parse(input).items; - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { const placeIds = new Set(); const transitionIds = new Set(); const arcIds = new Set(); @@ -649,7 +721,7 @@ export function createPetrinautActions( commitNodePositions(input) { const { commits: parsedCommits } = mutationActionInputSchemas.commitNodePositions.parse(input); - mutate((sdcpn) => { + mutateWithExtensionGuards((sdcpn) => { for (const { id, itemType, position } of parsedCommits) { if (itemType === "place") { for (const place of sdcpn.places) { diff --git a/libs/@hashintel/petrinaut-core/src/commands.test.ts b/libs/@hashintel/petrinaut-core/src/commands.test.ts index af4659e08a0..bca90864dce 100644 --- a/libs/@hashintel/petrinaut-core/src/commands.test.ts +++ b/libs/@hashintel/petrinaut-core/src/commands.test.ts @@ -66,6 +66,70 @@ describe("applyClipboardPaste", () => { expect(instance.definition.get().places[0]!.id).toBe(pastedPlace!.id); }); + test("strips pasted items for disabled extensions", () => { + const instance = createPetrinaut({ + document: createJsonDocHandle({ + initial: JSON.parse(JSON.stringify(emptySDCPN)) as SDCPN, + capabilities: { + disabledExtensions: ["colors", "dynamics", "parameters"], + }, + }), + }); + + const payload = buildClipboardPayload({ + places: [ + { + id: "place-1", + name: "Dynamic", + colorId: "type-1", + dynamicsEnabled: true, + differentialEquationId: "equation-1", + x: 0, + y: 0, + }, + ], + types: [ + { + id: "type-1", + name: "Particle", + iconSlug: "circle", + displayColor: "#34a0fa", + elements: [], + }, + ], + differentialEquations: [ + { + id: "equation-1", + name: "Motion", + colorId: "type-1", + code: "export default Dynamics(() => []);", + }, + ], + parameters: [ + { + id: "parameter-1", + name: "Rate", + variableName: "rate", + type: "real", + defaultValue: "1", + }, + ], + }); + + const { newItemIds } = instance.commands.applyClipboardPaste({ payload }); + const definition = instance.definition.get(); + + expect(newItemIds.map((item) => item.type)).toEqual(["place"]); + expect(definition.types).toEqual([]); + expect(definition.differentialEquations).toEqual([]); + expect(definition.parameters).toEqual([]); + expect(definition.places[0]).toMatchObject({ + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + }); + }); + test("throws when the payload fails schema validation", () => { const instance = createInstance(); diff --git a/libs/@hashintel/petrinaut-core/src/commands.ts b/libs/@hashintel/petrinaut-core/src/commands.ts index 3a698612a5f..18118a1fef0 100644 --- a/libs/@hashintel/petrinaut-core/src/commands.ts +++ b/libs/@hashintel/petrinaut-core/src/commands.ts @@ -7,6 +7,12 @@ import { } from "./action-schemas"; import { pastePayloadIntoSDCPN } from "./clipboard/paste"; import { commandActionInputSchemas } from "./command-schemas"; +import { + DEFAULT_PETRINAUT_EXTENSIONS, + isSelectionTypeAvailableForExtensions, + stripDisabledExtensionData, + type PetrinautExtensionSettings, +} from "./extensions"; import { calculateGraphLayout } from "./layout/calculate-graph-layout"; import { layoutNodeDimensions } from "./layout/dimensions"; @@ -114,6 +120,7 @@ const validateNewlyPastedItems = ( export function createPetrinautCommands( mutate: (fn: (sdcpn: SDCPN) => void) => void, read: () => SDCPN, + extensions: PetrinautExtensionSettings = DEFAULT_PETRINAUT_EXTENSIONS, ): CommandHelperFunctions { return { applyClipboardPaste(input) { @@ -122,7 +129,10 @@ export function createPetrinautCommands( let newItemIds: Array<{ type: string; id: string }> = []; mutate((sdcpn) => { const result = pastePayloadIntoSDCPN(sdcpn, payload); - newItemIds = result.newItemIds; + stripDisabledExtensionData(sdcpn, extensions); + newItemIds = result.newItemIds.filter((item) => + isSelectionTypeAvailableForExtensions(item.type, extensions), + ); validateNewlyPastedItems(sdcpn, newItemIds); }); return { newItemIds }; diff --git a/libs/@hashintel/petrinaut-core/src/extensions.ts b/libs/@hashintel/petrinaut-core/src/extensions.ts new file mode 100644 index 00000000000..3b624b4bb5b --- /dev/null +++ b/libs/@hashintel/petrinaut-core/src/extensions.ts @@ -0,0 +1,169 @@ +import { generateDefaultLambdaCode } from "./default-codes"; + +import type { SDCPN } from "./types/sdcpn"; + +export const PETRINAUT_EXTENSION_NAMES = [ + "colors", + "stochasticity", + "dynamics", + "parameters", +] as const; + +export type PetrinautExtension = (typeof PETRINAUT_EXTENSION_NAMES)[number]; + +export type PetrinautExtensionSettings = Readonly< + Record +>; + +export type PetrinautHandleCapabilities = { + /** + * Whether the document handle should be treated as read-only by Petrinaut. + * Host-level readonly config can still make a writable handle read-only. + */ + readonly?: boolean; + /** + * Petri-net extensions unavailable for this handle. Omitted means all + * extensions are enabled. + */ + disabledExtensions?: readonly PetrinautExtension[]; +}; + +export type ResolvedPetrinautHandleCapabilities = { + readonly: boolean; + disabledExtensions: readonly PetrinautExtension[]; + extensions: PetrinautExtensionSettings; +}; + +export const DEFAULT_PETRINAUT_EXTENSIONS: PetrinautExtensionSettings = { + colors: true, + stochasticity: true, + dynamics: true, + parameters: true, +}; + +export const resolvePetrinautHandleCapabilities = ( + capabilities?: PetrinautHandleCapabilities, +): ResolvedPetrinautHandleCapabilities => { + const disabled = new Set( + capabilities?.disabledExtensions ?? [], + ); + + if (disabled.has("colors")) { + disabled.add("dynamics"); + } + + const disabledExtensions = PETRINAUT_EXTENSION_NAMES.filter((extension) => + disabled.has(extension), + ); + + return { + readonly: capabilities?.readonly ?? false, + disabledExtensions, + extensions: { + colors: !disabled.has("colors"), + stochasticity: !disabled.has("stochasticity"), + dynamics: !disabled.has("colors") && !disabled.has("dynamics"), + parameters: !disabled.has("parameters"), + }, + }; +}; + +const canUseDynamics = (extensions: PetrinautExtensionSettings): boolean => + extensions.colors && extensions.dynamics; + +export const isSelectionTypeAvailableForExtensions = ( + type: string, + extensions: PetrinautExtensionSettings, +): boolean => { + if (type === "type") { + return extensions.colors; + } + if (type === "differentialEquation") { + return canUseDynamics(extensions); + } + if (type === "parameter") { + return extensions.parameters; + } + return true; +}; + +export const sanitizePlaceForExtensions = ( + place: Place, + extensions: PetrinautExtensionSettings, +): Place => { + if (extensions.colors && extensions.dynamics) { + return place; + } + + return { + ...place, + colorId: extensions.colors ? place.colorId : null, + dynamicsEnabled: canUseDynamics(extensions) + ? place.dynamicsEnabled + : false, + differentialEquationId: canUseDynamics(extensions) + ? place.differentialEquationId + : null, + visualizerCode: extensions.colors ? place.visualizerCode : undefined, + }; +}; + +export const sanitizeTransitionForExtensions = < + Transition extends SDCPN["transitions"][number], +>( + transition: Transition, + extensions: PetrinautExtensionSettings, +): Transition => { + if (extensions.stochasticity && extensions.colors) { + return transition; + } + + return { + ...transition, + lambdaType: + extensions.stochasticity || transition.lambdaType !== "stochastic" + ? transition.lambdaType + : "predicate", + lambdaCode: + extensions.stochasticity || transition.lambdaType !== "stochastic" + ? transition.lambdaCode + : generateDefaultLambdaCode("predicate"), + transitionKernelCode: extensions.colors + ? transition.transitionKernelCode + : "", + }; +}; + +export const stripDisabledExtensionData = ( + sdcpn: SDCPN, + extensions: PetrinautExtensionSettings, +): void => { + if (!extensions.colors) { + sdcpn.types.splice(0); + } + + if (!canUseDynamics(extensions)) { + sdcpn.differentialEquations.splice(0); + } + + if (!extensions.parameters) { + sdcpn.parameters.splice(0); + for (const scenario of sdcpn.scenarios ?? []) { + scenario.parameterOverrides = {}; + } + } + + for (const place of sdcpn.places) { + Object.assign(place, sanitizePlaceForExtensions(place, extensions)); + if (!extensions.colors) { + delete place.visualizerCode; + } + } + + for (const transition of sdcpn.transitions) { + Object.assign( + transition, + sanitizeTransitionForExtensions(transition, extensions), + ); + } +}; diff --git a/libs/@hashintel/petrinaut-core/src/handle.test.ts b/libs/@hashintel/petrinaut-core/src/handle.test.ts index 24b5c4755c5..f859676d380 100644 --- a/libs/@hashintel/petrinaut-core/src/handle.test.ts +++ b/libs/@hashintel/petrinaut-core/src/handle.test.ts @@ -56,6 +56,43 @@ describe("createJsonDocHandle", () => { }); expect(listener).not.toHaveBeenCalled(); }); + + it("can expose read-only capabilities and ignore direct changes", () => { + const handle = createJsonDocHandle({ + initial: empty(), + capabilities: { + readonly: true, + disabledExtensions: [ + "colors", + "stochasticity", + "dynamics", + "parameters", + ], + }, + }); + const listener = vi.fn(); + handle.subscribe(listener); + + handle.change((draft) => { + draft.types.push({ + id: "t1", + name: "Color 1", + iconSlug: "circle", + displayColor: "#FF0000", + elements: [], + }); + }); + + expect(handle.capabilities?.readonly).toBe(true); + expect(handle.capabilities?.disabledExtensions).toEqual([ + "colors", + "stochasticity", + "dynamics", + "parameters", + ]); + expect(handle.doc()?.types).toHaveLength(0); + expect(listener).not.toHaveBeenCalled(); + }); }); describe("createPetrinaut", () => { @@ -116,6 +153,51 @@ describe("createPetrinaut", () => { expect(instance.definition.get().types).toHaveLength(0); }); + + it("derives readonly and extension settings from handle capabilities", () => { + const handle = createJsonDocHandle({ + initial: empty(), + capabilities: { + readonly: true, + disabledExtensions: [ + "colors", + "stochasticity", + "dynamics", + "parameters", + ], + }, + }); + const instance = createPetrinaut({ document: handle }); + + expect(instance.readonly).toBe(true); + expect(instance.extensions).toEqual({ + colors: false, + stochasticity: false, + dynamics: false, + parameters: false, + }); + }); + + it("disables dynamics whenever colors are disabled", () => { + const handle = createJsonDocHandle({ + initial: empty(), + capabilities: { + disabledExtensions: ["colors"], + }, + }); + const instance = createPetrinaut({ document: handle }); + + expect(instance.capabilities.disabledExtensions).toEqual([ + "colors", + "dynamics", + ]); + expect(instance.extensions).toEqual({ + colors: false, + stochasticity: true, + dynamics: false, + parameters: true, + }); + }); }); describe("PetrinautDocHandle history", () => { diff --git a/libs/@hashintel/petrinaut-core/src/handle.ts b/libs/@hashintel/petrinaut-core/src/handle.ts index c5f8e3c2030..253023ce51e 100644 --- a/libs/@hashintel/petrinaut-core/src/handle.ts +++ b/libs/@hashintel/petrinaut-core/src/handle.ts @@ -6,6 +6,7 @@ import { } from "immer"; import type { SDCPN } from "./types/sdcpn"; +import type { PetrinautHandleCapabilities } from "./extensions"; enablePatches(); @@ -59,6 +60,7 @@ export interface PetrinautHistory { export interface PetrinautDocHandle { readonly id: DocumentId; + readonly capabilities?: PetrinautHandleCapabilities; readonly state: ReadableStore; whenReady(): Promise; doc(): SDCPN | undefined; @@ -119,6 +121,7 @@ const DEFAULT_HISTORY_LIMIT = 50; export type CreateJsonDocHandleOptions = { id?: DocumentId; initial: SDCPN; + capabilities?: PetrinautHandleCapabilities; /** * Maximum number of history checkpoints retained. Older entries are dropped * once the limit is exceeded. Pass `0` to disable history entirely. @@ -132,6 +135,7 @@ export function createJsonDocHandle( ): PetrinautDocHandle { const id = opts.id ?? generateId(); const historyLimit = opts.historyLimit ?? DEFAULT_HISTORY_LIMIT; + const capabilities = opts.capabilities; const stateStore = createReadableStore("ready"); const subscribers = new Set<(event: DocChangeEvent) => void>(); @@ -265,10 +269,14 @@ export function createJsonDocHandle( return { id, + capabilities, state: stateStore, whenReady: () => Promise.resolve(), doc: () => current, change(fn) { + if (capabilities?.readonly) { + return; + } const [next, patches, inversePatches] = produceWithPatches( current, (draft) => { diff --git a/libs/@hashintel/petrinaut-core/src/index.ts b/libs/@hashintel/petrinaut-core/src/index.ts index a01a81e0744..7d1dd4a3d21 100644 --- a/libs/@hashintel/petrinaut-core/src/index.ts +++ b/libs/@hashintel/petrinaut-core/src/index.ts @@ -16,6 +16,18 @@ export { type PetrinautPatch, type ReadableStore, } from "./handle"; +export { + DEFAULT_PETRINAUT_EXTENSIONS, + PETRINAUT_EXTENSION_NAMES, + isSelectionTypeAvailableForExtensions, + resolvePetrinautHandleCapabilities, +} from "./extensions"; +export type { + PetrinautExtension, + PetrinautExtensionSettings, + PetrinautHandleCapabilities, + ResolvedPetrinautHandleCapabilities, +} from "./extensions"; // --- Instance --- export { createPetrinaut } from "./instance"; diff --git a/libs/@hashintel/petrinaut-core/src/instance.ts b/libs/@hashintel/petrinaut-core/src/instance.ts index f4945d789d6..23cd24b3777 100644 --- a/libs/@hashintel/petrinaut-core/src/instance.ts +++ b/libs/@hashintel/petrinaut-core/src/instance.ts @@ -6,12 +6,17 @@ import { type CommandHelperFunctions, createPetrinautCommands, } from "./commands"; +import { resolvePetrinautHandleCapabilities } from "./extensions"; import type { PetrinautDocHandle, PetrinautPatch, ReadableStore, } from "./handle"; +import type { + PetrinautExtensionSettings, + ResolvedPetrinautHandleCapabilities, +} from "./extensions"; import type { SDCPN } from "./types/sdcpn"; const EMPTY_SDCPN: SDCPN = { @@ -59,6 +64,10 @@ export type Petrinaut = { /** Patch event stream. Only fires for handles that produce patches. */ readonly patches: EventStream; + readonly capabilities: ResolvedPetrinautHandleCapabilities; + + readonly extensions: PetrinautExtensionSettings; + /** Atomic, schema-driven mutations. */ readonly mutations: PetrinautMutations; @@ -125,6 +134,13 @@ function createPatchStream( export function createPetrinaut(config: CreatePetrinautConfig): Petrinaut { const { document: handle, readonly = false } = config; + const handleCapabilities = resolvePetrinautHandleCapabilities( + handle.capabilities, + ); + const capabilities: ResolvedPetrinautHandleCapabilities = { + ...handleCapabilities, + readonly: readonly || handleCapabilities.readonly, + }; const disposers: Array<() => void> = []; @@ -132,22 +148,28 @@ export function createPetrinaut(config: CreatePetrinautConfig): Petrinaut { const patches = createPatchStream(handle); const mutate = (fn: (draft: SDCPN) => void) => { - if (readonly) { + if (capabilities.readonly) { return; } handle.change(fn); }; - const mutations = createPetrinautActions(mutate); - const commands = createPetrinautCommands(mutate, () => definition.get()); + const mutations = createPetrinautActions(mutate, capabilities.extensions); + const commands = createPetrinautCommands( + mutate, + () => definition.get(), + capabilities.extensions, + ); return { handle, definition, patches, + capabilities, + extensions: capabilities.extensions, mutations, commands, - readonly, + readonly: capabilities.readonly, dispose() { for (const dispose of disposers) { dispose(); diff --git a/libs/@hashintel/petrinaut/ANALYSIS.md b/libs/@hashintel/petrinaut/ANALYSIS.md new file mode 100644 index 00000000000..864df44e891 --- /dev/null +++ b/libs/@hashintel/petrinaut/ANALYSIS.md @@ -0,0 +1,85 @@ +# SDCPN Extension Capabilities + +## Context + +The Linear issue was not accessible from the local/browser session, so this analysis is based on the ticket summary in the task prompt. + +CatCollab wants to integrate Petrinaut with a Petri-net definition that does not expose HASH's SDCPN extensions. A future `CatCollabPetriNetHandle` is a good fit for this boundary: it can adapt CatCollab's bare Petri-net shape into the existing Petrinaut document interface while declaring which Petrinaut capabilities are available. + +The handle implementation is intentionally out of scope for this change. + +## Implemented Shape + +`@hashintel/petrinaut-core` now supports handle-level capabilities: + +```ts +type PetrinautHandleCapabilities = { + readonly?: boolean; + disabledExtensions?: readonly PetrinautExtension[]; +}; +``` + +The supported extension keys are: + +- `colors` +- `stochasticity` +- `dynamics` +- `parameters` + +Omitting `disabledExtensions` keeps the current default behavior, where all extensions are enabled. For CatCollab's bare Petri-net integration, the future handle can declare: + +```ts +capabilities: { + disabledExtensions: ["colors", "stochasticity", "dynamics", "parameters"], + readonly: true, // or false, depending on CatCollab's intended mode +} +``` + +The effective Petrinaut instance is read-only when either the host config or the handle capabilities request read-only mode. + +## Touchpoints + +Core package: + +- `handle.ts`: accepts optional `capabilities` on `PetrinautDocHandle` and JSON handles. +- `instance.ts`: resolves capabilities once and exposes `extensions` and effective `readonly`. +- `actions.ts`: prevents disabled extension data from being created or retained during edits. +- `commands.ts`: strips disabled extension data from pasted definitions and skips unavailable selection types. +- `extensions.ts`: centralizes extension resolution, selection availability, and SDCPN sanitization. + +React and UI package: + +- `sdcpn-context.ts` and `sdcpn-provider.tsx`: expose active extension settings through context and selection typing. +- Left sidebar and search: hide token type, differential equation, and global parameter entry points when unavailable. +- Properties panel: hide color, dynamics, visualizer, differential-equation, and transition-result surfaces as applicable. +- Transition firing-time panel: hides the stochastic mode selector when stochasticity is unavailable. +- React Flow rendering: renders places and arcs without color semantics when colors are disabled, and disables dynamics markers when dynamics is unavailable. +- Simulation scenario and timeline views: treat all places as untyped when colors are disabled and ignore global parameter overrides when parameters are disabled. + +## Behavior With CatCollab-Style Bare Petri Nets + +With `disabledExtensions: ["colors", "stochasticity", "dynamics", "parameters"]`: + +- Token types are unavailable in navigation, search, properties, selection cleanup, paste, and mutation actions. +- Differential equations and place dynamics are unavailable. +- Place color IDs, visualizer code, and dynamics fields are cleared or ignored. +- Stochastic transitions are coerced back to predicate transitions. +- Transition kernel code is cleared. +- Global parameters are unavailable in navigation, search, properties, paste, mutation actions, scenario overrides, and simulation defaults. +- Core mutation and paste paths sanitize SDCPN data so unavailable extensions do not persist after edits. + +## Open Design Choices + +- `disabledExtensions` was chosen instead of `enabledExtensions` for backward compatibility: existing handles keep all current SDCPN behavior without extra metadata. +- `colors` currently gates token types and several dependent behaviors. `dynamics` is also treated as dependent on `colors`, because equations target colored token types in the current SDCPN model. +- `stochasticity` only controls stochastic firing-time behavior. It does not currently disable all probabilistic simulation concepts if those are introduced elsewhere later. +- `parameters` controls global net-level parameters only. Scenario-local parameters remain available because they belong to scenario configuration rather than the global Petri-net definition. +- The future CatCollab handle still needs a clear mapping from CatCollab's bare Petri-net model into Petrinaut's SDCPN document shape. + +## Potential Follow-Ups + +- Filter AI tool schemas and prompts by active extensions. Core mutations now sanitize disabled data, but tool availability could be made more explicit to the AI layer. +- Decide whether other features should become separate capability flags for a truly bare Petri-net mode, such as inhibitor arcs, scenarios, metrics, or simulation-only views. +- Add import/export level sanitation so external document conversion cannot accidentally reintroduce disabled extension data. +- Consider making read-only reasons more specific in the UI, for example distinguishing simulation read-only from handle-level read-only. +- Add an actual `CatCollabPetriNetHandle` once CatCollab's source model and edit semantics are finalized. diff --git a/libs/@hashintel/petrinaut/src/main.ts b/libs/@hashintel/petrinaut/src/main.ts index 1e11da74587..faa32865a58 100644 --- a/libs/@hashintel/petrinaut/src/main.ts +++ b/libs/@hashintel/petrinaut/src/main.ts @@ -26,6 +26,9 @@ export { type MutationHelperFunctions, type Parameter, type PetrinautDocHandle, + type PetrinautExtension, + type PetrinautExtensionSettings, + type PetrinautHandleCapabilities, type PetrinautHistory, type Petrinaut as PetrinautInstance, type PetrinautPatch, diff --git a/libs/@hashintel/petrinaut/src/react/experiments/provider.test.tsx b/libs/@hashintel/petrinaut/src/react/experiments/provider.test.tsx index cc147a0d3ce..23f1040d629 100644 --- a/libs/@hashintel/petrinaut/src/react/experiments/provider.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/experiments/provider.test.tsx @@ -6,6 +6,7 @@ import { use } from "react"; import { describe, expect, it, vi } from "vitest"; import { + DEFAULT_PETRINAUT_EXTENSIONS, type PlaceTokenCountDistributionFrame, type SDCPN, type WorkerLike, @@ -128,6 +129,7 @@ const sdcpnContextValue: SDCPNContextValue = { petriNetId: "test-net", petriNetDefinition: EMPTY_SDCPN, readonly: false, + extensions: DEFAULT_PETRINAUT_EXTENSIONS, setTitle: () => {}, title: "Test", getItemType: () => null, diff --git a/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx b/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx index 579207acd38..c908e8baa5b 100644 --- a/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx @@ -133,9 +133,10 @@ export const ExperimentsProvider: React.FC = ({ children, workerFactory, }) => { - const { petriNetDefinition } = use(SDCPNContext); + const { extensions, petriNetDefinition } = use(SDCPNContext); const { addNotification } = use(NotificationsContext); const petriNetDefinitionRef = useLatest(petriNetDefinition); + const extensionsRef = useLatest(extensions); const workerFactoryRef = useLatest(workerFactory ?? createMonteCarloWorker); const registrationsRef = useRef( new Map(), @@ -253,6 +254,12 @@ export const ExperimentsProvider: React.FC = ({ let parameterValues: Record = {}; let initialMarking: InitialMarking = {}; + const globalParameters = extensionsRef.current.parameters + ? sdcpn.parameters + : []; + const experimentSdcpn = extensionsRef.current.parameters + ? sdcpn + : { ...sdcpn, parameters: [] }; if (selectedScenario) { const parsedScenarioValues = parseScenarioParameterValues( @@ -265,7 +272,7 @@ export const ExperimentsProvider: React.FC = ({ const compiledScenario = compileScenario( selectedScenario, - sdcpn.parameters, + globalParameters, sdcpn.places, sdcpn.types, { scenarioParameterValues: parsedScenarioValues.values }, @@ -304,7 +311,7 @@ export const ExperimentsProvider: React.FC = ({ try { const handle = await createMonteCarloExperiment({ - sdcpn, + sdcpn: experimentSdcpn, initialMarking, parameterValues, seed: input.seed, diff --git a/libs/@hashintel/petrinaut/src/react/hooks/use-default-parameter-values.ts b/libs/@hashintel/petrinaut/src/react/hooks/use-default-parameter-values.ts index 39248323fea..75601344280 100644 --- a/libs/@hashintel/petrinaut/src/react/hooks/use-default-parameter-values.ts +++ b/libs/@hashintel/petrinaut/src/react/hooks/use-default-parameter-values.ts @@ -27,10 +27,14 @@ export { */ export function useDefaultParameterValues(): DefaultParameterValues { const { + extensions, petriNetDefinition: { parameters }, } = use(SDCPNContext); return useMemo(() => { + if (!extensions.parameters) { + return {}; + } return deriveDefaultParameterValues(parameters); - }, [parameters]); + }, [extensions.parameters, parameters]); } diff --git a/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-commands.test.tsx b/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-commands.test.tsx index 42ddcb608d1..f4249e98a8c 100644 --- a/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-commands.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-commands.test.tsx @@ -7,6 +7,7 @@ import { describe, expect, test } from "vitest"; import { CLIPBOARD_FORMAT_VERSION, + DEFAULT_PETRINAUT_EXTENSIONS, type ClipboardPayload, createJsonDocHandle, createPetrinaut, @@ -102,6 +103,7 @@ const createWrapper = (options: WrapperOptions = {}) => { petriNetId: "test-net", petriNetDefinition: instance.definition.get(), readonly, + extensions: DEFAULT_PETRINAUT_EXTENSIONS, setTitle: () => {}, title: "Test", getItemType: () => null, diff --git a/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-mutations.test.tsx b/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-mutations.test.tsx index 37fbc352b42..e8e73909888 100644 --- a/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-mutations.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/hooks/use-petrinaut-mutations.test.tsx @@ -6,6 +6,7 @@ import { type ReactNode } from "react"; import { describe, expect, test } from "vitest"; import { + DEFAULT_PETRINAUT_EXTENSIONS, createJsonDocHandle, createPetrinaut, type Petrinaut, @@ -105,6 +106,7 @@ const createWrapper = (options: WrapperOptions = {}) => { petriNetId: "test-net", petriNetDefinition: instance.definition.get(), readonly, + extensions: DEFAULT_PETRINAUT_EXTENSIONS, setTitle: () => {}, title: "Test", getItemType: () => null, diff --git a/libs/@hashintel/petrinaut/src/react/index.ts b/libs/@hashintel/petrinaut/src/react/index.ts index 64c9297858d..cc153601646 100644 --- a/libs/@hashintel/petrinaut/src/react/index.ts +++ b/libs/@hashintel/petrinaut/src/react/index.ts @@ -43,5 +43,8 @@ export type { EventStream, Petrinaut, PetrinautDocHandle, + PetrinautExtension, + PetrinautExtensionSettings, + PetrinautHandleCapabilities, ReadableStore, } from "@hashintel/petrinaut-core"; diff --git a/libs/@hashintel/petrinaut/src/react/sdcpn-provider.tsx b/libs/@hashintel/petrinaut/src/react/sdcpn-provider.tsx index 8731af4d20e..faad2987f02 100644 --- a/libs/@hashintel/petrinaut/src/react/sdcpn-provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/sdcpn-provider.tsx @@ -1,6 +1,9 @@ import { use, type ReactNode } from "react"; -import { ARC_ID_PREFIX } from "@hashintel/petrinaut-core"; +import { + ARC_ID_PREFIX, + isSelectionTypeAvailableForExtensions, +} from "@hashintel/petrinaut-core"; import { NetManagementContext } from "./net-management-context"; import { SDCPNContext, type SDCPNContextValue } from "./state/sdcpn-context"; @@ -24,6 +27,7 @@ export const SDCPNProvider: React.FC<{ children: ReactNode }> = ({ petriNetId: instance.handle.id, petriNetDefinition, readonly: instance.readonly, + extensions: instance.extensions, title: netManagement.title, setTitle: netManagement.setTitle, existingNets: netManagement.existingNets, @@ -35,17 +39,28 @@ export const SDCPNProvider: React.FC<{ children: ReactNode }> = ({ return "arc"; } - if (petriNetDefinition.types.some((type) => type.id === id)) { + if ( + isSelectionTypeAvailableForExtensions("type", instance.extensions) && + petriNetDefinition.types.some((type) => type.id === id) + ) { return "type"; } if ( + isSelectionTypeAvailableForExtensions( + "parameter", + instance.extensions, + ) && petriNetDefinition.parameters.some((parameter) => parameter.id === id) ) { return "parameter"; } if ( + isSelectionTypeAvailableForExtensions( + "differentialEquation", + instance.extensions, + ) && petriNetDefinition.differentialEquations.some( (equation) => equation.id === id, ) diff --git a/libs/@hashintel/petrinaut/src/react/simulation/provider.tsx b/libs/@hashintel/petrinaut/src/react/simulation/provider.tsx index bfdc92a9765..bd88b275b09 100644 --- a/libs/@hashintel/petrinaut/src/react/simulation/provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/simulation/provider.tsx @@ -113,10 +113,11 @@ export const SimulationProvider: React.FC = ({ workerFactory, }) => { const sdcpnContext = use(SDCPNContext); - const { petriNetDefinition } = sdcpnContext; + const { extensions, petriNetDefinition } = sdcpnContext; const { addNotification } = use(NotificationsContext); const petriNetDefinitionRef = useLatest(petriNetDefinition); + const extensionsRef = useLatest(extensions); const workerFactoryRef = useLatest(workerFactory ?? createSimulationWorker); // Configuration state (not managed by the simulation handle) @@ -285,6 +286,9 @@ export const SimulationProvider: React.FC = ({ }) => { const currentState = stateValuesRef.current; const sdcpn = petriNetDefinitionRef.current; + const simulationSdcpn = extensionsRef.current.parameters + ? sdcpn + : { ...sdcpn, parameters: [] }; // Dispose any in-flight simulation before starting a new one. Update both // the ref (synchronous, so callers in the same tick see `null` not the @@ -304,7 +308,7 @@ export const SimulationProvider: React.FC = ({ let sim: Simulation; try { sim = await createSimulation({ - sdcpn, + sdcpn: simulationSdcpn, // eslint-disable-next-line no-use-before-define -- closure; ref is defined later in render initialMarking: effectiveInitialMarkingRef.current, // eslint-disable-next-line no-use-before-define -- closure; ref is defined later in render @@ -347,7 +351,8 @@ export const SimulationProvider: React.FC = ({ const reset: SimulationContextValue["reset"] = () => { const sdcpn = petriNetDefinitionRef.current; - const defaultValues = deriveDefaultParameterValues(sdcpn.parameters); + const parameters = extensionsRef.current.parameters ? sdcpn.parameters : []; + const defaultValues = deriveDefaultParameterValues(parameters); const parameterValues: Record = {}; for (const [key, value] of Object.entries(defaultValues)) { @@ -448,7 +453,7 @@ export const SimulationProvider: React.FC = ({ }; const outcome = compileScenario( tweakedScenario, - petriNetDefinition.parameters, + extensions.parameters ? petriNetDefinition.parameters : [], petriNetDefinition.places, petriNetDefinition.types, ); @@ -460,7 +465,9 @@ export const SimulationProvider: React.FC = ({ // When a scenario is compiled, override parameterValues and initialMarking // with the scenario's resolved output. - let effectiveParameterValues = stateValues.parameterValues; + let effectiveParameterValues = extensions.parameters + ? stateValues.parameterValues + : {}; let effectiveInitialMarking = stateValues.initialMarking; if (compiledScenarioResult) { diff --git a/libs/@hashintel/petrinaut/src/react/state/sdcpn-context.ts b/libs/@hashintel/petrinaut/src/react/state/sdcpn-context.ts index 24f37660f29..11379a544b1 100644 --- a/libs/@hashintel/petrinaut/src/react/state/sdcpn-context.ts +++ b/libs/@hashintel/petrinaut/src/react/state/sdcpn-context.ts @@ -1,6 +1,11 @@ import { createContext } from "react"; -import type { MinimalNetMetadata, SDCPN } from "@hashintel/petrinaut-core"; +import { + DEFAULT_PETRINAUT_EXTENSIONS, + type MinimalNetMetadata, + type PetrinautExtensionSettings, + type SDCPN, +} from "@hashintel/petrinaut-core"; export type SDCPNProviderProps = { createNewNet: (params: { petriNetDefinition: SDCPN; title: string }) => void; @@ -9,6 +14,7 @@ export type SDCPNProviderProps = { petriNetId: string | null; petriNetDefinition: SDCPN; readonly: boolean; + extensions: PetrinautExtensionSettings; setTitle: (title: string) => void; title: string; }; @@ -39,6 +45,7 @@ const DEFAULT_CONTEXT_VALUE: SDCPNContextValue = { parameters: [], }, readonly: true, + extensions: DEFAULT_PETRINAUT_EXTENSIONS, setTitle: () => {}, title: "", getItemType: () => null, diff --git a/libs/@hashintel/petrinaut/src/react/state/use-is-read-only.ts b/libs/@hashintel/petrinaut/src/react/state/use-is-read-only.ts index f0cb8a05334..5759d29593e 100644 --- a/libs/@hashintel/petrinaut/src/react/state/use-is-read-only.ts +++ b/libs/@hashintel/petrinaut/src/react/state/use-is-read-only.ts @@ -4,7 +4,7 @@ import { useReadOnlyReason } from "./use-read-only-reason"; * Hook that determines if the editor is in read-only mode. * * The editor is read-only when any of the following are true: - * 1. The external `readonly` prop is set by the consumer + * 1. The external `readonly` prop is set by the consumer or the handle is read-only * 2. The global mode is "simulate" (user has switched to simulation mode) * 3. A simulation is currently running, paused, or complete * diff --git a/libs/@hashintel/petrinaut/src/react/state/use-read-only-reason.ts b/libs/@hashintel/petrinaut/src/react/state/use-read-only-reason.ts index 06b7ff75a73..1a237d0d4a1 100644 --- a/libs/@hashintel/petrinaut/src/react/state/use-read-only-reason.ts +++ b/libs/@hashintel/petrinaut/src/react/state/use-read-only-reason.ts @@ -8,7 +8,7 @@ import { SDCPNContext } from "./sdcpn-context"; * Why the editor currently disallows mutations, or `null` when mutations * are allowed. * - * - `host-readonly`: the consumer passed `readonly` to ``. + * - `host-readonly`: the consumer passed `readonly`, or the handle is read-only. * - `simulate-mode`: the user has switched to simulate mode. * - `simulation-active`: a simulation is Running, Paused, or Complete. */ diff --git a/libs/@hashintel/petrinaut/src/react/state/use-selection-cleanup.ts b/libs/@hashintel/petrinaut/src/react/state/use-selection-cleanup.ts index 32f9403a219..baec7bc69cb 100644 --- a/libs/@hashintel/petrinaut/src/react/state/use-selection-cleanup.ts +++ b/libs/@hashintel/petrinaut/src/react/state/use-selection-cleanup.ts @@ -9,7 +9,7 @@ import { SDCPNContext } from "./sdcpn-context"; * Reactively removes stale IDs from the selection when items are deleted from the SDCPN. */ export function useSelectionCleanup() { - const { petriNetDefinition } = use(SDCPNContext); + const { extensions, petriNetDefinition } = use(SDCPNContext); const { selection, setSelection, hoveredItem, clearHoveredItem } = use(EditorContext); @@ -40,14 +40,20 @@ export function useSelectionCleanup() { ); } } - for (const type of petriNetDefinition.types) { - validIds.add(type.id); + if (extensions.colors) { + for (const type of petriNetDefinition.types) { + validIds.add(type.id); + } } - for (const eq of petriNetDefinition.differentialEquations) { - validIds.add(eq.id); + if (extensions.colors && extensions.dynamics) { + for (const eq of petriNetDefinition.differentialEquations) { + validIds.add(eq.id); + } } - for (const param of petriNetDefinition.parameters) { - validIds.add(param.id); + if (extensions.parameters) { + for (const param of petriNetDefinition.parameters) { + validIds.add(param.id); + } } // Check if any selected ID is stale @@ -77,6 +83,7 @@ export function useSelectionCleanup() { } }, [ petriNetDefinition, + extensions, selection, setSelection, hoveredItem, diff --git a/libs/@hashintel/petrinaut/src/ui/constants/ui-messages.ts b/libs/@hashintel/petrinaut/src/ui/constants/ui-messages.ts index fb01c021686..a36807a4830 100644 --- a/libs/@hashintel/petrinaut/src/ui/constants/ui-messages.ts +++ b/libs/@hashintel/petrinaut/src/ui/constants/ui-messages.ts @@ -4,5 +4,5 @@ export const UI_MESSAGES = { AI_FEATURE_COMING_SOON: "AI generation feature coming soon", DYNAMICS_REQUIRES_TYPE: "Select a token type to enable dynamics", - READ_ONLY_MODE: "Editing disabled while in simulation mode", + READ_ONLY_MODE: "Editing is disabled", } as const; diff --git a/libs/@hashintel/petrinaut/src/ui/monaco/code-editor.stories.tsx b/libs/@hashintel/petrinaut/src/ui/monaco/code-editor.stories.tsx index 0b14ee8b395..eded4bdd82d 100644 --- a/libs/@hashintel/petrinaut/src/ui/monaco/code-editor.stories.tsx +++ b/libs/@hashintel/petrinaut/src/ui/monaco/code-editor.stories.tsx @@ -1,6 +1,7 @@ import { type ReactNode, useRef, useState } from "react"; import { css } from "@hashintel/ds-helpers/css"; +import { DEFAULT_PETRINAUT_EXTENSIONS } from "@hashintel/petrinaut-core"; import { LanguageClientProvider } from "../../react/lsp/provider"; import { @@ -103,6 +104,7 @@ const LSP_CONTEXT: SDCPNContextValue = { petriNetId: "story-net", petriNetDefinition: LAMBDA_SDCPN, readonly: false, + extensions: DEFAULT_PETRINAUT_EXTENSIONS, setTitle: () => {}, title: "Story Net", getItemType: () => null, diff --git a/libs/@hashintel/petrinaut/src/ui/petrinaut.stories.tsx b/libs/@hashintel/petrinaut/src/ui/petrinaut.stories.tsx index 75167abefce..28f0b698664 100644 --- a/libs/@hashintel/petrinaut/src/ui/petrinaut.stories.tsx +++ b/libs/@hashintel/petrinaut/src/ui/petrinaut.stories.tsx @@ -2,7 +2,11 @@ import { useMemo, useState, useEffect } from "react"; import { sirModel } from "@hashintel/petrinaut-core/examples"; -import { createJsonDocHandle, type SDCPN } from "../main"; +import { + createJsonDocHandle, + type PetrinautHandleCapabilities, + type SDCPN, +} from "../main"; import { Petrinaut } from "../ui/petrinaut"; import { PetrinautStoryProvider } from "./petrinaut-story-provider"; import { createStorybookAiTransport } from "./views/Editor/panels/create-storybook-ai-transport"; @@ -15,6 +19,70 @@ const emptySDCPN: SDCPN = { differentialEquations: [], }; +const barePetriNet: SDCPN = { + places: [ + { + id: "p_waiting", + name: "Waiting", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: 120, + y: 180, + showAsInitialState: true, + }, + { + id: "p_processing", + name: "Processing", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: 430, + y: 180, + }, + { + id: "p_done", + name: "Done", + colorId: null, + dynamicsEnabled: false, + differentialEquationId: null, + x: 740, + y: 180, + }, + ], + transitions: [ + { + id: "t_start", + name: "Start", + inputArcs: [{ placeId: "p_waiting", weight: 1, type: "standard" }], + outputArcs: [{ placeId: "p_processing", weight: 1 }], + lambdaType: "predicate", + lambdaCode: "return true;", + transitionKernelCode: "", + x: 290, + y: 205, + }, + { + id: "t_finish", + name: "Finish", + inputArcs: [{ placeId: "p_processing", weight: 1, type: "standard" }], + outputArcs: [{ placeId: "p_done", weight: 1 }], + lambdaType: "predicate", + lambdaCode: "return true;", + transitionKernelCode: "", + x: 600, + y: 205, + }, + ], + types: [], + parameters: [], + differentialEquations: [], +}; + +const barePetriNetCapabilities = { + disabledExtensions: ["colors", "stochasticity", "dynamics", "parameters"], +} satisfies PetrinautHandleCapabilities; + import type { Meta, StoryObj } from "@storybook/react-vite"; const meta = { @@ -68,18 +136,20 @@ export const WithAiAssistant: Story = { const HandleSpikeRender = ({ aiAssistant, + capabilities, initial, initialTitle, }: { aiAssistant?: { transport: ReturnType; }; + capabilities?: PetrinautHandleCapabilities; initial: SDCPN; initialTitle: string; }) => { const handle = useMemo( - () => createJsonDocHandle({ id: "spike-net", initial }), - [initial], + () => createJsonDocHandle({ id: "spike-net", initial, capabilities }), + [capabilities, initial], ); const [patchLog, setPatchLog] = useState([]); @@ -142,6 +212,16 @@ export const HandleSpikeWithSir: Story = { ), }; +export const ExtensionsDisabled: Story = { + render: () => ( + + ), +}; + export const HandleSpikeWithAi: Story = { render: () => ( { const { setGlobalMode } = use(EditorContext); const { + extensions, petriNetDefinition: { parameters, scenarios }, } = use(SDCPNContext); + const globalParameters = extensions.parameters ? parameters : []; const { state: simulationState, dt, @@ -210,7 +212,7 @@ const SimulationSettingsContent: React.FC = () => { type: sp.type, defaultValue: String(sp.default), })) - : parameters.map((p) => ({ + : globalParameters.map((p) => ({ key: p.id, name: p.name, variableName: p.variableName, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx index 54653913993..e3d7778e96e 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx @@ -214,12 +214,20 @@ const TimelineViewPicker: React.FC = () => { const { timelineView, setTimelineView, setGlobalMode, setSimulateViewMode } = use(EditorContext); const { + extensions, petriNetDefinition: { metrics = [] }, } = use(SDCPNContext); + const colorsEnabled = extensions.colors; const [isCreateOpen, setIsCreateOpen] = useState(false); const [isViewOpen, setIsViewOpen] = useState(false); + useEffect(() => { + if (!colorsEnabled && timelineView.kind === "per-type") { + setTimelineView({ kind: "per-place" }); + } + }, [colorsEnabled, setTimelineView, timelineView.kind]); + const selectedMetric = timelineView.kind === "metric" ? metrics.find((m) => m.id === timelineView.metricId) @@ -227,10 +235,16 @@ const TimelineViewPicker: React.FC = () => { const options = [ { value: PER_PLACE_VALUE, label: "Tokens per place" }, - { value: PER_TYPE_VALUE, label: "Tokens per type" }, + ...(colorsEnabled + ? [{ value: PER_TYPE_VALUE, label: "Tokens per type" }] + : []), { value: PER_TRANSITION_VALUE, label: "Transition firings" }, ...metrics.map((m) => ({ value: m.id, label: m.name })), ]; + const selectedValue = + colorsEnabled || timelineView.kind !== "per-type" + ? viewToSelectValue(timelineView) + : PER_PLACE_VALUE; return ( <> @@ -238,7 +252,7 @@ const TimelineViewPicker: React.FC = () => {
setTimelineView(selectValueToView(value))} /> diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/BottomPanel/subviews/simulation-timeline/use-streaming-data.ts b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/BottomPanel/subviews/simulation-timeline/use-streaming-data.ts index 487ba58dd24..a76966e000d 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/BottomPanel/subviews/simulation-timeline/use-streaming-data.ts +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/BottomPanel/subviews/simulation-timeline/use-streaming-data.ts @@ -144,8 +144,10 @@ export function useStreamingData(): { } { const { dt, getFramesInRange, totalFrames } = use(SimulationContext); const { + extensions, petriNetDefinition: { places, types, transitions, metrics }, } = use(SDCPNContext); + const colorsEnabled = extensions.colors; const { timelineView } = use(EditorContext); const selectedMetric = @@ -154,13 +156,17 @@ export function useStreamingData(): { : null; const compiledMetric = compileTimelineMetric(selectedMetric); + const availableTypes = colorsEnabled ? types : []; + const availablePlaces = colorsEnabled + ? places + : places.map((place) => ({ ...place, colorId: null })); // Computes the active timeline view mode described above into concrete uPlot // series metadata and the per-frame value extractor used while streaming. const seriesConfig = buildTimelineSeriesConfig({ timelineView, - places, - types, + places: availablePlaces, + types: availableTypes, transitions, selectedMetric, compiledMetric: compiledMetric.fn, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/panel.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/panel.tsx index aea22e65b05..7d4c332b651 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/panel.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/panel.tsx @@ -3,6 +3,7 @@ import { use, useMemo } from "react"; import { css, cva, cx } from "@hashintel/ds-helpers/css"; import { EditorContext } from "../../../../../react/state/editor-context"; +import { SDCPNContext } from "../../../../../react/state/sdcpn-context"; import { UserSettingsContext } from "../../../../../react/state/user-settings-context"; import { GlassPanel } from "../../../../components/glass-panel"; import { VerticalSubViewsContainer } from "../../../../components/sub-view/vertical/vertical-sub-views-container"; @@ -104,6 +105,7 @@ export const LeftSideBar: React.FC = () => { isPanelAnimating, isSearchOpen, } = use(EditorContext); + const { extensions } = use(SDCPNContext); const { keepPanelsMounted, useEntitiesTreeView } = use(UserSettingsContext); @@ -112,7 +114,18 @@ export const LeftSideBar: React.FC = () => { const sidebarSubViews = useEntitiesTreeView ? LEFT_SIDEBAR_TREE_SUBVIEWS - : LEFT_SIDEBAR_SUBVIEWS; + : LEFT_SIDEBAR_SUBVIEWS.filter((subView) => { + if (subView.id === "token-types-list") { + return extensions.colors; + } + if (subView.id === "differential-equations-list") { + return extensions.colors && extensions.dynamics; + } + if (subView.id === "parameters-list") { + return extensions.parameters; + } + return true; + }); const searchSubViews = useMemo(() => [searchSubView], []); diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/differential-equations-list.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/differential-equations-list.tsx index 80517eea479..95a58ebd50f 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/differential-equations-list.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/differential-equations-list.tsx @@ -24,20 +24,22 @@ import type { SubView } from "../../../../../components/sub-view/types"; export const DifferentialEquationsSectionHeaderAction: React.FC = () => { const { petriNetDefinition: { types, differentialEquations }, + extensions, } = use(SDCPNContext); const { addDifferentialEquation } = usePetrinautMutations(); const { selectItem } = use(EditorContext); const isReadOnly = useIsReadOnly(); + const isDisabled = isReadOnly || !extensions.colors || !extensions.dynamics; return ( +
+ )} + + ) + )} + + )}
{}, title: "Story Net", getItemType: () => null, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/main.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/main.tsx index 9387bb5162e..f8f15672983 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/main.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/main.tsx @@ -1,5 +1,8 @@ +import { use } from "react"; + import { css } from "@hashintel/ds-helpers/css"; +import { SDCPNContext } from "../../../../../../react/state/sdcpn-context"; import { useIsReadOnly } from "../../../../../../react/state/use-is-read-only"; import { VerticalSubViewsContainer } from "../../../../../components/sub-view/vertical/vertical-sub-views-container"; import { TransitionPropertiesProvider } from "./context"; @@ -38,11 +41,12 @@ export const TransitionProperties: React.FC = ({ removeArc, }) => { const isReadOnly = useIsReadOnly(); + const { extensions } = use(SDCPNContext); const subViews: SubView[] = [ transitionMainContentSubView, transitionFiringTimeSubView, - transitionResultsSubView, + ...(extensions.colors ? [transitionResultsSubView] : []), ]; return ( diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-firing-time/subview.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-firing-time/subview.tsx index 429456a4c3b..15d624135cd 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-firing-time/subview.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/transition-properties/subviews/transition-firing-time/subview.tsx @@ -5,6 +5,7 @@ import { css } from "@hashintel/ds-helpers/css"; import { generateDefaultLambdaCode } from "@hashintel/petrinaut-core"; import { EditorContext } from "../../../../../../../../react/state/editor-context"; +import { SDCPNContext } from "../../../../../../../../react/state/sdcpn-context"; import { Button } from "../../../../../../../components/button"; import { Menu } from "../../../../../../../components/menu"; import { SegmentGroup } from "../../../../../../../components/segment-group"; @@ -46,6 +47,7 @@ const aiMenuItemStyle = css({ const FiringTimeHeaderAction: React.FC = () => { const { transition, updateTransition } = useTransitionPropertiesContext(); const { globalMode } = use(EditorContext); + const { extensions } = use(SDCPNContext); if (globalMode !== "edit") { return null; @@ -72,7 +74,11 @@ const FiringTimeHeaderAction: React.FC = () => { updateTransition({ transitionId: transition.id, update: { - lambdaCode: generateDefaultLambdaCode(transition.lambdaType), + lambdaCode: generateDefaultLambdaCode( + extensions.stochasticity + ? transition.lambdaType + : "predicate", + ), }, }); }, @@ -103,31 +109,37 @@ const FiringTimeHeaderAction: React.FC = () => { const TransitionFiringTimeContent: React.FC = () => { const { transition, isReadOnly, updateTransition } = useTransitionPropertiesContext(); + const { extensions } = use(SDCPNContext); + const lambdaType = extensions.stochasticity + ? transition.lambdaType + : "predicate"; return (
-
- { - updateTransition({ - transitionId: transition.id, - update: { - lambdaType: value as "predicate" | "stochastic", - }, - }); - }} - disabled={isReadOnly} - tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} - /> -
+ {extensions.stochasticity && ( +
+ { + updateTransition({ + transitionId: transition.id, + update: { + lambdaType: value as "predicate" | "stochastic", + }, + }); + }} + disabled={isReadOnly} + tooltip={isReadOnly ? UI_MESSAGES.READ_ONLY_MODE : undefined} + /> +
+ )}
- {transition.lambdaType === "predicate" + {lambdaType === "predicate" ? "Define a boolean guard condition. The transition fires when this function returns true, enabling discrete control flow based on token data." : "Return a numeric rate representing the average number of firings per second. The transition fires stochastically according to this rate."}
diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/experiments-story-fixtures.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/experiments-story-fixtures.tsx index da5ff3e5189..014d0ed9755 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/experiments-story-fixtures.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/experiments/experiments-story-fixtures.tsx @@ -1,5 +1,6 @@ import { useMemo, useRef, useState, type ReactNode } from "react"; +import { DEFAULT_PETRINAUT_EXTENSIONS } from "@hashintel/petrinaut-core"; import { sirModel } from "@hashintel/petrinaut-core/examples"; import { @@ -24,6 +25,7 @@ export const sirSdcpnContextValue: SDCPNContextValue = { petriNetId: "sir-story-net", petriNetDefinition: sirModel.petriNetDefinition, readonly: false, + extensions: DEFAULT_PETRINAUT_EXTENSIONS, setTitle: () => {}, title: sirModel.title, getItemType: (id) => { diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/scenarios/create-scenario-drawer.stories.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/scenarios/create-scenario-drawer.stories.tsx index f6ae43bec0a..fbb6087cb28 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/scenarios/create-scenario-drawer.stories.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/scenarios/create-scenario-drawer.stories.tsx @@ -1,4 +1,5 @@ import { LanguageClientProvider } from "../../../../../../react/lsp/provider"; +import { DEFAULT_PETRINAUT_EXTENSIONS } from "@hashintel/petrinaut-core"; import { SDCPNContext, type SDCPNContextValue, @@ -23,6 +24,7 @@ const EMPTY_NET: SDCPNContextValue = { parameters: [], }, readonly: false, + extensions: DEFAULT_PETRINAUT_EXTENSIONS, setTitle: () => {}, title: "Empty Net", getItemType: () => null, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/scenarios/create-scenario-drawer.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/scenarios/create-scenario-drawer.tsx index 0cea8359bd6..72cd4c46069 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/scenarios/create-scenario-drawer.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/scenarios/create-scenario-drawer.tsx @@ -77,18 +77,20 @@ const CreateScenarioFooter = ({ // -- Standalone form body (used by drawer + stories) -------------------------- const CreateScenarioBody = ({ form }: { form: ScenarioFormInstance }) => { - const { petriNetDefinition } = use(SDCPNContext); + const { extensions, petriNetDefinition } = use(SDCPNContext); const typesById = new Map(); - for (const type of petriNetDefinition.types) { - typesById.set(type.id, type); + if (extensions.colors) { + for (const type of petriNetDefinition.types) { + typesById.set(type.id, type); + } } return ( void; }) => { - const { petriNetDefinition } = use(SDCPNContext); + const { extensions, petriNetDefinition } = use(SDCPNContext); const { updateScenario } = usePetrinautMutations(); const typesById = new Map(); - for (const type of petriNetDefinition.types) { - typesById.set(type.id, type); + if (extensions.colors) { + for (const type of petriNetDefinition.types) { + typesById.set(type.id, type); + } } // Names of OTHER scenarios — exclude the one being edited so it can keep @@ -174,7 +176,9 @@ const ViewScenarioContent = ({ type.id === place.colorId) : null; const hasColorType = !!(placeType && placeType.elements.length > 0); @@ -74,7 +74,8 @@ export function useSdcpnToReactFlow(): PetrinautReactFlowDefinitionObject { data: { label: place.name, type: "place", - dynamicsEnabled: place.dynamicsEnabled, + dynamicsEnabled: + extensions.colors && extensions.dynamics && place.dynamicsEnabled, hasColorType, typeColor: placeType?.displayColor, // Pass the type color for border styling }, @@ -102,7 +103,9 @@ export function useSdcpnToReactFlow(): PetrinautReactFlowDefinitionObject { data: { label: transition.name, type: "transition", - lambdaType: transition.lambdaType, + lambdaType: extensions.stochasticity + ? transition.lambdaType + : "predicate", frame: currentFrameReader?.getTransitionState(transition.id) ?? null, }, }); @@ -123,7 +126,7 @@ export function useSdcpnToReactFlow(): PetrinautReactFlowDefinitionObject { const place = petriNetDefinition.places.find( (pl) => pl.id === inputArc.placeId, ); - const placeType = place?.colorId + const placeType = extensions.colors && place?.colorId ? petriNetDefinition.types.find((type) => type.id === place.colorId) : null; let arcColor = placeType?.displayColor @@ -171,7 +174,7 @@ export function useSdcpnToReactFlow(): PetrinautReactFlowDefinitionObject { const place = petriNetDefinition.places.find( (pl) => pl.id === outputArc.placeId, ); - const placeType = place?.colorId + const placeType = extensions.colors && place?.colorId ? petriNetDefinition.types.find((type) => type.id === place.colorId) : null; let arcColor = placeType?.displayColor